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

module ModuleSpecs
  class Parent
    def method_to_undef() 1 end
    def another_method_to_undef() 1 end
  end

  class Ancestor
    def method_to_undef() 1 end
    def another_method_to_undef() 1 end
  end
end

describe "Module#undef_method" do
  before :each do
    @module = Module.new { def method_to_undef; end }
  end

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

  it "requires multiple arguments" do
    Module.instance_method(:undef_method).arity.should < 0
  end

  it "allows multiple methods to be removed at once" do
    klass = Class.new do
      def method_to_undef() 1 end
      def another_method_to_undef() 1 end
    end
    x = klass.new
    klass.send(:undef_method, :method_to_undef, :another_method_to_undef)

    -> { x.method_to_undef }.should raise_error(NoMethodError)
    -> { x.another_method_to_undef }.should raise_error(NoMethodError)
  end

  it "does not undef any instance methods when argument not given" do
    before = @module.instance_methods(true) + @module.private_instance_methods(true)
    @module.send :undef_method
    after = @module.instance_methods(true) + @module.private_instance_methods(true)
    before.sort.should == after.sort
  end

  it "returns self" do
    @module.send(:undef_method, :method_to_undef).should equal(@module)
  end

  it "raises a NameError when passed a missing name for a module" do
    -> { @module.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method `not_exist' for module `#{@module}'/) { |e|
      # a NameError and not a NoMethodError
      e.class.should == NameError
    }
  end

  it "raises a NameError when passed a missing name for a class" do
    klass = Class.new
    -> { klass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method `not_exist' for class `#{klass}'/) { |e|
      # a NameError and not a NoMethodError
      e.class.should == NameError
    }
  end

  it "raises a NameError when passed a missing name for a singleton class" do
    klass = Class.new
    obj = klass.new
    sclass = obj.singleton_class

    -> { sclass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method `not_exist' for class `#{sclass}'/) { |e|
      e.message.should include('`#<Class:#<#<Class:')

      # a NameError and not a NoMethodError
      e.class.should == NameError
    }
  end

  it "raises a NameError when passed a missing name for a metaclass" do
    klass = String.singleton_class
    -> { klass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method `not_exist' for class `String'/) { |e|
      # a NameError and not a NoMethodError
      e.class.should == NameError
    }
  end

  describe "on frozen instance" do
    before :each do
      @frozen = @module.dup.freeze
    end

    it "raises a #{frozen_error_class} when passed a name" do
      -> { @frozen.send :undef_method, :method_to_undef }.should raise_error(frozen_error_class)
    end

    it "raises a #{frozen_error_class} when passed a missing name" do
      -> { @frozen.send :undef_method, :not_exist }.should raise_error(frozen_error_class)
    end

    it "raises a TypeError when passed a not name" do
      -> { @frozen.send :undef_method, Object.new }.should raise_error(TypeError)
    end

    it "does not raise exceptions when no arguments given" do
      @frozen.send(:undef_method).should equal(@frozen)
    end
  end
end

describe "Module#undef_method with symbol" do
  it "removes a method defined in a class" do
    klass = Class.new do
      def method_to_undef() 1 end
      def another_method_to_undef() 1 end
    end
    x = klass.new

    x.method_to_undef.should == 1

    klass.send :undef_method, :method_to_undef

    -> { x.method_to_undef }.should raise_error(NoMethodError)
  end

  it "removes a method defined in a super class" do
    child_class = Class.new(ModuleSpecs::Parent)
    child = child_class.new
    child.method_to_undef.should == 1

    child_class.send :undef_method, :method_to_undef

    -> { child.method_to_undef }.should raise_error(NoMethodError)
  end

  it "does not remove a method defined in a super class when removed from a subclass" do
    descendant = Class.new(ModuleSpecs::Ancestor)
    ancestor = ModuleSpecs::Ancestor.new
    ancestor.method_to_undef.should == 1

    descendant.send :undef_method, :method_to_undef

    ancestor.method_to_undef.should == 1
  end
end

describe "Module#undef_method with string" do
  it "removes a method defined in a class" do
    klass = Class.new do
      def method_to_undef() 1 end
      def another_method_to_undef() 1 end
    end
    x = klass.new

    x.another_method_to_undef.should == 1

    klass.send :undef_method, 'another_method_to_undef'

    -> { x.another_method_to_undef }.should raise_error(NoMethodError)
  end

  it "removes a method defined in a super class" do
    child_class = Class.new(ModuleSpecs::Parent)
    child = child_class.new
    child.another_method_to_undef.should == 1

    child_class.send :undef_method, 'another_method_to_undef'

    -> { child.another_method_to_undef }.should raise_error(NoMethodError)
  end

  it "does not remove a method defined in a super class when removed from a subclass" do
    descendant = Class.new(ModuleSpecs::Ancestor)
    ancestor = ModuleSpecs::Ancestor.new
    ancestor.another_method_to_undef.should == 1

    descendant.send :undef_method, 'another_method_to_undef'

    ancestor.another_method_to_undef.should == 1
  end
end
