require "spec"
require "yaml"
require "../../support/finalize"

class YAMLAttrValue(T)
  include YAML::Serializable

  property value : T
end

record YAMLAttrPoint, x : Int32, y : Int32 do
  include YAML::Serializable
end

class YAMLAttrEmptyClass
  include YAML::Serializable

  def initialize; end
end

class YAMLAttrEmptyClassWithUnmapped
  include YAML::Serializable
  include YAML::Serializable::Unmapped

  def initialize; end
end

class YAMLAttrPerson
  include YAML::Serializable

  property name : String
  property age : Int32?

  def_equals name, age

  def initialize(@name : String)
  end
end

struct YAMLAttrPersonWithThreeFieldInInitialize
  include YAML::Serializable

  property name : String
  property bla : Int32
  property age : Int32

  def initialize(@name, @bla, @age)
  end
end

class StrictYAMLAttrPerson
  include YAML::Serializable
  include YAML::Serializable::Strict

  property name : String
  property age : Int32?
end

class YAMLAttrPersonExtraFields
  include YAML::Serializable
  include YAML::Serializable::Unmapped

  property name : String
  property age : Int32?
end

class YAMLAttrPersonEmittingNull
  include YAML::Serializable

  property name : String

  @[YAML::Field(emit_null: true)]
  property age : Int32?
end

struct YAMLAttrPersonWithSelectiveSerialization
  include YAML::Serializable

  property name : String

  @[YAML::Field(ignore_serialize: true)]
  property password : String

  @[YAML::Field(ignore_deserialize: true)]
  property generated : String = "generated-internally"

  def initialize(@name : String, @password : String)
  end
end

@[YAML::Serializable::Options(emit_nulls: true)]
class YAMLAttrPersonEmittingNullsByOptions
  include YAML::Serializable

  property name : String
  property age : Int32?
  property value1 : Int32?

  @[YAML::Field(emit_null: false)]
  property value2 : Int32?
end

class YAMLAttrWithTime
  include YAML::Serializable

  @[YAML::Field(converter: Time::Format.new("%F %T"))]
  property value : Time
end

class YAMLAttrWithNilableTime
  include YAML::Serializable

  @[YAML::Field(converter: Time::Format.new("%F"))]
  property value : Time?

  def initialize
  end
end

class YAMLAttrWithNilableTimeEmittingNull
  include YAML::Serializable

  @[YAML::Field(converter: Time::Format.new("%F"), emit_null: true)]
  property value : Time?

  def initialize
  end
end

class YAMLAttrWithTimeArray1
  include YAML::Serializable

  @[YAML::Field(converter: YAML::ArrayConverter(Time::EpochConverter))]
  property value : Array(Time)
end

class YAMLAttrWithTimeArray2
  include YAML::Serializable

  @[YAML::Field(converter: YAML::ArrayConverter.new(Time::EpochConverter))]
  property value : Array(Time)
end

class YAMLAttrWithTimeArray3
  include YAML::Serializable

  @[YAML::Field(converter: YAML::ArrayConverter.new(Time::Format.new("%F %T")))]
  property value : Array(Time)
end

module YAMLAttrPointConverter
  def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
    YAMLAttrPoint.new(ctx, node)
  end

  def self.to_yaml(value, yaml : YAML::Nodes::Builder)
    value.to_yaml(yaml)
  end
end

class YAMLAttrWithSerializableArray
  include YAML::Serializable

  @[YAML::Field(converter: YAML::ArrayConverter(YAMLAttrPointConverter))]
  property value : Array(YAMLAttrPoint)
end

class YAMLAttrWithSimpleMapping
  include YAML::Serializable

  property name : String
  property age : Int32
end

class YAMLAttrWithKeywordsMapping
  include YAML::Serializable

  property end : Int32
  property abstract : Int32
end

class YAMLAttrWithProblematicKeys
  include YAML::Serializable

  property key : Int32
  property pull : Int32
end

class YAMLAttrRecursive
  include YAML::Serializable

  property name : String
  property other : YAMLAttrRecursive
end

class YAMLAttrRecursiveNilable
  include YAML::Serializable
  property name : String
  property other : YAMLAttrRecursiveNilable?
end

class YAMLAttrRecursiveArray
  include YAML::Serializable
  property name : String
  property other : Array(YAMLAttrRecursiveArray)
end

class YAMLAttrRecursiveHash
  include YAML::Serializable
  property name : String
  property other : Hash(String, YAMLAttrRecursiveHash)
end

class YAMLAttrWithDefaults
  include YAML::Serializable

  property a = 11
  property b = "Haha"
  property c = true
  property d = false
  property e : Bool? = false
  property f : Int32? = 1
  property g : Int32?
  property h = [1, 2, 3]
end

class YAMLAttrWithSmallIntegers
  include YAML::Serializable

  property foo : Int16
  property bar : Int8
end

class YAMLAttrWithTimeEpoch
  include YAML::Serializable

  @[YAML::Field(converter: Time::EpochConverter)]
  property value : Time
end

class YAMLAttrNilableWithTimeEpoch
  include YAML::Serializable

  @[YAML::Field(converter: Time::EpochConverter)]
  property value : Time?
end

class YAMLAttrDefaultWithTimeEpoch
  include YAML::Serializable

  @[YAML::Field(converter: Time::EpochConverter)]
  property value : Time = Time.unix(0)
end

class YAMLAttrWithTimeEpochMillis
  include YAML::Serializable

  @[YAML::Field(converter: Time::EpochMillisConverter)]
  property value : Time
end

class YAMLAttrWithPresence
  include YAML::Serializable

  @[YAML::Field(presence: true)]
  property first_name : String?

  @[YAML::Field(presence: true)]
  property last_name : String?

  @[YAML::Field(ignore: true)]
  getter? first_name_present : Bool

  @[YAML::Field(ignore: true)]
  getter? last_name_present : Bool
end

class YAMLAttrWithPresenceAndIgnoreSerialize
  include YAML::Serializable

  @[YAML::Field(presence: true, ignore_serialize: ignore_first_name?)]
  property first_name : String?

  @[YAML::Field(presence: true, ignore_serialize: last_name.nil? && !last_name_present?, emit_null: true)]
  property last_name : String?

  @[YAML::Field(ignore: true)]
  getter? first_name_present : Bool = false

  @[YAML::Field(ignore: true)]
  getter? last_name_present : Bool = false

  def initialize(@first_name : String? = nil, @last_name : String? = nil)
  end

  def ignore_first_name?
    first_name.nil? || first_name == ""
  end
end

class YAMLAttrWithQueryAttributes
  include YAML::Serializable

  property? foo : Bool

  @[YAML::Field(key: "is_bar", presence: true)]
  property? bar : Bool = false

  @[YAML::Field(ignore: true)]
  getter? bar_present : Bool
end

private class YAMLAttrWithFinalize
  include YAML::Serializable
  include FinalizeCounter

  property value : YAML::Any

  @[YAML::Field(ignore: true)]
  property key : String?
end

module YAMLAttrModule
  property moo : Int32 = 10
end

class YAMLAttrModuleTest
  include YAMLAttrModule
  include YAML::Serializable

  @[YAML::Field(key: "phoo")]
  property foo = 15

  def initialize; end

  def to_tuple
    {@moo, @foo}
  end
end

class YAMLAttrModuleTest2 < YAMLAttrModuleTest
  property bar : Int32

  def initialize(@bar : Int32); end

  def to_tuple
    {@moo, @foo, @bar}
  end
end

module YAMLAttrModuleWithSameNameClass
  class YAMLAttrModuleWithSameNameClass
  end

  class Test
    include YAML::Serializable

    property foo = 42
  end
end

struct YAMLAttrWithGenericConverter(T)
  include YAML::Serializable

  @[YAML::Field(converter: T)]
  property value : Time
end

abstract class YAMLShape
  include YAML::Serializable

  use_yaml_discriminator "type", {point: YAMLPoint, circle: YAMLCircle}

  property type : String
end

class YAMLPoint < YAMLShape
  property x : Int32
  property y : Int32
end

class YAMLCircle < YAMLShape
  property x : Int32
  property y : Int32
  property radius : Int32
end

module YAMLNamespace
  struct FooRequest
    include YAML::Serializable

    getter foo : Foo
    getter bar = Bar.new
  end

  struct Foo
    include YAML::Serializable
    getter id = "id:foo"
  end

  struct Bar
    include YAML::Serializable
    getter id = "id:bar"

    def initialize # Allow for default value above
    end
  end
end

enum YAMLVariableDiscriminatorEnumFoo
  Foo = 4
end

enum YAMLVariableDiscriminatorEnumFoo8 : UInt8
  Foo = 1_8
end

class YAMLVariableDiscriminatorValueType
  include YAML::Serializable

  use_yaml_discriminator "type", {
                                         0 => YAMLVariableDiscriminatorNumber,
    "1"                                    => YAMLVariableDiscriminatorString,
    true                                   => YAMLVariableDiscriminatorBool,
    YAMLVariableDiscriminatorEnumFoo::Foo  => YAMLVariableDiscriminatorEnum,
    YAMLVariableDiscriminatorEnumFoo8::Foo => YAMLVariableDiscriminatorEnum8,
  }
end

class YAMLVariableDiscriminatorNumber < YAMLVariableDiscriminatorValueType
end

class YAMLVariableDiscriminatorString < YAMLVariableDiscriminatorValueType
end

class YAMLVariableDiscriminatorBool < YAMLVariableDiscriminatorValueType
end

class YAMLVariableDiscriminatorEnum < YAMLVariableDiscriminatorValueType
end

class YAMLVariableDiscriminatorEnum8 < YAMLVariableDiscriminatorValueType
end

class YAMLStrictDiscriminator
  include YAML::Serializable
  include YAML::Serializable::Strict

  property type : String

  use_yaml_discriminator "type", {foo: YAMLStrictDiscriminatorFoo, bar: YAMLStrictDiscriminatorBar}
end

class YAMLStrictDiscriminatorFoo < YAMLStrictDiscriminator
end

class YAMLStrictDiscriminatorBar < YAMLStrictDiscriminator
  property x : YAMLStrictDiscriminator
  property y : YAMLStrictDiscriminator
end

class YAMLSomething
  include YAML::Serializable

  property value : YAMLSomething?
end

module YAMLDiscriminatorBug
  abstract class Base
    include YAML::Serializable

    use_yaml_discriminator("type", {"a" => A, "b" => B, "c" => C})
  end

  class A < Base
  end

  class B < Base
    property source : Base
    property value : Int32 = 1
  end

  class C < B
  end
end

class YAMLInitializeOpts
  include YAML::Serializable

  property value : Int32

  def initialize(**opts)
    @value = opts.size
  end
end

record Namespaced::YAML::Wrapper, name : String, options : Hash(String, ::YAML::Any::Type)? = nil do
  include ::YAML::Serializable
end

describe "YAML::Serializable" do
  it "works with classes within `YAML` namespace" do
    Namespaced::YAML::Wrapper
      .from_yaml(<<-YAML)
          name: foo
          options:
            foo: true
        YAML
      .to_yaml
  end

  it "works with record" do
    YAMLAttrPoint.new(1, 2).to_yaml.should eq "---\nx: 1\ny: 2\n"
    YAMLAttrPoint.from_yaml("---\nx: 1\ny: 2\n").should eq YAMLAttrPoint.new(1, 2)
  end

  it "works with anchors of value types" do
    YAMLAttrPoint.from_yaml(<<-YAML).should eq YAMLAttrPoint.new(123, 123)
      ---
      x: &foo 123
      y: *foo
      YAML
  end

  it "empty class" do
    e = YAMLAttrEmptyClass.new
    e.to_yaml.should eq "--- {}\n"
    YAMLAttrEmptyClass.from_yaml("---\n")
  end

  it "empty class with unmapped" do
    YAMLAttrEmptyClassWithUnmapped.from_yaml("---\nname: John\nage: 30\n").yaml_unmapped.should eq({"name" => "John", "age" => 30})
  end

  it "parses person" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\nage: 30\n")
    person.should be_a(YAMLAttrPerson)
    person.name.should eq("John")
    person.age.should eq(30)
  end

  it "parses person without age" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\n")
    person.should be_a(YAMLAttrPerson)
    person.name.should eq("John")
    person.name.size.should eq(4) # This verifies that name is not nilable
    person.age.should be_nil
  end

  it "parses person with blank age" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\nage:\n")
    person.should be_a(YAMLAttrPerson)
    person.name.should eq("John")
    person.name.size.should eq(4) # This verifies that name is not nilable
    person.age.should be_nil
  end

  it "parses array of people" do
    people = Array(YAMLAttrPerson).from_yaml("---\n- name: John\n- name: Doe\n")
    people.size.should eq(2)
    people[0].name.should eq("John")
    people[1].name.should eq("Doe")
  end

  it "parses array of people with merge" do
    yaml = <<-YAML
      - &1
        name: foo
        age: 1
      -
        <<: *1
        age: 2
      YAML

    people = Array(YAMLAttrPerson).from_yaml(yaml)
    people[1].name.should eq("foo")
    people[1].age.should eq(2)
  end

  it "parses array of people with merge, doesn't hang on infinite recursion" do
    yaml = <<-YAML
      - &1
        name: foo
        <<: *1
        <<: [ *1, *1 ]
        age: 1
      YAML

    people = Array(YAMLAttrPerson).from_yaml(yaml)
    people[0].name.should eq("foo")
    people[0].age.should eq(1)
  end

  it "works with class with three fields" do
    person1 = YAMLAttrPersonWithThreeFieldInInitialize.from_yaml("---\nname: John\nbla: 1\nage: 30\n")
    person2 = YAMLAttrPersonWithThreeFieldInInitialize.new("John", 1, 30)
    person1.should eq person2
  end

  it "parses person with unknown attributes" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\nunknown: [1, 2, 3]\nage: 30\n")
    person.should be_a(YAMLAttrPerson)
    person.name.should eq("John")
    person.age.should eq(30)
  end

  it "parses strict person with unknown attributes" do
    ex = expect_raises YAML::ParseException, "Unknown yaml attribute: foo" do
      StrictYAMLAttrPerson.from_yaml <<-YAML
        ---
        name: John
        foo: [1, 2, 3]
        age: 30
        YAML
    end
    ex.location.should eq({3, 1})
  end

  it "works with selective serialization" do
    person = YAMLAttrPersonWithSelectiveSerialization.new("Vasya", "P@ssw0rd")
    person.to_yaml.should eq "---\nname: Vasya\ngenerated: generated-internally\n"

    person_yaml = "---\nname: Vasya\ngenerated: should not set\npassword: update\n"
    person = YAMLAttrPersonWithSelectiveSerialization.from_yaml(person_yaml)
    person.generated.should eq "generated-internally"
    person.password.should eq "update"
  end

  it "does to_yaml" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\nage: 30\n")
    person2 = YAMLAttrPerson.from_yaml(person.to_yaml)
    person2.should eq(person)
  end

  it "doesn't emit null when doing to_yaml" do
    person = YAMLAttrPerson.from_yaml("---\nname: John\n")
    person.to_yaml.should_not match /age/
  end

  it "raises if non-nilable attribute is nil" do
    error_message = "Missing YAML attribute: name at line 2, column 1"
    ex = expect_raises YAML::ParseException, error_message do
      YAMLAttrPerson.from_yaml <<-YAML
        ---
        age: 30
        YAML
    end
    ex.location.should eq({2, 1})
  end

  it "doesn't raises on false value when not-nil" do
    yaml = YAMLAttrValue(Bool).from_yaml("---\nvalue: false\n")
    yaml.value.should be_false
  end

  it "should parse extra fields (YAMLAttrPersonExtraFields with on_unknown_yaml_attribute)" do
    person = YAMLAttrPersonExtraFields.from_yaml(<<-YAML)
        ---
        name: John
        x: 1
        y: "1-2"
        age: 30
        z:
          - 1
          - 2
          - 3
        YAML
    person.name.should eq("John")
    person.age.should eq(30)
    person.yaml_unmapped.should eq({"x" => 1_i64, "y" => "1-2", "z" => [1_i64, 2_i64, 3_i64]})
  end

  it "should to store extra fields (YAMLAttrPersonExtraFields with on_to_yaml)" do
    person = YAMLAttrPersonExtraFields.from_yaml(<<-YAML)
        ---
        name: John
        x: 1
        y: "1-2"
        age: 30
        z:
          - 1
          - 2
          - 3
        YAML
    person.name = "John1"
    person.yaml_unmapped.delete("y")
    person.yaml_unmapped["q"] = YAML::Any.new("w")
    person.to_yaml.should eq "---\nname: John1\nage: 30\nx: 1\nz:\n- 1\n- 2\n- 3\nq: w\n"
  end

  it "raises if not an object" do
    error_message = "Expected mapping, not YAML::Nodes::Scalar at line 1, column 1"
    ex = expect_raises YAML::ParseException, error_message do
      StrictYAMLAttrPerson.from_yaml <<-YAML
        "foo"
        YAML
    end
    ex.location.should eq({1, 1})
  end

  it "raises if data type does not match" do
    error_message = "Couldn't parse (Int32 | Nil) at line 3, column 10"
    ex = expect_raises YAML::ParseException, error_message do
      StrictYAMLAttrPerson.from_yaml <<-YAML
        {
          "name": "John",
          "age": "foo",
          "foo": "bar"
        }
        YAML
    end
    ex.location.should eq({3, 10})
  end

  it "emits null on request when doing to_yaml" do
    person = YAMLAttrPersonEmittingNull.from_yaml("---\nname: John\n")
    person.to_yaml.should match /age/
  end

  it "emit_nulls option" do
    person = YAMLAttrPersonEmittingNullsByOptions.from_yaml("---\nname: John\n")
    person.to_yaml.should match /\A---\nname: John\nage: ?\nvalue1: ?\n\z/
  end

  it "parses yaml with Time::Format converter" do
    yaml = YAMLAttrWithTime.from_yaml("---\nvalue: 2014-10-31 23:37:16\n")
    yaml.value.should be_a(Time)
    yaml.value.to_s.should eq("2014-10-31 23:37:16 UTC")
    yaml.value.should eq(Time.utc(2014, 10, 31, 23, 37, 16))
    yaml.to_yaml.should eq("---\nvalue: 2014-10-31 23:37:16\n")
  end

  it "allows setting a nilable property to nil" do
    person = YAMLAttrPerson.new("John")
    person.age = 1
    person.age = nil
  end

  it "parses simple mapping" do
    person = YAMLAttrWithSimpleMapping.from_yaml("---\nname: John\nage: 30\n")
    person.should be_a(YAMLAttrWithSimpleMapping)
    person.name.should eq("John")
    person.age.should eq(30)
  end

  it "outputs with converter when nilable" do
    yaml = YAMLAttrWithNilableTime.new
    yaml.to_yaml.should eq("--- {}\n")
  end

  it "outputs with converter when nilable when emit_null is true" do
    yaml = YAMLAttrWithNilableTimeEmittingNull.new
    yaml.to_yaml.should match(/\A---\nvalue: ?\n\z/)
  end

  it "outputs YAML with Hash" do
    input = {
      value: {"foo" => "bar"},
    }.to_yaml
    yaml = YAMLAttrValue(Hash(String, String)).from_yaml(input)
    yaml.to_yaml.should eq(input)
  end

  it "parses yaml with keywords" do
    yaml = YAMLAttrWithKeywordsMapping.from_yaml(%({"end": 1, "abstract": 2}))
    yaml.end.should eq(1)
    yaml.abstract.should eq(2)
  end

  it "parses yaml with any" do
    yaml = YAMLAttrValue(YAML::Any).from_yaml("value: hello")
    yaml.value.as_s.should eq("hello")

    yaml = YAMLAttrValue(YAML::Any).from_yaml({:value => ["foo", "bar"]}.to_yaml)
    yaml.value[1].as_s.should eq("bar")

    yaml = YAMLAttrValue(YAML::Any).from_yaml({:value => {:foo => :bar}}.to_yaml)
    yaml.value["foo"].as_s.should eq("bar")

    yaml = YAMLAttrValue(YAML::Any).from_yaml("extra: &foo hello\nvalue: *foo")
    yaml.value.as_s.should eq("hello")

    expect_raises YAML::ParseException, "Unknown anchor 'foo' at line 1, column 8" do
      YAMLAttrValue(YAML::Any).from_yaml("value: *foo")
    end
  end

  it "parses yaml with problematic keys" do
    yaml = YAMLAttrWithProblematicKeys.from_yaml(%({"key": 1, "pull": 2}))
    yaml.key.should eq(1)
    yaml.pull.should eq(2)
  end

  it "allows small types of integer" do
    yaml = YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21, "bar": 7}))

    yaml.foo.should eq(21)
    typeof(yaml.foo).should eq(Int16)

    yaml.bar.should eq(7)
    typeof(yaml.bar).should eq(Int8)
  end

  it "checks that values fit into integer types" do
    expect_raises(YAML::ParseException, /Can't read Int16/) do
      YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21000000, "bar": 7}))
    end

    expect_raises(YAML::ParseException, /Can't read Int8/) do
      YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21, "bar": 7000}))
    end
  end

  it "checks that non-integer values for integer fields report the expected type" do
    expect_raises(YAML::ParseException, /Can't read Int16/) do
      YAMLAttrWithSmallIntegers.from_yaml(%({"foo": "a", "bar": 7}))
    end

    expect_raises(YAML::ParseException, /Can't read Int8/) do
      YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21, "bar": "a"}))
    end
  end

  it "parses recursive" do
    yaml = <<-YAML
      --- &1
      name: foo
      other: *1
      YAML

    rec = YAMLAttrRecursive.from_yaml(yaml)
    rec.name.should eq("foo")
    rec.other.should be(rec)
  end

  it "parses recursive nilable (1)" do
    yaml = <<-YAML
      --- &1
      name: foo
      other: *1
      YAML

    rec = YAMLAttrRecursiveNilable.from_yaml(yaml)
    rec.name.should eq("foo")
    rec.other.should be(rec)
  end

  it "parses recursive nilable (2)" do
    yaml = <<-YAML
      --- &1
      name: foo
      YAML

    rec = YAMLAttrRecursiveNilable.from_yaml(yaml)
    rec.name.should eq("foo")
    rec.other.should be_nil
  end

  it "parses recursive array" do
    yaml = <<-YAML
      ---
      name: foo
      other: &1
        - name: bar
          other: *1
      YAML

    rec = YAMLAttrRecursiveArray.from_yaml(yaml)
    rec.other[0].other.should be(rec.other)
  end

  it "parses recursive hash" do
    yaml = <<-YAML
      ---
      name: foo
      other: &1
        foo:
          name: bar
          other: *1
      YAML

    rec = YAMLAttrRecursiveHash.from_yaml(yaml)
    rec.other["foo"].other.should be(rec.other)
  end

  describe "parses yaml with defaults" do
    it "mixed" do
      yaml = YAMLAttrWithDefaults.from_yaml(%({"a":1,"b":"bla"}))
      yaml.a.should eq 1
      yaml.b.should eq "bla"

      yaml = YAMLAttrWithDefaults.from_yaml(%({"a":1}))
      yaml.a.should eq 1
      yaml.b.should eq "Haha"

      yaml = YAMLAttrWithDefaults.from_yaml(%({"b":"bla"}))
      yaml.a.should eq 11
      yaml.b.should eq "bla"

      yaml = YAMLAttrWithDefaults.from_yaml(%({}))
      yaml.a.should eq 11
      yaml.b.should eq "Haha"

      yaml = YAMLAttrWithDefaults.from_yaml(%({"a":null,"b":null,"f":null}))
      yaml.a.should eq 11
      yaml.b.should eq "Haha"
      yaml.f.should be_nil

      yaml = YAMLAttrWithDefaults.from_yaml(%({"b":""}))
      yaml.b.should eq ""
      yaml = YAMLAttrWithDefaults.from_yaml(%({"b":''}))
      yaml.b.should eq ""
      yaml = YAMLAttrWithDefaults.from_yaml(%({"b":}))
      yaml.b.should eq "Haha"
    end

    it "bool" do
      yaml = YAMLAttrWithDefaults.from_yaml(%({}))
      yaml.c.should be_true
      typeof(yaml.c).should eq Bool
      yaml.d.should be_false
      typeof(yaml.d).should eq Bool

      yaml = YAMLAttrWithDefaults.from_yaml(%({"c":false}))
      yaml.c.should be_false
      yaml = YAMLAttrWithDefaults.from_yaml(%({"c":true}))
      yaml.c.should be_true

      yaml = YAMLAttrWithDefaults.from_yaml(%({"d":false}))
      yaml.d.should be_false
      yaml = YAMLAttrWithDefaults.from_yaml(%({"d":true}))
      yaml.d.should be_true
    end

    it "with nilable" do
      yaml = YAMLAttrWithDefaults.from_yaml(%({}))

      yaml.e.should be_false
      typeof(yaml.e).should eq(Bool | Nil)

      yaml.f.should eq 1
      typeof(yaml.f).should eq(Int32 | Nil)

      yaml.g.should be_nil
      typeof(yaml.g).should eq(Int32 | Nil)

      yaml = YAMLAttrWithDefaults.from_yaml(%({"e":false}))
      yaml.e.should be_false
      yaml = YAMLAttrWithDefaults.from_yaml(%({"e":true}))
      yaml.e.should be_true
    end

    it "create new array every time" do
      yaml = YAMLAttrWithDefaults.from_yaml(%({}))
      yaml.h.should eq [1, 2, 3]
      yaml.h << 4
      yaml.h.should eq [1, 2, 3, 4]

      yaml = YAMLAttrWithDefaults.from_yaml(%({}))
      yaml.h.should eq [1, 2, 3]
    end
  end

  it "converter with null value (#13655)" do
    YAMLAttrNilableWithTimeEpoch.from_yaml(%({"value": null})).value.should be_nil
    YAMLAttrNilableWithTimeEpoch.from_yaml(%({"value":1459859781})).value.should eq Time.unix(1459859781)
  end

  it "converter with default value" do
    YAMLAttrDefaultWithTimeEpoch.from_yaml(%({"value": null})).value.should eq Time.unix(0)
    YAMLAttrDefaultWithTimeEpoch.from_yaml(%({"value":1459859781})).value.should eq Time.unix(1459859781)
  end

  it "uses Time::EpochConverter" do
    string = %({"value":1459859781})
    yaml = YAMLAttrWithTimeEpoch.from_yaml(string)
    yaml.value.should be_a(Time)
    yaml.value.should eq(Time.unix(1459859781))
    yaml.to_yaml.should eq("---\nvalue: 1459859781\n")
  end

  it "uses Time::EpochMillisConverter" do
    string = %({"value":1459860483856})
    yaml = YAMLAttrWithTimeEpochMillis.from_yaml(string)
    yaml.value.should be_a(Time)
    yaml.value.should eq(Time.unix_ms(1459860483856))
    yaml.to_yaml.should eq("---\nvalue: 1459860483856\n")
  end

  describe YAML::ArrayConverter do
    it "uses converter metaclass" do
      string = %(---\nvalue:\n- 1459859781\n)
      yaml = YAMLAttrWithTimeArray1.from_yaml(string)
      yaml.value.should be_a(Array(Time))
      yaml.value.should eq([Time.unix(1459859781)])
      yaml.to_yaml.should eq(string)
    end

    it "uses converter instance with nested converter metaclass" do
      string = %(---\nvalue:\n- 1459859781\n)
      yaml = YAMLAttrWithTimeArray2.from_yaml(string)
      yaml.value.should be_a(Array(Time))
      yaml.value.should eq([Time.unix(1459859781)])
      yaml.to_yaml.should eq(string)
    end

    it "uses converter instance with nested converter instance" do
      string = %(---\nvalue:\n- 2014-10-31 23:37:16\n)
      yaml = YAMLAttrWithTimeArray3.from_yaml(string)
      yaml.value.should be_a(Array(Time))
      yaml.value.map(&.to_s).should eq(["2014-10-31 23:37:16 UTC"])
      yaml.to_yaml.should eq(string)
    end

    it "uses correct array element type" do
      string = %(---\nvalue:\n- x: 1\n  y: 2\n)
      yaml = YAMLAttrWithSerializableArray.from_yaml(string)
      yaml.value.should be_a(Array(YAMLAttrPoint))
      yaml.value.should eq([YAMLAttrPoint.new(1, 2)])
      yaml.to_yaml.should eq(string)
    end
  end

  it "parses nilable union" do
    obj = YAMLAttrValue(Int32?).from_yaml(%({"value": 1}))
    obj.value.should eq(1)
    obj.to_yaml.should eq("---\nvalue: 1\n")

    obj = YAMLAttrValue(Int32?).from_yaml(%({"value": null}))
    obj.value.should be_nil
    obj.to_yaml.should eq("--- {}\n")

    obj = YAMLAttrValue(Int32?).from_yaml(%({}))
    obj.value.should be_nil
    obj.to_yaml.should eq("--- {}\n")
  end

  describe "parses YAML with presence markers" do
    it "parses person with absent attributes" do
      yaml = YAMLAttrWithPresence.from_yaml("---\nfirst_name:\n")
      yaml.first_name.should be_nil
      yaml.first_name_present?.should be_true
      yaml.last_name.should be_nil
      yaml.last_name_present?.should be_false
    end
  end

  describe "serializes YAML with presence markers and ignore_serialize" do
    context "ignore_serialize is set to a method which returns true when value is nil or empty string" do
      it "ignores field when value is empty string" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"first_name": ""}))
        yaml.first_name_present?.should be_true
        yaml.to_yaml.should eq("--- {}\n")
      end

      it "ignores field when value is nil" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"first_name": null}))
        yaml.first_name_present?.should be_true
        yaml.to_yaml.should eq("--- {}\n")
      end
    end

    context "ignore_serialize is set to conditional expressions 'last_name.nil? && !last_name_present?'" do
      it "emits null when value is null and @last_name_present is true" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"last_name": null}))
        yaml.last_name_present?.should be_true

        # libyaml 0.2.5 removes trailing space for empty scalar nodes
        if YAML.libyaml_version >= SemanticVersion.new(0, 2, 5)
          yaml.to_yaml.should eq("---\nlast_name:\n")
        else
          yaml.to_yaml.should eq("---\nlast_name: \n")
        end
      end
      it "does not emit null when value is null and @last_name_present is false" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({}))
        yaml.last_name_present?.should be_false
        yaml.to_yaml.should eq("--- {}\n")
      end

      it "emits field when value is not nil and @last_name_present is false" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.new(last_name: "something")
        yaml.last_name_present?.should be_false
        yaml.to_yaml.should eq("---\nlast_name: something\n")
      end

      it "emits field when value is not nil and @last_name_present is true" do
        yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"last_name":"something"}))
        yaml.last_name_present?.should be_true
        yaml.to_yaml.should eq("---\nlast_name: something\n")
      end
    end
  end

  describe "with query attributes" do
    it "defines query getter" do
      yaml = YAMLAttrWithQueryAttributes.from_yaml(%({"foo": true}))
      yaml.foo?.should be_true
      yaml.bar?.should be_false
    end

    it "defines query getter with class restriction" do
      {% begin %}
        {% methods = YAMLAttrWithQueryAttributes.methods %}
        {{ methods.find(&.name.==("foo?")).return_type }}.should eq(Bool)
        {{ methods.find(&.name.==("bar?")).return_type }}.should eq(Bool)
      {% end %}
    end

    it "defines non-query setter and presence methods" do
      yaml = YAMLAttrWithQueryAttributes.from_yaml(%({"foo": false}))
      yaml.bar_present?.should be_false
      yaml.bar = true
      yaml.bar?.should be_true
    end

    it "maps non-query attributes" do
      yaml = YAMLAttrWithQueryAttributes.from_yaml(%({"foo": false, "is_bar": false}))
      yaml.bar_present?.should be_true
      yaml.bar?.should be_false
      yaml.bar = true
      yaml.to_yaml.should eq("---\nfoo: false\nis_bar: true\n")
    end

    it "raises if non-nilable attribute is nil" do
      error_message = "Missing YAML attribute: foo at line 1, column 1"
      ex = expect_raises YAML::ParseException, error_message do
        YAMLAttrWithQueryAttributes.from_yaml(%({"is_bar": true}))
      end
      ex.location.should eq({1, 1})
    end
  end

  it "calls #finalize" do
    assert_finalizes("yaml") { YAMLAttrWithFinalize.from_yaml("---\nvalue: 1\n") }
  end

  describe "work with module and inheritance" do
    it { YAMLAttrModuleTest.from_yaml(%({"phoo": 20})).to_tuple.should eq({10, 20}) }
    it { YAMLAttrModuleTest.from_yaml(%({"phoo": 20})).to_tuple.should eq({10, 20}) }
    it { YAMLAttrModuleTest2.from_yaml(%({"phoo": 20, "bar": 30})).to_tuple.should eq({10, 20, 30}) }
    it { YAMLAttrModuleTest2.from_yaml(%({"bar": 30, "moo": 40})).to_tuple.should eq({40, 15, 30}) }
  end

  describe "work with inned class using same module name" do
    it { YAMLAttrModuleWithSameNameClass::Test.from_yaml(%({"foo": 42})).foo.should eq(42) }
  end

  describe "use_yaml_discriminator" do
    it "deserializes with discriminator" do
      point = YAMLShape.from_yaml(%({"type": "point", "x": 1, "y": 2})).as(YAMLPoint)
      point.x.should eq(1)
      point.y.should eq(2)

      circle = YAMLShape.from_yaml(%({"type": "circle", "x": 1, "y": 2, "radius": 3})).as(YAMLCircle)
      circle.x.should eq(1)
      circle.y.should eq(2)
      circle.radius.should eq(3)
    end

    it "raises if missing discriminator" do
      expect_raises(YAML::ParseException, "Missing YAML discriminator field 'type'") do
        YAMLShape.from_yaml("{}")
      end
    end

    it "raises if unknown discriminator value" do
      expect_raises(YAML::ParseException, %(Unknown 'type' discriminator value: "unknown")) do
        YAMLShape.from_yaml(%({"type": "unknown"}))
      end
    end

    it "deserializes type which nests type with discriminator (#9849)" do
      container = YAMLAttrValue(YAMLShape).from_yaml(%({"value": {"type": "point", "x": 1, "y": 2}}))
      point = container.value.as(YAMLPoint)
      point.x.should eq(1)
      point.y.should eq(2)
    end

    it "deserializes with variable discriminator value type" do
      object_number = YAMLVariableDiscriminatorValueType.from_yaml(%({"type": 0}))
      object_number.should be_a(YAMLVariableDiscriminatorNumber)

      object_string = YAMLVariableDiscriminatorValueType.from_yaml(%({"type": "1"}))
      object_string.should be_a(YAMLVariableDiscriminatorString)

      object_bool = YAMLVariableDiscriminatorValueType.from_yaml(%({"type": true}))
      object_bool.should be_a(YAMLVariableDiscriminatorBool)

      object_enum = YAMLVariableDiscriminatorValueType.from_yaml(%({"type": 4}))
      object_enum.should be_a(YAMLVariableDiscriminatorEnum)

      object_enum = YAMLVariableDiscriminatorValueType.from_yaml(%({"type": 18}))
      object_enum.should be_a(YAMLVariableDiscriminatorEnum8)
    end

    it "deserializes with discriminator, strict recursive type" do
      foo = YAMLStrictDiscriminator.from_yaml(%({"type": "foo"}))
      foo = foo.should be_a(YAMLStrictDiscriminatorFoo)

      bar = YAMLStrictDiscriminator.from_yaml(%({"type": "bar", "x": {"type": "foo"}, "y": {"type": "foo"}}))
      bar = bar.should be_a(YAMLStrictDiscriminatorBar)
      bar.x.should be_a(YAMLStrictDiscriminatorFoo)
      bar.y.should be_a(YAMLStrictDiscriminatorFoo)
    end

    it "deserializes with discriminator, another recursive type, fixes: #13429" do
      c = YAMLDiscriminatorBug::Base.from_yaml %q({"type": "c", "source": {"type": "a"}, "value": 2})
      c.as(YAMLDiscriminatorBug::C).value.should eq 2

      c = YAMLDiscriminatorBug::Base.from_yaml %q({"type": "c", "source": {"type": "a"}})
      c.as(YAMLDiscriminatorBug::C).value.should eq 1
    end
  end

  describe "namespaced classes" do
    it "lets default values use the object's own namespace" do
      request = YAMLNamespace::FooRequest.from_yaml(%({"foo":{}}))
      request.foo.id.should eq "id:foo"
      request.bar.id.should eq "id:bar"
    end
  end

  it "fixes #13337" do
    YAMLSomething.from_yaml(%({"value":{}})).value.should_not be_nil
  end

  it "works when type has constructor with double splat parameter (#16140)" do
    YAMLInitializeOpts.from_yaml(%({"value":123})).value.should eq(123)
  end

  it "supports generic type variables in converters" do
    YAMLAttrWithGenericConverter(Time::EpochConverter).from_yaml(%({"value":1459859781})).value.should eq(Time.unix(1459859781))
  end
end
