require 'spec_helper'

module Ci
  describe GitlabCiYamlProcessor, :lib do
    subject { described_class.new(config, path) }
    let(:path) { 'path' }

    describe 'our current .gitlab-ci.yml' do
      let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }

      it 'is valid' do
        error_message = described_class.validation_message(config)

        expect(error_message).to be_nil
      end
    end

    describe '#build_attributes' do
      subject { described_class.new(config, path).build_attributes(:rspec) }

      describe 'coverage entry' do
        describe 'code coverage regexp' do
          let(:config) do
            YAML.dump(rspec: { script: 'rspec',
                               coverage: '/Code coverage: \d+\.\d+/' })
          end

          it 'includes coverage regexp in build attributes' do
            expect(subject)
              .to include(coverage_regex: 'Code coverage: \d+\.\d+')
          end
        end
      end

      describe 'retry entry' do
        context 'when retry count is specified' do
          let(:config) do
            YAML.dump(rspec: { script: 'rspec', retry: 3 })
          end

          it 'includes retry count in build options attribute' do
            expect(subject[:options]).to include(retry: 3)
          end
        end

        context 'when retry count is not specified' do
          let(:config) do
            YAML.dump(rspec: { script: 'rspec' })
          end

          it 'does not persist retry count in the database' do
            expect(subject[:options]).not_to have_key(:retry)
          end
        end
      end

      describe 'allow failure entry' do
        context 'when job is a manual action' do
          context 'when allow_failure is defined' do
            let(:config) do
              YAML.dump(rspec: { script: 'rspec',
                                 when: 'manual',
                                 allow_failure: false })
            end

            it 'is not allowed to fail' do
              expect(subject[:allow_failure]).to be false
            end
          end

          context 'when allow_failure is not defined' do
            let(:config) do
              YAML.dump(rspec: { script: 'rspec',
                                 when: 'manual' })
            end

            it 'is allowed to fail' do
              expect(subject[:allow_failure]).to be true
            end
          end
        end

        context 'when job is not a manual action' do
          context 'when allow_failure is defined' do
            let(:config) do
              YAML.dump(rspec: { script: 'rspec',
                                 allow_failure: false })
            end

            it 'is not allowed to fail' do
              expect(subject[:allow_failure]).to be false
            end
          end

          context 'when allow_failure is not defined' do
            let(:config) do
              YAML.dump(rspec: { script: 'rspec' })
            end

            it 'is not allowed to fail' do
              expect(subject[:allow_failure]).to be false
            end
          end
        end
      end
    end

    describe '#stage_seeds' do
      context 'when no refs policy is specified' do
        let(:config) do
          YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
                    rspec: { stage: 'test', script: 'rspec' },
                    spinach: { stage: 'test', script: 'spinach' })
        end

        let(:pipeline) { create(:ci_empty_pipeline) }

        it 'correctly fabricates a stage seeds object' do
          seeds = subject.stage_seeds(pipeline)

          expect(seeds.size).to eq 2
          expect(seeds.first.stage[:name]).to eq 'test'
          expect(seeds.second.stage[:name]).to eq 'deploy'
          expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
          expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
          expect(seeds.second.builds.dig(0, :name)).to eq 'production'
        end
      end

      context 'when refs policy is specified' do
        let(:config) do
          YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
                    spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
        end

        let(:pipeline) do
          create(:ci_empty_pipeline, ref: 'feature', tag: true)
        end

        it 'returns stage seeds only assigned to master to master' do
          seeds = subject.stage_seeds(pipeline)

          expect(seeds.size).to eq 1
          expect(seeds.first.stage[:name]).to eq 'test'
          expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
        end
      end

      context 'when source policy is specified' do
        let(:config) do
          YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
                    spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
        end

        let(:pipeline) do
          create(:ci_empty_pipeline, source: :schedule)
        end

        it 'returns stage seeds only assigned to schedules' do
          seeds = subject.stage_seeds(pipeline)

          expect(seeds.size).to eq 1
          expect(seeds.first.stage[:name]).to eq 'test'
          expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
        end
      end
    end

    describe "#builds_for_ref" do
      let(:type) { 'test' }

      it "returns builds if no branch specified" do
        config = YAML.dump({
          before_script: ["pwd"],
          rspec: { script: "rspec" }
        })

        config_processor = GitlabCiYamlProcessor.new(config, path)

        expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
        expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
          stage: "test",
          stage_idx: 1,
          name: "rspec",
          commands: "pwd\nrspec",
          coverage_regex: nil,
          tag_list: [],
          options: {
            before_script: ["pwd"],
            script: ["rspec"]
          },
          allow_failure: false,
          when: "on_success",
          environment: nil,
          yaml_variables: []
        })
      end

      describe 'only' do
        it "does not return builds if only has another branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", only: ["deploy"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
        end

        it "does not return builds if only has regexp with another branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", only: ["/^deploy$/"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
        end

        it "returns builds if only has specified this branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", only: ["master"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
        end

        it "returns builds if only has a list of branches including specified" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, only: %w(master deploy) }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
        end

        it "returns builds if only has a branches keyword specified" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, only: ["branches"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
        end

        it "does not return builds if only has a tags keyword" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, only: ["tags"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
        end

        it "returns builds if only has special keywords specified and source matches" do
          possibilities = [{ keyword: 'pushes', source: 'push' },
                           { keyword: 'web', source: 'web' },
                           { keyword: 'triggers', source: 'trigger' },
                           { keyword: 'schedules', source: 'schedule' },
                           { keyword: 'api', source: 'api' },
                           { keyword: 'external', source: 'external' }]

          possibilities.each do |possibility|
            config = YAML.dump({
                                 before_script: ["pwd"],
                                 rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
                               })

            config_processor = GitlabCiYamlProcessor.new(config, path)

            expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
          end
        end

        it "does not return builds if only has special keywords specified and source doesn't match" do
          possibilities = [{ keyword: 'pushes', source: 'web' },
                           { keyword: 'web', source: 'push' },
                           { keyword: 'triggers', source: 'schedule' },
                           { keyword: 'schedules', source: 'external' },
                           { keyword: 'api', source: 'trigger' },
                           { keyword: 'external', source: 'api' }]

          possibilities.each do |possibility|
            config = YAML.dump({
                                 before_script: ["pwd"],
                                 rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
                               })

            config_processor = GitlabCiYamlProcessor.new(config, path)

            expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
          end
        end

        it "returns builds if only has current repository path" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, only: ["branches@path"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
        end

        it "does not return builds if only has different repository path" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, only: ["branches@fork"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
        end

        it "returns build only for specified type" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: "test", only: %w(master deploy) },
                               staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
                               production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, 'fork')

          expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
          expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
          expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
        end

        context 'for invalid value' do
          let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
          let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }

          shared_examples 'raises an error' do
            it do
              expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps')
            end
          end

          context 'when it is integer' do
            let(:only) { 1 }

            it_behaves_like 'raises an error'
          end

          context 'when it is an array of integers' do
            let(:only) { [1, 1] }

            it_behaves_like 'raises an error'
          end

          context 'when it is invalid regex' do
            let(:only) { ["/*invalid/"] }

            it_behaves_like 'raises an error'
          end
        end
      end

      describe 'except' do
        it "returns builds if except has another branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", except: ["deploy"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
        end

        it "returns builds if except has regexp with another branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", except: ["/^deploy$/"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
        end

        it "does not return builds if except has specified this branch" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", except: ["master"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
        end

        it "does not return builds if except has a list of branches including specified" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, except: %w(master deploy) }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
        end

        it "does not return builds if except has a branches keyword specified" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, except: ["branches"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
        end

        it "returns builds if except has a tags keyword" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, except: ["tags"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
        end

        it "does not return builds if except has special keywords specified and source matches" do
          possibilities = [{ keyword: 'pushes', source: 'push' },
                           { keyword: 'web', source: 'web' },
                           { keyword: 'triggers', source: 'trigger' },
                           { keyword: 'schedules', source: 'schedule' },
                           { keyword: 'api', source: 'api' },
                           { keyword: 'external', source: 'external' }]

          possibilities.each do |possibility|
            config = YAML.dump({
                                 before_script: ["pwd"],
                                 rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
                               })

            config_processor = GitlabCiYamlProcessor.new(config, path)

            expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
          end
        end

        it "returns builds if except has special keywords specified and source doesn't match" do
          possibilities = [{ keyword: 'pushes', source: 'web' },
                           { keyword: 'web', source: 'push' },
                           { keyword: 'triggers', source: 'schedule' },
                           { keyword: 'schedules', source: 'external' },
                           { keyword: 'api', source: 'trigger' },
                           { keyword: 'external', source: 'api' }]

          possibilities.each do |possibility|
            config = YAML.dump({
                                 before_script: ["pwd"],
                                 rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
                               })

            config_processor = GitlabCiYamlProcessor.new(config, path)

            expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
          end
        end

        it "does not return builds if except has current repository path" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, except: ["branches@path"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
        end

        it "returns builds if except has different repository path" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: type, except: ["branches@fork"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
        end

        it "returns build except specified type" do
          config = YAML.dump({
                               before_script: ["pwd"],
                               rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
                               staging: { script: "deploy", type: "deploy", except: ["master"] },
                               production: { script: "deploy", type: "deploy", except: ["master@fork"] }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, 'fork')

          expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
          expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
          expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
        end

        context 'for invalid value' do
          let(:config) { { rspec: { script: "rspec", except: except } } }
          let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }

          shared_examples 'raises an error' do
            it do
              expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps')
            end
          end

          context 'when it is integer' do
            let(:except) { 1 }

            it_behaves_like 'raises an error'
          end

          context 'when it is an array of integers' do
            let(:except) { [1, 1] }

            it_behaves_like 'raises an error'
          end

          context 'when it is invalid regex' do
            let(:except) { ["/*invalid/"] }

            it_behaves_like 'raises an error'
          end
        end
      end
    end

    describe "Scripts handling" do
      let(:config_data) { YAML.dump(config) }
      let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }

      subject { config_processor.builds_for_stage_and_ref("test", "master").first }

      describe "before_script" do
        context "in global context" do
          let(:config) do
            {
              before_script: ["global script"],
              test: { script: ["script"] }
            }
          end

          it "return commands with scripts concencaced" do
            expect(subject[:commands]).to eq("global script\nscript")
          end
        end

        context "overwritten in local context" do
          let(:config) do
            {
              before_script: ["global script"],
              test: { before_script: ["local script"], script: ["script"] }
            }
          end

          it "return commands with scripts concencaced" do
            expect(subject[:commands]).to eq("local script\nscript")
          end
        end
      end

      describe "script" do
        let(:config) do
          {
            test: { script: ["script"] }
          }
        end

        it "return commands with scripts concencaced" do
          expect(subject[:commands]).to eq("script")
        end
      end

      describe "after_script" do
        context "in global context" do
          let(:config) do
            {
              after_script: ["after_script"],
              test: { script: ["script"] }
            }
          end

          it "return after_script in options" do
            expect(subject[:options][:after_script]).to eq(["after_script"])
          end
        end

        context "overwritten in local context" do
          let(:config) do
            {
              after_script: ["local after_script"],
              test: { after_script: ["local after_script"], script: ["script"] }
            }
          end

          it "return after_script in options" do
            expect(subject[:options][:after_script]).to eq(["local after_script"])
          end
        end
      end
    end

    describe "Image and service handling" do
      context "when extended docker configuration is used" do
        it "returns image and service when defined" do
          config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
                               services: ["mysql", { name: "docker:dind", alias: "docker",
                                                     entrypoint: ["/usr/local/bin/init", "run"],
                                                     command: ["/usr/local/bin/init", "run"] }],
                               before_script: ["pwd"],
                               rspec: { script: "rspec" } })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
          expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
            stage: "test",
            stage_idx: 1,
            name: "rspec",
            commands: "pwd\nrspec",
            coverage_regex: nil,
            tag_list: [],
            options: {
              before_script: ["pwd"],
              script: ["rspec"],
              image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
              services: [{ name: "mysql" },
                         { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
                           command: ["/usr/local/bin/init", "run"] }]
            },
            allow_failure: false,
            when: "on_success",
            environment: nil,
            yaml_variables: []
          })
        end

        it "returns image and service when overridden for job" do
          config = YAML.dump({ image: "ruby:2.1",
                               services: ["mysql"],
                               before_script: ["pwd"],
                               rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
                                        services: [{ name: "postgresql", alias: "db-pg",
                                                     entrypoint: ["/usr/local/bin/init", "run"],
                                                     command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
                                        script: "rspec" } })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
          expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
            stage: "test",
            stage_idx: 1,
            name: "rspec",
            commands: "pwd\nrspec",
            coverage_regex: nil,
            tag_list: [],
            options: {
              before_script: ["pwd"],
              script: ["rspec"],
              image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
              services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
                           command: ["/usr/local/bin/init", "run"] },
                         { name: "docker:dind" }]
            },
            allow_failure: false,
            when: "on_success",
            environment: nil,
            yaml_variables: []
          })
        end
      end

      context "when etended docker configuration is not used" do
        it "returns image and service when defined" do
          config = YAML.dump({ image: "ruby:2.1",
                               services: ["mysql", "docker:dind"],
                               before_script: ["pwd"],
                               rspec: { script: "rspec" } })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
          expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
            stage: "test",
            stage_idx: 1,
            name: "rspec",
            commands: "pwd\nrspec",
            coverage_regex: nil,
            tag_list: [],
            options: {
              before_script: ["pwd"],
              script: ["rspec"],
              image: { name: "ruby:2.1" },
              services: [{ name: "mysql" }, { name: "docker:dind" }]
            },
            allow_failure: false,
            when: "on_success",
            environment: nil,
            yaml_variables: []
          })
        end

        it "returns image and service when overridden for job" do
          config = YAML.dump({ image: "ruby:2.1",
                               services: ["mysql"],
                               before_script: ["pwd"],
                               rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
          expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
            stage: "test",
            stage_idx: 1,
            name: "rspec",
            commands: "pwd\nrspec",
            coverage_regex: nil,
            tag_list: [],
            options: {
              before_script: ["pwd"],
              script: ["rspec"],
              image: { name: "ruby:2.5" },
              services: [{ name: "postgresql" }, { name: "docker:dind" }]
            },
            allow_failure: false,
            when: "on_success",
            environment: nil,
            yaml_variables: []
          })
        end
      end
    end

    describe 'Variables' do
      let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }

      subject { config_processor.builds.first[:yaml_variables] }

      context 'when global variables are defined' do
        let(:variables) do
          { 'VAR1' => 'value1', 'VAR2' => 'value2' }
        end
        let(:config) do
          {
            variables: variables,
            before_script: ['pwd'],
            rspec: { script: 'rspec' }
          }
        end

        it 'returns global variables' do
          expect(subject).to contain_exactly(
            { key: 'VAR1', value: 'value1', public: true },
            { key: 'VAR2', value: 'value2', public: true }
          )
        end
      end

      context 'when job and global variables are defined' do
        let(:global_variables) do
          { 'VAR1' => 'global1', 'VAR3' => 'global3' }
        end
        let(:job_variables) do
          { 'VAR1' => 'value1', 'VAR2' => 'value2' }
        end
        let(:config) do
          {
            before_script: ['pwd'],
            variables: global_variables,
            rspec: { script: 'rspec', variables: job_variables }
          }
        end

        it 'returns all unique variables' do
          expect(subject).to contain_exactly(
            { key: 'VAR3', value: 'global3', public: true },
            { key: 'VAR1', value: 'value1', public: true },
            { key: 'VAR2', value: 'value2', public: true }
          )
        end
      end

      context 'when job variables are defined' do
        let(:config) do
          {
            before_script: ['pwd'],
            rspec: { script: 'rspec', variables: variables }
          }
        end

        context 'when syntax is correct' do
          let(:variables) do
            { 'VAR1' => 'value1', 'VAR2' => 'value2' }
          end

          it 'returns job variables' do
            expect(subject).to contain_exactly(
              { key: 'VAR1', value: 'value1', public: true },
              { key: 'VAR2', value: 'value2', public: true }
            )
          end
        end

        context 'when syntax is incorrect' do
          context 'when variables defined but invalid' do
            let(:variables) do
              %w(VAR1 value1 VAR2 value2)
            end

            it 'raises error' do
              expect { subject }
                .to raise_error(GitlabCiYamlProcessor::ValidationError,
                                 /jobs:rspec:variables config should be a hash of key value pairs/)
            end
          end

          context 'when variables key defined but value not specified' do
            let(:variables) do
              nil
            end

            it 'returns empty array' do
              ##
              # When variables config is empty, we assume this is a valid
              # configuration, see issue #18775
              #
              expect(subject).to be_an_instance_of(Array)
              expect(subject).to be_empty
            end
          end
        end
      end

      context 'when job variables are not defined' do
        let(:config) do
          {
            before_script: ['pwd'],
            rspec: { script: 'rspec' }
          }
        end

        it 'returns empty array' do
          expect(subject).to be_an_instance_of(Array)
          expect(subject).to be_empty
        end
      end
    end

    describe "When" do
      %w(on_success on_failure always).each do |when_state|
        it "returns #{when_state} when defined" do
          config = YAML.dump({
                               rspec: { script: "rspec", when: when_state }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          builds = config_processor.builds_for_stage_and_ref("test", "master")
          expect(builds.size).to eq(1)
          expect(builds.first[:when]).to eq(when_state)
        end
      end
    end

    describe 'cache' do
      context 'when cache definition has unknown keys' do
        it 'raises relevant validation error' do
          config = YAML.dump(
            { cache: { untracked: true, invalid: 'key' },
              rspec: { script: 'rspec' } })

          expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
            GitlabCiYamlProcessor::ValidationError,
            'cache config contains unknown keys: invalid'
          )
        end
      end

      it "returns cache when defined globally" do
        config = YAML.dump({
                             cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
                             rspec: {
                               script: "rspec"
                             }
                           })

        config_processor = GitlabCiYamlProcessor.new(config)

        expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
        expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
          paths: ["logs/", "binaries/"],
          untracked: true,
          key: 'key',
          policy: 'pull-push'
        )
      end

      it "returns cache when defined in a job" do
        config = YAML.dump({
                             rspec: {
                               cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
                               script: "rspec"
                             }
                           })

        config_processor = GitlabCiYamlProcessor.new(config)

        expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
        expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
          paths: ["logs/", "binaries/"],
          untracked: true,
          key: 'key',
          policy: 'pull-push'
        )
      end

      it "overwrite cache when defined for a job and globally" do
        config = YAML.dump({
                             cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
                             rspec: {
                               script: "rspec",
                               cache: { paths: ["test/"], untracked: false, key: 'local' }
                             }
                           })

        config_processor = GitlabCiYamlProcessor.new(config)

        expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
        expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
          paths: ["test/"],
          untracked: false,
          key: 'local',
          policy: 'pull-push'
        )
      end
    end

    describe "Artifacts" do
      it "returns artifacts when defined" do
        config = YAML.dump({
                             image:         "ruby:2.1",
                             services:      ["mysql"],
                             before_script: ["pwd"],
                             rspec:         {
                               artifacts: {
                                 paths: ["logs/", "binaries/"],
                                 untracked: true,
                                 name: "custom_name",
                                 expire_in: "7d"
                               },
                               script: "rspec"
                             }
                           })

        config_processor = GitlabCiYamlProcessor.new(config)

        expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
        expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
          stage: "test",
          stage_idx: 1,
          name: "rspec",
          commands: "pwd\nrspec",
          coverage_regex: nil,
          tag_list: [],
          options: {
            before_script: ["pwd"],
            script: ["rspec"],
            image: { name: "ruby:2.1" },
            services: [{ name: "mysql" }],
            artifacts: {
              name: "custom_name",
              paths: ["logs/", "binaries/"],
              untracked: true,
              expire_in: "7d"
            }
          },
          when: "on_success",
          allow_failure: false,
          environment: nil,
          yaml_variables: []
        })
      end

      %w[on_success on_failure always].each do |when_state|
        it "returns artifacts for when #{when_state}  defined" do
          config = YAML.dump({
                               rspec: {
                                 script: "rspec",
                                 artifacts: { paths: ["logs/", "binaries/"], when: when_state }
                               }
                             })

          config_processor = GitlabCiYamlProcessor.new(config, path)

          builds = config_processor.builds_for_stage_and_ref("test", "master")
          expect(builds.size).to eq(1)
          expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
        end
      end
    end

    describe '#environment' do
      let(:config) do
        {
          deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
        }
      end

      let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
      let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }

      context 'when a production environment is specified' do
        let(:environment) { 'production' }

        it 'does return production' do
          expect(builds.size).to eq(1)
          expect(builds.first[:environment]).to eq(environment)
          expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
        end
      end

      context 'when hash is specified' do
        let(:environment) do
          { name: 'production',
            url: 'http://production.gitlab.com' }
        end

        it 'does return production and URL' do
          expect(builds.size).to eq(1)
          expect(builds.first[:environment]).to eq(environment[:name])
          expect(builds.first[:options]).to include(environment: environment)
        end

        context 'the url has a port as variable' do
          let(:environment) do
            { name: 'production',
              url: 'http://production.gitlab.com:$PORT' }
          end

          it 'allows a variable for the port' do
            expect(builds.size).to eq(1)
            expect(builds.first[:environment]).to eq(environment[:name])
            expect(builds.first[:options]).to include(environment: environment)
          end
        end
      end

      context 'when no environment is specified' do
        let(:environment) { nil }

        it 'does return nil environment' do
          expect(builds.size).to eq(1)
          expect(builds.first[:environment]).to be_nil
        end
      end

      context 'is not a string' do
        let(:environment) { 1 }

        it 'raises error' do
          expect { builds }.to raise_error(
            'jobs:deploy_to_production:environment config should be a hash or a string')
        end
      end

      context 'is not a valid string' do
        let(:environment) { 'production:staging' }

        it 'raises error' do
          expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
        end
      end

      context 'when on_stop is specified' do
        let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
        let(:config) { { review: review, close_review: close_review }.compact }

        context 'with matching job' do
          let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }

          it 'does return a list of builds' do
            expect(builds.size).to eq(2)
            expect(builds.first[:environment]).to eq('review')
          end
        end

        context 'without matching job' do
          let(:close_review) { nil  }

          it 'raises error' do
            expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
          end
        end

        context 'with close job without environment' do
          let(:close_review) { { stage: 'deploy', script: 'test' } }

          it 'raises error' do
            expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
          end
        end

        context 'with close job for different environment' do
          let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }

          it 'raises error' do
            expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
          end
        end

        context 'with close job without stop action' do
          let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }

          it 'raises error' do
            expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
          end
        end
      end
    end

    describe "Dependencies" do
      let(:config) do
        {
          build1: { stage: 'build', script: 'test' },
          build2: { stage: 'build', script: 'test' },
          test1: { stage: 'test', script: 'test', dependencies: dependencies },
          test2: { stage: 'test', script: 'test' },
          deploy: { stage: 'test', script: 'test' }
        }
      end

      subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }

      context 'no dependencies' do
        let(:dependencies) { }

        it { expect { subject }.not_to raise_error }
      end

      context 'dependencies to builds' do
        let(:dependencies) { %w(build1 build2) }

        it { expect { subject }.not_to raise_error }
      end

      context 'dependencies to builds defined as symbols' do
        let(:dependencies) { [:build1, :build2] }

        it { expect { subject }.not_to raise_error }
      end

      context 'undefined dependency' do
        let(:dependencies) { ['undefined'] }

        it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
      end

      context 'dependencies to deploy' do
        let(:dependencies) { ['deploy'] }

        it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
      end
    end

    describe "Hidden jobs" do
      let(:config_processor) { GitlabCiYamlProcessor.new(config) }
      subject { config_processor.builds_for_stage_and_ref("test", "master") }

      shared_examples 'hidden_job_handling' do
        it "doesn't create jobs that start with dot" do
          expect(subject.size).to eq(1)
          expect(subject.first).to eq({
            stage: "test",
            stage_idx: 1,
            name: "normal_job",
            commands: "test",
            coverage_regex: nil,
            tag_list: [],
            options: {
              script: ["test"]
            },
            when: "on_success",
            allow_failure: false,
            environment: nil,
            yaml_variables: []
          })
        end
      end

      context 'when hidden job have a script definition' do
        let(:config) do
          YAML.dump({
                      '.hidden_job' => { image: 'ruby:2.1', script: 'test' },
                      'normal_job' => { script: 'test' }
                    })
        end

        it_behaves_like 'hidden_job_handling'
      end

      context "when hidden job doesn't have a script definition" do
        let(:config) do
          YAML.dump({
                      '.hidden_job' => { image: 'ruby:2.1' },
                      'normal_job' => { script: 'test' }
                    })
        end

        it_behaves_like 'hidden_job_handling'
      end
    end

    describe "YAML Alias/Anchor" do
      let(:config_processor) { GitlabCiYamlProcessor.new(config) }
      subject { config_processor.builds_for_stage_and_ref("build", "master") }

      shared_examples 'job_templates_handling' do
        it "is correctly supported for jobs" do
          expect(subject.size).to eq(2)
          expect(subject.first).to eq({
            stage: "build",
            stage_idx: 0,
            name: "job1",
            commands: "execute-script-for-job",
            coverage_regex: nil,
            tag_list: [],
            options: {
              script: ["execute-script-for-job"]
            },
            when: "on_success",
            allow_failure: false,
            environment: nil,
            yaml_variables: []
          })
          expect(subject.second).to eq({
            stage: "build",
            stage_idx: 0,
            name: "job2",
            commands: "execute-script-for-job",
            coverage_regex: nil,
            tag_list: [],
            options: {
              script: ["execute-script-for-job"]
            },
            when: "on_success",
            allow_failure: false,
            environment: nil,
            yaml_variables: []
          })
        end
      end

      context 'when template is a job' do
        let(:config) do
          <<EOT
job1: &JOBTMPL
  stage: build
  script: execute-script-for-job

job2: *JOBTMPL
EOT
        end

        it_behaves_like 'job_templates_handling'
      end

      context 'when template is a hidden job' do
        let(:config) do
          <<EOT
.template: &JOBTMPL
  stage: build
  script: execute-script-for-job

job1: *JOBTMPL

job2: *JOBTMPL
EOT
        end

        it_behaves_like 'job_templates_handling'
      end

      context 'when job adds its own keys to a template definition' do
        let(:config) do
          <<EOT
.template: &JOBTMPL
  stage: build

job1:
  <<: *JOBTMPL
  script: execute-script-for-job

job2:
  <<: *JOBTMPL
  script: execute-script-for-job
EOT
        end

        it_behaves_like 'job_templates_handling'
      end
    end

    describe "Error handling" do
      it "fails to parse YAML" do
        expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
      end

      it "indicates that object is invalid" do
        expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
      end

      it "returns errors if tags parameter is invalid" do
        config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
      end

      it "returns errors if before_script parameter is invalid" do
        config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
      end

      it "returns errors if job before_script parameter is not an array of strings" do
        config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
      end

      it "returns errors if after_script parameter is invalid" do
        config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
      end

      it "returns errors if job after_script parameter is not an array of strings" do
        config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
      end

      it "returns errors if image parameter is invalid" do
        config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string")
      end

      it "returns errors if job name is blank" do
        config = YAML.dump({ '' => { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
      end

      it "returns errors if job name is non-string" do
        config = YAML.dump({ 10 => { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
      end

      it "returns errors if job image parameter is invalid" do
        config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
      end

      it "returns errors if services parameter is not an array" do
        config = YAML.dump({ services: "test", rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array")
      end

      it "returns errors if services parameter is not an array of strings" do
        config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
      end

      it "returns errors if job services parameter is not an array" do
        config = YAML.dump({ rspec: { script: "test", services: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
      end

      it "returns errors if job services parameter is not an array of strings" do
        config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
      end

      it "returns error if job configuration is invalid" do
        config = YAML.dump({ extra: "bundle update" })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
      end

      it "returns errors if services configuration is not correct" do
        config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array")
      end

      it "returns errors if there are no jobs defined" do
        config = YAML.dump({ before_script: ["bundle update"] })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
      end

      it "returns errors if there are no visible jobs defined" do
        config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
      end

      it "returns errors if job allow_failure parameter is not an boolean" do
        config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
      end

      it "returns errors if job stage is not a string" do
        config = YAML.dump({ rspec: { script: "test", type: 1 } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
      end

      it "returns errors if job stage is not a pre-defined stage" do
        config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
      end

      it "returns errors if job stage is not a defined stage" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
      end

      it "returns errors if stages is not an array" do
        config = YAML.dump({ stages: "test", rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
      end

      it "returns errors if stages is not an array of strings" do
        config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
      end

      it "returns errors if variables is not a map" do
        config = YAML.dump({ variables: "test", rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
      end

      it "returns errors if variables is not a map of key-value strings" do
        config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
      end

      it "returns errors if job when is not on_success, on_failure or always" do
        config = YAML.dump({ rspec: { script: "test", when: 1 } })
        expect do
          GitlabCiYamlProcessor.new(config, path)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
      end

      it "returns errors if job artifacts:name is not an a string" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
      end

      it "returns errors if job artifacts:when is not an a predefined value" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
      end

      it "returns errors if job artifacts:expire_in is not an a string" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
      end

      it "returns errors if job artifacts:expire_in is not an a valid duration" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
      end

      it "returns errors if job artifacts:untracked is not an array of strings" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
      end

      it "returns errors if job artifacts:paths is not an array of strings" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
      end

      it "returns errors if cache:untracked is not an array of strings" do
        config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
      end

      it "returns errors if cache:paths is not an array of strings" do
        config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings")
      end

      it "returns errors if cache:key is not a string" do
        config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
      end

      it "returns errors if job cache:key is not an a string" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
      end

      it "returns errors if job cache:untracked is not an array of strings" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
      end

      it "returns errors if job cache:paths is not an array of strings" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
      end

      it "returns errors if job dependencies is not an array of strings" do
        config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
        expect do
          GitlabCiYamlProcessor.new(config)
        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
      end
    end

    describe "Validate configuration templates" do
      templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")

      templates.each do |file|
        it "does not return errors for #{file}" do
          file = File.read(file)

          expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error
        end
      end
    end

    describe "#validation_message" do
      context "when the YAML could not be parsed" do
        it "returns an error about invalid configutaion" do
          content = YAML.dump("invalid: yaml: test")

          expect(GitlabCiYamlProcessor.validation_message(content))
            .to eq "Invalid configuration format"
        end
      end

      context "when the tags parameter is invalid" do
        it "returns an error about invalid tags" do
          content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })

          expect(GitlabCiYamlProcessor.validation_message(content))
            .to eq "jobs:rspec tags should be an array of strings"
        end
      end

      context "when YAML content is empty" do
        it "returns an error about missing content" do
          expect(GitlabCiYamlProcessor.validation_message(''))
            .to eq "Please provide content of .gitlab-ci.yml"
        end
      end

      context "when the YAML is valid" do
        it "does not return any errors" do
          content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))

          expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
        end
      end
    end
  end
end