# frozen_string_literal: true

require 'spec_helper'

describe Gitlab::Ci::Config::Entry::Job do
  let(:entry) { described_class.new(config, name: :rspec) }

  describe '.nodes' do
    context 'when filtering all the entry/node names' do
      subject { described_class.nodes.keys }

      let(:result) do
        %i[before_script script stage type after_script cache
           image services only except rules variables artifacts
           environment coverage retry]
      end

      it { is_expected.to match_array result }
    end
  end

  describe '.matching?' do
    subject { described_class.matching?(name, config) }

    context 'when config is not a hash' do
      let(:name) { :rspec }
      let(:config) { 'string' }

      it { is_expected.to be_falsey }
    end

    context 'when config is a regular job' do
      let(:name) { :rspec }
      let(:config) do
        { script: 'ls -al' }
      end

      it { is_expected.to be_truthy }
    end

    context 'when config is a bridge job' do
      let(:name) { :rspec }
      let(:config) do
        { trigger: 'other-project' }
      end

      it { is_expected.to be_falsey }
    end

    context 'when config is a hidden job' do
      let(:name) { '.rspec' }
      let(:config) do
        { script: 'ls -al' }
      end

      it { is_expected.to be_falsey }
    end
  end

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

    context 'when entry config value is correct' do
      let(:config) { { script: 'rspec' } }

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

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

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

      context 'when delayed job' do
        context 'when start_in is specified' do
          let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } }

          it { expect(entry).to be_valid }
        end
      end

      context 'when has needs' do
        let(:config) do
          {
            stage: 'test',
            script: 'echo',
            needs: ['another-job']
          }
        end

        it { expect(entry).to be_valid }

        context 'when has dependencies' do
          let(:config) do
            {
              stage: 'test',
              script: 'echo',
              dependencies: ['another-job'],
              needs: ['another-job']
            }
          end

          it { expect(entry).to be_valid }
        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 unknown keys detected' do
        let(:config) { { unknown: true } }

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

      context 'when script is not provided' do
        let(:config) { { stage: 'test' } }

        it 'returns error about missing script entry' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include "job script can't be blank"
        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 parallel value is not correct' do
        context 'when it is not a numeric value' do
          let(:config) { { parallel: true } }

          it 'returns error about invalid type' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job parallel is not a number'
          end
        end

        context 'when it is lower than two' do
          let(:config) { { parallel: 1 } }

          it 'returns error about value too low' do
            expect(entry).not_to be_valid
            expect(entry.errors)
              .to include 'job parallel must be greater than or equal to 2'
          end
        end

        context 'when it is bigger than 50' do
          let(:config) { { parallel: 51 } }

          it 'returns error about value too high' do
            expect(entry).not_to be_valid
            expect(entry.errors)
              .to include 'job parallel must be less than or equal to 50'
          end
        end

        context 'when it is not an integer' do
          let(:config) { { parallel: 1.5 } }

          it 'returns error about wrong value' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job parallel must be an integer'
          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
      end

      context 'when delayed job' do
        context 'when start_in is specified' do
          let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } }

          it 'returns error about invalid type' do
            expect(entry).to be_valid
          end
        end

        context 'when start_in is empty' do
          let(:config) { { when: 'delayed', start_in: nil } }

          it 'returns error about invalid type' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job start in should be a duration'
          end
        end

        context 'when start_in is not formatted as a duration' do
          let(:config) { { when: 'delayed', start_in: 'test' } }

          it 'returns error about invalid type' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job start in should be a duration'
          end
        end

        context 'when start_in is longer than one day' do
          let(:config) { { when: 'delayed', start_in: '2 days' } }

          it 'returns error about exceeding the limit' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job start in should not exceed the limit'
          end
        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

      context 'when start_in specified without delayed specification' do
        let(:config) { { start_in: '1 day' } }

        it 'returns error about invalid type' do
          expect(entry).not_to be_valid
          expect(entry.errors).to include 'job start in must be blank'
        end
      end

      context 'when has dependencies' do
        context 'that are not a array of strings' do
          let(:config) do
            { script: 'echo', dependencies: 'build-job' }
          end

          it 'returns error about invalid type' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job dependencies should be an array of strings'
          end
        end
      end

      context 'when has needs' do
        context 'that are not a array of strings' do
          let(:config) do
            {
              stage: 'test',
              script: 'echo',
              needs: 'build-job'
            }
          end

          it 'returns error about invalid type' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job needs should be an array of strings'
          end
        end

        context 'when have dependencies that are not subset of needs' do
          let(:config) do
            {
              stage: 'test',
              script: 'echo',
              dependencies: ['another-job'],
              needs: ['build-job']
            }
          end

          it 'returns error about invalid data' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job dependencies the another-job should be part of needs'
          end
        end

        context 'when stage: is missing' do
          let(:config) do
            {
              script: 'echo',
              needs: ['build-job']
            }
          end

          it 'returns error about invalid data' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job config missing required keys: stage'
          end
        end
      end

      context 'when timeout value is not correct' do
        context 'when it is higher than instance wide timeout' do
          let(:config) { { timeout: '3 months' } }

          it 'returns error about value too high' do
            expect(entry).not_to be_valid
            expect(entry.errors)
              .to include "job timeout should not exceed the limit"
          end
        end

        context 'when it is not a duration' do
          let(:config) { { timeout: 100 } }

          it 'returns error about wrong value' do
            expect(entry).not_to be_valid
            expect(entry.errors).to include 'job timeout should be a duration'
          end
        end
      end

      context 'when timeout value is correct' do
        let(:config) { { script: 'echo', timeout: '1m 1s' } }

        it 'returns correct timeout' do
          expect(entry).to be_valid
          expect(entry.errors).to be_empty
          expect(entry.timeout).to eq('1m 1s')
        end
      end
    end
  end

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

      expect(entry).to be_relevant
    end
  end

  describe '#compose!' do
    let(:specified) do
      double('specified', 'specified?' => true, value: 'specified')
    end

    let(:unspecified) { double('unspecified', 'specified?' => false) }
    let(:default) { double('default', '[]' => unspecified) }
    let(:deps) { double('deps', 'default' => default, '[]' => unspecified) }

    context 'when job config overrides default config' do
      before do
        entry.compose!(deps)
      end

      let(:config) do
        { script: 'rspec', image: 'some_image', cache: { key: 'test' } }
      end

      it 'overrides default config' do
        expect(entry[:image].value).to eq(name: 'some_image')
        expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
      end
    end

    context 'when job config does not override default config' do
      before do
        allow(default).to receive('[]').with(:image).and_return(specified)

        entry.compose!(deps)
      end

      let(:config) { { script: 'ls', cache: { key: 'test' } } }

      it 'uses config from default entry' do
        expect(entry[:image].value).to eq 'specified'
        expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
      end
    end
  end

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

    describe '#value' do
      before do
        entry.compose!
      end

      context 'when entry is correct' do
        let(:config) do
          { before_script: %w[ls pwd],
            script: 'rspec',
            after_script: %w[cleanup] }
        end

        it 'returns correct value' do
          expect(entry.value)
            .to eq(name: :rspec,
                   before_script: %w[ls pwd],
                   script: %w[rspec],
                   stage: 'test',
                   ignore: false,
                   after_script: %w[cleanup],
                   only: { refs: %w[branches tags] },
                   variables: {})
        end
      end
    end
  end

  describe '#manual_action?' do
    context 'when job is a manual action' do
      let(:config) { { script: 'deploy', when: 'manual' } }

      it 'is a manual action' do
        expect(entry).to be_manual_action
      end
    end

    context 'when job is not a manual action' do
      let(:config) { { script: 'deploy' } }

      it 'is not a manual action' do
        expect(entry).not_to be_manual_action
      end
    end
  end

  describe '#delayed?' do
    context 'when job is a delayed' do
      let(:config) { { script: 'deploy', when: 'delayed' } }

      it 'is a delayed' do
        expect(entry).to be_delayed
      end
    end

    context 'when job is not a delayed' do
      let(:config) { { script: 'deploy' } }

      it 'is not a delayed' do
        expect(entry).not_to be_delayed
      end
    end
  end

  describe '#ignored?' do
    context 'when job is a manual action' do
      context 'when it is not specified if job is allowed to fail' do
        let(:config) do
          { script: 'deploy', when: 'manual' }
        end

        it 'is an ignored job' do
          expect(entry).to be_ignored
        end
      end

      context 'when job is allowed to fail' do
        let(:config) do
          { script: 'deploy', when: 'manual', allow_failure: true }
        end

        it 'is an ignored job' do
          expect(entry).to be_ignored
        end
      end

      context 'when job is not allowed to fail' do
        let(:config) do
          { script: 'deploy', when: 'manual', allow_failure: false }
        end

        it 'is not an ignored job' do
          expect(entry).not_to be_ignored
        end
      end
    end

    context 'when job is not a manual action' do
      context 'when it is not specified if job is allowed to fail' do
        let(:config) { { script: 'deploy' } }

        it 'is not an ignored job' do
          expect(entry).not_to be_ignored
        end
      end

      context 'when job is allowed to fail' do
        let(:config) { { script: 'deploy', allow_failure: true } }

        it 'is an ignored job' do
          expect(entry).to be_ignored
        end
      end

      context 'when job is not allowed to fail' do
        let(:config) { { script: 'deploy', allow_failure: false } }

        it 'is not an ignored job' do
          expect(entry).not_to be_ignored
        end
      end
    end
  end
end
