require 'spec_helper'
require 'parametric/struct'

describe Parametric::Struct do
  it "works" do
    friend_class = Class.new do
      include Parametric::Struct

      schema do
        field(:name).type(:string).present
        field(:age).type(:integer)
      end
    end

    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).default([]).schema friend_class
      end
    end

    new_instance = klass.new
    expect(new_instance.title).to eq ''
    expect(new_instance.friends).to eq []
    expect(new_instance.valid?).to be false
    expect(new_instance.errors['$.title']).not_to be_nil

    instance = klass.new({
      title: 'foo',
      friends: [
        {name: 'Ismael', age: 40},
        {name: 'Joe', age: 39},
      ]
    })

    expect(instance.title).to eq 'foo'
    expect(instance.friends.size).to eq 2
    expect(instance.friends.first.name).to eq 'Ismael'
    expect(instance.friends.first).to be_a friend_class

    # Hash access with #[]
    expect(instance[:title]).to eq instance.title
    expect(instance[:friends].first[:name]).to eq instance.friends.first.name

    invalid_instance = klass.new({
      friends: [
        {name: 'Ismael', age: 40},
        {age: 39},
      ]
    })

    expect(invalid_instance.valid?).to be false
    expect(invalid_instance.errors['$.title']).not_to be_nil
    expect(invalid_instance.errors['$.friends[1].name']).not_to be_nil
    expect(invalid_instance.friends[1].errors['$.name']).not_to be_nil
  end

  it "is inmutable by default" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).default([])
        field(:friend).type(:object)
      end
    end

    instance = klass.new
    expect {
      instance.title = "foo"
    }.to raise_error NoMethodError

    expect {
      instance.friends << 1
    }.to raise_error RuntimeError
  end

  it "works with anonymous nested schemas" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).schema do
          field(:age).type(:integer)
        end
      end
    end

    instance = klass.new({
      title: 'foo',
      friends: [
        {age: 10},
        {age: 39},
      ]
    })

    expect(instance.title).to eq 'foo'
    expect(instance.friends.size).to eq 2
    expect(instance.friends.first.age).to eq 10
  end

  it "wraps regular schemas in structs" do
    friend_schema = Parametric::Schema.new do
      field(:name)
    end

    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).schema friend_schema
      end
    end

    instance = klass.new({
      title: 'foo',
      friends: [{name: 'Ismael'}]
    })

    expect(instance.friends.first.name).to eq 'Ismael'
  end

  it "#to_h" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).schema do
          field(:name).type(:string)
          field(:age).type(:integer).default(20)
        end
      end
    end

    instance = klass.new({
      title: 'foo',
      friends: [
        {name: 'Jane'},
        {name: 'Joe', age: '39'},
      ]
    })

    expect(instance.to_h).to eq({
      title: 'foo',
      friends: [
        {name: 'Jane', age: 20},
        {name: 'Joe', age: 39},
      ]
    })

    new_instance = klass.new(instance.to_h)
    expect(new_instance.title).to eq 'foo'

    # it returns a copy so we can't break things!
    data = new_instance.to_h
    data[:title] = 'nope'
    expect(new_instance.to_h[:title]).to eq 'foo'
  end

  it "works with inheritance" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).schema do
          field(:name).type(:string)
          field(:age).type(:integer).default(20)
        end
      end
    end

    subclass = Class.new(klass) do
      schema do
        field(:email)
      end
    end

    instance = subclass.new(
      title: 'foo',
      email: 'email@me.com',
      friends: [
        {name: 'Jane', age: 20},
        {name: 'Joe', age: 39},
      ]
    )

    expect(instance.title).to eq 'foo'
    expect(instance.email).to eq 'email@me.com'
    expect(instance.friends.size).to eq 2
  end

  it "implements deep struct equality" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:friends).type(:array).schema do
          field(:age).type(:integer)
        end
      end
    end

    s1 = klass.new({
      title: 'foo',
      friends: [
        {age: 10},
        {age: 39},
      ]
    })


    s2 = klass.new({
      title: 'foo',
      friends: [
        {age: 10},
        {age: 39},
      ]
    })

    s3 = klass.new({
      title: 'foo',
      friends: [
        {age: 11},
        {age: 39},
      ]
    })

    s4 = klass.new({
      title: 'bar',
      friends: [
        {age: 10},
        {age: 39},
      ]
    })

    expect(s1 == s2).to be true
    expect(s1 == s3).to be false
    expect(s1 == s4).to be false
  end

  it "#merge returns a new instance" do
    klass = Class.new do
      include Parametric::Struct

      schema do
        field(:title).type(:string).present
        field(:desc)
        field(:friends).type(:array).schema do
          field(:name).type(:string)
        end
      end
    end

    original = klass.new(
      title: 'foo',
      desc: 'no change',
      friends: [{name: 'joe'}]
    )

    copy = original.merge(
      title: 'bar',
      friends: [{name: 'jane'}]
    )

    expect(original.title).to eq 'foo'
    expect(original.desc).to eq 'no change'
    expect(original.friends.first.name).to eq 'joe'

    expect(copy.title).to eq 'bar'
    expect(copy.desc).to eq 'no change'
    expect(copy.friends.first.name).to eq 'jane'
  end

  describe '.new!' do
    it 'raises a useful exception if invalid data' do
      klass = Class.new do
        include Parametric::Struct

        schema do
          field(:title).type(:string).present
        end
      end

      begin
        klass.new!(title: '')
      rescue Parametric::InvalidStructError => e
        expect(e.errors['$.title']).not_to be nil
      end

      valid = klass.new!(title: 'foo')
      expect(valid.title).to eq 'foo'
    end
  end
end
