require_relative '../../spec_helper'
require_relative 'fixtures/classes'

describe "Struct.new" do
  it "creates a constant in Struct namespace with string as first argument" do
    struct = Struct.new('Animal', :name, :legs, :eyeballs)
    struct.should == Struct::Animal
    struct.name.should == "Struct::Animal"
  end

  it "overwrites previously defined constants with string as first argument" do
    first = Struct.new('Person', :height, :weight)
    first.should == Struct::Person

    second = nil
    -> {
      second = Struct.new('Person', :hair, :sex)
    }.should complain(/constant/)
    second.should == Struct::Person

    first.members.should_not == second.members
  end

  it "calls to_str on its first argument (constant name)" do
    obj = mock('Foo')
    def obj.to_str() "Foo" end
    struct = Struct.new(obj)
    struct.should == Struct::Foo
    struct.name.should == "Struct::Foo"
  end

  it "creates a new anonymous class with nil first argument" do
    struct = Struct.new(nil, :foo)
    struct.new("bar").foo.should == "bar"
    struct.should be_kind_of(Class)
    struct.name.should be_nil
  end

  it "creates a new anonymous class with symbol arguments" do
    struct = Struct.new(:make, :model)
    struct.should be_kind_of(Class)
    struct.name.should == nil
  end

  it "does not create a constant with symbol as first argument" do
    Struct.new(:Animal2, :name, :legs, :eyeballs)
    Struct.const_defined?("Animal2").should be_false
  end

  it "allows non-ASCII member name" do
    name = "r\xe9sum\xe9".force_encoding(Encoding::ISO_8859_1).to_sym
    struct = Struct.new(name)
    struct.new("foo").send(name).should == "foo"
  end

  it "fails with invalid constant name as first argument" do
    -> { Struct.new('animal', :name, :legs, :eyeballs) }.should raise_error(NameError)
  end

  it "raises a TypeError if object doesn't respond to to_sym" do
    -> { Struct.new(:animal, mock('giraffe'))      }.should raise_error(TypeError)
    -> { Struct.new(:animal, 1.0)                  }.should raise_error(TypeError)
    -> { Struct.new(:animal, Time.now)             }.should raise_error(TypeError)
    -> { Struct.new(:animal, Class)                }.should raise_error(TypeError)
    -> { Struct.new(:animal, nil)                  }.should raise_error(TypeError)
    -> { Struct.new(:animal, true)                 }.should raise_error(TypeError)
    -> { Struct.new(:animal, ['chris', 'evan'])    }.should raise_error(TypeError)
  end

  ruby_version_is ""..."3.2" do
    it "raises a TypeError or ArgumentError if passed a Hash with an unknown key" do
      # CRuby < 3.2 raises ArgumentError: unknown keyword: :name, but that seems a bug:
      # https://bugs.ruby-lang.org/issues/18632
      -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(StandardError) { |e|
        [ArgumentError, TypeError].should.include?(e.class)
      }
    end
  end

  ruby_version_is "3.2" do
    it "raises a TypeError if passed a Hash with an unknown key" do
      -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(TypeError)
    end
  end

  ruby_version_is ""..."3.3" do
    it "raises ArgumentError if not provided any arguments" do
      -> { Struct.new }.should raise_error(ArgumentError)
    end
  end

  ruby_version_is "3.3" do
    it "works when not provided any arguments" do
      c = Struct.new
      c.should be_kind_of(Class)
      c.superclass.should == Struct
    end
  end

  it "raises ArgumentError when there is a duplicate member" do
    -> { Struct.new(:foo, :foo) }.should raise_error(ArgumentError, "duplicate member: foo")
  end

  it "raises a TypeError if object is not a Symbol" do
    obj = mock(':ruby')
    def obj.to_sym() :ruby end
    -> { Struct.new(:animal, obj) }.should raise_error(TypeError)
  end

  it "processes passed block with instance_eval" do
    klass = Struct.new(:something) { @something_else = 'something else entirely!' }
    klass.instance_variables.should include(:@something_else)
  end

  context "with a block" do
    it "allows class to be modified via the block" do
      klass = Struct.new(:version) do
        def platform
          :ruby
        end
      end
      instance = klass.new('2.2')

      instance.version.should == '2.2'
      instance.platform.should == :ruby
    end

    it "passes same struct class to the block" do
      given = nil
      klass = Struct.new(:attr) do |block_parameter|
        given = block_parameter
      end
      klass.should equal(given)
    end
  end

  context "on subclasses" do
    it "creates a constant in subclass' namespace" do
      struct = StructClasses::Apple.new('Computer', :size)
      struct.should == StructClasses::Apple::Computer
    end

    it "creates an instance" do
      StructClasses::Ruby.new.kind_of?(StructClasses::Ruby).should == true
    end

    it "creates reader methods" do
      StructClasses::Ruby.new.should have_method(:version)
      StructClasses::Ruby.new.should have_method(:platform)
    end

    it "creates writer methods" do
      StructClasses::Ruby.new.should have_method(:version=)
      StructClasses::Ruby.new.should have_method(:platform=)
    end

    it "fails with too many arguments" do
      -> { StructClasses::Ruby.new('2.0', 'i686', true) }.should raise_error(ArgumentError)
    end

    ruby_version_is ''...'3.1' do
      it "passes a hash as a normal argument" do
        type = Struct.new(:args)

        obj = suppress_warning {type.new(keyword: :arg)}
        obj2 = type.new(*[{keyword: :arg}])

        obj.should == obj2
        obj.args.should == {keyword: :arg}
        obj2.args.should == {keyword: :arg}
      end
    end

    ruby_version_is '3.2' do
      it "accepts keyword arguments to initialize" do
        type = Struct.new(:args)

        obj = type.new(args: 42)
        obj2 = type.new(42)

        obj.should == obj2
        obj.args.should == 42
        obj2.args.should == 42
      end
    end
  end

  context "keyword_init: true option" do
    before :all do
      @struct_with_kwa = Struct.new(:name, :legs, keyword_init: true)
    end

    it "creates a class that accepts keyword arguments to initialize" do
      obj = @struct_with_kwa.new(name: "elefant", legs: 4)
      obj.name.should == "elefant"
      obj.legs.should == 4
    end

    it "raises when there is a duplicate member" do
      -> { Struct.new(:foo, :foo, keyword_init: true) }.should raise_error(ArgumentError, "duplicate member: foo")
    end

    describe "new class instantiation" do
      it "accepts arguments as hash as well" do
        obj = @struct_with_kwa.new({name: "elefant", legs: 4})
        obj.name.should == "elefant"
        obj.legs.should == 4
      end

      it "allows missing arguments" do
        obj = @struct_with_kwa.new(name: "elefant")
        obj.name.should == "elefant"
        obj.legs.should be_nil
      end

      it "allows no arguments" do
        obj = @struct_with_kwa.new
        obj.name.should be_nil
        obj.legs.should be_nil
      end

      it "raises ArgumentError when passed not declared keyword argument" do
        -> {
          @struct_with_kwa.new(name: "elefant", legs: 4, foo: "bar")
        }.should raise_error(ArgumentError, /unknown keywords: foo/)
      end

      it "raises ArgumentError when passed a list of arguments" do
        -> {
          @struct_with_kwa.new("elefant", 4)
        }.should raise_error(ArgumentError, /wrong number of arguments/)
      end

      it "raises ArgumentError when passed a single non-hash argument" do
        -> {
          @struct_with_kwa.new("elefant")
        }.should raise_error(ArgumentError, /wrong number of arguments/)
      end
    end
  end

  context "keyword_init: false option" do
    before :all do
      @struct_without_kwa = Struct.new(:name, :legs, keyword_init: false)
    end

    it "behaves like it does without :keyword_init option" do
      obj = @struct_without_kwa.new("elefant", 4)
      obj.name.should == "elefant"
      obj.legs.should == 4
    end
  end
end
