# frozen_string_literal: true

require 'spec_helper'

describe Sentry::Client do
  let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
  let(:token) { 'test-token' }

  let(:issues_sample_response) do
    Gitlab::Utils.deep_indifferent_access(
      JSON.parse(fixture_file('sentry/issues_sample_response.json'))
    )
  end

  let(:projects_sample_response) do
    Gitlab::Utils.deep_indifferent_access(
      JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
    )
  end

  subject(:client) { described_class.new(sentry_url, token) }

  # Requires sentry_api_url and subject to be defined
  shared_examples 'no redirects' do
    let(:redirect_to) { 'https://redirected.example.com' }
    let(:other_url) { 'https://other.example.org' }

    let!(:redirected_req_stub) { stub_sentry_request(other_url) }

    let!(:redirect_req_stub) do
      stub_sentry_request(
        sentry_api_url,
        status: 302,
        headers: { location: redirect_to }
      )
    end

    it 'does not follow redirects' do
      expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302')
      expect(redirect_req_stub).to have_been_requested
      expect(redirected_req_stub).not_to have_been_requested
    end
  end

  shared_examples 'has correct return type' do |klass|
    it "returns objects of type #{klass}" do
      expect(subject).to all( be_a(klass) )
    end
  end

  shared_examples 'has correct length' do |length|
    it { expect(subject.length).to eq(length) }
  end

  # Requires sentry_api_request and subject to be defined
  shared_examples 'calls sentry api' do
    it 'calls sentry api' do
      subject

      expect(sentry_api_request).to have_been_requested
    end
  end

  shared_examples 'maps exceptions' do
    exceptions = {
      Gitlab::HTTP::Error => 'Error when connecting to Sentry',
      Net::OpenTimeout => 'Connection to Sentry timed out',
      SocketError => 'Received SocketError when trying to connect to Sentry',
      OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data',
      Errno::ECONNREFUSED => 'Connection refused',
      StandardError => 'Sentry request failed due to StandardError'
    }

    exceptions.each do |exception, message|
      context "#{exception}" do
        before do
          stub_request(:get, sentry_request_url).to_raise(exception)
        end

        it do
          expect { subject }
            .to raise_exception(Sentry::Client::Error, message)
        end
      end
    end
  end

  describe '#list_issues' do
    let(:issue_status) { 'unresolved' }
    let(:limit) { 20 }
    let(:sentry_api_response) { issues_sample_response }
    let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }

    let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }

    subject { client.list_issues(issue_status: issue_status, limit: limit) }

    it_behaves_like 'calls sentry api'

    it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
    it_behaves_like 'has correct length', 1

    shared_examples 'has correct external_url' do
      context 'external_url' do
        it 'is constructed correctly' do
          expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
        end
      end
    end

    context 'error object created from sentry response' do
      using RSpec::Parameterized::TableSyntax

      where(:error_object, :sentry_response) do
        :id           | :id
        :first_seen   | :firstSeen
        :last_seen    | :lastSeen
        :title        | :title
        :type         | :type
        :user_count   | :userCount
        :count        | :count
        :message      | [:metadata, :value]
        :culprit      | :culprit
        :short_id     | :shortId
        :status       | :status
        :frequency    | [:stats, '24h']
        :project_id   | [:project, :id]
        :project_name | [:project, :name]
        :project_slug | [:project, :slug]
      end

      with_them do
        it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
      end

      it_behaves_like 'has correct external_url'
    end

    context 'redirects' do
      let(:sentry_api_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }

      it_behaves_like 'no redirects'
    end

    # Sentry API returns 404 if there are extra slashes in the URL!
    context 'extra slashes in URL' do
      let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' }

      let(:sentry_request_url) do
        'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
          'issues/?limit=20&query=is:unresolved'
      end

      it 'removes extra slashes in api url' do
        expect(client.url).to eq(sentry_url)
        expect(Gitlab::HTTP).to receive(:get).with(
          URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'),
          anything
        ).and_call_original

        subject

        expect(sentry_api_request).to have_been_requested
      end
    end

    context 'Older sentry versions where keys are not present' do
      let(:sentry_api_response) do
        issues_sample_response[0...1].map do |issue|
          issue[:project].delete(:id)
          issue
        end
      end

      it_behaves_like 'calls sentry api'

      it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
      it_behaves_like 'has correct length', 1

      it_behaves_like 'has correct external_url'
    end

    context 'essential keys missing in API response' do
      let(:sentry_api_response) do
        issues_sample_response[0...1].map do |issue|
          issue.except(:id)
        end
      end

      it 'raises exception' do
        expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
      end
    end

    it_behaves_like 'maps exceptions'
  end

  describe '#list_projects' do
    let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' }

    let(:sentry_api_response) { projects_sample_response }

    let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: sentry_api_response) }

    subject { client.list_projects }

    it_behaves_like 'calls sentry api'

    it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
    it_behaves_like 'has correct length', 2

    context 'essential keys missing in API response' do
      let(:sentry_api_response) do
        projects_sample_response[0...1].map do |project|
          project.except(:slug)
        end
      end

      it 'raises exception' do
        expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"')
      end
    end

    context 'optional keys missing in sentry response' do
      let(:sentry_api_response) do
        projects_sample_response[0...1].map do |project|
          project[:organization].delete(:id)
          project.delete(:id)
          project.except(:status)
        end
      end

      it_behaves_like 'calls sentry api'

      it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
      it_behaves_like 'has correct length', 1
    end

    context 'error object created from sentry response' do
      using RSpec::Parameterized::TableSyntax

      where(:sentry_project_object, :sentry_response) do
        :id                | :id
        :name              | :name
        :status            | :status
        :slug              | :slug
        :organization_name | [:organization, :name]
        :organization_id   | [:organization, :id]
        :organization_slug | [:organization, :slug]
      end

      with_them do
        it do
          expect(subject[0].public_send(sentry_project_object)).to(
            eq(sentry_api_response[0].dig(*sentry_response))
          )
        end
      end
    end

    context 'redirects' do
      let(:sentry_api_url) { sentry_list_projects_url }

      it_behaves_like 'no redirects'
    end

    # Sentry API returns 404 if there are extra slashes in the URL!
    context 'extra slashes in URL' do
      let(:sentry_url) { 'https://sentrytest.gitlab.com/api//0/projects//' }
      let(:client) { described_class.new(sentry_url, token) }

      let!(:valid_req_stub) do
        stub_sentry_request(sentry_list_projects_url)
      end

      it 'removes extra slashes in api url' do
        expect(Gitlab::HTTP).to receive(:get).with(
          URI(sentry_list_projects_url),
          anything
        ).and_call_original

        subject

        expect(valid_req_stub).to have_been_requested
      end
    end

    context 'when exception is raised' do
      let(:sentry_request_url) { sentry_list_projects_url }

      it_behaves_like 'maps exceptions'
    end
  end

  private

  def stub_sentry_request(url, body: {}, status: 200, headers: {})
    stub_request(:get, url)
      .to_return(
        status: status,
        headers: { 'Content-Type' => 'application/json' }.merge(headers),
        body: body.to_json
      )
  end
end
