# frozen_string_literal: true

require 'spec_helper'

describe Projects::FeatureFlagsController do
  include Gitlab::Routing
  include FeatureFlagHelpers

  let_it_be(:project) { create(:project) }
  let(:user) { developer }
  let(:developer) { create(:user) }
  let(:reporter) { create(:user) }
  let(:feature_enabled) { true }

  before do
    project.add_developer(developer)
    project.add_reporter(reporter)

    sign_in(user)
    stub_licensed_features(feature_flags: feature_enabled)
  end

  describe 'GET index' do
    render_views

    subject { get(:index, params: view_params) }

    context 'when there is no feature flags' do
      before do
        subject
      end

      it 'renders page' do
        expect(response).to be_ok
      end
    end

    context 'for a list of feature flags' do
      let!(:feature_flags) { create_list(:operations_feature_flag, 50, project: project) }

      before do
        subject
      end

      it 'renders page' do
        expect(response).to have_gitlab_http_status(:ok)
      end
    end

    context 'when feature is not available' do
      let(:feature_enabled) { false }

      before do
        subject
      end

      it 'shows not found' do
        expect(subject).to have_gitlab_http_status(404)
      end
    end
  end

  describe 'GET #index.json' do
    subject { get(:index, params: view_params, format: :json) }

    let!(:feature_flag_active) do
      create(:operations_feature_flag, project: project, active: true)
    end

    let!(:feature_flag_inactive) do
      create(:operations_feature_flag, project: project, active: false)
    end

    it 'returns all feature flags as json response' do
      subject

      expect(json_response['feature_flags'].count).to eq(2)
      expect(json_response['feature_flags'].first['name']).to eq(feature_flag_active.name)
      expect(json_response['feature_flags'].second['name']).to eq(feature_flag_inactive.name)
    end

    it 'returns CRUD paths' do
      subject

      expected_edit_path = edit_project_feature_flag_path(project, feature_flag_active)
      expected_update_path = project_feature_flag_path(project, feature_flag_active)
      expected_destroy_path = project_feature_flag_path(project, feature_flag_active)

      feature_flag_json = json_response['feature_flags'].first

      expect(feature_flag_json['edit_path']).to eq(expected_edit_path)
      expect(feature_flag_json['update_path']).to eq(expected_update_path)
      expect(feature_flag_json['destroy_path']).to eq(expected_destroy_path)
    end

    it 'returns the summary of feature flags' do
      subject

      expect(json_response['count']['all']).to eq(2)
      expect(json_response['count']['enabled']).to eq(1)
      expect(json_response['count']['disabled']).to eq(1)
    end

    it 'matches json schema' do
      subject

      expect(response).to match_response_schema('feature_flags', dir: 'ee')
    end

    it 'returns false for active when the feature flag is inactive even if it has an active scope' do
      create(:operations_feature_flag_scope,
             feature_flag: feature_flag_inactive,
             environment_scope: 'production',
             active: true)

      subject

      expect(response).to have_gitlab_http_status(:ok)
      feature_flag_json = json_response['feature_flags'].second

      expect(feature_flag_json['active']).to eq(false)
    end

    context 'when scope is specified' do
      let(:view_params) do
        { namespace_id: project.namespace, project_id: project, scope: scope }
      end

      context 'when all feature flags are requested' do
        let(:scope) { 'all' }

        it 'returns all feature flags' do
          subject

          expect(json_response['feature_flags'].count).to eq(2)
        end
      end

      context 'when enabled feature flags are requested' do
        let(:scope) { 'enabled' }

        it 'returns enabled feature flags' do
          subject

          expect(json_response['feature_flags'].count).to eq(1)
          expect(json_response['feature_flags'].first['active']).to be_truthy
        end
      end

      context 'when disabled feature flags are requested' do
        let(:scope) { 'disabled' }

        it 'returns disabled feature flags' do
          subject

          expect(json_response['feature_flags'].count).to eq(1)
          expect(json_response['feature_flags'].first['active']).to be_falsy
        end
      end
    end

    context 'when feature flags have additional scopes' do
      let!(:feature_flag_active_scope) do
        create(:operations_feature_flag_scope,
               feature_flag: feature_flag_active,
               environment_scope: 'production',
               active: false)
      end

      let!(:feature_flag_inactive_scope) do
        create(:operations_feature_flag_scope,
               feature_flag: feature_flag_inactive,
               environment_scope: 'staging',
               active: false)
      end

      it 'returns a correct summary' do
        subject

        expect(json_response['count']['all']).to eq(2)
        expect(json_response['count']['enabled']).to eq(1)
        expect(json_response['count']['disabled']).to eq(1)
      end

      it 'recognizes feature flag 1 as active' do
        subject

        expect(json_response['feature_flags'].first['active']).to be_truthy
      end

      it 'recognizes feature flag 2 as inactive' do
        subject

        expect(json_response['feature_flags'].second['active']).to be_falsy
      end

      it 'has ordered scopes' do
        subject

        expect(json_response['feature_flags'][0]['scopes'][0]['id'])
          .to be < json_response['feature_flags'][0]['scopes'][1]['id']
        expect(json_response['feature_flags'][1]['scopes'][0]['id'])
          .to be < json_response['feature_flags'][1]['scopes'][1]['id']
      end

      it 'does not have N+1 problem' do
        recorded = ActiveRecord::QueryRecorder.new { subject }

        related_count = recorded.log
          .select { |query| query.include?('operations_feature_flag') }.count

        expect(related_count).to be_within(5).of(2)
      end
    end
  end

  describe 'GET new' do
    render_views

    subject { get(:new, params: view_params) }

    it 'renders the form' do
      subject

      expect(response).to be_ok
    end
  end

  describe 'GET #show.json' do
    subject { get(:show, params: params, format: :json) }

    let!(:feature_flag) do
      create(:operations_feature_flag, project: project)
    end

    let(:params) do
      {
        namespace_id: project.namespace,
        project_id: project,
        id: feature_flag.id
      }
    end

    it 'returns all feature flags as json response' do
      subject

      expect(json_response['name']).to eq(feature_flag.name)
      expect(json_response['active']).to eq(feature_flag.active)
    end

    it 'matches json schema' do
      subject

      expect(response).to match_response_schema('feature_flag', dir: 'ee')
    end

    context 'when feature flag is not found' do
      let!(:feature_flag) { }

      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: 1
        }
      end

      it 'returns 404' do
        subject

        expect(response).to have_gitlab_http_status(404)
      end
    end

    context 'when user is reporter' do
      let(:user) { reporter }

      it 'returns 404' do
        subject

        expect(response).to have_gitlab_http_status(404)
      end
    end

    context 'when feature flags have additional scopes' do
      context 'when there is at least one active scope' do
        let!(:feature_flag) do
          create(:operations_feature_flag, project: project, active: false)
        end

        let!(:feature_flag_scope_production) do
          create(:operations_feature_flag_scope,
                feature_flag: feature_flag,
                environment_scope: 'review/*',
                active: true)
        end

        it 'returns false for active' do
          subject

          expect(json_response['active']).to eq(false)
        end
      end

      context 'when all scopes are inactive' do
        let!(:feature_flag) do
          create(:operations_feature_flag, project: project, active: false)
        end

        let!(:feature_flag_scope_production) do
          create(:operations_feature_flag_scope,
                feature_flag: feature_flag,
                environment_scope: 'production',
                active: false)
        end

        it 'recognizes the feature flag as inactive' do
          subject

          expect(json_response['active']).to be_falsy
        end
      end
    end
  end

  describe 'POST create.json' do
    subject { post(:create, params: params, format: :json) }

    let(:params) do
      {
        namespace_id: project.namespace,
        project_id: project,
        operations_feature_flag: {
          name: 'my_feature_flag',
          active: true
        }
      }
    end

    it 'returns 200' do
      subject

      expect(response).to have_gitlab_http_status(200)
    end

    it 'creates a new feature flag' do
      subject

      expect(json_response['name']).to eq('my_feature_flag')
      expect(json_response['active']).to be_truthy
    end

    it 'creates a default scope' do
      subject

      expect(json_response['scopes'].count).to eq(1)
      expect(json_response['scopes'].first['environment_scope']).to eq('*')
      expect(json_response['scopes'].first['active']).to be_truthy
    end

    it 'matches json schema' do
      subject

      expect(response).to match_response_schema('feature_flag', dir: 'ee')
    end

    context 'when the same named feature flag has already existed' do
      before do
        create(:operations_feature_flag, name: 'my_feature_flag', project: project)
      end

      it 'returns 400' do
        subject

        expect(response).to have_gitlab_http_status(400)
      end

      it 'returns an error message' do
        subject

        expect(json_response['message']).to include('Name has already been taken')
      end
    end

    context 'without the active parameter' do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          operations_feature_flag: {
            name: 'my_feature_flag'
          }
        }
      end

      it 'creates a flag with active set to true' do
        expect { subject }.to change { Operations::FeatureFlag.count }.by(1)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('feature_flag', dir: 'ee')
        expect(json_response['active']).to eq(true)
        expect(Operations::FeatureFlag.last.active).to eq(true)
      end
    end

    context 'when user is reporter' do
      let(:user) { reporter }

      it 'returns 404' do
        subject

        expect(response).to have_gitlab_http_status(404)
      end
    end

    context 'when creates additional scope' do
      let(:params) do
        view_params.merge({
          operations_feature_flag: {
            name: 'my_feature_flag',
            active: true,
            scopes_attributes: [{ environment_scope: '*', active: true },
                                { environment_scope: 'production', active: false }]
          }
        })
      end

      it 'creates feature flag scopes successfully' do
        expect { subject }.to change { Operations::FeatureFlagScope.count }.by(2)

        expect(response).to have_gitlab_http_status(200)
      end

      it 'creates feature flag scopes in a correct order' do
        subject

        expect(json_response['scopes'].first['environment_scope']).to eq('*')
        expect(json_response['scopes'].second['environment_scope']).to eq('production')
      end

      context 'when default scope is not placed first' do
        let(:params) do
          view_params.merge({
            operations_feature_flag: {
              name: 'my_feature_flag',
              active: true,
              scopes_attributes: [{ environment_scope: 'production', active: false },
                                  { environment_scope: '*', active: true }]
            }
          })
        end

        it 'returns 400' do
          subject

          expect(response).to have_gitlab_http_status(400)
          expect(json_response['message'])
            .to include('Default scope has to be the first element')
        end
      end
    end

    context 'when creates additional scope with a percentage rollout' do
      it 'creates a strategy for the scope' do
        params = view_params.merge({
          operations_feature_flag: {
            name: 'my_feature_flag',
            active: true,
            scopes_attributes: [{ environment_scope: '*', active: true },
                                { environment_scope: 'production', active: false,
                                  strategies: [{ name: 'gradualRolloutUserId',
                                                 parameters: { groupId: 'default', percentage: '42' } }] }]
          }
        })

        post(:create, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        production_strategies_json = json_response['scopes'].second['strategies']
        expect(production_strategies_json).to eq([{
          'name' => 'gradualRolloutUserId',
          'parameters' => { "groupId" => "default", "percentage" => "42" }
        }])
      end
    end

    context 'when creates additional scope with a userWithId strategy' do
      it 'creates a strategy for the scope' do
        params = view_params.merge({
          operations_feature_flag: {
            name: 'my_feature_flag',
            active: true,
            scopes_attributes: [{ environment_scope: '*', active: true },
                                { environment_scope: 'production', active: false,
                                  strategies: [{ name: 'userWithId',
                                                 parameters: { userIds: '123,4,6722' } }] }]
          }
        })

        post(:create, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        production_strategies_json = json_response['scopes'].second['strategies']
        expect(production_strategies_json).to eq([{
          'name' => 'userWithId',
          'parameters' => { "userIds" => "123,4,6722" }
        }])
      end
    end

    context 'when creates an additional scope without a strategy' do
      it 'creates a default strategy' do
        params = view_params.merge({
          operations_feature_flag: {
            name: 'my_feature_flag',
            active: true,
            scopes_attributes: [{ environment_scope: '*', active: true }]
          }
        })

        post(:create, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        default_strategies_json = json_response['scopes'].first['strategies']
        expect(default_strategies_json).to eq([{ "name" => "default", "parameters" => {} }])
      end
    end
  end

  describe 'DELETE destroy.json' do
    subject { delete(:destroy, params: params, format: :json) }

    let!(:feature_flag) { create(:operations_feature_flag, project: project) }

    let(:params) do
      {
        namespace_id: project.namespace,
        project_id: project,
        id: feature_flag.id
      }
    end

    it 'returns 200' do
      subject

      expect(response).to have_gitlab_http_status(200)
    end

    it 'deletes one feature flag' do
      expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
    end

    it 'destroys the default scope' do
      expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-1)
    end

    it 'matches json schema' do
      subject

      expect(response).to match_response_schema('feature_flag', dir: 'ee')
    end

    context 'when user is reporter' do
      let(:user) { reporter }

      it 'returns 404' do
        subject

        expect(response).to have_gitlab_http_status(404)
      end
    end

    context 'when there is an additional scope' do
      let!(:scope) { create_scope(feature_flag, 'production', false) }

      it 'destroys the default scope and production scope' do
        expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-2)
      end
    end
  end

  describe 'PUT update.json' do
    subject { put(:update, params: params, format: :json) }

    let!(:feature_flag) do
      create(:operations_feature_flag,
        name: 'ci_live_trace',
        active: true,
        project: project)
    end

    let(:params) do
      {
        namespace_id: project.namespace,
        project_id: project,
        id: feature_flag.id,
        operations_feature_flag: {
          name: 'ci_new_live_trace'
        }
      }
    end

    it 'returns 200' do
      subject

      expect(response).to have_gitlab_http_status(200)
    end

    it 'updates the name of the feature flag name' do
      subject

      expect(json_response['name']).to eq('ci_new_live_trace')
    end

    it 'matches json schema' do
      subject

      expect(response).to match_response_schema('feature_flag', dir: 'ee')
    end

    context 'when updates active' do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            active: false
          }
        }
      end

      it 'updates active from true to false' do
        expect { subject }
          .to change { feature_flag.reload.active }.from(true).to(false)
      end

      it "does not change default scope's active" do
        expect { subject }
          .not_to change { feature_flag.default_scope.reload.active }.from(true)
      end

      it 'updates active from false to true when an inactive feature flag has an active scope' do
        feature_flag = create(:operations_feature_flag, project: project, name: 'my_flag', active: false)
        create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production', active: true)

        params = {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: { active: true }
        }
        put(:update, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('feature_flag', dir: 'ee')
        expect(json_response['active']).to eq(true)
        expect(feature_flag.reload.active).to eq(true)
        expect(feature_flag.default_scope.reload.active).to eq(false)
      end
    end

    context 'when user is reporter' do
      let(:user) { reporter }

      it 'returns 404' do
        subject

        expect(response).to have_gitlab_http_status(404)
      end
    end

    context "when creates an additional scope for production environment" do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [{ environment_scope: 'production', active: false }]
          }
        }
      end

      it 'creates a production scope' do
        expect { subject }.to change { feature_flag.reload.scopes.count }.by(1)

        expect(json_response['scopes'].last['environment_scope']).to eq('production')
        expect(json_response['scopes'].last['active']).to be_falsy
      end
    end

    context "when creates a default scope" do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [{ environment_scope: '*', active: false }]
          }
        }
      end

      it 'returns 400' do
        subject

        expect(response).to have_gitlab_http_status(400)
      end
    end

    context "when updates a default scope's active value" do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [
              {
                id: feature_flag.default_scope.id,
                environment_scope: '*',
                active: false
              }
            ]
          }
        }
      end

      it "updates successfully" do
        subject

        expect(json_response['scopes'].first['environment_scope']).to eq('*')
        expect(json_response['scopes'].first['active']).to be_falsy
      end
    end

    context "when changes default scope's spec" do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [
              {
                id: feature_flag.default_scope.id,
                environment_scope: 'review/*'
              }
            ]
          }
        }
      end

      it 'returns 400' do
        subject

        expect(response).to have_gitlab_http_status(400)
      end
    end

    context "when destroys the default scope" do
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [
              {
                id: feature_flag.default_scope.id,
                _destroy: 1
              }
            ]
          }
        }
      end

      it 'raises an error' do
        expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord)
      end
    end

    context "when destroys a production scope" do
      let!(:production_scope) { create_scope(feature_flag, 'production', true) }
      let(:params) do
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [
              {
                id: production_scope.id,
                _destroy: 1
              }
            ]
          }
        }
      end

      it 'destroys successfully' do
        subject

        scopes = json_response['scopes']
        expect(scopes.any? { |scope| scope['environment_scope'] == 'production' })
          .to be_falsy
      end
    end

    describe "updating the strategy" do
      def request_params(scope, strategies)
        {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [
              {
                id: scope.id,
                strategies: strategies
              }
            ]
          }
        }
      end

      it 'creates a default strategy' do
        scope = create_scope(feature_flag, 'production', true, [])
        params = request_params(scope, [{ name: 'default', parameters: {} }])

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "default",
          "parameters" => {}
        }])
      end

      it 'creates a gradualRolloutUserId strategy' do
        scope = create_scope(feature_flag, 'production', true, [])
        params = request_params(scope, [{ name: 'gradualRolloutUserId',
                                          parameters: { groupId: 'default', percentage: "70" } }])

        put(:update, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "gradualRolloutUserId",
          "parameters" => {
            "groupId" => "default",
            "percentage" => "70"
          }
        }])
      end

      it 'creates a userWithId strategy' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = request_params(scope, [{ name: 'userWithId', parameters: { userIds: 'sam,fred' } }])

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "userWithId",
          "parameters" => { "userIds" => "sam,fred" }
        }])
      end

      it 'updates an existing strategy' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = request_params(scope, [{ name: 'gradualRolloutUserId',
                                          parameters: { groupId: 'default', percentage: "50" } }])

        put(:update, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "gradualRolloutUserId",
          "parameters" => {
            "groupId" => "default",
            "percentage" => "50"
          }
        }])
      end

      it 'clears an existing strategy' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = request_params(scope, [])

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([])
      end

      it 'accepts multiple strategies' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = request_params(scope, [
          { name: 'gradualRolloutUserId', parameters: { groupId: 'mygroup', percentage: '55' } },
          { name: 'userWithId', parameters: { userIds: 'joe' } }
        ])

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies'].length).to eq(2)
        expect(scope_json['strategies']).to include({
          "name" => "gradualRolloutUserId",
          "parameters" => { "groupId" => "mygroup", "percentage" => "55" }
        })
        expect(scope_json['strategies']).to include({
          "name" => "userWithId",
          "parameters" => { "userIds" => "joe" }
        })
      end

      it 'does not modify strategies when there is no strategies key in the params' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [{ id: scope.id }]
          }
        }

        put(:update, params: params, format: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "default",
          "parameters" => {}
        }])
      end

      it 'leaves an existing strategy when there are no strategies in the params' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'gradualRolloutUserId',
                                                                  parameters: { groupId: 'default', percentage: '10' } }])
        params = {
          namespace_id: project.namespace,
          project_id: project,
          id: feature_flag.id,
          operations_feature_flag: {
            scopes_attributes: [{ id: scope.id }]
          }
        }

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:ok)
        scope_json = json_response['scopes'].select do |s|
          s['environment_scope'] == 'production'
        end.first
        expect(scope_json['strategies']).to eq([{
          "name" => "gradualRolloutUserId",
          "parameters" => { "groupId" => "default", "percentage" => "10" }
        }])
      end

      it 'does not accept extra parameters in the strategy params' do
        scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }])
        params = request_params(scope, [{ name: 'userWithId', parameters: { userIds: 'joe', groupId: 'default' } }])

        put(:update, params: params, format: :json, as: :json)

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']).to eq(["Scopes strategies parameters are invalid"])
      end
    end
  end

  private

  def view_params
    { namespace_id: project.namespace, project_id: project }
  end
end