# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Changelog::AST::Identifier do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'evaluates a selector' do
      data = { 'number' => 10 }

      expect(described_class.new('number').evaluate(state, data)).to eq(10)
    end

    it 'returns nil if the key is not set' do
      expect(described_class.new('number').evaluate(state, {})).to be_nil
    end

    it 'returns nil if the input is not a Hash' do
      expect(described_class.new('number').evaluate(state, 45)).to be_nil
    end

    it 'returns the current data when using the special identifier "it"' do
      expect(described_class.new('it').evaluate(state, 45)).to eq(45)
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Integer do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'evaluates a selector' do
      expect(described_class.new(0).evaluate(state, [10])).to eq(10)
    end

    it 'returns nil if the index is not set' do
      expect(described_class.new(1).evaluate(state, [10])).to be_nil
    end

    it 'returns nil if the input is not an Array' do
      expect(described_class.new(0).evaluate(state, {})).to be_nil
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Selector do
  let(:state) { Gitlab::Changelog::EvalState.new }
  let(:data) { { 'numbers' => [10] } }

  describe '#evaluate' do
    it 'evaluates a selector' do
      ident = Gitlab::Changelog::AST::Identifier.new('numbers')
      int = Gitlab::Changelog::AST::Integer.new(0)

      expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10)
    end

    it 'evaluates a selector that returns nil' do
      int = Gitlab::Changelog::AST::Integer.new(0)

      expect(described_class.new([int]).evaluate(state, data)).to be_nil
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Variable do
  let(:state) { Gitlab::Changelog::EvalState.new }
  let(:data) { { 'numbers' => [10] } }

  describe '#evaluate' do
    it 'evaluates a variable' do
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{{numbers.0}}')
        .nodes[0]

      expect(node.evaluate(state, data)).to eq('10')
    end

    it 'evaluates an undefined variable' do
      node =
        Gitlab::Changelog::Parser.new.parse_and_transform('{{foobar}}').nodes[0]

      expect(node.evaluate(state, data)).to eq('')
    end

    it 'evaluates the special variable "it"' do
      node =
        Gitlab::Changelog::Parser.new.parse_and_transform('{{it}}').nodes[0]

      expect(node.evaluate(state, data)).to eq(data.to_s)
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Expressions do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'evaluates all expressions' do
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{{number}}foo')

      expect(node.evaluate(state, { 'number' => 10 })).to eq('10foo')
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Text do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'returns the text' do
      expect(described_class.new('foo').evaluate(state, {})).to eq('foo')
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::If do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'evaluates a truthy if expression without an else clause' do
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{% if thing %}foo{% end %}')
        .nodes[0]

      expect(node.evaluate(state, { 'thing' => true })).to eq('foo')
    end

    it 'evaluates a falsy if expression without an else clause' do
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{% if thing %}foo{% end %}')
        .nodes[0]

      expect(node.evaluate(state, { 'thing' => false })).to eq('')
    end

    it 'evaluates a falsy if expression with an else clause' do
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{% if thing %}foo{% else %}bar{% end %}')
        .nodes[0]

      expect(node.evaluate(state, { 'thing' => false })).to eq('bar')
    end
  end

  describe '#truthy?' do
    it 'returns true for a non-empty String' do
      expect(described_class.new.truthy?('foo')).to eq(true)
    end

    it 'returns true for a non-empty Array' do
      expect(described_class.new.truthy?([10])).to eq(true)
    end

    it 'returns true for a Boolean true' do
      expect(described_class.new.truthy?(true)).to eq(true)
    end

    it 'returns false for an empty String' do
      expect(described_class.new.truthy?('')).to eq(false)
    end

    it 'returns true for an empty Array' do
      expect(described_class.new.truthy?([])).to eq(false)
    end

    it 'returns false for a Boolean false' do
      expect(described_class.new.truthy?(false)).to eq(false)
    end
  end
end

RSpec.describe Gitlab::Changelog::AST::Each do
  let(:state) { Gitlab::Changelog::EvalState.new }

  describe '#evaluate' do
    it 'evaluates the expression' do
      data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] }
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{% each animals %}{{name}}{% end %}')
        .nodes[0]

      expect(node.evaluate(state, data)).to eq('CatDog')
    end

    it 'returns an empty string when the input is not a collection' do
      data = { 'animals' => 10 }
      node = Gitlab::Changelog::Parser
        .new
        .parse_and_transform('{% each animals %}{{name}}{% end %}')
        .nodes[0]

      expect(node.evaluate(state, data)).to eq('')
    end

    it 'disallows too many nested loops' do
      data = {
        'foo' => [
          {
            'bar' => [
              {
                'baz' => [
                  {
                    'quix' => [
                      {
                        'foo' => [{ 'name' => 'Alice' }]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }

      template = <<~TPL
        {% each foo %}
          {% each bar %}
            {% each baz %}
              {% each quix %}
                {% each foo %}
                  {{name}}
                {% end %}
              {% end %}
            {% end %}
          {% end %}
        {% end %}
      TPL

      node =
        Gitlab::Changelog::Parser.new.parse_and_transform(template).nodes[0]

      expect { node.evaluate(state, data) }
        .to raise_error(Gitlab::Changelog::Error)
    end
  end
end