# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Ci::Config::Entry::Processable do
  let(:node_class) do
    Class.new(::Gitlab::Config::Entry::Node) do
      include Gitlab::Ci::Config::Entry::Processable

      entry :tags, ::Gitlab::Config::Entry::ArrayOfStrings,
        description: 'Set the default tags.',
        inherit: true

      def self.name
        'job'
      end
    end
  end

  let(:entry) { node_class.new(config, name: :rspec) }

  describe 'validations' do
    before do
      entry.compose!
    end

    context 'when entry config value is correct' do
      let(:config) { { stage: 'test' } }

      describe '#valid?' do
        it 'is valid' do
          expect(entry).to be_valid
        end
      end

      context 'when job name is empty' do
        let(:entry) { node_class.new(config, name: ''.to_sym) }

        it 'reports error' do
          expect(entry.errors).to include "job name can't be blank"
        end
      end
    end

    context 'when entry value is not correct' do
      context 'incorrect config value type' do
        let(:config) { ['incorrect'] }

        describe '#errors' do
          it 'reports error about a config type' do
            expect(entry.errors)
              .to include 'job config should be a hash'
          end
        end
      end

      context 'when config is empty' do
        let(:config) { {} }

        describe '#valid' do
          it 'is invalid' do
            expect(entry).not_to be_valid
          end
        end
      end

      context 'when extends key is not a string' do
        let(:config) { { extends: 123 } }

        it 'returns error about wrong value type' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include "job extends should be an array of strings or a string"
        end
      end

      context 'when it uses both "when:" and "rules:"' do
        let(:config) do
          {
            script: 'echo',
            when: 'on_failure',
            rules: [{ if: '$VARIABLE', when: 'on_success' }]
          }
        end

        it 'returns an error about when: being combined with rules' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include 'job config key may not be used with `rules`: when'
        end
      end

      context 'when only: is used with rules:' do
        let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }

        it 'returns error about mixing only: with rules:' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include /may not be used with `rules`/
        end

        context 'and only: is blank' do
          let(:config) { { only: nil, rules: [{ if: '$THIS' }] } }

          it 'returns error about mixing only: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end

        context 'and rules: is blank' do
          let(:config) { { only: ['merge_requests'], rules: nil } }

          it 'returns error about mixing only: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end
      end

      context 'when except: is used with rules:' do
        let(:config) { { except: { refs: %w[master] }, rules: [{ if: '$THIS' }] } }

        it 'returns error about mixing except: with rules:' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include /may not be used with `rules`/
        end

        context 'and except: is blank' do
          let(:config) { { except: nil, rules: [{ if: '$THIS' }] } }

          it 'returns error about mixing except: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end

        context 'and rules: is blank' do
          let(:config) { { except: { refs: %w[master] }, rules: nil } }

          it 'returns error about mixing except: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end
      end

      context 'when only: and except: are both used with rules:' do
        let(:config) do
          {
            only: %w[merge_requests],
            except: { refs: %w[master] },
            rules: [{ if: '$THIS' }]
          }
        end

        it 'returns errors about mixing both only: and except: with rules:' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include /may not be used with `rules`/
          expect(entry.errors).to include /may not be used with `rules`/
        end

        context 'when only: and except: as both blank' do
          let(:config) do
            { only: nil, except: nil, rules: [{ if: '$THIS' }] }
          end

          it 'returns errors about mixing both only: and except: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end

        context 'when rules: is blank' do
          let(:config) do
            { only: %w[merge_requests], except: { refs: %w[master] }, rules: nil }
          end

          it 'returns errors about mixing both only: and except: with rules:' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include /may not be used with `rules`/
            expect(entry.errors).to include /may not be used with `rules`/
          end
        end
      end
    end
  end

  describe '#relevant?' do
    it 'is a relevant entry' do
      entry = node_class.new({ stage: 'test' }, name: :rspec)

      expect(entry).to be_relevant
    end
  end

  describe '#compose!' do
    let(:unspecified) { double('unspecified', 'specified?' => false) }
    let(:default) { double('default', '[]' => unspecified) }
    let(:workflow) { double('workflow', 'has_rules?' => false) }
    let(:variables) { }

    let(:deps) do
      double('deps',
        default_entry: default,
        workflow_entry: workflow,
        variables_value: variables)
    end

    context 'with workflow rules' do
      using RSpec::Parameterized::TableSyntax

      where(:name, :has_workflow_rules?, :only, :rules, :result) do
        "uses default only"    | false | nil          | nil    | { refs: %w[branches tags] }
        "uses user only"       | false | %w[branches] | nil    | { refs: %w[branches] }
        "does not define only" | false | nil          | []     | nil
        "does not define only" | true  | nil          | nil    | nil
        "uses user only"       | true  | %w[branches] | nil    | { refs: %w[branches] }
        "does not define only" | true  | nil          | []     | nil
      end

      with_them do
        let(:config) { { script: 'ls', rules: rules, only: only }.compact }

        it "#{name}" do
          expect(workflow).to receive(:has_rules?) { has_workflow_rules? }

          entry.compose!(deps)

          expect(entry.only_value).to eq(result)
        end
      end
    end

    shared_examples 'has no warnings' do
      it 'does not raise the warning' do
        expect(entry.warnings).to be_empty
      end
    end

    context 'when workflow rules is used' do
      let(:workflow) { double('workflow', 'has_rules?' => true) }

      before do
        entry.compose!(deps)
      end

      context 'when rules are used' do
        let(:config) { { script: 'ls', cache: { key: 'test' }, rules: [] } }

        it 'does not define only' do
          expect(entry).not_to be_only_defined
        end
      end

      context 'when rules are not used and only is defined' do
        let(:config) { { script: 'ls', cache: { key: 'test' }, only: [] } }

        it 'keeps only entry' do
          expect(entry).to be_only_defined
        end
      end
    end

    context 'when workflow rules is not used' do
      let(:workflow) { double('workflow', 'has_rules?' => false) }
      let(:feature_flag_value) { true }

      before do
        stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: feature_flag_value)
        entry.compose!(deps)
      end

      context 'when rules are valid' do
        let(:config) do
          {
            script: 'ls',
            rules: [
              { if: '$CI_COMMIT_BRANCH', when: 'on_success' },
              last_rule
            ]
          }
        end

        context 'when last rule contains only `when`' do
          let(:last_rule) { { when: when_value } }

          context 'and its value is not `never`' do
            let(:when_value) { 'on_success' }

            it 'raises a warning' do
              expect(entry.warnings).to contain_exactly(/may allow multiple pipelines/)
            end

            context 'when feature flag is disabled' do
              let(:feature_flag_value) { false }

              it_behaves_like 'has no warnings'
            end
          end

          context 'and its value is `never`' do
            let(:when_value) { 'never' }

            it_behaves_like 'has no warnings'
          end
        end

        context 'when last rule does not contain only `when`' do
          let(:last_rule) { { if: '$CI_MERGE_REQUEST_ID', when: 'always' } }

          it_behaves_like 'has no warnings'
        end
      end

      context 'when rules are invalid' do
        let(:config) { { script: 'ls', rules: { when: 'always' } } }

        it_behaves_like 'has no warnings'
      end
    end

    context 'when workflow rules is used' do
      let(:workflow) { double('workflow', 'has_rules?' => true) }

      before do
        entry.compose!(deps)
      end

      context 'when last rule contains only `when' do
        let(:config) do
          {
            script: 'ls',
            rules: [
              { if: '$CI_COMMIT_BRANCH', when: 'on_success' },
              { when: 'always' }
            ]
          }
        end

        it_behaves_like 'has no warnings'
      end
    end

    context 'with inheritance' do
      context 'of variables' do
        let(:config) do
          { variables: { A: 'job', B: 'job' } }
        end

        before do
          entry.compose!(deps)
        end

        context 'with only job variables' do
          it 'does return defined variables' do
            expect(entry.value).to include(
              variables: { 'A' => 'job', 'B' => 'job' }
            )
          end
        end

        context 'when root yaml variables are used' do
          let(:variables) do
            Gitlab::Ci::Config::Entry::Variables.new(
              A: 'root', C: 'root', D: 'root'
            ).value
          end

          it 'does return all variables and overwrite them' do
            expect(entry.value).to include(
              variables: { 'A' => 'job', 'B' => 'job', 'C' => 'root', 'D' => 'root' }
            )
          end

          context 'when inherit of defaults is disabled' do
            let(:config) do
              {
                variables: { A: 'job', B: 'job' },
                inherit: { variables: false }
              }
            end

            it 'does return only job variables' do
              expect(entry.value).to include(
                variables: { 'A' => 'job', 'B' => 'job' }
              )
            end
          end

          context 'when inherit of only specific variable is enabled' do
            let(:config) do
              {
                variables: { A: 'job', B: 'job' },
                inherit: { variables: ['D'] }
              }
            end

            it 'does return only job variables' do
              expect(entry.value).to include(
                variables: { 'A' => 'job', 'B' => 'job', 'D' => 'root' }
              )
            end
          end
        end
      end

      context 'of default:tags' do
        using RSpec::Parameterized::TableSyntax

        where(:name, :default_tags, :tags, :inherit_default, :result) do
          "only local tags"       | nil     | %w[a b] | nil       | %w[a b]
          "only local tags"       | nil     | %w[a b] | true      | %w[a b]
          "only local tags"       | nil     | %w[a b] | false     | %w[a b]
          "global and local tags" | %w[b c] | %w[a b] | nil       | %w[a b]
          "global and local tags" | %w[b c] | %w[a b] | true      | %w[a b]
          "global and local tags" | %w[b c] | %w[a b] | false     | %w[a b]
          "only global tags"      | %w[b c] | nil     | nil       | %w[b c]
          "only global tags"      | %w[b c] | nil     | true      | %w[b c]
          "only global tags"      | %w[b c] | nil     | false     | nil
          "only global tags"      | %w[b c] | nil     | %w[image] | nil
          "only global tags"      | %w[b c] | nil     | %w[tags]  | %w[b c]
        end

        with_them do
          let(:config) do
            { tags: tags,
              inherit: { default: inherit_default } }
          end

          let(:default_specified_tags) do
            double('tags',
              'specified?' => true,
              'valid?' => true,
              'value' => default_tags,
              'errors' => [])
          end

          before do
            allow(default).to receive('[]').with(:tags).and_return(default_specified_tags)

            entry.compose!(deps)

            expect(entry).to be_valid
          end

          it { expect(entry.tags_value).to eq(result) }
        end
      end
    end
  end

  context 'when composed' do
    before do
      entry.compose!
    end

    describe '#value' do
      context 'when entry is correct' do
        let(:config) do
          { stage: 'test' }
        end

        it 'returns correct value' do
          expect(entry.value).to eq(
            name: :rspec,
            stage: 'test',
            only: { refs: %w[branches tags] },
            variables: {}
          )
        end
      end
    end
  end
end
