require 'spec_helper'

describe "Looksee.adapter" do
  include TemporaryClasses

  before do
    @adapter = NATIVE_ADAPTER
  end

  describe "#lookup_modules" do
    #
    # Filter out modules which are hard to test against, and returns
    # the list of module names.  #inspect strings are used for names
    # of singleton classes, since they have no name.
    #
    def filtered_lookup_modules(object)
      result = @adapter.lookup_modules(object).
        map { |mod| @adapter.describe_module(mod) }.
        select{ |description| deterministic_module?(description) }
    end

    #
    # Return true if the given module name is of a module we can test
    # for.
    #
    # This excludes ruby version dependent modules, and modules tossed
    # into the hierarchy by testing frameworks.
    #
    def deterministic_module?(description)
      junk_patterns = [
        # pollution from testing libraries
        'Mocha', 'Spec',
        # RSpec adds this under ruby 1.8.6
        'InstanceExecHelper',
        # RSpec 2
        'RSpec::',
        # only in ruby 1.9
        'BasicObject',
        # something pulls this in under ruby 1.9
        'PP',
        # our own pollution,
        'Looksee::ObjectMixin',
      ]
      pattern = /\b(#{junk_patterns.join('|')})\b/
      description !~ pattern
    end

    it "should contain an entry for each module in the object's lookup path" do
      temporary_module :Mod1
      temporary_module :Mod2
      temporary_class :Base
      temporary_class :Derived, :superclass => Base do
        include Mod1
        include Mod2
      end
      filtered_lookup_modules(Derived.new) ==
        ['Derived', 'Mod2', 'Mod1', 'Base', 'Object', 'Kernel']
    end

    it "should contain an entry for the object's singleton class if it exists" do
      object = Object.new
      object.singleton_class

      filtered_lookup_modules(object).should ==
        ['[Object instance]', 'Object', 'Kernel']
    end

    it "should contain entries for singleton classes of all ancestors for class objects" do
      temporary_class :C
      filtered_lookup_modules(C).should ==
        ['[C]', '[Object]', 'Class', 'Module', 'Object', 'Kernel']
    end

    it "should work for immediate objects" do
      filtered_lookup_modules(1).first.should == 'Fixnum'
    end
  end

  describe "internal instance methods:" do
    def self.target_method(name)
      define_method(:target_method){name}
    end

    def self.it_should_list_methods_with_visibility(visibility)
      it "should return the list of #{visibility} instance methods defined directly on a class" do
        temporary_class :C
        add_methods C, visibility => [:one, :two]
        @adapter.send(target_method, C).to_set.should == Set[:one, :two]
      end

      it "should return the list of #{visibility} instance methods defined directly on a module" do
        temporary_module :M
        add_methods M, visibility => [:one, :two]
        @adapter.send(target_method, M).to_set.should == Set[:one, :two]
      end

      it "should return the list of #{visibility} instance methods defined directly on a singleton class" do
        temporary_class :C
        c = C.new
        add_methods c.singleton_class, visibility => [:one, :two]
        @adapter.send(target_method, c.singleton_class).to_set.should == Set[:one, :two]
      end

      it "should return the list of #{visibility} instance methods defined directly on a class' singleton class" do
        temporary_class :C
        add_methods C.singleton_class, visibility => [:one, :two], :class_singleton => true
        @adapter.send(target_method, C.singleton_class).to_set.should == Set[:one, :two]
      end

      it "should not return undefined methods" do
        temporary_class :C
        add_methods C, visibility => [:removed]
        C.send(:undef_method, :removed)
        @adapter.send(target_method, C).to_set.should == Set[]
      end

      if RUBY_VERSION >= '2'
        it "should return methods only for origin classes" do
          temporary_class :C
          add_methods C, visibility => :one
          temporary_module :M
          add_methods M, visibility => :one
          C.send(:prepend, M)
          @adapter.send(target_method, C).should be_empty

          origin = @adapter.lookup_modules(C.new).
            find { |mod| @adapter.describe_module(mod) == 'C (origin)' }
          @adapter.send(target_method, origin).should_not be_empty
        end
      end
    end

    def self.it_should_not_list_methods_with_visibility(visibility1, visibility2)
      it "should not return any #{visibility1} or #{visibility2} instance methods" do
        temporary_class :C
        add_methods C, {visibility1 => [:a], visibility2 => [:b]}
        @adapter.send(target_method, C).to_set.should == Set[]
      end
    end

    describe ".internal_public_instance_methods" do
      target_method :internal_public_instance_methods
      it_should_list_methods_with_visibility :public
      it_should_not_list_methods_with_visibility :private, :protected
    end

    describe ".internal_protected_instance_methods" do
      target_method :internal_protected_instance_methods
      it_should_list_methods_with_visibility :protected
      it_should_not_list_methods_with_visibility :public, :private
    end

    describe ".internal_private_instance_methods" do
      target_method :internal_private_instance_methods
      it_should_list_methods_with_visibility :private
      it_should_not_list_methods_with_visibility :public, :protected
    end

    describe ".internal_undefined_instance_methods" do
      it "should return the list of undefined instance methods directly on a class" do
        temporary_class :C
        C.send(:define_method, :f){}
        C.send(:undef_method, :f)
        @adapter.internal_undefined_instance_methods(C).should == [:f]
      end

      it "should return the list of undefined instance methods directly on a module" do
        temporary_module :M
        M.send(:define_method, :f){}
        M.send(:undef_method, :f)
        @adapter.internal_undefined_instance_methods(M).should == [:f]
      end

      it "should return the list of undefined instance methods directly on a singleton class" do
        temporary_class :C
        c = C.new
        c.singleton_class.send(:define_method, :f){}
        c.singleton_class.send(:undef_method, :f)
        @adapter.internal_undefined_instance_methods(c.singleton_class).should == [:f]
      end

      it "should return the list of undefined instance methods directly on a class' singleton class" do
        temporary_class :C
        C.singleton_class.send(:define_method, :f){}
        C.singleton_class.send(:undef_method, :f)
        @adapter.internal_undefined_instance_methods(C.singleton_class).should == [:f]
      end

      it "should not return defined methods" do
        temporary_class :C
        C.send(:define_method, :f){}
        @adapter.internal_undefined_instance_methods(C).should == []
      end

      it "should not return removed methods" do
        temporary_class :C
        C.send(:define_method, :f){}
        C.send(:remove_method, :f)
        @adapter.internal_undefined_instance_methods(C).should == []
      end

      it "should handle the MRI allocator being undefined (e.g. Struct)" do
        struct_singleton_class = (class << Struct; self; end)
        @adapter.internal_undefined_instance_methods(struct_singleton_class).should == []
      end

      if RUBY_VERSION >= '2'
        it "should return an empty list for non-origin classes" do
          temporary_class :C
          C.send(:define_method, :f){}
          C.send(:undef_method, :f)
          temporary_module :M
          M.send(:define_method, :f){}
          M.send(:undef_method, :f)
          C.send(:prepend, M)

          @adapter.internal_undefined_instance_methods(C).should be_empty

          origin = @adapter.lookup_modules(C.new).
            find { |mod| @adapter.describe_module(mod) == 'C (origin)' }
          @adapter.internal_undefined_instance_methods(origin).should_not be_empty
        end
      end
    end
  end

  describe "#singleton_class?" do
    it "should return true if the object is a singleton class of an object" do
      object = (class << Object.new; self; end)
      @adapter.singleton_class?(object).should == true
    end

    it "should return true if the object is a singleton class of a class" do
      object = (class << Class.new; self; end)
      @adapter.singleton_class?(object).should == true
    end

    it "should return true if the object is a singleton class of a singleton class" do
      object = (class << (class << Class.new; self; end); self; end)
      @adapter.singleton_class?(object).should == true
    end

    it "should return false if the object is just a class" do
      object = Class.new
      @adapter.singleton_class?(object).should == false
    end

    it "should return false if the object is just a module" do
      object = Module.new
      @adapter.singleton_class?(object).should == false
    end

    it "should return false if the object is just an object" do
      object = Object.new
      @adapter.singleton_class?(object).should == false
    end

    it "should return false if the object is TrueClass" do
      @adapter.singleton_class?(TrueClass).should == false
    end

    it "should return false if the object is FalseClass" do
      @adapter.singleton_class?(FalseClass).should == false
    end

    it "should return false if the object is NilClass" do
      @adapter.singleton_class?(NilClass).should == false
    end
  end

  describe "singleton_instance" do
    it "should return the instance of the given singleton class" do
      object = Object.new
      @adapter.singleton_instance((class << object; self; end)).should equal(object)
    end

    it "should return the instance of the given class singleton class" do
      klass = Class.new
      @adapter.singleton_instance((class << klass; self; end)).should equal(klass)
    end

    it "should return the instance of the given module singleton class" do
      mod = Module.new
      @adapter.singleton_instance((class << mod; self; end)).should equal(mod)
    end

    it "should raise a TypeError if the given object is just a class" do
      lambda do
        @adapter.singleton_instance(Class.new)
      end.should raise_error(TypeError)
    end

    it "should raise a TypeError if the given object is just a module" do
      lambda do
        @adapter.singleton_instance(Module.new)
      end.should raise_error(TypeError)
    end

    it "should raise a TypeError if the given object is just a object" do
      lambda do
        @adapter.singleton_instance(Object.new)
      end.should raise_error(TypeError)
    end
  end

  describe "#module_name" do
    it "should return the fully-qualified name of the given module" do
      ::M = Module.new
      M::N = Module.new
      begin
        @adapter.module_name(M::N).should == 'M::N'
      ensure
        Object.send :remove_const, :M
      end
    end

    it "should return the fully-qualified name of the given class" do
      ::M = Module.new
      M::C = Class.new
      begin
        @adapter.module_name(M::C).should == 'M::C'
      ensure
        Object.send :remove_const, :M
      end
    end

    it "should not be affected by overridding the module's #to_s or #name" do
      begin
        ::M = Module.new
        ::M::C = Class.new do
          def name
            'overridden'
          end
          def to_s
            'overridden'
          end
        end
        @adapter.describe_module(M::C).should == 'M::C'
      ensure
        Object.send :remove_const, :M
      end
    end

    it "should return an empty string for unnamed modules" do
      @adapter.module_name(Module.new).should == ''
    end

    it "should return an empty string for unnamed classes" do
      @adapter.module_name(Class.new).should == ''
    end

    it "should return an empty string for singleton classes" do
      object = Object.new
      @adapter.module_name((class << object; self; end)).should == ''
    end

    it "should raise a TypeError if the argumeent is not a module" do
      lambda do
        @adapter.module_name(Object.new)
      end.should raise_error(TypeError)
    end
  end

  describe "#describe_module" do
    it "should return the fully-qualified name of a module" do
      begin
        ::M = Module.new
        ::M::N = Module.new
        @adapter.describe_module(::M::N).should == 'M::N'
      ensure
        Object.send :remove_const, :M
      end
    end

    it "should not be affected by overridding the module's #to_s or #name" do
      begin
        ::M = Module.new
        ::M::C = Class.new do
          def name
            'overridden'
          end
          def to_s
            'overridden'
          end
        end
        @adapter.describe_module(::M::C).should == 'M::C'
      ensure
        Object.send :remove_const, :M
      end
    end

    describe "for an unnamed class" do
      it "should describe the object as 'unnamed Class'" do
        @adapter.describe_module(Class.new).should == 'unnamed Class'
      end
    end

    describe "for an unnamed module" do
      it "should describe the object as 'unnamed Module'" do
        @adapter.describe_module(Module.new).should == 'unnamed Module'
      end
    end

    describe "for an included class of an unnamed module" do
      it "should describe the object as 'unnamed Module'" do
        klass = Class.new do
          include Module.new
        end
        mod = @adapter.lookup_modules(klass.new)[1]
        @adapter.describe_module(mod).should == 'unnamed Module'
      end
    end

    describe "for a singleton class of an object" do
      it "should describe the object in brackets" do
        begin
          ::M = Module.new
          ::M::C = Class.new
          object = M::C.new
          @adapter.describe_module(object.singleton_class).should == '[M::C instance]'
        ensure
          Object.send :remove_const, :M
        end
      end
    end

    describe "for a singleton class of a named class" do
      it "should return the class name in brackets" do
        begin
          ::M = Module.new
          ::M::C = Class.new
          @adapter.describe_module(M::C.singleton_class).should == '[M::C]'
        ensure
          Object.send :remove_const, :M
        end
      end
    end

    describe "for a singleton class of an unnamed class" do
      it "should describe the object as '[unnamed Class]'" do
        klass = Class.new
        @adapter.describe_module(klass.singleton_class).should == '[unnamed Class]'
      end
    end

    describe "for a singleton class of an unnamed module" do
      it "should describe the object as '[unnamed Module]'" do
        mod = Module.new
        @adapter.describe_module(mod.singleton_class).should == '[unnamed Module]'
      end
    end

    describe "for a singleton class of a singleton class" do
      it "should return the object's class name in two pairs of brackets" do
        begin
          ::M = Module.new
          ::M::C = Class.new
          klass = M::C.singleton_class.singleton_class
          @adapter.describe_module(klass).should == '[[M::C]]'
        ensure
          Object.send :remove_const, :M
        end
      end
    end
  end

  describe "#source_location" do
    def load_source(source)
      @tmp = "#{ROOT}/spec/tmp"
      # rbx 1.2.3 caches the file content by file name - ensure file names are different.
      @source_path = "#@tmp/c#{__id__}.rb"
      FileUtils.mkdir_p @tmp
      open(@source_path, 'w') { |f| f.print source }
      load @source_path
      @source_path
    end

    after do
      FileUtils.rm_rf @tmp if @tmp
      Object.send(:remove_const, :C) if Object.const_defined?(:C)
    end

    it "should return the file and line number the given method was defined on" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |end
      EOS
      method = C.instance_method(:f)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should work for methods defined via a block (MRI BMETHOD)" do
      path = load_source <<-EOS.demargin
        |class C
        |  define_method :f do
        |  end
        |end
      EOS
      method = C.instance_method(:f)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should work for methods defined via a proc (MRI BMETHOD)" do
      path = load_source <<-EOS.demargin
        |class C
        |  f = lambda do
        |  end
        |  define_method :f, f
        |end
      EOS
      method = C.instance_method(:f)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should work for methods defined via a UnboundMethod (MRI DMETHOD)" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |  define_method :g, instance_method(:f)
        |end
      EOS
      method = C.instance_method(:g)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should work for methods defined via a BoundMethod (MRI DMETHOD)" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |  define_method :g, new.method(:f)
        |end
      EOS
      method = C.instance_method(:g)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should work for methods whose visibility is overridden in a subclass (MRI ZSUPER)" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |end
        |class D < C
        |  private :f
        |end
      EOS
      begin
        method = D.instance_method(:f)
        @adapter.source_location(method).should == [path, 2]
      ensure
        Object.send(:remove_const, :D)
      end
    end

    it "should work for aliases (MRI FBODY)" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |  alias g f
        |end
      EOS
      method = C.instance_method(:g)
      @adapter.source_location(method).should == [path, 2]
    end

    it "should return nil for primitive methods (MRI CBODY)" do
      method = Array.instance_method(:size)
      @adapter.source_location(method).should == nil
    end

    it "should raise a TypeError if the argument is not an UnboundMethod" do
      path = load_source <<-EOS.demargin
        |class C
        |  def f
        |  end
        |end
      EOS
      lambda do
        @adapter.source_location(nil)
      end.should raise_error(TypeError)
    end
  end
end