# frozen_string_literal: true

require_relative '../spec_helper'

module Aws
  class TestClass; end

  describe ParamValidator do
    let(:shapes) { ApiHelper.sample_shapes }

    let(:service) { ApiHelper.sample_service(shapes: shapes) }

    let(:api) { service.const_get(:ClientApi).const_get(:API) }

    let(:types) { service.const_get(:Types) }

    def validate(params, expected_errors = [])
      rules = api.operation(:example_operation).input
      if expected_errors.empty?
        ParamValidator.new(rules).validate!(params)
      else
        expect do
          ParamValidator.new(rules).validate!(params)
        end.to raise_error(ArgumentError) do |error|
          match_errors(error, expected_errors)
        end
      end
    end

    def match_errors(error, expected_errors)
      expected_errors = [expected_errors] unless expected_errors.is_a?(Array)
      expected_errors.each do |expected_error|
        if String === expected_error
          expect(error.message).to include(expected_error)
        else
          expect(error.message).to match(expected_error)
        end
      end
    end

    describe 'empty rules' do
      it 'accepts an empty hash of params when rules are empty' do
        expect(validate({}))
      end
    end

    describe 'structures' do
      it 'validates nested structures' do
        validate('abc',
                 'expected params to be a hash, got class String instead.')
        validate({ nested: 'abc' },
                 'expected params[:nested] to be a hash, got class String instead.')
        validate({ nested: { nested: 'abc' } },
                 'expected params[:nested][:nested] to be a hash, got class String instead.')
      end

      it 'accepts hashes' do
        validate({})
      end

      it 'raises an error when a required parameter is missing' do
        shapes['StructureShape']['required'] = %w[String]
        validate({}, 'missing required parameter params[:string]')
      end

      it 'raises an error when a given parameter is unexpected' do
        validate({ foo: 'bar' }, 'unexpected value at params[:foo]')
      end

      it 'accepts members that pass validation' do
        shapes['StructureShape']['required'] = %w[String]
        validate(string: 'abc')
      end

      it 'aggregates errors for members' do
        shapes['StructureShape']['required'] = %w[String]
        validate({ nested: { foo: 'bar' } }, [
                   'missing required parameter params[:string]',
                   'missing required parameter params[:nested][:string]',
                   'unexpected value at params[:nested][:foo]'
                 ])
      end

      it 'raises an error when providing eventstream at input' do
        shapes['StructureShape']['members'] = { 'EventStream' => { 'shape' => 'EventStream' } }
        shapes['EventStream'] = {
          'type' => 'structure',
          'members' => { 'EventA' => { 'shape' => 'EventA' } },
          'eventstream' => true
        }
        shapes['EventA'] = {
          'type' => 'structure',
          'members' => { 'MemberA' => { 'shape' => 'StringShape' } },
          'event' => true
        }
        validate(
          { event_stream: [].each },
          'instead of providing value directly for eventstreams at input, ' \
            'expected to use #signal events per stream'
        )
      end

      it 'accepts no eventstream input even when marked required' do
        shapes['StructureShape']['required'] = %w[EventStream]
        validate({})
      end
    end

    describe 'unions' do
      it 'raises an error when no values are set' do
        shapes['StructureShape']['union'] = true
        validate({}, 'No values provided to union')
      end

      it 'raises an error when multiple values are set' do
        shapes['StructureShape']['union'] = true
        validate({ string: 's', boolean: true }, 'multiple values provided to union')
      end

      it 'accepts a struct with exactly one value set' do
        shapes['StructureShape']['union'] = true
        validate({ string: 's' })
      end

      it 'accepts a modeled type' do
        shapes['StructureShape']['union'] = true
        input = types.const_get('StructureShape').new(string: 's')
        validate(input)
      end
    end

    describe 'lists' do
      it 'accepts arrays' do
        validate(number_list: [])
        validate(nested_list: [{}, {}])
      end

      it 'expects the value to be an Array' do
        validate({ nested_list: [] })
        validate({ nested_list: 'abc' },
                 'expected params[:nested_list] to be an Array, got class String instead.')
      end

      it 'validates each member of the list' do
        validate({ nested_list: [{}] })
        validate({ number_list: ['abc'] },
                 'expected params[:number_list][0] to be an Integer, got class String instead.')
        validate({ nested_list: [{}, 'abc'] },
                 'expected params[:nested_list][1] to be a hash, got class String instead.')
        validate({ nested_list: [{ number_list: ['abc'] }] },
                 'expected params[:nested_list][0][:number_list][0] to be an Integer, got class String instead.')
        validate({ nested_list: [{ foo: 'abc' }] },
                 'unexpected value at params[:nested_list][0][:foo]')
      end
    end

    describe 'maps' do
      it 'accepts hashes' do
        validate({ string_map: {} })
        validate({ string_map: 'abc' },
                 'expected params[:string_map] to be a hash, got class String instead.')
      end

      it 'validates map keys' do
        validate({ string_map: { 'abc' => 'mno' } })
        validate({ string_map: { 123 => 'xyz' } },
                 [/expected params\[:string_map\] 123 key to be a String, got class (Fixnum|Integer) instead./])
      end

      it 'validates map values' do
        validate({ string_map: { 'foo' => 'bar' } })
        validate({ string_map: { 'foo' => 123 } },
                 [/expected params\[:string_map\]\["foo"\] to be a String, got class (Fixnum|Integer) instead./])
      end
    end

    describe 'integers' do
      it 'accepts integers' do
        validate(integer: 123)
        validate({ integer: '123' },
                 'expected params[:integer] to be an Integer, got class String instead.')
      end
    end

    describe 'floats' do
      it 'accepts integers' do
        validate(float: 123.0)
        validate({ float: 123 },
                 [/expected params\[:float\] to be a Float, got class (Fixnum|Integer) instead./])
      end
    end

    describe 'timestamps' do
      it 'accepts time objects' do
        validate(timestamp: Time.now)
        validate({ timestamp: Date.new },
                 ['expected params[:timestamp] to be a Time object, got ', 'class Date instead.'])
      end
    end

    describe 'booleans' do
      it 'accepts TrueClass and FalseClass' do
        validate(boolean: true)
        validate(boolean: false)
        validate({ boolean: 'true' },
                 'expected params[:boolean] to be true or false, got class String instead.')
      end
    end

    describe 'blobs' do
      it 'accepts strings and io objects for payload members' do
        validate(blob: StringIO.new('abc'))
        validate(blob: double('d', read: 'abc', size: 3, rewind: 0))
        validate({ blob: 'abc' })
        validate({ blob: 123 },
                 [/expected params\[:blob\] to be a String or IO like object that supports read, rewind, and size, got class (Fixnum|Integer) instead./])
      end
    end

    describe 'strings' do
      it 'accepts string objects' do
        validate(string: 'john doe')
        validate({ string: 123 },
                 [/expected params\[:string\] to be a String, got class (Fixnum|Integer) instead./])
      end
    end

    describe 'document types' do
      it 'accepts numeric objects' do
        validate(document_type: 123)
        validate(document_type: 3.14159)
      end

      it 'accepts string objects' do
        validate(document_type: 'test string')
      end

      it 'accepts booleans' do
        validate(document_type: true)
        validate(document_type: false)
      end

      it 'accepts nil' do
        validate(document_type: nil)
      end

      it 'rejects Objects' do
        validate({ document_type: TestClass.new },
                 [/expected params\[:document_type\] to be one of Hash, Array, Numeric, String, TrueClass, FalseClass, NilClass, got class Aws::TestClass instead/])
      end

      describe 'Hash' do
        it 'accepts a flat Hash' do
          validate(document_type: { a: 1, b: 2 })
        end

        it 'accepts a nested Hash' do
          validate(document_type: { a: 1, b: { c: 'nested' } })
        end

        it 'recursively validates and rejects a nested arbitrary Object' do
          validate({ document_type: { a: 1, b: { c: TestClass.new } } },
                   [/expected params\[:document_type\]\[b\]\[c\] to be one of Hash, Array, Numeric, String, TrueClass, FalseClass, NilClass, got class Aws::TestClass instead/])
        end
      end

      describe 'Array' do
        it 'accepts an array' do
          validate(document_type: [1, 2, 3])
        end

        it 'rejects an array with arbitrary Objects' do
          validate({ document_type: [1, 2, TestClass.new] },
                   [/expected params\[:document_type\] to be one of Hash, Array, Numeric, String, TrueClass, FalseClass, NilClass, got class Aws::TestClass instead/])
        end
      end
    end
  end
end
