# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'test_help'

class TestSchemaValidator < Test::Unit::TestCase
  def validate!(schema, value, options=nil)
    Avro::SchemaValidator.validate!(schema, value, options)
  end

  def validate_simple!(schema, value)
    Avro::SchemaValidator.validate!(schema, value, recursive: false)
  end

  def hash_to_schema(hash)
    Avro::Schema.parse(hash.to_json)
  end

  def assert_failed_validation(messages)
    error = assert_raise(Avro::SchemaValidator::ValidationError) { yield }

    assert_messages = [messages].flatten
    result_errors = error.result.errors
    assert_messages.each do |message|
      assert(result_errors.include?(message), "expected '#{message}' to be in '#{result_errors}'")
    end
    assert_equal(assert_messages.size, result_errors.size)
  end

  def assert_valid_schema(schema, valid, invalid, simple = false)
    valid.each do |value|
      assert_nothing_raised { Avro::SchemaValidator.validate!(schema, value) }
      assert_nothing_raised { Avro::SchemaValidator.validate!(schema, value, recursive: false) } if simple
    end

    invalid.each do |value|
      assert_raise { Avro::SchemaValidator.validate!(schema, value) }
      assert_raise { Avro::SchemaValidator.validate!(schema, value, recursive: false) } if simple
      assert_nothing_raised { Avro::SchemaValidator.validate!(schema, value, recursive: false) } unless simple
    end
  end

  def test_validate_nil
    schema = hash_to_schema(type: 'null', name: 'name')

    assert_nothing_raised { validate!(schema, nil) }
    assert_nothing_raised { validate_simple!(schema, nil) }

    assert_failed_validation('at . expected type null, got int with value 1') do
      validate!(schema, 1)
    end

    assert_failed_validation('at . expected type null, got int with value 1') do
      validate_simple!(schema, 1)
    end
  end

  def test_validate_boolean
    schema = hash_to_schema(type: 'boolean', name: 'name')

    assert_nothing_raised { validate!(schema, true) }
    assert_nothing_raised { validate!(schema, false) }
    assert_nothing_raised { validate_simple!(schema, true) }
    assert_nothing_raised { validate_simple!(schema, false) }

    assert_failed_validation('at . expected type boolean, got int with value 1') do
      validate!(schema, 1)
    end
    assert_failed_validation('at . expected type boolean, got int with value 1') do
      validate_simple!(schema, 1)
    end

    assert_failed_validation('at . expected type boolean, got null') do
      validate!(schema, nil)
    end
    assert_failed_validation('at . expected type boolean, got null') do
      validate_simple!(schema, nil)
    end
  end

  def test_fixed_size_string
    schema = hash_to_schema(type: 'fixed', name: 'some', size: 3)

    assert_nothing_raised { validate!(schema, 'baf') }
    assert_nothing_raised { validate_simple!(schema, 'baf') }

    assert_failed_validation('at . expected fixed with size 3, got "some" with size 4') do
      validate!(schema, 'some')
    end
    assert_failed_validation('at . expected fixed with size 3, got "some" with size 4') do
      validate_simple!(schema, 'some')
    end

    assert_failed_validation('at . expected fixed with size 3, got null') do
      validate!(schema, nil)
    end
    assert_failed_validation('at . expected fixed with size 3, got null') do
      validate_simple!(schema, nil)
    end

    assert_failed_validation("at . expected fixed with size 3, got \"a\u2014b\" with size 5") do
      validate!(schema, "a\u2014b")
    end
    assert_failed_validation("at . expected fixed with size 3, got \"a\u2014b\" with size 5") do
      validate_simple!(schema, "a\u2014b")
    end
  end

  def test_original_validate_nil
    schema = hash_to_schema(type: 'null', name: 'name')

    assert_valid_schema(schema, [nil], ['something'], true)
  end

  def test_original_validate_boolean
    schema = hash_to_schema(type: 'boolean', name: 'name')

    assert_valid_schema(schema, [true, false], [nil, 1], true)
  end

  def test_validate_string
    schema = hash_to_schema(type: 'string', name: 'name')

    assert_valid_schema(schema, ['string'], [nil, 1], true)
  end

  def test_validate_bytes
    schema = hash_to_schema(type: 'bytes', name: 'name')

    assert_valid_schema(schema, ['string'], [nil, 1], true)
  end

  def test_validate_int
    schema = hash_to_schema(type: 'int', name: 'name')

    assert_valid_schema(
      schema,
      [Avro::Schema::INT_MIN_VALUE, Avro::Schema::INT_MAX_VALUE, 1],
      [Avro::Schema::LONG_MIN_VALUE, Avro::Schema::LONG_MAX_VALUE, 'string'],
      true
    )
    assert_failed_validation('at . out of bound value 9223372036854775807') do
      validate!(schema, Avro::Schema::LONG_MAX_VALUE)
    end
    assert_failed_validation('at . out of bound value 9223372036854775807') do
      validate_simple!(schema, Avro::Schema::LONG_MAX_VALUE)
    end
  end

  def test_validate_long
    schema = hash_to_schema(type: 'long', name: 'name')

    assert_valid_schema(schema, [Avro::Schema::LONG_MIN_VALUE, Avro::Schema::LONG_MAX_VALUE, 1], [1.1, 'string'], true)
  end

  def test_validate_float
    schema = hash_to_schema(type: 'float', name: 'name')

    assert_valid_schema(schema, [1.1, 1, Avro::Schema::LONG_MAX_VALUE], ['string'], true)
  end

  def test_validate_double
    schema = hash_to_schema(type: 'double', name: 'name')

    assert_valid_schema(schema, [1.1, 1, Avro::Schema::LONG_MAX_VALUE], ['string'], true)
  end

  def test_validate_fixed
    schema = hash_to_schema(type: 'fixed', name: 'name', size: 3)

    assert_valid_schema(schema, ['abc'], ['ab', 1, 1.1, true], true)
  end

  def test_validate_original_num
    schema = hash_to_schema(type: 'enum', name: 'name', symbols: %w(a b))

    assert_valid_schema(schema, ['a', 'b'], ['c'], true)
  end

  def test_validate_record
    schema = hash_to_schema(type: 'record', name: 'name', fields: [{ type: 'null', name: 'sub' }])

    assert_valid_schema(schema, [{ 'sub' => nil }], [{ 'sub' => 1 }])
  end

  def test_validate_shallow_record
    schema = hash_to_schema(
      type: 'record', name: 'name', fields: [{ type: 'int', name: 'sub' }]
    )

    assert_nothing_raised { validate!(schema, 'sub' => 1) }
    assert_nothing_raised { validate_simple!(schema, 'sub' => 1) }

    assert_failed_validation('at .sub expected type int, got null') do
      validate!(schema, {})
    end
    assert_nothing_raised { validate_simple!(schema, {}) }

    assert_failed_validation('at . expected type record, got float with value 1.2') do
      validate!(schema, 1.2)
    end
    assert_nothing_raised { validate_simple!(schema, 1.2) }

    assert_failed_validation('at .sub expected type int, got float with value 1.2') do
      validate!(schema, 'sub' => 1.2)
    end
    assert_nothing_raised { validate_simple!(schema, 'sub' => 1.2) }
  end

  def test_validate_array
    schema = hash_to_schema(type: 'array',
                            name: 'person',
                            items: [{ type: 'int', name: 'height' }])

    assert_nothing_raised { validate!(schema, []) }
    assert_nothing_raised { validate_simple!(schema, []) }

    assert_failed_validation 'at . expected type array, got null' do
      validate!(schema, nil)
    end
    assert_nothing_raised { validate_simple!(schema, nil) }

    assert_failed_validation('at .[0] expected type int, got null') do
      validate!(schema, [nil])
    end
    assert_nothing_raised { validate_simple!(schema, [nil]) }

    assert_failed_validation('at .[3] expected type int, got string with value "so wrong"') do
      validate!(schema, [1, 3, 9, 'so wrong'])
    end
    assert_nothing_raised { validate_simple!(schema, [1, 3, 9, 'so wrong']) }
  end

  def test_validate_enum
    schema = hash_to_schema(type: 'enum',
                            name: 'person',
                            symbols: %w(one two three))

    assert_nothing_raised { validate!(schema, 'one') }
    assert_nothing_raised { validate_simple!(schema, 'one') }

    assert_failed_validation('at . expected enum with values ["one", "two", "three"], got string with value "five"') do
      validate!(schema, 'five')
    end
    assert_failed_validation('at . expected enum with values ["one", "two", "three"], got string with value "five"') do
      validate_simple!(schema, 'five')
    end
  end

  def test_validate_union_on_primitive_types
    schema = hash_to_schema(
      name: 'should_not_matter',
      type: 'record',
      fields: [
        { name: 'what_ever', type: %w(long string) }
      ]
    )

    assert_failed_validation('at .what_ever expected union of [\'long\', \'string\'], got null') {
      validate!(schema, 'what_ever' => nil)
    }
    assert_nothing_raised { validate_simple!(schema, 'what_ever' => nil) }
  end

  def test_validate_union_of_nil_and_record_inside_array
    schema = hash_to_schema(
      name: 'this_does_not_matter',
      type: 'record',
      fields: [
        {
          name: 'person',
          type: {
            name: 'person_entry',
            type: 'record',
            fields: [
              {
                name: 'houses',
                type: [
                  'null',
                  {
                    name: 'houses_entry',
                    type: 'array',
                    items: [
                      {
                        name: 'house_entry',
                        type: 'record',
                        fields: [
                          { name: 'number_of_rooms', type: 'long' }
                        ]
                      }
                    ]
                  }
                ],
              }
            ]
          }
        }
      ]
    )

    assert_failed_validation('at .person expected type record, got null') {
      validate!(schema, 'not at all' => nil)
    }
    assert_nothing_raised { validate_simple!(schema, 'person' => {}) }

    assert_nothing_raised { validate!(schema, 'person' => {}) }
    assert_nothing_raised { validate!(schema, 'person' => { houses: [] }) }
    assert_nothing_raised { validate!(schema, 'person' => { 'houses' => [{ 'number_of_rooms' => 1 }] }) }

    assert_nothing_raised { validate_simple!(schema, 'person' => {}) }
    assert_nothing_raised { validate_simple!(schema, 'person' => { houses: [] }) }
    assert_nothing_raised { validate_simple!(schema, 'person' => { 'houses' => [{ 'number_of_rooms' => 1 }] }) }

    message = 'at .person.houses[1].number_of_rooms expected type long, got string with value "not valid at all"'
    datum = {
      'person' => {
        'houses' => [
          { 'number_of_rooms' => 2 },
          { 'number_of_rooms' => 'not valid at all' }
        ]
      }
    }
    assert_failed_validation(message) { validate!(schema, datum) }
    assert_nothing_raised { validate_simple!(schema, datum) }
  end

  def test_validate_map
    schema = hash_to_schema(type: 'map',
                            name: 'numbers',
                            values: [
                              { name: 'some', type: 'int' }
                            ])

    assert_nothing_raised { validate!(schema, 'some' => 1) }
    assert_nothing_raised { validate_simple!(schema, 'some' => 1) }

    assert_failed_validation('at .some expected type int, got string with value "nope"') do
      validate!(schema, 'some' => 'nope')
    end
    assert_nothing_raised { validate_simple!(schema, 'some' => 'nope')}

    assert_failed_validation("at . unexpected key type 'Symbol' in map") do
      validate!(schema, some: 1)
    end
    assert_nothing_raised { validate_simple!(schema, some: 1) }

    assert_failed_validation('at . expected type map, got null') do
      validate!(schema, nil)
    end
    assert_nothing_raised { validate_simple!(schema, nil) }
  end

  def test_validate_deep_record
    schema = hash_to_schema(type: 'record',
                            name: 'person',
                            fields: [
                              {
                                name: 'head',
                                type: {
                                  name: 'head',
                                  type: 'record',
                                  fields: [
                                    {
                                      name: 'hair',
                                      type: {
                                        name: 'hair',
                                        type: 'record',
                                        fields: [
                                          {
                                            name: 'color',
                                            type: 'string'
                                          }
                                        ]
                                      }
                                    }
                                  ]
                                }
                              }
                            ])

    assert_nothing_raised { validate!(schema, 'head' => { 'hair' => { 'color' => 'black' } }) }
    assert_nothing_raised { validate_simple!(schema, 'head' => { 'hair' => { 'color' => 'black' } }) }

    assert_failed_validation('at .head.hair.color expected type string, got null') do
      validate!(schema, 'head' => { 'hair' => { 'color' => nil } })
    end
    assert_nothing_raised { validate_simple!(schema, 'head' => { 'hair' => { 'color' => nil } }) }

    assert_failed_validation('at .head.hair.color expected type string, got null') do
      validate!(schema, 'head' => { 'hair' => {} })
    end
    assert_nothing_raised { validate_simple!(schema, 'head' => { 'hair' => {} }) }

    assert_failed_validation('at .head.hair expected type record, got null') do
      validate!(schema, 'head' => {})
    end
    assert_nothing_raised { validate_simple!(schema, 'head' => {}) }

    assert_failed_validation('at . expected type record, got null') do
      validate!(schema, nil)
    end
    assert_nothing_raised { validate_simple!(schema, nil) }
  end

  def test_validate_deep_record_with_array
    schema = hash_to_schema(type: 'record',
                            name: 'fruits',
                            fields: [
                              {
                                name: 'fruits',
                                type: {
                                  name: 'fruits',
                                  type: 'array',
                                  items: [
                                    {
                                      name: 'fruit',
                                      type: 'record',
                                      fields: [
                                        { name: 'name', type: 'string' },
                                        { name: 'weight', type: 'float' }
                                      ]
                                    }
                                  ]
                                }
                              }
                            ])
    assert_nothing_raised { validate!(schema, 'fruits' => [{ 'name' => 'apple', 'weight' => 30.2 }]) }
    assert_nothing_raised { validate_simple!(schema, 'fruits' => [{ 'name' => 'apple', 'weight' => 30.2 }]) }

    assert_failed_validation('at .fruits[0].name expected type string, got null') do
      validate!(schema, 'fruits' => [{ 'name' => nil, 'weight' => 30.2 }])
    end
    assert_nothing_raised { validate_simple!(schema, 'fruits' => [{ 'name' => nil, 'weight' => 30.2 }]) }

    assert_failed_validation('at .fruits expected type array, got int with value 1') do
      validate!(schema, 'fruits' => 1)
    end
    assert_nothing_raised { validate_simple!(schema, 'fruits' => 1) }
  end

  def test_validate_multiple_errors
    schema = hash_to_schema(type: 'array',
                            name: 'ages',
                            items: [
                              { type: 'int', name: 'age' }
                            ])

    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, [nil, 'e'])
    end
    assert_nothing_raised { validate_simple!(schema, [nil, 'e']) }
    assert_equal 2, exception.result.errors.size
    assert_equal(
      "at .[0] expected type int, got null\nat .[1] expected type int, got string with value \"e\"",
      exception.to_s
    )
  end

  def test_validate_extra_fields
    schema = hash_to_schema(
               type: 'record',
               name: 'fruits',
               fields: [
                 {
                   name: 'veggies',
                   type: 'string'
                 }
                     ]
    )
    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, {'veggies' => 'tomato', 'bread' => 'rye'}, fail_on_extra_fields: true)
    end
    assert_equal(1, exception.result.errors.size)
    assert_equal("at . extra field 'bread' - not in schema",
                 exception.to_s)
  end

  def test_validate_subrecord_extra_fields
   schema = hash_to_schema(type: 'record',
                           name: 'top',
                           fields: [
                             {
                                name: 'fruit',
                                type: {
                                  name: 'fruit',
                                  type: 'record',
                                  fields: [{ name: 'name', type: 'string' }]
                                }
                             }
                           ])
    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, { 'fruit' => { 'name' => 'orange', 'color' => 'orange' } }, fail_on_extra_fields: true)
    end
    assert_equal(1, exception.result.errors.size)
    assert_equal("at .fruit extra field 'color' - not in schema", exception.to_s)
  end

  def test_validate_array_extra_fields
    schema = hash_to_schema(type: 'array',
                            items: {
                              name: 'fruit',
                              type: 'record',
                              fields: [{ name: 'name', type: 'string' }]
                            })
    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, [{ 'name' => 'orange', 'color' => 'orange' }], fail_on_extra_fields: true)
    end
    assert_equal(1, exception.result.errors.size)
    assert_equal("at .[0] extra field 'color' - not in schema", exception.to_s)
  end

  def test_validate_map_extra_fields
    schema = hash_to_schema(type: 'map',
                            values: {
                              name: 'fruit',
                              type: 'record',
                              fields: [{ name: 'color', type: 'string' }]
                            })
    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, { 'apple' => { 'color' => 'green', 'extra' => 1 } }, fail_on_extra_fields: true)
    end
    assert_equal(1, exception.result.errors.size)
    assert_equal("at .apple extra field 'extra' - not in schema", exception.to_s)
  end

  def test_validate_union_extra_fields
    schema = hash_to_schema([
                              'null',
                              {
                                type: 'record',
                                name: 'fruit',
                                fields: [{ name: 'name', type: 'string' }]
                              }
                            ])
    exception = assert_raise(Avro::SchemaValidator::ValidationError) do
      validate!(schema, { 'name' => 'apple', 'color' => 'green' }, fail_on_extra_fields: true)
    end
    assert_equal(1, exception.result.errors.size)
    assert_equal("at . extra field 'color' - not in schema", exception.to_s)
  end
end
