require "spec_helper"
require "virt_disk"

def new_test_class(cname, methods_exported = [], methods_not_exported = [])
  klass = Class.new { include VirtDisk::ExportMethods }
  stub_const cname, klass
  add_test_methods(klass, methods_exported, methods_not_exported)
  klass
end

def add_test_methods(klass, methods_exported, methods_not_exported)
  klass.class_eval do
    (methods_exported + methods_not_exported).each do |mn|
      define_method(mn) { "In #{self.class.name}##{mn}" }
    end
    export(*methods_exported)
  end
end

describe VirtDisk::ExportMethods do
  let(:group_a_methods) do
    %i( method1 method2 method3 )
  end

  let(:group_b_methods) do
    %i( method4 method5 method6 )
  end

  let(:group_c_methods) do
    %i( method7 )
  end

  describe "defined methods" do
    before(:each) do
      new_test_class('TestMod1')
    end

    describe "Class methods" do
      it "should respond to export" do
        expect(TestMod1.respond_to?(:export)).to be true
      end

      it "should respond to exports" do
        expect(TestMod1.respond_to?(:exports)).to be true
      end

      it "should respond to exported?" do
        expect(TestMod1.respond_to?(:exported?)).to be true
      end
    end

    describe "Instance methods" do
      let(:mod1_obj) { TestMod1.new }

      it "should respond to exported?" do
        expect(mod1_obj.respond_to?(:exported?)).to be true
      end

      it "should respond to delegate=" do
        expect(mod1_obj.respond_to?(:delegate=)).to be true
      end

      it "should respond to delegate" do
        expect(mod1_obj.respond_to?(:delegate)).to be true
      end
    end
  end

  describe "Class methods" do
    describe "export" do
      before(:each) do
        new_test_class('TestMod1', group_a_methods, group_c_methods)
      end

      it "should raise an error when method isn't defined" do
        expect do
          TestMod1.export(:foo)
        end.to raise_exception(RuntimeError, "Method not defined in class: foo")
      end

      it "should return nil on success" do
        expect(TestMod1.export(group_c_methods.first)).to be_nil
      end

      it "should add the method to the exports list" do
        meth = group_c_methods.first
        expect(TestMod1.exports.include?(meth)).to be false
        TestMod1.export(meth)
        expect(TestMod1.exports.include?(meth)).to be true
      end
    end

    describe "exports" do
      before(:each) do
        new_test_class('TestMod1', [], group_a_methods)
      end

      it "should return an empty array when nothing exported" do
        expect(TestMod1.exports).to match_array([])
      end

      it "should return the expected array" do
        expect(TestMod1.exports).to match_array([])
        TestMod1.export(*group_a_methods)
        expect(TestMod1.exports).to match_array(group_a_methods)
      end
    end

    describe "exported?" do
      before(:each) do
        new_test_class('TestMod1', [], group_a_methods)
      end

      it "should return false when nothing exported" do
        expect(TestMod1.exported?(group_a_methods.first)).to be false
      end

      it "should return false when method isn't exported" do
        TestMod1.export(*group_a_methods)
        expect(TestMod1.exported?(group_b_methods.first)).to be false
      end

      it "should return true when method is exported" do
        TestMod1.export(*group_a_methods)
        expect(TestMod1.exported?(group_a_methods.first)).to be true
      end
    end
  end

  describe "Instance methods" do
    let(:no_export_obj) do
      new_test_class('NoExportMod')
      NoExportMod.new
    end

    let(:export_obj) do
      new_test_class('ExportMod', group_a_methods)
      ExportMod.new
    end

    describe "exported?" do
      it "should return false when nothing exported" do
        expect(no_export_obj.exported?(group_a_methods.first)).to be false
      end

      it "should return false when method isn't exported" do
        expect(export_obj.exported?(group_b_methods.first)).to be false
      end

      it "should return true when method is exported" do
        expect(export_obj.exported?(group_a_methods.first)).to be true
      end
    end

    describe "delegate, delegate=" do
      it "should return nil when no delegate" do
        expect(no_export_obj.delegate).to be nil
      end

      it "should return the object it is passed" do
        expect(no_export_obj.delegate = export_obj).to eq(export_obj)
      end

      it "should set the delegate accordingly" do
        no_export_obj.delegate = export_obj
        expect(no_export_obj.delegate).to eq(export_obj)
      end
    end
  end

  describe "Operation" do
    let(:obja) do
      new_test_class('ModA', group_a_methods)
      ModA.new
    end

    let(:objb) do
      new_test_class('ModB', group_b_methods)
      ModB.new
    end

    let(:objc) do
      new_test_class('ModC', group_c_methods)
      ModC.new
    end

    it "should 'respond_to?' top-level methods" do
      expect(obja.respond_to?(group_a_methods.first)).to eq true
    end

    it "should call top-level methods directly" do
      method_name = group_a_methods.first
      expect(obja.send(method_name)).to eq("In #{obja.class.name}##{method_name}")
    end

    it "should 'respond_to?' 2nd-level methods" do
      expect(obja.respond_to?(group_a_methods.first)).to eq true
      expect(obja.respond_to?(group_b_methods.first)).to eq false

      obja.delegate = objb
      expect(obja.respond_to?(group_b_methods.first)).to eq true
    end

    it "should call 2nd-level methods" do
      method_a_name = group_a_methods.first
      method_b_name = group_b_methods.first

      expect(obja.send(method_a_name)).to eq("In #{obja.class.name}##{method_a_name}")

      expect do
        obja.send(method_b_name)
      end.to raise_exception(NoMethodError, /undefined method `#{method_b_name}' for.*/)

      obja.delegate = objb
      expect(obja.send(method_b_name)).to eq("In #{objb.class.name}##{method_b_name}")
    end

    it "should 'respond_to?' 3rd-level methods" do
      expect(obja.respond_to?(group_a_methods.first)).to eq true
      expect(obja.respond_to?(group_b_methods.first)).to eq false
      expect(obja.respond_to?(group_c_methods.first)).to eq false

      obja.delegate = objb
      expect(obja.respond_to?(group_b_methods.first)).to eq true
      expect(obja.respond_to?(group_c_methods.first)).to eq false

      objb.delegate = objc
      expect(obja.respond_to?(group_c_methods.first)).to eq true
    end

    it "should call 3rd-level methods" do
      method_a_name = group_a_methods.first
      method_b_name = group_b_methods.first
      method_c_name = group_c_methods.first

      expect(obja.send(method_a_name)).to eq("In #{obja.class.name}##{method_a_name}")

      expect do
        obja.send(method_b_name)
      end.to raise_exception(NoMethodError, /undefined method `#{method_b_name}' for.*/)

      expect do
        obja.send(method_c_name)
      end.to raise_exception(NoMethodError, /undefined method `#{method_c_name}' for.*/)

      obja.delegate = objb
      expect(obja.send(method_b_name)).to eq("In #{objb.class.name}##{method_b_name}")

      expect do
        obja.send(method_c_name)
      end.to raise_exception(NoMethodError, /undefined method `#{method_c_name}' for.*/)

      objb.delegate = objc
      expect(obja.send(method_c_name)).to eq("In #{objc.class.name}##{method_c_name}")
    end
  end
end