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 2
        'RSpec::',
        # not sure what pulls these in
        'PP', 'JSON::Ext::Generator::GeneratorMethods::Object',
        # 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).should ==
        ['Derived', 'Mod2', 'Mod1', 'Base', 'Object', 'Kernel', 'BasicObject']
    end

    it "should contain an entry for the object's singleton class if it has methods" do
      object = Object.new
      def object.f; end

      filtered_lookup_modules(object).should ==
        ['[Object instance]', 'Object', 'Kernel', 'BasicObject']
    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]', '[BasicObject]', 'Class', 'Module', 'Object', 'Kernel', 'BasicObject']
    end

    it "should contain entries for modules included in the singleton class" do
      temporary_module :M
      c = Object.new
      c.extend(M)
      filtered_lookup_modules(c).should ==
        ['[Object instance]', 'M', 'Object', 'Kernel', 'BasicObject']
    end

    it "should work for immediate objects" do
      if RUBY_VERSION >= "2.4.0"
        filtered_lookup_modules(1).first.should == 'Integer'
      else
        filtered_lookup_modules(1).first.should == 'Fixnum'
      end
    end
  end

  describe ".undefined_instance_methods" do
    def self.target_method(name)
      define_method(:target_method){name}
    end

    if Looksee.ruby_engine == 'ruby' && RUBY_VERSION >= '2.3' && RUBY_VERSION < '3.2'
      it "just returns an empty array" do
        temporary_class :C
        add_methods C, undefined: [:f]
        @adapter.undefined_instance_methods(C).should == []
      end
    else
      it "should return the list of undefined instance methods directly on a class" do
        temporary_class :C
        add_methods(C, undefined: [:f])
        @adapter.undefined_instance_methods(C).should == [:f]
      end

      it "should return the list of undefined instance methods directly on a module" do
        temporary_module :M
        add_methods(M, undefined: [:f])
        @adapter.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
        add_methods(c.singleton_class, undefined: [:f])
        @adapter.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
        add_methods(C.singleton_class, undefined: [:f])
        @adapter.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.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.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.undefined_instance_methods(struct_singleton_class).should == []
      end
    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 return the instance of the given singleton class singleton class" do
      singleton_class = (class << Class.new; self; end)
      super_singleton_class = (class << singleton_class; self; end)
      @adapter.singleton_instance(super_singleton_class).should equal(singleton_class)
    end

    it "should return nil if the given object is just a class" do
      @adapter.singleton_instance(Class.new).should be_nil
    end

    it "should return nil if the given object is just a module" do
      @adapter.singleton_instance(Module.new).should be_nil
    end

    it "should return nil if the given object is just a object" do
      @adapter.singleton_instance(Object.new).should be_nil
    end

    it "should return nil if the given object is an immediate object" do
      @adapter.singleton_instance(nil).should be_nil
      @adapter.singleton_instance(true).should be_nil
      @adapter.singleton_instance(false).should be_nil
      @adapter.singleton_instance(1).should be_nil
      @adapter.singleton_instance(:hi).should be_nil
    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 return the fully-qualified name of a class" do
      begin
        ::M = Module.new
        ::M::N = Class.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

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