require_relative '../spec_helper'
require_relative '../fixtures/class'

ClassSpecsNumber = 12

module ClassSpecs
  Number = 12
end

describe "The class keyword" do
  it "creates a new class with semicolon" do
    class ClassSpecsKeywordWithSemicolon; end
    ClassSpecsKeywordWithSemicolon.should.instance_of?(Class)
  end

  it "does not raise a SyntaxError when opening a class without a semicolon" do
    eval "class ClassSpecsKeywordWithoutSemicolon end"
    ClassSpecsKeywordWithoutSemicolon.should.instance_of?(Class)
  end

  it "can redefine a class when called from a block" do
    ClassSpecs::DEFINE_CLASS.call
    A.should.instance_of?(Class)

    Object.send(:remove_const, :A)
    defined?(A).should == nil

    ClassSpecs::DEFINE_CLASS.call
    A.should.instance_of?(Class)
  ensure
    Object.send(:remove_const, :A) if defined?(::A)
  end
end

describe "A class definition" do
  it "creates a new class" do
    ClassSpecs::A.should.is_a?(Class)
    ClassSpecs::A.new.should.is_a?(ClassSpecs::A)
  end

  it "has no class variables" do
    ClassSpecs::A.class_variables.should == []
  end

  it "raises TypeError if constant given as class name exists and is not a Module" do
    -> {
      class ClassSpecsNumber
      end
    }.should.raise(TypeError, /\AClassSpecsNumber is not a class/)
  end

  it "raises TypeError if constant given as class name exists and is a Module but not a Class" do
    -> {
      class ClassSpecs
      end
    }.should.raise(TypeError, /\AClassSpecs is not a class/)
  end

  # test case known to be detecting bugs (JRuby, MRI)
  it "raises TypeError if the constant qualifying the class is nil" do
    -> {
      class nil::Foo
      end
    }.should.raise(TypeError)
  end

  it "raises TypeError if any constant qualifying the class is not a Module" do
    -> {
      class ClassSpecs::Number::MyClass
      end
    }.should.raise(TypeError)

    -> {
      class ClassSpecsNumber::MyClass
      end
    }.should.raise(TypeError)
  end

  it "inherits from Object by default" do
    ClassSpecs::A.superclass.should == Object
  end

  it "raises an error when trying to change the superclass" do
    module ClassSpecs
      class SuperclassResetToSubclass < L
      end
      -> {
        class SuperclassResetToSubclass < M
        end
      }.should.raise(TypeError, /superclass mismatch/)
    end
  end

  it "raises an error when reopening a class with BasicObject as superclass" do
    module ClassSpecs
      class SuperclassReopenedBasicObject < A
      end
      SuperclassReopenedBasicObject.superclass.should == A

      -> {
        class SuperclassReopenedBasicObject < BasicObject
        end
      }.should.raise(TypeError, /superclass mismatch/)
      SuperclassReopenedBasicObject.superclass.should == A
    end
  end

  # [Bug #12367] [ruby-core:75446]
  it "raises an error when reopening a class with Object as superclass" do
    module ClassSpecs
      class SuperclassReopenedObject < A
      end
      SuperclassReopenedObject.superclass.should == A

      -> {
        class SuperclassReopenedObject < Object
        end
      }.should.raise(TypeError, /superclass mismatch/)
      SuperclassReopenedObject.superclass.should == A
    end
  end

  it "allows reopening a class without specifying the superclass" do
    module ClassSpecs
      class SuperclassNotGiven < A
      end
      SuperclassNotGiven.superclass.should == A

      class SuperclassNotGiven
      end
      SuperclassNotGiven.superclass.should == A
    end
  end

  it "does not allow to set the superclass even if it was not specified by the first declaration" do
    module ClassSpecs
      class NoSuperclassSet
      end

      -> {
        class NoSuperclassSet < String
        end
      }.should.raise(TypeError, /superclass mismatch/)
    end
  end

  it "allows using self as the superclass if self is a class" do
    ClassSpecs::I::J.superclass.should == ClassSpecs::I

    -> {
      class ShouldNotWork < self; end
    }.should.raise(TypeError)
  end

  it "first evaluates the superclass before checking if the class already exists" do
    module ClassSpecs
      class SuperclassEvaluatedFirst
      end
      a = SuperclassEvaluatedFirst

      class SuperclassEvaluatedFirst < remove_const(:SuperclassEvaluatedFirst)
      end
      b = SuperclassEvaluatedFirst
      b.superclass.should == a
    end
  end

  it "raises a TypeError if inheriting from a metaclass" do
    obj = mock("metaclass super")
    meta = obj.singleton_class
    -> { class ClassSpecs::MetaclassSuper < meta; end }.should.raise(TypeError)
  end

  it "allows the declaration of class variables in the body" do
    ClassSpecs.string_class_variables(ClassSpecs::B).should == ["@@cvar"]
    ClassSpecs::B.send(:class_variable_get, :@@cvar).should == :cvar
  end

  it "stores instance variables defined in the class body in the class object" do
    ClassSpecs.string_instance_variables(ClassSpecs::B).should.include?("@ivar")
    ClassSpecs::B.instance_variable_get(:@ivar).should == :ivar
  end

  it "allows the declaration of class variables in a class method" do
    ClassSpecs::C.class_variables.should == []
    ClassSpecs::C.make_class_variable
    ClassSpecs.string_class_variables(ClassSpecs::C).should == ["@@cvar"]
    ClassSpecs::C.remove_class_variable :@@cvar
  end

  it "allows the definition of class-level instance variables in a class method" do
    ClassSpecs.string_instance_variables(ClassSpecs::C).should_not.include?("@civ")
    ClassSpecs::C.make_class_instance_variable
    ClassSpecs.string_instance_variables(ClassSpecs::C).should.include?("@civ")
    ClassSpecs::C.remove_instance_variable :@civ
  end

  it "allows the declaration of class variables in an instance method" do
    ClassSpecs::D.class_variables.should == []
    ClassSpecs::D.new.make_class_variable
    ClassSpecs.string_class_variables(ClassSpecs::D).should == ["@@cvar"]
    ClassSpecs::D.remove_class_variable :@@cvar
  end

  it "allows the definition of instance methods" do
    ClassSpecs::E.new.meth.should == :meth
  end

  it "allows the definition of class methods" do
    ClassSpecs::E.cmeth.should == :cmeth
  end

  it "allows the definition of class methods using class << self" do
    ClassSpecs::E.smeth.should == :smeth
  end

  it "allows the definition of Constants" do
    Object.const_defined?('CONSTANT').should == false
    ClassSpecs::E.const_defined?('CONSTANT').should == true
    ClassSpecs::E::CONSTANT.should == :constant!
  end

  it "returns the value of the last statement in the body" do
    class ClassSpecs::Empty; end.should == nil
    class ClassSpecs::Twenty; 20; end.should == 20
    class ClassSpecs::Plus; 10 + 20; end.should == 30
    class ClassSpecs::Singleton; class << self; :singleton; end; end.should == :singleton
  end

  describe "within a block creates a new class in the lexical scope" do
    it "for named classes at the toplevel" do
      klass = Class.new do
        class CS_CONST_CLASS_SPECS
        end

        def self.get_class_name
          CS_CONST_CLASS_SPECS.name
        end
      end

      klass.get_class_name.should == 'CS_CONST_CLASS_SPECS'
      ::CS_CONST_CLASS_SPECS.name.should == 'CS_CONST_CLASS_SPECS'
    end

    it "for named classes in a module" do
      klass = ClassSpecs::ANON_CLASS_FOR_NEW.call

      ClassSpecs::NamedInModule.name.should == 'ClassSpecs::NamedInModule'
      klass.get_class_name.should == 'ClassSpecs::NamedInModule'
    end

    it "for anonymous classes" do
      klass = Class.new do
        def self.get_class
          Class.new do
            def self.foo
              'bar'
            end
          end
        end

        def self.get_result
          get_class.foo
        end
      end

      klass.get_result.should == 'bar'
    end

    it "for anonymous classes assigned to a constant" do
      klass = Class.new do
        AnonWithConstant = Class.new

        def self.get_class_name
          AnonWithConstant.name
        end
      end

      AnonWithConstant.name.should == 'AnonWithConstant'
      klass.get_class_name.should == 'AnonWithConstant'
    ensure
      Object.send(:remove_const, :AnonWithConstant)
    end
  end
end

describe "An outer class definition" do
  it "contains the inner classes" do
    ClassSpecs::Container.constants.should.include?(:A)
    ClassSpecs::Container.constants.should.include?(:B)
  end
end

describe "A class definition extending an object (sclass)" do
  it "allows adding methods" do
    ClassSpecs::O.smeth.should == :smeth
  end

  it "raises a TypeError when trying to extend numbers" do
    -> {
      eval <<-CODE
        class << 1
          def xyz
            self
          end
        end
      CODE
    }.should.raise(TypeError)
  end

  it "raises a TypeError when trying to extend non-Class" do
    error_msg = /superclass must be a.* Class/
    -> { class TestClass < "";              end }.should.raise(TypeError, error_msg)
    -> { class TestClass < 1;               end }.should.raise(TypeError, error_msg)
    -> { class TestClass < :symbol;         end }.should.raise(TypeError, error_msg)
    -> { class TestClass < mock('o');       end }.should.raise(TypeError, error_msg)
    -> { class TestClass < Module.new;      end }.should.raise(TypeError, error_msg)
    -> { class TestClass < BasicObject.new; end }.should.raise(TypeError, error_msg)
  end

  it "does not allow accessing the block of the original scope" do
    -> {
      ClassSpecs.sclass_with_block { 123 }
    }.should.raise(SyntaxError)
  end

  it "can use return to cause the enclosing method to return" do
    ClassSpecs.sclass_with_return.should == :inner
  end
end

describe "Reopening a class" do
  it "extends the previous definitions" do
    c = ClassSpecs::F.new
    c.meth.should == :meth
    c.another.should == :another
  end

  it "overwrites existing methods" do
    ClassSpecs::G.new.override.should == :override
  end

  it "raises a TypeError when superclasses mismatch" do
    -> { class ClassSpecs::A < Array; end }.should.raise(TypeError)
  end

  it "adds new methods to subclasses" do
    -> { ClassSpecs::M.m }.should.raise(NoMethodError)
    class ClassSpecs::L
      def self.m
        1
      end
    end
    ClassSpecs::M.m.should == 1
    ClassSpecs::L.singleton_class.send(:remove_method, :m)
  end

  it "does not reopen a class included in Object" do
    ruby_exe(<<~RUBY).should == "false"
      module IncludedInObject
        class IncludedClass
        end
      end
      class Object
        include IncludedInObject
      end
      class IncludedClass
      end
      print IncludedInObject::IncludedClass == Object::IncludedClass
    RUBY
  end

  it "does not reopen a class included in non-Object modules" do
    ruby_exe(<<~RUBY).should == "false/false"
      module Included
        module IncludedClass; end
      end
      module M
        include Included
        module IncludedClass; end
      end
      class C
        include Included
        module IncludedClass; end
      end
      print Included::IncludedClass == M::IncludedClass, "/",
            Included::IncludedClass == C::IncludedClass
    RUBY
  end
end

describe "class provides hooks" do
  it "calls inherited when a class is created" do
    ClassSpecs::H.track_inherited.should == [ClassSpecs::K]
  end
end
