# frozen_string_literal: true

require "spec_helper"

RSpec.describe API::Integrations, feature_category: :integrations do
  include Integrations::TestHelpers

  let_it_be(:user) { create(:user) }
  let_it_be(:user2) { create(:user) }
  let_it_be(:project, reload: true) { create(:project, creator_id: user.id, namespace: user.namespace, owners: [user]) }
  let_it_be(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) }
  let_it_be(:group, reload: true) { create(:group, owners: user) }

  context "Project level integrations" do
    let_it_be(:available_integration_names) do
      excluded_integrations = [Integrations::GitlabSlackApplication.to_param, Integrations::Zentao.to_param]
      Integration.available_integration_names(include_instance_specific: false) - excluded_integrations
    end

    let_it_be(:project_integrations_map) do
      available_integration_names.index_with do |name|
        # Activate Confluence so it appears in API response
        traits = (name == 'confluence' ? [] : [:inactive])
        create(integration_factory(name), *traits, project: project)
      end
    end

    %w[integrations services].each do |endpoint|
      describe "GET /projects/:id/#{endpoint}" do
        it 'returns authentication error when unauthenticated' do
          get api("/projects/#{project.id}/#{endpoint}")

          expect(response).to have_gitlab_http_status(:unauthorized)
        end

        it "returns error when authenticated but user is not a project owner" do
          project.add_developer(user2)
          get api("/projects/#{project.id}/#{endpoint}", user2)

          expect(response).to have_gitlab_http_status(:forbidden)
        end

        it "returns a list of all active integrations" do
          get api("/projects/#{project.id}/#{endpoint}", user)

          aggregate_failures 'expect successful response with all active integrations' do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response).to be_an Array
            expect(json_response.count).to eq(2)
            expect(json_response.first['slug']).to eq('confluence')
            expect(response).to match_response_schema('public_api/v4/integrations')
          end
        end
      end

      where(:integration) do
        # The integrations API supports all project integrations.
        # You cannot create a GitLab for Slack app. You must install the app from the GitLab UI.
        unavailable_integration_names = [
          Integrations::GitlabSlackApplication.to_param,
          Integrations::JiraCloudApp.to_param,
          Integrations::Prometheus.to_param,
          Integrations::Zentao.to_param
        ]

        names = Integration.available_integration_names(include_instance_specific: false)
        names.reject { |name| unavailable_integration_names.include?(name) }
      end

      with_them do
        integration = params[:integration]

        describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'set up an integration', endpoint: endpoint, integration: integration, parent_resource_name: 'project' do
            let(:parent_resource) { project }
            let(:integrations_map) { project_integrations_map }
          end
        end

        describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'disable an integration', endpoint: endpoint, integration: integration, parent_resource_name: 'project' do
            let(:parent_resource) { project }
            let(:integrations_map) { project_integrations_map }
          end
        end

        describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'get an integration settings', endpoint: endpoint, integration: integration, parent_resource_name: 'project' do
            let(:parent_resource) { project }
            let(:integrations_map) { project_integrations_map }
          end
        end
      end

      describe "POST /projects/:id/#{endpoint}/:slug/trigger" do
        describe 'Mattermost integration' do
          let(:integration_name) { 'mattermost_slash_commands' }

          context 'when no integration is available' do
            it 'returns a not found message' do
              post api("/projects/#{project.id}/#{endpoint}/idonotexist/trigger")

              expect(response).to have_gitlab_http_status(:not_found)
              expect(json_response["error"]).to eq("404 Not Found")
            end
          end

          context 'when the integration exists' do
            let(:params) { { token: 'secrettoken' } }

            context 'when the integration is not active' do
              before do
                project_integrations_map[integration_name].deactivate!
              end

              it 'when the integration is inactive' do
                post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:not_found)
              end
            end

            context 'when the integration is active' do
              before do
                project_integrations_map[integration_name].activate!
              end

              it 'returns status 200' do
                post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:ok)
              end
            end

            context 'when the project can not be found' do
              it 'returns a generic 404' do
                post api("/projects/404/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:not_found)
                expect(json_response["message"]).to eq("404 Integration Not Found")
              end
            end
          end
        end

        describe 'Slack Integration' do
          let(:integration_name) { 'slack_slash_commands' }
          let(:params) { { token: 'secrettoken', text: 'help' } }

          context 'when no integration is available' do
            it 'returns a not found message' do
              post api("/projects/#{project.id}/#{endpoint}/idonotexist/trigger")

              expect(response).to have_gitlab_http_status(:not_found)
              expect(json_response["error"]).to eq("404 Not Found")
            end
          end

          context 'when the integration exists' do
            context 'when the integration is not active' do
              before do
                project_integrations_map[integration_name].deactivate!
              end

              it 'when the integration is inactive' do
                post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:not_found)
              end
            end

            context 'when the integration is active' do
              before do
                project_integrations_map[integration_name].activate!
              end

              it 'returns status 200' do
                post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:ok)
                expect(json_response['response_type']).to eq("ephemeral")
              end
            end

            context 'when the project can not be found' do
              it 'returns a generic 404' do
                post api("/projects/404/#{endpoint}/#{integration_name}/trigger"), params: params

                expect(response).to have_gitlab_http_status(:not_found)
                expect(json_response["message"]).to eq("404 Integration Not Found")
              end
            end
          end
        end
      end

      describe 'Mattermost integration' do
        let(:integration_name) { 'mattermost' }
        let(:params) do
          { webhook: 'https://hook.example.com', username: 'username' }
        end

        before do
          project_integrations_map[integration_name].activate!
        end

        it 'accepts a username for update' do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(username: 'new_username')

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']['username']).to eq('new_username')
        end
      end

      describe 'Microsoft Teams integration' do
        let_it_be(:group) { create(:group) }
        let(:integration_name) { 'microsoft-teams' }
        let(:params) do
          {
            webhook: 'https://hook.example.com',
            branches_to_be_notified: 'default',
            notify_only_broken_pipelines: false
          }
        end

        before do
          create(:microsoft_teams_integration, group: group, project: nil)
          project.update!(namespace: group)
          project_integrations_map[integration_name.underscore].activate!
        end

        it 'accepts branches_to_be_notified for update' do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user),
            params: params.merge(branches_to_be_notified: 'all')

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']['branches_to_be_notified']).to eq('all')
        end

        it 'accepts notify_only_broken_pipelines for update' do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user),
            params: params.merge(notify_only_broken_pipelines: true)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
        end

        it 'accepts `use_inherited_settings` for inheritance' do
          expect do
            put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user),
              params: params.merge(use_inherited_settings: true)
          end.to change { project_integrations_map[integration_name.underscore].reload.inherit_from_id }.from(nil)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['inherited']).to eq(true)
        end
      end

      describe 'Hangouts Chat integration' do
        let(:integration_name) { 'hangouts-chat' }
        let(:params) do
          {
            webhook: 'https://hook.example.com',
            branches_to_be_notified: 'default'
          }
        end

        before do
          project_integrations_map[integration_name.underscore].activate!
        end

        it 'accepts branches_to_be_notified for update', :aggregate_failures do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all')

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']['branches_to_be_notified']).to eq('all')
        end

        it 'only requires the webhook param' do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: { webhook: 'https://hook.example.com' }

          expect(response).to have_gitlab_http_status(:ok)
        end
      end

      describe 'Jira integration' do
        let(:integration_name) { 'jira' }
        let(:params) do
          { url: 'https://jira.example.com', username: 'username', password: 'password', jira_auth_type: 0 }
        end

        before do
          project_integrations_map[integration_name].properties = params
          project_integrations_map[integration_name].activate!
        end

        it 'returns the jira_issue_transition_id for get request' do
          get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']).to include('jira_issue_transition_id' => '56-1')
        end

        it 'returns the jira_issue_transition_id for put request' do
          put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(jira_issue_transition_id: '1')

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['properties']['jira_issue_transition_id']).to eq('1')
        end
      end

      describe 'Pipelines Email Integration' do
        let(:integration_name) { 'pipelines-email' }

        context 'notify_only_broken_pipelines property was saved as a string' do
          before do
            project_integrations_map[integration_name.underscore].activate!
          end

          it 'returns boolean values for notify_only_broken_pipelines' do
            get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user)

            expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
          end
        end
      end

      describe 'GitLab for Slack app integration' do
        before do
          stub_application_setting(slack_app_enabled: true)
          create(:gitlab_slack_application_integration, project: project)
        end

        describe "PUT /projects/:id/#{endpoint}/gitlab-slack-application" do
          context 'for integration creation' do
            before do
              project.gitlab_slack_application_integration.destroy!
            end

            it 'returns 422' do
              put api("/projects/#{project.id}/#{endpoint}/gitlab-slack-application", user)

              expect(response).to have_gitlab_http_status(:unprocessable_entity)
              expect(json_response['message']).to eq('You cannot create the GitLab for Slack app integration from the API')
            end
          end

          context 'for integration update' do
            before do
              project.gitlab_slack_application_integration.update!(active: false)
            end

            it "does not enable the integration" do
              put api("/projects/#{project.id}/#{endpoint}/gitlab-slack-application", user)

              expect(response).to have_gitlab_http_status(:ok)
              expect(project.gitlab_slack_application_integration.reload).to have_attributes(active: false)
            end
          end
        end

        describe "GET /projects/:id/#{endpoint}/gitlab-slack-application" do
          it "fetches the integration and returns the correct fields" do
            get api("/projects/#{project.id}/#{endpoint}/gitlab-slack-application", user)

            expect(response).to have_gitlab_http_status(:ok)
            assert_correct_response_fields(json_response['properties'].keys, project.gitlab_slack_application_integration)
          end
        end

        describe "DELETE /projects/:id/#{endpoint}/gitlab-slack-application" do
          it "disables the integration" do
            expect { delete api("/projects/#{project.id}/#{endpoint}/gitlab-slack-application", user) }
              .to change { project.gitlab_slack_application_integration.reload.activated? }.from(true).to(false)

            expect(response).to have_gitlab_http_status(:no_content)
          end
        end
      end

      describe 'GitLab for Jira Cloud app integration' do
        before do
          stub_application_setting(jira_connect_application_key: 'mock_key')
          create(:jira_cloud_app_integration, project: project)
        end

        describe "PUT /projects/:id/#{endpoint}/jira-cloud-app" do
          context 'for integration creation' do
            before do
              project.jira_cloud_app_integration.destroy!
            end

            it 'returns 422' do
              put api("/projects/#{project.id}/#{endpoint}/jira-cloud-app", user)

              expect(response).to have_gitlab_http_status(:unprocessable_entity)
              expect(json_response['message']).to eq('You cannot create the GitLab for Jira Cloud app integration from the API')
            end
          end

          context 'for integration update' do
            before do
              project.jira_cloud_app_integration.update!(active: false)
            end

            it "does not enable the integration" do
              put api("/projects/#{project.id}/#{endpoint}/jira-cloud-app", user)

              expect(response).to have_gitlab_http_status(:ok)
              expect(project.jira_cloud_app_integration.reload).to have_attributes(active: false)
            end
          end
        end

        describe "GET /projects/:id/#{endpoint}/jira-cloud-app" do
          it "fetches the integration and returns the correct fields" do
            get api("/projects/#{project.id}/#{endpoint}/jira-cloud-app", user)

            expect(response).to have_gitlab_http_status(:ok)
            assert_correct_response_fields(json_response['properties'].keys, project.jira_cloud_app_integration)
          end
        end

        describe "DELETE /projects/:id/#{endpoint}/jira-cloud-app" do
          it "does not disable the integration" do
            expect { delete api("/projects/#{project.id}/#{endpoint}/jira-cloud-app", user) }
              .not_to change { project.jira_cloud_app_integration.reload.activated? }.from(true)

            expect(response).to have_gitlab_http_status(:unprocessable_entity)
            expect(json_response['message']).to eq('You cannot disable the GitLab for Jira Cloud app integration from the API')
          end
        end
      end

      private

      def assert_correct_response_fields(response_keys, integration)
        assert_fields_match_integration(response_keys, integration)
        assert_secret_fields_filtered(response_keys, integration)
      end

      def assert_fields_match_integration(response_keys, integration)
        expect(response_keys).to match_array(integration.api_field_names)
      end

      def assert_secret_fields_filtered(response_keys, integration)
        expect(response_keys).not_to include(*integration.secret_fields) unless integration.secret_fields.empty?
      end
    end
  end

  context "Group level integrations" do
    let_it_be(:available_integration_names) do
      excluded_integrations = [Integrations::GitlabSlackApplication.to_param, Integrations::Zentao.to_param]
      Integration.available_integration_names(include_project_specific: false, include_instance_specific: false) - excluded_integrations
    end

    let_it_be(:group_integrations_map) do
      available_integration_names.index_with do |name|
        traits = (name == 'confluence' ? [] : [:inactive])
        create(integration_factory(name), *traits, :group, group: group)
      end
    end

    'integrations'.tap do |endpoint|
      describe "GET /groups/:id/#{endpoint}" do
        let_it_be(:project_only_integration) { Integration::PROJECT_LEVEL_ONLY_INTEGRATION_NAMES }

        it 'returns authentication error when unauthenticated' do
          get api("/groups/#{group.id}/#{endpoint}")

          expect(response).to have_gitlab_http_status(:unauthorized)
        end

        it "returns error when authenticated but user is not a group owner" do
          group.add_developer(user2)
          get api("/groups/#{group.id}/#{endpoint}", user2)

          expect(response).to have_gitlab_http_status(:forbidden)
        end

        it "returns a list of all active integrations" do
          get api("/groups/#{group.id}/#{endpoint}", user)

          aggregate_failures 'expect successful response with all active integrations' do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response).to be_an Array
            expect(json_response.count).to eq(2)
            expect(json_response.first['slug']).to eq('confluence')
            expect(response).to match_response_schema('public_api/v4/integrations')
          end
        end

        Integration::PROJECT_LEVEL_ONLY_INTEGRATION_NAMES.map(&:dasherize).each do |integration_name|
          it "returns 400 when trying to access a project-only integration" do
            get api("/groups/#{group.id}/integrations/#{integration_name}", user)

            expect(response).to have_gitlab_http_status(:bad_request)
            expect(json_response["message"]).to eq("400 Integration not available")
          end
        end
      end

      where(:integration) do
        # You cannot create a GitLab for Slack app. You must install the app from the GitLab UI.
        unavailable_integration_names = [
          Integrations::GitlabSlackApplication.to_param,
          Integrations::Prometheus.to_param,
          Integrations::Zentao.to_param
        ]

        names = Integration.available_integration_names(include_instance_specific: false, include_project_specific: false)
        names.reject { |name| unavailable_integration_names.include?(name) }
      end

      with_them do
        integration = params[:integration]

        describe "PUT /groups/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'set up an integration', endpoint: endpoint, integration: integration, parent_resource_name: 'group' do
            let(:parent_resource) { group }
            let(:integrations_map) { group_integrations_map }
          end
        end

        describe "DELETE /groups/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'disable an integration', endpoint: endpoint, integration: integration, parent_resource_name: 'group' do
            let(:parent_resource) { group }
            let(:integrations_map) { group_integrations_map }
          end
        end

        describe "GET /groups/:id/#{endpoint}/#{integration.dasherize}" do
          it_behaves_like 'get an integration settings', endpoint: endpoint, integration: integration, parent_resource_name: 'group' do
            let(:parent_resource) { group }
            let(:integrations_map) { group_integrations_map }
          end
        end
      end
    end
  end

  describe 'POST /slack/trigger' do
    before do
      stub_application_setting(slack_app_verification_token: 'token')
    end

    it 'returns status 200' do
      post api('/slack/trigger'), params: { token: 'token', text: 'help' }

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['response_type']).to eq("ephemeral")
    end

    it 'returns status 404 when token is invalid' do
      post api('/slack/trigger'), params: { token: 'invalid', text: 'foo' }

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['response_type']).to be_blank
    end
  end
end
