pcall(require, "luacov")

local h = require("tests.helpers")
local Object = require("nui.object")
local spy = require("luassert.spy")

local function assert_class(Class, SuperClass, name)
  h.eq(type(Class), "table")

  h.eq(Class.super, SuperClass)
  h.eq(Class.name, name)
  h.eq(tostring(Class), "class " .. name)

  h.eq(type(Class.new), "function")
  h.eq(type(Class.extend), "function")

  local is_callable = pcall(function()
    return Class()
  end)
  h.eq(is_callable, true)
end

local function assert_instance(instance, Class)
  h.eq(instance.class, Class)
  h.eq(tostring(instance), "instance of class " .. Class.name)

  h.eq(instance.name, nil)
  h.eq(instance.super, nil)
  h.eq(instance.static, nil)

  h.eq(instance.new, nil)
  h.eq(instance.extend, nil)
end

local function create_classes(...)
  local by_name = {}
  local classes = {}

  for i, def in ipairs({ ... }) do
    if type(def) == "string" then
      local class = Object(def)
      assert_class(class, nil, def)
      by_name[def] = class
      classes[i] = class
    elseif type(def) == "table" then
      local super = type(def[2]) == "table" and def[2] or (by_name[def[2]] and by_name[def[2]] or nil)
      local class = super and super:extend(def[1]) or Object(def[1])
      assert_class(class, super, def[1])
      by_name[def[1]] = class
      classes[i] = class
    else
      error("invalid argument")
    end
  end

  return unpack(classes)
end

describe("nui.object", function()
  describe("class", function()
    it("can be created", function()
      local Class = Object("Class")
      assert_class(Class, nil, "Class")
    end)

    describe("static", function()
      describe("method", function()
        describe(":new", function()
          it("is called when creating instance", function()
            local Class = Object("Class")

            spy.on(Class.static, "new")
            Class()
            assert.spy(Class.static.new).called_with(Class)
            Class.static.new:revert()

            spy.on(Class.static, "new")
            Class:new()
            assert.spy(Class.static.new).called_with(Class)
            Class.static.new:revert()
          end)

          it("creates new instance", function()
            local Class = Object("Class")

            local instance = Class:new()
            assert_instance(instance, Class)
          end)
        end)

        describe(":extend", function()
          it("creates subclass", function()
            local Class = Object("Class")

            local SubClass = Class:extend("SubClass")
            assert_class(SubClass, Class, "SubClass")
          end)
        end)

        describe(":is_subclass_of", function()
          it("works", function()
            local A, B, C = create_classes("A", { "B", "A" }, { "C", "B" })

            for _, class in ipairs({ A, B, C }) do
              h.eq(class.is_subclass_of, Object.is_subclass)
            end

            h.eq(A:is_subclass_of(A), false)
            h.eq(A:is_subclass_of(B), false)
            h.eq(A:is_subclass_of(C), false)

            h.eq(B:is_subclass_of(A), true)
            h.eq(B:is_subclass_of(B), false)
            h.eq(B:is_subclass_of(C), false)

            h.eq(C:is_subclass_of(A), true)
            h.eq(C:is_subclass_of(B), true)
            h.eq(C:is_subclass_of(C), false)
          end)
        end)
      end)

      local function define_static_say_level(A)
        A.static.level = 1
        function A.static.say_level(class)
          return "Level: " .. class.level
        end

        h.eq(A.level, 1)
        h.eq(A:say_level(), "Level: 1")
      end

      it("can be defined for class", function()
        local A = create_classes("A")
        define_static_say_level(A)
      end)

      it("is inherited by subclass", function()
        local A, B = create_classes("A", { "B", "A" })

        define_static_say_level(A)

        h.eq(B.level, 1)
        h.eq(B:say_level(), "Level: 1")

        local C, D = create_classes({ "C", A }, { "D", B })

        h.eq(C.level, 1)
        h.eq(C:say_level(), "Level: 1")

        h.eq(D.level, 1)
        h.eq(D:say_level(), "Level: 1")
      end)

      it("can be redefined for subclass", function()
        local A = create_classes("A")
        define_static_say_level(A)

        local B = create_classes({ "B", A })

        B.static.level = 2
        h.eq(B:say_level(), "Level: 2")

        function B.static.say_level(class)
          return "LEVEL: " .. class.level
        end
        h.eq(B:say_level(), "LEVEL: 2")

        local C, D = create_classes({ "C", A }, { "D", B })

        C.static.level = 2
        h.eq(C:say_level(), "Level: 2")

        D.static.level = 3
        h.eq(D:say_level(), "LEVEL: 3")
      end)

      it("for subclass does not affect super", function()
        local A = create_classes("A")
        define_static_say_level(A)

        local B = create_classes({ "B", A })

        B.static.level = 2
        function B.static.say_level(class)
          return "LEVEL: " .. class.level
        end

        h.eq(A:say_level(), "Level: 1")

        local C = create_classes({ "C", B })

        function C.static.say_name(class)
          return class.name
        end

        h.eq(C:say_name(), "C")

        h.eq(type(C.say_name), "function")
        h.eq(type(B.say_name), "nil")
        h.eq(type(A.say_name), "nil")
      end)
    end)

    describe("instance", function()
      it("can be created", function()
        local A = create_classes("A")

        local a = A:new()
        assert_instance(a, A)
      end)

      describe("method", function()
        describe(":is_instance_of", function()
          it("works", function()
            local A, B, C, D = create_classes("A", { "B", "A" }, { "C", "B" }, "D")

            local a, b, c, d = A:new(), B:new(), C:new(), D:new()

            for _, instance in ipairs({ a, b, c, d }) do
              h.eq(instance.is_instance_of, Object.is_instance)
            end

            h.eq(a:is_instance_of(A), true)
            h.eq(a:is_instance_of(B), false)
            h.eq(a:is_instance_of(C), false)
            h.eq(a:is_instance_of(D), false)

            h.eq(b:is_instance_of(A), true)
            h.eq(b:is_instance_of(B), true)
            h.eq(b:is_instance_of(C), false)
            h.eq(b:is_instance_of(D), false)

            h.eq(c:is_instance_of(A), true)
            h.eq(c:is_instance_of(B), true)
            h.eq(c:is_instance_of(C), true)
            h.eq(c:is_instance_of(D), false)

            h.eq(d:is_instance_of(A), false)
            h.eq(d:is_instance_of(B), false)
            h.eq(d:is_instance_of(C), false)
            h.eq(d:is_instance_of(D), true)
          end)
        end)

        it("can be defined", function()
          local A = create_classes("A")

          function A:before_instance_creation()
            return "before " .. self.class.name .. " instance"
          end

          local a = A:new()

          function A:after_instance_creation()
            return "after " .. self.class.name .. " instance"
          end

          h.eq(a:before_instance_creation(), "before A instance")
          h.eq(a:after_instance_creation(), "after A instance")
        end)

        it("can be inherited", function()
          local A, B = create_classes("A", { "B", "A" })

          function A:say_class_name()
            return self.class.name
          end

          local a = A:new()
          h.eq(a:say_class_name(), "A")

          local b = B:new()
          h.eq(b:say_class_name(), "B")

          local C = create_classes({ "C", B })

          local c = C:new()
          h.eq(c:say_class_name(), "C")
        end)

        it("can be redefined", function()
          local A, B = create_classes("A", { "B", "A" })

          function A:say_class_name()
            return self.class.name
          end

          local a = A:new()
          h.eq(a:say_class_name(), "A")

          function B:say_class_name()
            return string.lower(self.class.name)
          end

          local b = B:new()
          h.eq(b:say_class_name(), "b")

          local C = create_classes({ "C", B })

          local c = C:new()
          h.eq(c:say_class_name(), "c")

          function C:say_class_name()
            return string.rep(self.class.name, 3)
          end

          h.eq(c:say_class_name(), "CCC")

          C.say_class_name = nil

          h.eq(c:say_class_name(), "c")

          B.say_class_name = nil

          h.eq(c:say_class_name(), "C")
        end)
      end)

      describe("metamethod", function()
        describe("__index", function()
          it("can be set to table", function()
            local A = create_classes("A")

            function A:upper(str) -- luacheck: no unused args
              return string.upper(str)
            end

            A.__index = {
              upper = function(_, str)
                return str
              end,
              lower = function(_, str)
                return string.lower(str)
              end,
            }

            local a = A()

            h.eq(a:upper("y"), "Y")

            h.eq(a:lower("Y"), "y")

            A.__index = nil

            h.eq(type(a.lower), "nil")
          end)

          it("can be set to function", function()
            local A = create_classes("A")

            function A:upper(str) -- luacheck: no unused args
              return string.upper(str)
            end

            local index = {
              upper = function(self, str) -- luacheck: no unused args
                return str
              end,
              lower = function(self, str) -- luacheck: no unused args
                return string.lower(str)
              end,
            }

            A.__index = function(self, key) -- luacheck: no unused args
              return index[key]
            end

            local a = A()

            h.eq(a:upper("y"), "Y")

            h.eq(a:lower("Y"), "y")

            A.__index = nil

            h.eq(type(a.lower), "nil")
          end)
        end)

        describe("__tostring", function()
          it("can be redefined", function()
            local A, B = create_classes("A", { "B", "A" })

            local a = A()

            h.eq(tostring(a), "instance of class A")

            function A:__tostring()
              return "class " .. self.class.name .. "'s child"
            end

            h.eq(tostring(a), "class A's child")

            local b = B()

            h.eq(tostring(b), "class B's child")

            function B:__tostring()
              return "child of " .. self.class.name
            end

            h.eq(tostring(b), "child of B")

            B.__tostring = nil

            h.eq(tostring(b), "class B's child")
          end)
        end)
      end)
    end)
  end)
end)