# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Ci::Config::Interpolation::Interpolator, feature_category: :pipeline_composition do
  let_it_be(:project) { create(:project) }

  let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) }
  let(:yaml_context) { ::Gitlab::Ci::Config::Yaml::Context.new }

  subject { described_class.new(result, arguments, yaml_context) }

  context 'when input data is valid' do
    let(:header) do
      { spec: { inputs: { website: nil } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.website ]]' }
    end

    let(:arguments) do
      { website: 'gitlab.com' }
    end

    it 'correctly interpolates the config' do
      subject.interpolate!

      expect(subject).to be_interpolated
      expect(subject).to be_valid
      expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
    end
  end

  context 'when config has a syntax error' do
    let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }

    let(:arguments) do
      { website: 'gitlab.com' }
    end

    it 'surfaces an error about invalid config' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.error_message).to eq subject.errors.first
      expect(subject.errors).to include 'Invalid configuration format'
    end
  end

  context 'when spec header is missing but inputs are specified' do
    let(:header) { nil }
    let(:content) { { test: 'echo' } }
    let(:arguments) { { foo: 'bar' } }

    it 'surfaces an error about invalid inputs' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.error_message).to eq subject.errors.first
      expect(subject.errors).to include('Given inputs not defined in the `spec` section of the included ' \
                                        'configuration file')
    end
  end

  context 'when spec header is invalid' do
    let(:header) do
      { spec: { arguments: { website: nil } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.website ]]' }
    end

    let(:arguments) do
      { website: 'gitlab.com' }
    end

    it 'surfaces an error about invalid header' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.error_message).to eq subject.errors.first
      expect(subject.errors).to include('header:spec config contains unknown keys: arguments')
    end
  end

  context 'when provided interpolation arguments are not a hash' do
    let(:header) do
      { spec: { inputs: { website: nil } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.website ]]' }
    end

    let(:arguments) do
      'invalid'
    end

    it 'returns an error' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.errors).to include 'Given inputs must be a hash'
    end
  end

  context 'when provided interpolation argument is invalid' do
    let(:header) do
      { spec: { inputs: { number_of_deploy: { type: 'number' } } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.number_of_deploy ]]' }
    end

    let(:arguments) do
      { number_of_deploy: 'hello' }
    end

    it 'returns an error' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.error_message).to eq subject.errors.first
      expect(subject.errors).to include '`number_of_deploy` input: provided value is not a number'
    end
  end

  context 'when interpolation block is invalid' do
    let(:header) do
      { spec: { inputs: { website: nil } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.abc ]]' }
    end

    let(:arguments) do
      { website: 'gitlab.com' }
    end

    it 'returns an error' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.errors).to include 'unknown interpolation provided: `abc` in `inputs.abc`'
      expect(subject.error_message).to eq 'unknown interpolation provided: `abc` in `inputs.abc`'
    end
  end

  context 'when multiple interpolation blocks are invalid' do
    let(:header) do
      { spec: { inputs: { website: nil } } }
    end

    let(:content) do
      { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
    end

    let(:arguments) do
      { website: 'gitlab.com' }
    end

    it 'returns an error' do
      subject.interpolate!

      expect(subject).not_to be_valid
      expect(subject.error_message)
        .to eq 'unknown interpolation provided: `something` in `inputs.something.abc`'
    end
  end

  context 'when using component interpolation' do
    let(:yaml_context) do
      ::Gitlab::Ci::Config::Yaml::Context.new(
        variables: [],
        component: { name: 'my-component', sha: 'abc123' }
      )
    end

    context 'when component values are specified in spec' do
      let(:header) do
        { spec: { component: %w[name sha] } }
      end

      let(:content) do
        { test: 'Component $[[ component.name ]] at $[[ component.sha ]]' }
      end

      let(:arguments) { {} }

      it 'correctly interpolates component data' do
        subject.interpolate!

        expect(subject).to be_interpolated
        expect(subject).to be_valid
        expect(subject.to_hash).to eq({ test: 'Component my-component at abc123' })
      end
    end

    context 'when component value not in spec is accessed' do
      let(:header) do
        { spec: { component: %w[name] } }
      end

      let(:content) do
        { test: 'Component $[[ component.name ]] at $[[ component.sha ]]' }
      end

      let(:arguments) { {} }

      it 'returns an error for unspecified component value' do
        subject.interpolate!

        expect(subject).not_to be_valid
        expect(subject.errors).to include 'unknown interpolation provided: `sha` in `component.sha`'
      end
    end

    context 'when both inputs and component are used' do
      let(:header) do
        { spec: { inputs: { env: nil }, component: %w[name] } }
      end

      let(:content) do
        {
          test: 'Deploy to $[[ inputs.env ]] using $[[ component.name ]]'
        }
      end

      let(:arguments) { { env: 'production' } }

      it 'correctly interpolates both inputs and component data' do
        subject.interpolate!

        expect(subject).to be_interpolated
        expect(subject).to be_valid
        expect(subject.to_hash).to eq({ test: 'Deploy to production using my-component' })
      end
    end
  end

  describe '#to_hash' do
    context 'when interpolation is not used' do
      let(:result) do
        ::Gitlab::Ci::Config::Yaml::Result.new(config: content)
      end

      let(:content) do
        { test: 'deploy production' }
      end

      let(:arguments) { nil }

      it 'returns original content' do
        subject.interpolate!

        expect(subject.to_hash).to eq(content)
      end
    end

    context 'when interpolation is available' do
      let(:header) do
        { spec: { inputs: { website: nil } } }
      end

      let(:content) do
        { test: 'deploy $[[ inputs.website ]]' }
      end

      let(:arguments) do
        { website: 'gitlab.com' }
      end

      it 'correctly interpolates content' do
        subject.interpolate!

        expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
      end
    end
  end
end
