require_relative '../spec_helper'
require_relative 'fixtures/def'

# Language-level method behaviour
describe "Redefining a method" do
  it "replaces the original method" do
    def barfoo; 100; end
    barfoo.should == 100

    def barfoo; 200; end
    barfoo.should == 200
  end
end

describe "Defining a method at the top-level" do
  it "defines it on Object with private visibility by default" do
    Object.should have_private_instance_method(:some_toplevel_method, false)
  end

  it "defines it on Object with public visibility after calling public" do
    Object.should have_public_instance_method(:public_toplevel_method, false)
  end
end

describe "Defining an 'initialize' method" do
  it "sets the method's visibility to private" do
    class DefInitializeSpec
      def initialize
      end
    end
    DefInitializeSpec.should have_private_instance_method(:initialize, false)
  end
end

describe "Defining an 'initialize_copy' method" do
  it "sets the method's visibility to private" do
    class DefInitializeCopySpec
      def initialize_copy
      end
    end
    DefInitializeCopySpec.should have_private_instance_method(:initialize_copy, false)
  end
end

describe "Defining an 'initialize_dup' method" do
  it "sets the method's visibility to private" do
    class DefInitializeDupSpec
      def initialize_dup
      end
    end
    DefInitializeDupSpec.should have_private_instance_method(:initialize_dup, false)
  end
end

describe "Defining an 'initialize_clone' method" do
  it "sets the method's visibility to private" do
    class DefInitializeCloneSpec
      def initialize_clone
      end
    end
    DefInitializeCloneSpec.should have_private_instance_method(:initialize_clone, false)
  end
end

describe "Defining a 'respond_to_missing?' method" do
  it "sets the method's visibility to private" do
    class DefRespondToMissingPSpec
      def respond_to_missing?
      end
    end
    DefRespondToMissingPSpec.should have_private_instance_method(:respond_to_missing?, false)
  end
end

describe "Defining a method" do
  it "returns a symbol of the method name" do
    method_name = def some_method; end
    method_name.should == :some_method
  end
end

describe "An instance method" do
  it "raises an error with too few arguments" do
    def foo(a, b); end
    -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2)')
  end

  it "raises an error with too many arguments" do
    def foo(a); end
    -> { foo 1, 2 }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
  end

  it "raises FrozenError with the correct class name" do
    -> {
      Module.new do
        self.freeze
        def foo; end
      end
    }.should raise_error(FrozenError) { |e|
      msg_class = ruby_version_is("4.0") ? "Module" : "module"
      e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}"
    }

    -> {
      Class.new do
        self.freeze
        def foo; end
      end
    }.should raise_error(FrozenError){ |e|
      msg_class = ruby_version_is("4.0") ? "Class" : "class"
      e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}"
    }
  end
end

describe "An instance method definition with a splat" do
  it "accepts an unnamed '*' argument" do
    def foo(*); end;

    foo.should == nil
    foo(1, 2).should == nil
    foo(1, 2, 3, 4, :a, :b, 'c', 'd').should == nil
  end

  it "accepts a named * argument" do
    def foo(*a); a; end;
    foo.should == []
    foo(1, 2).should == [1, 2]
    foo([:a]).should == [[:a]]
  end

  it "accepts non-* arguments before the * argument" do
    def foo(a, b, c, d, e, *f); [a, b, c, d, e, f]; end
    foo(1, 2, 3, 4, 5, 6, 7, 8).should == [1, 2, 3, 4, 5, [6, 7, 8]]
  end

  it "allows only a single * argument" do
    -> { eval 'def foo(a, *b, *c); end' }.should raise_error(SyntaxError)
  end

  it "requires the presence of any arguments that precede the *" do
    def foo(a, b, *c); end
    -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2+)')
  end
end

describe "An instance method with a default argument" do
  it "evaluates the default when no arguments are passed" do
    def foo(a = 1)
      a
    end
    foo.should == 1
    foo(2).should == 2
  end

  it "evaluates the default empty expression when no arguments are passed" do
    def foo(a = ())
      a
    end
    foo.should == nil
    foo(2).should == 2
  end

  it "assigns an empty Array to an unused splat argument" do
    def foo(a = 1, *b)
      [a,b]
    end
    foo.should == [1, []]
    foo(2).should == [2, []]
  end

  it "evaluates the default when required arguments precede it" do
    def foo(a, b = 2)
      [a,b]
    end
    -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)')
    foo(1).should == [1, 2]
  end

  it "prefers to assign to a default argument before a splat argument" do
    def foo(a, b = 2, *c)
      [a,b,c]
    end
    -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1+)')
    foo(1).should == [1,2,[]]
  end

  it "prefers to assign to a default argument when there are no required arguments" do
    def foo(a = 1, *args)
      [a,args]
    end
    foo(2,2).should == [2,[2]]
  end

  it "does not evaluate the default when passed a value and a * argument" do
    def foo(a, b = 2, *args)
      [a,b,args]
    end
    foo(2,3,3).should == [2,3,[3]]
  end

  ruby_version_is ""..."3.4" do
    it "raises a SyntaxError if using the argument in its default value" do
      -> {
        eval "def foo(bar = bar)
          bar
        end"
      }.should raise_error(SyntaxError)
    end
  end

  ruby_version_is "3.4" do
    it "is nil if using the argument in its default value" do
      -> {
        eval "def foo(bar = bar)
          bar
        end
        foo"
      }.call.should == nil
    end
  end

  it "calls a method with the same name as the local when explicitly using ()" do
    def bar
      1
    end
    def foo(bar = bar())
      bar
    end
    foo.should == 1
    foo(2).should == 2
  end
end

describe "A singleton method definition" do
  it "can be declared for a local variable" do
    a = Object.new
    def a.foo
      5
    end
    a.foo.should == 5
  end

  it "can be declared for an instance variable" do
    @a = Object.new
    def @a.foo
      6
    end
    @a.foo.should == 6
  end

  it "can be declared for a global variable" do
    $__a__ = +"hi"
    def $__a__.foo
      7
    end
    $__a__.foo.should == 7
  end

  it "can be declared with an empty method body" do
    class DefSpec
      def self.foo;end
    end
    DefSpec.foo.should == nil
  end

  it "can be redefined" do
    obj = Object.new
    def obj.==(other)
      1
    end
    (obj==1).should == 1
    def obj.==(other)
      2
    end
    (obj==2).should == 2
  end

  it "raises FrozenError if frozen" do
    obj = Object.new
    obj.freeze
    -> { def obj.foo; end }.should raise_error(FrozenError)
  end

  it "raises FrozenError with the correct class name" do
    obj = Object.new
    obj.freeze
    msg_class = ruby_version_is("4.0") ? "Object" : "object"
    -> { def obj.foo; end }.should raise_error(FrozenError, "can't modify frozen #{msg_class}: #{obj}")

    obj = Object.new
    c = obj.singleton_class
    c.singleton_class.freeze
    -> { def c.foo; end }.should raise_error(FrozenError, "can't modify frozen Class: #{c}")

    c = Class.new
    c.freeze
    -> { def c.foo; end }.should raise_error(FrozenError, "can't modify frozen Class: #{c}")

    m = Module.new
    m.freeze
    -> { def m.foo; end }.should raise_error(FrozenError, "can't modify frozen Module: #{m}")
  end
end

describe "Redefining a singleton method" do
  it "does not inherit a previously set visibility" do
    o = Object.new

    class << o; private; def foo; end; end;

    class << o; should have_private_instance_method(:foo); end

    class << o; def foo; end; end;

    class << o; should_not have_private_instance_method(:foo); end
    class << o; should have_instance_method(:foo); end

  end
end

describe "Redefining a singleton method" do
  it "does not inherit a previously set visibility" do
    o = Object.new

    class << o; private; def foo; end; end;

    class << o; should have_private_instance_method(:foo); end

    class << o; def foo; end; end;

    class << o; should_not have_private_instance_method(:foo); end
    class << o; should have_instance_method(:foo); end

  end
end

describe "A method defined with extreme default arguments" do
  it "can redefine itself when the default is evaluated" do
    class DefSpecs
      def foo(x = (def foo; "hello"; end;1));x;end
    end

    d = DefSpecs.new
    d.foo(42).should == 42
    d.foo.should == 1
    d.foo.should == 'hello'
  end

  it "may use an fcall as a default" do
    def bar
      1
    end
    def foo(x = bar())
      x
    end
    foo.should == 1
    foo(2).should == 2
  end

  it "evaluates the defaults in the method's scope" do
    def foo(x = ($foo_self = self; nil)); end
    foo
    $foo_self.should == self
  end

  it "may use preceding arguments as defaults" do
    def foo(obj, width=obj.length)
      width
    end
    foo('abcde').should == 5
  end

  it "may use a lambda as a default" do
    def foo(output = 'a', prc = -> n { output * n })
      prc.call(5)
    end
    foo.should == 'aaaaa'
  end
end

describe "A singleton method defined with extreme default arguments" do
  it "may use a method definition as a default" do
    $__a = Object.new
    def $__a.foo(x = (def $__a.foo; "hello"; end;1));x;end

    $__a.foo(42).should == 42
    $__a.foo.should == 1
    $__a.foo.should == 'hello'
  end

  it "may use an fcall as a default" do
    a = Object.new
    def a.bar
      1
    end
    def a.foo(x = bar())
      x
    end
    a.foo.should == 1
    a.foo(2).should == 2
  end

  it "evaluates the defaults in the singleton scope" do
    a = Object.new
    def a.foo(x = ($foo_self = self; nil)); 5 ;end
    a.foo
    $foo_self.should == a
  end

  it "may use preceding arguments as defaults" do
    a = Object.new
    def a.foo(obj, width=obj.length)
      width
    end
    a.foo('abcde').should == 5
  end

  it "may use a lambda as a default" do
    a = Object.new
    def a.foo(output = 'a', prc = -> n { output * n })
      prc.call(5)
    end
    a.foo.should == 'aaaaa'
  end
end

describe "A method definition inside a metaclass scope" do
  it "can create a class method" do
    class DefSpecSingleton
      class << self
        def a_class_method;self;end
      end
    end

    DefSpecSingleton.a_class_method.should == DefSpecSingleton
    -> { Object.a_class_method }.should raise_error(NoMethodError)
  end

  it "can create a singleton method" do
    obj = Object.new
    class << obj
      def a_singleton_method;self;end
    end

    obj.a_singleton_method.should == obj
    -> { Object.new.a_singleton_method }.should raise_error(NoMethodError)
  end

  it "raises FrozenError if frozen" do
    obj = Object.new
    obj.freeze

    class << obj
      -> { def foo; end }.should raise_error(FrozenError)
    end
  end
end

describe "A nested method definition" do
  it "creates an instance method when evaluated in an instance method" do
    class DefSpecNested
      def create_instance_method
        def an_instance_method;self;end
        an_instance_method
      end
    end

    obj = DefSpecNested.new
    obj.create_instance_method.should == obj
    obj.an_instance_method.should == obj

    other = DefSpecNested.new
    other.an_instance_method.should == other

    DefSpecNested.should have_instance_method(:an_instance_method)
  end

  it "creates a class method when evaluated in a class method" do
    class DefSpecNested
      class << self
        # cleanup
        remove_method :a_class_method if method_defined? :a_class_method
        def create_class_method
          def a_class_method;self;end
          a_class_method
        end
      end
    end

    -> { DefSpecNested.a_class_method }.should raise_error(NoMethodError)
    DefSpecNested.create_class_method.should == DefSpecNested
    DefSpecNested.a_class_method.should == DefSpecNested
    -> { Object.a_class_method }.should raise_error(NoMethodError)
    -> { DefSpecNested.new.a_class_method }.should raise_error(NoMethodError)
  end

  it "creates a singleton method when evaluated in the metaclass of an instance" do
    class DefSpecNested
      def create_singleton_method
        class << self
          def a_singleton_method;self;end
        end
        a_singleton_method
      end
    end

    obj = DefSpecNested.new
    obj.create_singleton_method.should == obj
    obj.a_singleton_method.should == obj

    other = DefSpecNested.new
    -> { other.a_singleton_method }.should raise_error(NoMethodError)
  end

  it "creates a method in the surrounding context when evaluated in a def expr.method" do
    class DefSpecNested
      TARGET = Object.new
      def TARGET.defs_method
        def inherited_method;self;end
      end
    end

    DefSpecNested::TARGET.defs_method
    DefSpecNested.should have_instance_method :inherited_method
    DefSpecNested::TARGET.should_not have_method :inherited_method

    obj = DefSpecNested.new
    obj.inherited_method.should == obj
  ensure
    DefSpecNested.send(:remove_const, :TARGET)
  end

  # See http://yugui.jp/articles/846#label-3
  it "inside an instance_eval creates a singleton method" do
    class DefSpecNested
      OBJ = Object.new
      OBJ.instance_eval do
        def create_method_in_instance_eval(a = (def arg_method; end))
          def body_method; end
        end
      end
    end

    obj = DefSpecNested::OBJ
    obj.create_method_in_instance_eval

    obj.should have_method :arg_method
    obj.should have_method :body_method

    DefSpecNested.should_not have_instance_method :arg_method
    DefSpecNested.should_not have_instance_method :body_method
  ensure
    DefSpecNested.send(:remove_const, :OBJ)
  end

  it "creates an instance method inside Class.new" do
    cls = Class.new do
      def do_def
        def new_def
          1
        end
      end
    end

    obj = cls.new
    obj.do_def
    obj.new_def.should == 1

    cls.new.new_def.should == 1

    -> { Object.new.new_def }.should raise_error(NoMethodError)
  end
end

describe "A method definition always resets the visibility to public for nested definitions" do
  it "in Class.new" do
    cls = Class.new do
      private
      def do_def
        def new_def
          1
        end
      end
    end

    obj = cls.new
    -> { obj.do_def }.should raise_error(NoMethodError, /private/)
    obj.send :do_def
    obj.new_def.should == 1

    cls.new.new_def.should == 1

    -> { Object.new.new_def }.should raise_error(NoMethodError)
  end

  it "at the toplevel" do
    obj = Object.new
    -> { obj.toplevel_define_other_method }.should raise_error(NoMethodError, /private/)
    toplevel_define_other_method
    nested_method_in_toplevel_method.should == 42

    Object.new.nested_method_in_toplevel_method.should == 42
  end
end

describe "A method definition inside an instance_eval" do
  it "creates a singleton method" do
    obj = Object.new
    obj.instance_eval do
      def an_instance_eval_method;self;end
    end
    obj.an_instance_eval_method.should == obj

    other = Object.new
    -> { other.an_instance_eval_method }.should raise_error(NoMethodError)
  end

  it "creates a singleton method when evaluated inside a metaclass" do
    obj = Object.new
    obj.instance_eval do
      class << self
        def a_metaclass_eval_method;self;end
      end
    end
    obj.a_metaclass_eval_method.should == obj

    other = Object.new
    -> { other.a_metaclass_eval_method }.should raise_error(NoMethodError)
  end

  it "creates a class method when the receiver is a class" do
    DefSpecNested.instance_eval do
      def an_instance_eval_class_method;self;end
    end

    DefSpecNested.an_instance_eval_class_method.should == DefSpecNested
    -> { Object.an_instance_eval_class_method }.should raise_error(NoMethodError)
  end

  it "creates a class method when the receiver is an anonymous class" do
    m = Class.new
    m.instance_eval do
      def klass_method
        :test
      end
    end

    m.klass_method.should == :test
    -> { Object.klass_method }.should raise_error(NoMethodError)
  end

  it "creates a class method when instance_eval is within class" do
    m = Class.new do
      instance_eval do
        def klass_method
          :test
        end
      end
    end

    m.klass_method.should == :test
    -> { Object.klass_method }.should raise_error(NoMethodError)
  end
end

describe "A method definition inside an instance_exec" do
  it "creates a class method when the receiver is a class" do
    DefSpecNested.instance_exec(1) do |param|
      @stuff = param

      def an_instance_exec_class_method; @stuff; end
    end

    DefSpecNested.an_instance_exec_class_method.should == 1
    -> { Object.an_instance_exec_class_method }.should raise_error(NoMethodError)
  end

  it "creates a class method when the receiver is an anonymous class" do
    m = Class.new
    m.instance_exec(1) do |param|
      @stuff = param

      def klass_method
        @stuff
      end
    end

    m.klass_method.should == 1
    -> { Object.klass_method }.should raise_error(NoMethodError)
  end

  it "creates a class method when instance_exec is within class" do
    m = Class.new do
      instance_exec(2) do |param|
        @stuff = param

        def klass_method
          @stuff
        end
      end
    end

    m.klass_method.should == 2
    -> { Object.klass_method }.should raise_error(NoMethodError)
  end
end

describe "A method definition in an eval" do
  it "creates an instance method" do
    class DefSpecNested
      def eval_instance_method
        eval "def an_eval_instance_method;self;end", binding
        an_eval_instance_method
      end
    end

    obj = DefSpecNested.new
    obj.eval_instance_method.should == obj
    obj.an_eval_instance_method.should == obj

    other = DefSpecNested.new
    other.an_eval_instance_method.should == other

    -> { Object.new.an_eval_instance_method }.should raise_error(NoMethodError)
  end

  it "creates a class method" do
    class DefSpecNestedB
      class << self
        def eval_class_method
          eval "def an_eval_class_method;self;end" #, binding
          an_eval_class_method
        end
      end
    end

    DefSpecNestedB.eval_class_method.should == DefSpecNestedB
    DefSpecNestedB.an_eval_class_method.should == DefSpecNestedB

    -> { Object.an_eval_class_method }.should raise_error(NoMethodError)
    -> { DefSpecNestedB.new.an_eval_class_method}.should raise_error(NoMethodError)
  end

  it "creates a singleton method" do
    class DefSpecNested
      def eval_singleton_method
        class << self
          eval "def an_eval_singleton_method;self;end", binding
        end
        an_eval_singleton_method
      end
    end

    obj = DefSpecNested.new
    obj.eval_singleton_method.should == obj
    obj.an_eval_singleton_method.should == obj

    other = DefSpecNested.new
    -> { other.an_eval_singleton_method }.should raise_error(NoMethodError)
  end
end

describe "a method definition that sets more than one default parameter all to the same value" do
  def foo(a=b=c={})
    [a,b,c]
  end
  it "assigns them all the same object by default" do
    foo.should == [{},{},{}]
    a, b, c = foo
    a.should eql(b)
    a.should eql(c)
  end

  it "allows the first argument to be given, and sets the rest to null" do
    foo(1).should == [1,nil,nil]
  end

  it "assigns the parameters different objects across different default calls" do
    a, _b, _c = foo
    d, _e, _f = foo
    a.should_not equal(d)
  end

  it "only allows overriding the default value of the first such parameter in each set" do
    -> { foo(1,2) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)')
  end

  def bar(a=b=c=1,d=2)
    [a,b,c,d]
  end

  it "treats the argument after the multi-parameter normally" do
    bar.should == [1,1,1,2]
    bar(3).should == [3,nil,nil,2]
    bar(3,4).should == [3,nil,nil,4]
    -> { bar(3,4,5) }.should raise_error(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)')
  end
end

describe "The def keyword" do
  describe "within a closure" do
    it "looks outside the closure for the visibility" do
      module DefSpecsLambdaVisibility
        private

        -> {
          def some_method; end
        }.call
      end

      DefSpecsLambdaVisibility.should have_private_instance_method("some_method")
    end
  end
end
