# frozen_string_literal: true

require 'spec_helper'

RSpec.describe API::Unleash, feature_category: :feature_flags do
  include FeatureFlagHelpers

  let_it_be(:project, refind: true) { create(:project) }

  let(:project_id) { project.id }
  let(:params) {}
  let(:headers) {}

  shared_examples 'authenticated request' do
    context 'when using instance id' do
      let(:client) { create(:operations_feature_flags_client, project: project) }
      let(:params) { { instance_id: client.token } }

      it 'responds with OK' do
        subject

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

      context 'when repository is disabled' do
        before do
          project.project_feature.update!(
            repository_access_level: ::ProjectFeature::DISABLED,
            merge_requests_access_level: ::ProjectFeature::DISABLED,
            builds_access_level: ::ProjectFeature::DISABLED
          )
        end

        it 'responds with forbidden' do
          subject

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

      context 'when repository is private' do
        before do
          project.project_feature.update!(
            repository_access_level: ::ProjectFeature::PRIVATE,
            merge_requests_access_level: ::ProjectFeature::DISABLED,
            builds_access_level: ::ProjectFeature::DISABLED
          )
        end

        it 'responds with OK' do
          subject

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

    context 'when using header' do
      let(:client) { create(:operations_feature_flags_client, project: project) }
      let(:headers) { { "UNLEASH-INSTANCEID" => client.token } }

      it 'responds with OK' do
        subject

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

    context 'when using bogus instance id' do
      let(:params) { { instance_id: 'token' } }

      it 'responds with unauthorized' do
        subject

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

    context 'when using not existing project' do
      let(:project_id) { -5000 }
      let(:params) { { instance_id: 'token' } }

      it 'responds with unauthorized' do
        subject

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

  describe 'GET /feature_flags/unleash/:project_id/client/features', :use_clean_rails_redis_caching do
    specify do
      get api("/feature_flags/unleash/#{project_id}/client/features"), params: params, headers: headers

      is_expected.to have_request_urgency(:medium)
    end
  end

  %w[/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features].each do |features_endpoint|
    describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
      let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
      let(:client) { create(:operations_feature_flags_client, project: project) }

      subject { get api(features_url), params: params, headers: headers }

      it_behaves_like 'authenticated request'

      context 'when a client fetches feature flags several times' do
        let(:headers) { { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } }

        before do
          create_list(:operations_feature_flag, 3, project: project)
        end

        it 'serializes feature flags for the first time and read cached data from the second time' do
          expect(API::Entities::Unleash::ClientFeatureFlags)
            .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args)
            .once

          5.times { get api(features_url), params: params, headers: headers }
        end

        it 'increments the cache key when feature flags are modified' do
          expect(API::Entities::Unleash::ClientFeatureFlags)
            .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args)
            .twice

          2.times { get api(features_url), params: params, headers: headers }

          ::FeatureFlags::CreateService.new(project, project.owner, name: 'feature_flag').execute

          3.times { get api(features_url), params: params, headers: headers }
        end
      end

      context 'with version 2 feature flags' do
        it 'does not return a flag without any strategies' do
          create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to be_empty
        end

        it 'returns a flag with a default strategy' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
          create(:operations_scope, strategy: strategy, environment_scope: 'production')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'feature1',
            'enabled' => true,
            'strategies' => [{
              'name' => 'default',
              'parameters' => {}
            }]
          }])
        end

        it 'returns a flag with a userWithId strategy' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy = create(:operations_strategy,
            feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user123,user456' })
          create(:operations_scope, strategy: strategy, environment_scope: 'production')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'feature1',
            'enabled' => true,
            'strategies' => [{
              'name' => 'userWithId',
              'parameters' => { 'userIds' => 'user123,user456' }
            }]
          }])
        end

        it 'returns a flag with multiple strategies' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy_a = create(:operations_strategy,
            feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user_a,user_b' })
          strategy_b = create(:operations_strategy,
            feature_flag: feature_flag,
            name: 'gradualRolloutUserId',
            parameters: { groupId: 'default', percentage: '45' })
          create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
          create(:operations_scope, strategy: strategy_b, environment_scope: 'production')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature1'])
          features_json = json_response['features'].map do |feature|
            feature.merge(feature.slice('strategies').transform_values { |v| v.sort_by { |s| s['name'] } })
          end
          expect(features_json).to eq([{
            'name' => 'feature1',
            'enabled' => true,
            'strategies' => [{
              'name' => 'gradualRolloutUserId',
              'parameters' => { 'groupId' => 'default', 'percentage' => '45' }
            }, {
              'name' => 'userWithId',
              'parameters' => { 'userIds' => 'user_a,user_b' }
            }]
          }])
        end

        it 'returns only flags matching the environment scope' do
          feature_flag_a = create(:operations_feature_flag,
            project: project, name: 'feature1', active: true, version: 2)
          strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
          create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
          feature_flag_b = create(:operations_feature_flag,
            project: project, name: 'feature2', active: true, version: 2)
          strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
          create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features'].map { |f| f['name'] }.sort).to eq(['feature2'])
          expect(json_response['features']).to eq([{
            'name' => 'feature2',
            'enabled' => true,
            'strategies' => [{
              'name' => 'default',
              'parameters' => {}
            }]
          }])
        end

        it 'returns only strategies matching the environment scope' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy_a = create(:operations_strategy,
            feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
          create(:operations_scope, strategy: strategy_a, environment_scope: 'production')
          strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
          create(:operations_scope, strategy: strategy_b, environment_scope: 'staging')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'feature1',
            'enabled' => true,
            'strategies' => [{
              'name' => 'userWithId',
              'parameters' => { 'userIds' => 'user2,user8,user4' }
            }]
          }])
        end

        it 'returns only flags for the given project' do
          project_b = create(:project)
          feature_flag_a = create(:operations_feature_flag,
            project: project, name: 'feature_a', active: true, version: 2)
          strategy_a = create(:operations_strategy, feature_flag: feature_flag_a)
          create(:operations_scope, strategy: strategy_a, environment_scope: 'sandbox')
          feature_flag_b = create(:operations_feature_flag,
            project: project_b, name: 'feature_b', active: true, version: 2)
          strategy_b = create(:operations_strategy, feature_flag: feature_flag_b)
          create(:operations_scope, strategy: strategy_b, environment_scope: 'sandbox')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'sandbox' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'feature_a',
            'enabled' => true,
            'strategies' => [{
              'name' => 'default',
              'parameters' => {}
            }]
          }])
        end

        it 'returns all strategies with a matching scope' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy_a = create(:operations_strategy,
            feature_flag: feature_flag, name: 'userWithId', parameters: { userIds: 'user2,user8,user4' })
          create(:operations_scope, strategy: strategy_a, environment_scope: '*')
          strategy_b = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
          create(:operations_scope, strategy: strategy_b, environment_scope: 'review/*')
          strategy_c = create(:operations_strategy,
            feature_flag: feature_flag,
            name: 'gradualRolloutUserId',
            parameters: { groupId: 'default', percentage: '15' })
          create(:operations_scope, strategy: strategy_c, environment_scope: 'review/patch-1')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'review/patch-1' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features'].first['strategies'].sort_by { |s| s['name'] }).to eq([{
            'name' => 'default',
            'parameters' => {}
          }, {
            'name' => 'gradualRolloutUserId',
            'parameters' => { 'groupId' => 'default', 'percentage' => '15' }
          }, {
            'name' => 'userWithId',
            'parameters' => { 'userIds' => 'user2,user8,user4' }
          }])
        end

        it 'returns a strategy with more than one matching scope' do
          feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 2)
          strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
          create(:operations_scope, strategy: strategy, environment_scope: 'production')
          create(:operations_scope, strategy: strategy, environment_scope: '*')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'feature1',
            'enabled' => true,
            'strategies' => [{
              'name' => 'default',
              'parameters' => {}
            }]
          }])
        end

        it 'returns a disabled flag with a matching scope' do
          feature_flag = create(:operations_feature_flag,
            project: project, name: 'myfeature', active: false, version: 2)
          strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
          create(:operations_scope, strategy: strategy, environment_scope: 'production')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'myfeature',
            'enabled' => false,
            'strategies' => [{
              'name' => 'default',
              'parameters' => {}
            }]
          }])
        end

        it 'returns a userWithId strategy for a gitlabUserList strategy' do
          feature_flag = create(:operations_feature_flag, :new_version_flag,
            project: project, name: 'myfeature', active: true)
          user_list = create(:operations_feature_flag_user_list,
            project: project, name: 'My List', user_xids: 'user1,user2')
          strategy = create(:operations_strategy,
            feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list)
          create(:operations_scope, strategy: strategy, environment_scope: 'production')

          get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['features']).to eq([{
            'name' => 'myfeature',
            'enabled' => true,
            'strategies' => [{
              'name' => 'userWithId',
              'parameters' => { 'userIds' => 'user1,user2' }
            }]
          }])
        end
      end
    end
  end

  describe 'POST /feature_flags/unleash/:project_id/client/register' do
    subject { post api("/feature_flags/unleash/#{project_id}/client/register"), params: params, headers: headers }

    it_behaves_like 'authenticated request'
  end

  describe 'POST /feature_flags/unleash/:project_id/client/metrics' do
    subject { post api("/feature_flags/unleash/#{project_id}/client/metrics"), params: params, headers: headers }

    it_behaves_like 'authenticated request'
  end
end
