RSpec.describe ActiveInteraction::ArrayFilter, :filter do
  include_context 'filters'
  it_behaves_like 'a filter'

  context 'with multiple nested filters' do
    let(:block) do
      proc do
        array
        array
      end
    end

    it 'raises an error' do
      expect { filter }.to raise_error ActiveInteraction::InvalidFilterError
    end
  end

  context 'with a nested name' do
    let(:block) { proc { array :a } }

    it 'raises an error' do
      expect { filter }.to raise_error ActiveInteraction::InvalidFilterError
    end
  end

  describe '#process' do
    let(:result) { filter.process(value, nil) }

    context 'with an Array' do
      let(:value) { [] }

      it 'returns an ArrayInput' do
        expect(result).to be_an_instance_of ActiveInteraction::ArrayInput
      end

      it 'returns the Array' do
        expect(result.value).to eql value
      end

      it 'has no children' do
        expect(result.children).to eql []
      end
    end

    context 'with an implicit Array' do
      let(:value) do
        Class.new do
          def to_ary
            [1, 2, 3]
          end
        end.new
      end

      it 'returns the Array' do
        expect(result.value).to eql value.to_ary
      end
    end

    context 'with a heterogenous Array' do
      let(:value) { [[], false, 0.0, {}, 0, '', :''] }

      it 'returns the Array' do
        expect(result.value).to eql value
      end
    end

    context 'with a nested filter where the value transforms' do
      let(:block) { proc { symbol } }
      let(:name) { :test }
      let(:value) { ['test'] }

      it 'returns the transformed value' do
        expect(result.value).to eql [:test]
      end

      it 'does not modify the original value' do
        expect(result.value.object_id).to_not eql value.object_id
        expect(value).to eql ['test']
      end
    end

    context 'with a nested filter' do
      let(:block) { proc { array } }

      context 'with an Array' do
        let(:child_value) { [] }
        let(:value) { [child_value, child_value] }

        it 'returns the Array' do
          expect(result.value).to eql value
        end

        it 'has children' do
          expect(result.children.size).to be 2
          result.children.each do |child|
            expect(child).to be_an_instance_of ActiveInteraction::ArrayInput
            expect(child.value).to eql child_value
          end
        end

        context 'given a nil value' do
          let(:value) { nil }

          it 'returns an error' do
            error = result.errors.first

            expect(result.value).to eql value
            expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
            expect(error.type).to be :missing
          end
        end
      end

      context 'with an Array of Arrays' do
        let(:value) { [[]] }

        it 'returns the Array' do
          expect(result.value).to eql value
        end
      end

      context 'with a heterogenous Array' do
        let(:value) { [[], false, 0.0] }

        it 'indicates an error' do
          error = result.errors.first

          expect(result.errors.size).to be 1
          expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
          expect(error.name).to be filter.name
          expect(error.type).to be :invalid_type
        end

        context 'when :index_errors is true' do
          before do
            options[:index_errors] = true
          end

          it 'shows the index of where the error occurred' do
            expect(result.errors.size).to be 2

            result.errors.each.with_index(1) do |error, i|
              expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
              expect(error.name).to be :"#{filter.name}[#{i}]"
              expect(error.type).to be :invalid_type
            end
          end
        end

        context 'when ActiveRecord.index_nested_attribute_errors is true' do
          before do
            if ActiveRecord.respond_to?(:index_nested_attribute_errors)
              allow(ActiveRecord).to receive(:index_nested_attribute_errors).and_return(true)
            else
              allow(ActiveRecord::Base).to receive(:index_nested_attribute_errors).and_return(true)
            end
          end

          it 'shows the index of where the error occurred' do
            expect(result.errors.size).to be 2

            result.errors.each.with_index(1) do |error, i|
              expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
              expect(error.name).to be :"#{filter.name}[#{i}]"
              expect(error.type).to be :invalid_type
            end
          end

          context 'when :index_errors is false' do
            before do
              options[:index_errors] = false
            end

            it 'does not attach the index' do
              error = result.errors.first

              expect(result.errors.size).to be 1
              expect(error).to be_an_instance_of ActiveInteraction::Filter::Error
              expect(error.name).to be filter.name
              expect(error.type).to be :invalid_type
            end
          end
        end
      end
    end

    [
      %i[object class],
      %i[record class],
      %i[interface from]
    ].each do |(type, option)|
      context "with a nested #{type} filter" do
        let(:block) { proc { public_send(type) } }
        let(:name) { :objects }
        let(:value) { [''] }

        it 'does not raise an error' do
          expect { result }.to_not raise_error
        end

        it 'has a filter with the right key' do
          expect(filter.filters).to have_key(:'0')
        end

        it 'has a filter with the right option' do
          expect(filter.filters[:'0'].options).to have_key(option)
        end

        context 'with a class set' do
          let(:block) { proc { public_send(type, "#{option}": String) } }

          it "does not override the #{option}" do
            expect(filter.filters[:'0'].options[option]).to eql String
          end
        end
      end
    end

    context 'with a nested interface type' do
      context 'with the methods option set' do
        let(:block) { proc { interface(methods: %i[to_s]) } }

        it 'has a filter with the right option' do
          expect(filter.filters[:'0'].options).to have_key(:methods)
          expect(filter.filters[:'0'].options[:methods]).to eql %i[to_s]
        end
      end

      context 'with another option set' do
        let(:block) { proc { object(converter: :new) } }
        let(:name) { :objects }

        it 'has a filter with the right options' do
          expect(filter.filters[:'0'].options).to have_key(:class)
          expect(filter.filters[:'0'].options[:class]).to be :Object
          expect(filter.filters[:'0'].options).to have_key(:converter)
          expect(filter.filters[:'0'].options[:converter]).to be :new
        end
      end
    end
  end

  describe '#database_column_type' do
    it 'returns :string' do
      expect(filter.database_column_type).to be :string
    end
  end
end
