# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::AlertManagement::Payload::Base do
  let_it_be(:project) { create(:project) }

  let(:raw_payload) { {} }
  let(:payload_class) { described_class }

  subject(:parsed_payload) { payload_class.new(project: project, payload: raw_payload) }

  describe '.attribute' do
    subject { parsed_payload.test }

    context 'with a single path provided' do
      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test']]
        end
      end

      it { is_expected.to be_nil }

      context 'and a matching value' do
        let(:raw_payload) { { 'test' => 'value' } }

        it { is_expected.to eq 'value' }
      end
    end

    context 'with multiple paths provided' do
      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test'], %w[alt test]]
        end
      end

      it { is_expected.to be_nil }

      context 'and a matching value' do
        let(:raw_payload) { { 'alt' => { 'test' => 'value' } } }

        it { is_expected.to eq 'value' }
      end
    end

    context 'with a fallback provided' do
      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test']], fallback: -> { 'fallback' }
        end
      end

      it { is_expected.to eq('fallback') }

      context 'and a matching value' do
        let(:raw_payload) { { 'test' => 'value' } }

        it { is_expected.to eq 'value' }
      end
    end

    context 'with a time type provided' do
      let(:test_time) { Time.current.change(usec: 0) }

      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test']], type: :time
        end
      end

      it { is_expected.to be_nil }

      context 'with a compatible matching value' do
        let(:raw_payload) { { 'test' => test_time.to_s } }

        it { is_expected.to eq test_time }
      end

      context 'with a value in rfc3339 format' do
        let(:raw_payload) { { 'test' => test_time.rfc3339 } }

        it { is_expected.to eq test_time }
      end

      context 'with an incompatible matching value' do
        let(:raw_payload) { { 'test' => 'bad time' } }

        it { is_expected.to be_nil }
      end

      context 'with time in seconds' do
        let(:raw_payload) { { 'test' => 1618877936 } }

        it { is_expected.to be_nil }
      end
    end

    context 'with an integer type provided' do
      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test']], type: :integer
        end
      end

      it { is_expected.to be_nil }

      context 'with a compatible matching value' do
        let(:raw_payload) { { 'test' => '15' } }

        it { is_expected.to eq 15 }
      end

      context 'with an incompatible matching value' do
        let(:raw_payload) { { 'test' => String } }

        it { is_expected.to be_nil }
      end

      context 'with an incompatible matching value' do
        let(:raw_payload) { { 'test' => 'apple' } }

        it { is_expected.to be_nil }
      end
    end

    context 'with a time_with_epoch_millis type provided' do
      let(:timestamp) { 3.hours.ago }
      let(:epoch_millis) { (timestamp.to_f * 1000).to_i }

      let(:payload_class) do
        Class.new(described_class) do
          attribute :test, paths: [['test']], type: :time_with_epoch_millis
        end
      end

      it { is_expected.to be_nil }

      context 'with a compatible matching value' do
        let(:raw_payload) { { 'test' => epoch_millis } }

        it { is_expected.to be_within(1.second).of(timestamp) }
      end

      context 'with an incompatible matching value' do
        let(:raw_payload) { { 'test' => String } }

        it { is_expected.to be_nil }
      end

      context 'with an incompatible matching value' do
        let(:raw_payload) { { 'test' => 'apple' } }

        it { is_expected.to be_nil }
      end
    end
  end

  describe '#alert_params' do
    subject { parsed_payload.alert_params }

    context 'with every key' do
      let_it_be(:raw_payload) { { 'key' => 'value' } }
      let_it_be(:stubs) do
        {
          description: 'description',
          ends_at: Time.current,
          environment: create(:environment, project: project),
          gitlab_fingerprint: 'gitlab_fingerprint',
          hosts: 'hosts',
          monitoring_tool: 'monitoring_tool',
          service: 'service',
          severity: 'critical',
          starts_at: Time.current,
          title: 'title'
        }
      end

      let(:expected_result) do
        {
          description: stubs[:description],
          ended_at: stubs[:ends_at],
          environment: stubs[:environment],
          fingerprint: stubs[:gitlab_fingerprint],
          hosts: [stubs[:hosts]],
          monitoring_tool: stubs[:monitoring_tool],
          payload: raw_payload,
          project_id: project.id,
          service: stubs[:service],
          severity: stubs[:severity],
          started_at: stubs[:starts_at],
          title: stubs[:title]
        }
      end

      before do
        allow(parsed_payload).to receive_messages(stubs)
      end

      it { is_expected.to eq(expected_result) }

      it 'can generate a valid new alert' do
        expect(::AlertManagement::Alert.new(subject.except(:ended_at))).to be_valid
      end
    end

    context 'with too-long strings' do
      let_it_be(:stubs) do
        {
          description: 'a' * (::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH + 1),
          hosts: 'b' * (::AlertManagement::Alert::HOSTS_MAX_LENGTH + 1),
          monitoring_tool: 'c' * (::AlertManagement::Alert::TOOL_MAX_LENGTH + 1),
          service: 'd' * (::AlertManagement::Alert::SERVICE_MAX_LENGTH + 1),
          title: 'e' * (::AlertManagement::Alert::TITLE_MAX_LENGTH + 1)
        }
      end

      before do
        allow(parsed_payload).to receive_messages(stubs)
      end

      it do
        is_expected.to eq({
          description: stubs[:description].truncate(AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
          hosts: ['b' * ::AlertManagement::Alert::HOSTS_MAX_LENGTH],
          monitoring_tool: stubs[:monitoring_tool].truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH),
          service: stubs[:service].truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH),
          project_id: project.id,
          title: stubs[:title].truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH)
        })
      end
    end

    context 'with too-long hosts array' do
      let(:hosts) { %w[abc def ghij] }
      let(:shortened_hosts) { %w[abc def ghi] }

      before do
        stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9)
        allow(parsed_payload).to receive(:hosts).and_return(hosts)
      end

      it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) }

      context 'with host cut off between elements' do
        let(:hosts) { %w[abcde fghij] }
        let(:shortened_hosts) { %w[abcde fghi] }

        it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
      end

      context 'with nested hosts' do
        let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray
        let(:shortened_hosts) { %w[abc de f g hi] }

        it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
      end
    end

    context 'with present, non-string values for string fields' do
      let_it_be(:stubs) do
        {
          description: { "description" => "description" },
          monitoring_tool: ['datadog', 5],
          service: 4356875,
          title: true
        }
      end

      before do
        allow(parsed_payload).to receive_messages(stubs)
      end

      it 'casts values to strings' do
        is_expected.to eq({
          description: "{\"description\"=>\"description\"}",
          monitoring_tool: "[\"datadog\", 5]",
          service: '4356875',
          project_id: project.id,
          title: "true"
        })
      end
    end

    context 'with blank values for string fields' do
      let_it_be(:stubs) do
        {
          description: nil,
          monitoring_tool: '',
          service: {},
          title: []
        }
      end

      it 'leaves the fields blank' do
        is_expected.to eq({ project_id: project.id })
      end
    end
  end

  describe '#gitlab_fingerprint' do
    subject { parsed_payload.gitlab_fingerprint }

    it { is_expected.to be_nil }

    context 'when plain_gitlab_fingerprint is defined' do
      before do
        allow(parsed_payload)
          .to receive(:plain_gitlab_fingerprint)
          .and_return('fingerprint')
      end

      it 'returns a fingerprint' do
        is_expected.to eq(Digest::SHA1.hexdigest('fingerprint'))
      end
    end
  end

  describe '#environment' do
    let_it_be(:environment) { create(:environment, project: project, name: 'production') }

    subject { parsed_payload.environment }

    before do
      allow(parsed_payload).to receive(:environment_name).and_return(environment_name)
    end

    context 'without an environment name' do
      let(:environment_name) { nil }

      it { is_expected.to be_nil }
    end

    context 'with a non-matching environment name' do
      let(:environment_name) { 'other_environment' }

      it { is_expected.to be_nil }
    end

    context 'with a matching environment name' do
      let(:environment_name) { 'production' }

      it { is_expected.to eq(environment) }
    end
  end

  describe '#resolved?' do
    before do
      allow(parsed_payload).to receive(:status).and_return(status)
    end

    subject { parsed_payload.resolved? }

    context 'when status is not defined' do
      let(:status) { nil }

      it { is_expected.to be_falsey }
    end

    context 'when status is not resovled' do
      let(:status) { 'firing' }

      it { is_expected.to be_falsey }
    end

    context 'when status is resovled' do
      let(:status) { 'resolved' }

      it { is_expected.to be_truthy }
    end
  end

  describe '#has_required_attributes?' do
    subject { parsed_payload.has_required_attributes? }

    it { is_expected.to be(true) }
  end

  describe '#source' do
    subject { parsed_payload.source }

    it { is_expected.to be_nil }

    context 'with alerting integration provided' do
      before do
        parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION')
      end

      it { is_expected.to eq('INTEGRATION') }
    end

    context 'with monitoring tool defined in the raw payload' do
      before do
        allow(parsed_payload).to receive(:monitoring_tool).and_return('TOOL')
      end

      it { is_expected.to eq('TOOL') }
    end
  end
end
