require File.expand_path('../../../spec_helper', __FILE__)
require File.expand_path('../fixtures/classes', __FILE__)

class DefineMethodSpecClass
end

describe "passed { |a, b = 1|  } creates a method that" do
  before :each do
    @klass = Class.new do
      define_method(:m) { |a, b = 1| return a, b }
    end
  end

  it "raises an ArgumentError when passed zero arguments" do
    lambda { @klass.new.m }.should raise_error(ArgumentError)
  end

  it "has a default value for b when passed one argument" do
    @klass.new.m(1).should == [1, 1]
  end

  it "overrides the default argument when passed two arguments" do
    @klass.new.m(1, 2).should == [1, 2]
  end

  it "raises an ArgumentError when passed three arguments" do
    lambda { @klass.new.m(1, 2, 3) }.should raise_error(ArgumentError)
  end
end

describe "Module#define_method when given an UnboundMethod" do
  it "passes the given arguments to the new method" do
    klass = Class.new do
      def test_method(arg1, arg2)
        [arg1, arg2]
      end
      define_method(:another_test_method, instance_method(:test_method))
    end

    klass.new.another_test_method(1, 2).should == [1, 2]
  end

  it "adds the new method to the methods list" do
    klass = Class.new do
      def test_method(arg1, arg2)
        [arg1, arg2]
      end
      define_method(:another_test_method, instance_method(:test_method))
    end
    klass.new.should have_method(:another_test_method)
  end

  describe "defining a method on a singleton class" do
    before do
      klass = Class.new
      class << klass
        def test_method
          :foo
        end
      end
      child = Class.new(klass)
      sc = class << child; self; end
      sc.send :define_method, :another_test_method, klass.method(:test_method).unbind

      @class = child
    end

    it "doesn't raise TypeError when calling the method" do
      @class.another_test_method.should == :foo
    end
  end

  it "sets the new method's visibility to the current frame's visibility" do
    foo = Class.new do
      def ziggy
        'piggy'
      end
      private :ziggy

      # make sure frame visibility is public
      public

      define_method :piggy, instance_method(:ziggy)
    end

    lambda { foo.new.ziggy }.should raise_error(NoMethodError)
    foo.new.piggy.should == 'piggy'
  end
end

describe "Module#define_method when name is not a special private name" do
  describe "given an UnboundMethod" do
    describe "and called from the target module" do
      it "sets the visibility of the method to the current visibility" do
        klass = Class.new do
          define_method(:bar, ModuleSpecs::EmptyFooMethod)
          private
          define_method(:baz, ModuleSpecs::EmptyFooMethod)
        end

        klass.should have_public_instance_method(:bar)
        klass.should have_private_instance_method(:baz)
      end
    end

    describe "and called from another module" do
      it "sets the visibility of the method to public" do
        klass = Class.new
        Class.new do
          klass.send(:define_method, :bar, ModuleSpecs::EmptyFooMethod)
          private
          klass.send(:define_method, :baz, ModuleSpecs::EmptyFooMethod)
        end

        klass.should have_public_instance_method(:bar)
        klass.should have_public_instance_method(:baz)
      end
    end
  end

  describe "passed a block" do
    describe "and called from the target module" do
      it "sets the visibility of the method to the current visibility" do
        klass = Class.new do
          define_method(:bar) {}
          private
          define_method(:baz) {}
        end

        klass.should have_public_instance_method(:bar)
        klass.should have_private_instance_method(:baz)
      end
    end

    describe "and called from another module" do
      it "sets the visibility of the method to public" do
        klass = Class.new
        Class.new do
          klass.send(:define_method, :bar) {}
          private
          klass.send(:define_method, :baz) {}
        end

        klass.should have_public_instance_method(:bar)
        klass.should have_public_instance_method(:baz)
      end
    end
  end
end

describe "Module#define_method when name is :initialize" do
  describe "passed a block" do
    it "sets visibility to private when method name is :initialize" do
      klass = Class.new do
        define_method(:initialize) { }
      end
      klass.should have_private_instance_method(:initialize)
    end
  end

  describe "given an UnboundMethod" do
    it "sets the visibility to private when method is named :initialize" do
      klass = Class.new do
        def test_method
        end
        define_method(:initialize, instance_method(:test_method))
      end
      klass.should have_private_instance_method(:initialize)
    end
  end
end

describe "Module#define_method" do
  it "defines the given method as an instance method with the given name in self" do
    class DefineMethodSpecClass
      def test1
        "test"
      end
      define_method(:another_test, instance_method(:test1))
    end

    o = DefineMethodSpecClass.new
    o.test1.should == o.another_test
  end

  it "calls #method_added after the method is added to the Module" do
    DefineMethodSpecClass.should_receive(:method_added).with(:test_ma)

    class DefineMethodSpecClass
      define_method(:test_ma) { true }
    end
  end

  it "defines a new method with the given name and the given block as body in self" do
    class DefineMethodSpecClass
      define_method(:block_test1) { self }
      define_method(:block_test2, &lambda { self })
    end

    o = DefineMethodSpecClass.new
    o.block_test1.should == o
    o.block_test2.should == o
  end

  it "raises a TypeError when the given method is no Method/Proc" do
    lambda {
      Class.new { define_method(:test, "self") }
    }.should raise_error(TypeError)

    lambda {
      Class.new { define_method(:test, 1234) }
    }.should raise_error(TypeError)

    lambda {
      Class.new { define_method(:test, nil) }
    }.should raise_error(TypeError)
  end

  it "raises an ArgumentError when no block is given" do
    lambda {
      Class.new { define_method(:test) }
    }.should raise_error(ArgumentError)
  end

  ruby_version_is "2.3" do
    it "does not use the caller block when no block is given" do
      o = Object.new
      def o.define(name)
        self.class.class_eval do
          define_method(name)
        end
      end

      lambda {
        o.define(:foo) { raise "not used" }
      }.should raise_error(ArgumentError)
    end
  end

  it "does not change the arity check style of the original proc" do
    class DefineMethodSpecClass
      prc = Proc.new { || true }
      define_method("proc_style_test", &prc)
    end

    obj = DefineMethodSpecClass.new
    lambda { obj.proc_style_test :arg }.should raise_error(ArgumentError)
  end

  it "raises a #{frozen_error_class} if frozen" do
    lambda {
      Class.new { freeze; define_method(:foo) {} }
    }.should raise_error(frozen_error_class)
  end

  it "accepts a Method (still bound)" do
    class DefineMethodSpecClass
      attr_accessor :data
      def inspect_data
        "data is #{@data}"
      end
    end
    o = DefineMethodSpecClass.new
    o.data = :foo
    m = o.method(:inspect_data)
    m.should be_an_instance_of(Method)
    klass = Class.new(DefineMethodSpecClass)
    klass.send(:define_method,:other_inspect, m)
    c = klass.new
    c.data = :bar
    c.other_inspect.should == "data is bar"
    lambda{o.other_inspect}.should raise_error(NoMethodError)
  end

  it "raises a TypeError when a Method from a singleton class is defined on another class" do
    c = Class.new do
      class << self
        def foo
        end
      end
    end
    m = c.method(:foo)

    lambda {
      Class.new { define_method :bar, m }
    }.should raise_error(TypeError)
  end

  it "raises a TypeError when a Method from one class is defined on an unrelated class" do
    c = Class.new do
      def foo
      end
    end
    m = c.new.method(:foo)

    lambda {
      Class.new { define_method :bar, m }
    }.should raise_error(TypeError)
  end

  it "accepts an UnboundMethod from an attr_accessor method" do
    class DefineMethodSpecClass
      attr_accessor :accessor_method
    end

    m = DefineMethodSpecClass.instance_method(:accessor_method)
    o = DefineMethodSpecClass.new

    DefineMethodSpecClass.send(:undef_method, :accessor_method)
    lambda { o.accessor_method }.should raise_error(NoMethodError)

    DefineMethodSpecClass.send(:define_method, :accessor_method, m)

    o.accessor_method = :abc
    o.accessor_method.should == :abc
  end

  it "accepts a proc from a method" do
    class ProcFromMethod
      attr_accessor :data
      def cool_method
        "data is #{@data}"
      end
    end

    object1 = ProcFromMethod.new
    object1.data = :foo

    method_proc = object1.method(:cool_method).to_proc
    klass = Class.new(ProcFromMethod)
    klass.send(:define_method, :other_cool_method, &method_proc)

    object2 = klass.new
    object2.data = :bar
    object2.other_cool_method.should == "data is foo"
  end

  it "maintains the Proc's scope" do
    class DefineMethodByProcClass
      in_scope = true
      method_proc = proc { in_scope }

      define_method(:proc_test, &method_proc)
    end

    o = DefineMethodByProcClass.new
    o.proc_test.should be_true
  end

  it "accepts a String method name" do
    klass = Class.new do
      define_method("string_test") do
        "string_test result"
      end
    end

    klass.new.string_test.should == "string_test result"
  end

  ruby_version_is ''...'2.5' do
    it "is a private method" do
      Module.should have_private_instance_method(:define_method)
    end
  end
  ruby_version_is '2.5' do
    it "is a public method" do
      Module.should have_public_instance_method(:define_method)
    end
  end

  it "returns its symbol" do
    class DefineMethodSpecClass
      method = define_method("return_test") { true }
      method.should == :return_test
    end
  end

  it "allows an UnboundMethod from a module to be defined on a class" do
    klass = Class.new {
      define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo)
    }
    klass.new.should respond_to(:bar)
  end

  it "allows an UnboundMethod from a parent class to be defined on a child class" do
    parent = Class.new { define_method(:foo) { :bar } }
    child = Class.new(parent) {
      define_method :baz, parent.instance_method(:foo)
    }
    child.new.should respond_to(:baz)
  end

  it "allows an UnboundMethod from a module to be defined on another unrelated module" do
    mod = Module.new {
      define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo)
    }
    klass = Class.new { include mod }
    klass.new.should respond_to(:bar)
  end

  it "raises a TypeError when an UnboundMethod from a child class is defined on a parent class" do
    lambda {
      ParentClass = Class.new { define_method(:foo) { :bar } }
      ChildClass = Class.new(ParentClass) { define_method(:foo) { :baz } }
      ParentClass.send :define_method, :foo, ChildClass.instance_method(:foo)
    }.should raise_error(TypeError)
  end

  it "raises a TypeError when an UnboundMethod from one class is defined on an unrelated class" do
    lambda {
      DestinationClass = Class.new {
        define_method :bar, ModuleSpecs::InstanceMeth.instance_method(:foo)
      }
    }.should raise_error(TypeError)
  end
end

describe "Module#define_method" do
  describe "passed {  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { :called }
      end
    end

    it "returns the value computed by the block when passed zero arguments" do
      @klass.new.m().should == :called
    end

    it "raises an ArgumentError when passed one argument" do
      lambda { @klass.new.m 1 }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed two arguments" do
      lambda { @klass.new.m 1, 2 }.should raise_error(ArgumentError)
    end
  end

  describe "passed { ||  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { || :called }
      end
    end

    it "returns the value computed by the block when passed zero arguments" do
      @klass.new.m().should == :called
    end

    it "raises an ArgumentError when passed one argument" do
      lambda { @klass.new.m 1 }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed two arguments" do
      lambda { @klass.new.m 1, 2 }.should raise_error(ArgumentError)
    end
  end

  describe "passed { |a|  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { |a| a }
      end
    end

    it "raises an ArgumentError when passed zero arguments" do
      lambda { @klass.new.m }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed zero arguments and a block" do
      lambda { @klass.new.m { :computed } }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed two arguments" do
      lambda { @klass.new.m 1, 2 }.should raise_error(ArgumentError)
    end

    it "receives the value passed as the argument when passed one argument" do
      @klass.new.m(1).should == 1
    end

  end

  describe "passed { |*a|  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { |*a| a }
      end
    end

    it "receives an empty array as the argument when passed zero arguments" do
      @klass.new.m().should == []
    end

    it "receives the value in an array when passed one argument" do
      @klass.new.m(1).should == [1]
    end

    it "receives the values in an array when passed two arguments" do
      @klass.new.m(1, 2).should == [1, 2]
    end
  end

  describe "passed { |a, *b|  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { |a, *b| return a, b }
      end
    end

    it "raises an ArgumentError when passed zero arguments" do
      lambda { @klass.new.m }.should raise_error(ArgumentError)
    end

    it "returns the value computed by the block when passed one argument" do
      @klass.new.m(1).should == [1, []]
    end

    it "returns the value computed by the block when passed two arguments" do
      @klass.new.m(1, 2).should == [1, [2]]
    end

    it "returns the value computed by the block when passed three arguments" do
      @klass.new.m(1, 2, 3).should == [1, [2, 3]]
    end
  end

  describe "passed { |a, b|  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { |a, b| return a, b }
      end
    end

    it "returns the value computed by the block when passed two arguments" do
      @klass.new.m(1, 2).should == [1, 2]
    end

    it "raises an ArgumentError when passed zero arguments" do
      lambda { @klass.new.m }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed one argument" do
      lambda { @klass.new.m 1 }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed one argument and a block" do
      lambda { @klass.new.m(1) { } }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed three arguments" do
      lambda { @klass.new.m 1, 2, 3 }.should raise_error(ArgumentError)
    end
  end

  describe "passed { |a, b, *c|  } creates a method that" do
    before :each do
      @klass = Class.new do
        define_method(:m) { |a, b, *c| return a, b, c }
      end
    end

    it "raises an ArgumentError when passed zero arguments" do
      lambda { @klass.new.m }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed one argument" do
      lambda { @klass.new.m 1 }.should raise_error(ArgumentError)
    end

    it "raises an ArgumentError when passed one argument and a block" do
      lambda { @klass.new.m(1) { } }.should raise_error(ArgumentError)
    end

    it "receives an empty array as the third argument when passed two arguments" do
      @klass.new.m(1, 2).should == [1, 2, []]
    end

    it "receives the third argument in an array when passed three arguments" do
      @klass.new.m(1, 2, 3).should == [1, 2, [3]]
    end
  end
end

describe "Method#define_method when passed a Method object" do
  before :each do
    @klass = Class.new do
      def m(a, b, *c)
        :m
      end
    end

    @obj = @klass.new
    m = @obj.method :m

    @klass.class_exec do
      define_method :n, m
    end
  end

  it "defines a method with the same #arity as the original" do
    @obj.method(:n).arity.should == @obj.method(:m).arity
  end

  it "defines a method with the same #parameters as the original" do
    @obj.method(:n).parameters.should == @obj.method(:m).parameters
  end
end

describe "Method#define_method when passed an UnboundMethod object" do
  before :each do
    @klass = Class.new do
      def m(a, b, *c)
        :m
      end
    end

    @obj = @klass.new
    m = @klass.instance_method :m

    @klass.class_exec do
      define_method :n, m
    end
  end

  it "defines a method with the same #arity as the original" do
    @obj.method(:n).arity.should == @obj.method(:m).arity
  end

  it "defines a method with the same #parameters as the original" do
    @obj.method(:n).parameters.should == @obj.method(:m).parameters
  end
end

describe "Method#define_method when passed a Proc object" do
  describe "and a method is defined inside" do
    it "defines the nested method in the default definee where the Proc was created" do
      prc = nil
      t = Class.new do
        prc = -> {
          def nested_method_in_proc_for_define_method
            42
          end
        }
      end

      c = Class.new do
        define_method(:test, prc)
      end

      o = c.new
      o.test
      o.should_not have_method :nested_method_in_proc_for_define_method

      t.new.nested_method_in_proc_for_define_method.should == 42
    end
  end
end
