describe("Tests keybind-related functions", function()
  describe("Tests the functionality of getKeyCode", function()
    setup(function()
      -- tempKey creates temporary keybinds that killKey can actually remove
      -- tempKey(modifier, key code, lua code)
      _G.testKeyID = tempKey(mudlet.keymodifier.Control, mudlet.key.F1, [[echo("Test key pressed")]])
    end)

    teardown(function()
      if _G.testKeyID then
        killKey(_G.testKeyID)
      end
      _G.testKeyID = nil
    end)

    it("should return key code and modifiers for a valid keybind by ID", function()
      local keyCode, modifiers = getKeyCode(_G.testKeyID)

      assert.is_not_nil(keyCode, "Expected keyCode to be returned")
      assert.is_not_nil(modifiers, "Expected modifiers to be returned")
      assert.equals(mudlet.key.F1, keyCode, "Expected key code to match F1")
      assert.equals(mudlet.keymodifier.Control, modifiers, "Expected modifiers to include Control")
    end)

    it("should return nil and error message for non-existent keybind name", function()
      local nonExistentName = "NonExistentKeyBind_" .. os.time()
      local keyCode, errorMsg = getKeyCode(nonExistentName)

      assert.is_nil(keyCode, "Expected nil for non-existent keybind")
      assert.is_string(errorMsg, "Expected error message string")
    end)

    it("should return nil and error message for non-existent keybind ID", function()
      local nonExistentID = 999999
      local keyCode, errorMsg = getKeyCode(nonExistentID)

      assert.is_nil(keyCode, "Expected nil for non-existent keybind ID")
      assert.is_string(errorMsg, "Expected error message string")
    end)

    it("should return nil and error message for invalid ID (negative number)", function()
      local invalidID = -1
      local keyCode, errorMsg = getKeyCode(invalidID)

      assert.is_nil(keyCode, "Expected nil for invalid negative ID")
      assert.is_string(errorMsg, "Expected error message string")
    end)

    it("should raise an error if called with no arguments", function()
      assert.has_error(function()
        getKeyCode()
      end)
    end)

    it("should raise an error if called with a boolean argument", function()
      assert.has_error(function()
        getKeyCode(true)
      end)
    end)

    it("should raise an error if called with a table argument", function()
      assert.has_error(function()
        getKeyCode({})
      end)
    end)

    it("should raise an error if called with a nil argument", function()
      assert.has_error(function()
        getKeyCode(nil)
      end)
    end)

    it("should work with keybinds that have no modifiers", function()
      local testKeyID2 = tempKey(mudlet.keymodifier.None, mudlet.key.F2, [[echo("Test")]])

      local keyCode, modifiers = getKeyCode(testKeyID2)

      killKey(testKeyID2)

      assert.is_not_nil(keyCode, "Expected keyCode to be returned")
      assert.equals(mudlet.key.F2, keyCode, "Expected key code to match F2")
      assert.equals(mudlet.keymodifier.None, modifiers, "Expected no modifiers")
    end)

    it("should work with keybinds that have multiple modifiers", function()
      local multiMod = mudlet.keymodifier.Control + mudlet.keymodifier.Shift
      local testKeyID3 = tempKey(multiMod, mudlet.key.F3, [[echo("Test")]])

      local keyCode, modifiers = getKeyCode(testKeyID3)

      killKey(testKeyID3)

      assert.is_not_nil(keyCode, "Expected keyCode to be returned")
      assert.equals(mudlet.key.F3, keyCode, "Expected key code to match F3")
      local expectedMod = mudlet.keymodifier.Control + mudlet.keymodifier.Shift
      assert.equals(expectedMod, modifiers, "Expected Control + Shift modifiers")
    end)
  end)

  describe("tempKey creation and cleanup", function()
    it("creates a key from a modifier, key code and string body", function()
      local id = tempKey(mudlet.keymodifier.Alt, mudlet.key.F5, [[echo("hi")]])
      assert.is_number(id)
      assert.are.equal(1, exists(id, "keybind"), "the temp key should exist after creation")
      local keyCode, modifiers = getKeyCode(id)
      assert.are.equal(mudlet.key.F5, keyCode)
      assert.are.equal(mudlet.keymodifier.Alt, modifiers)
      killKey(id)
    end)

    it("creates a key from the two-argument (key code, body) form", function()
      local id = tempKey(mudlet.key.F6, [[echo("hi")]])
      assert.is_number(id)
      local keyCode, modifiers = getKeyCode(id)
      assert.are.equal(mudlet.key.F6, keyCode)
      assert.are.equal(mudlet.keymodifier.None, modifiers, "the two-arg form should have no modifier")
      killKey(id)
    end)

    it("creates a key from a function body", function()
      -- a key cannot be pressed headlessly, so this only asserts creation
      local id = tempKey(mudlet.key.F7, function() end)
      assert.is_number(id)
      assert.are.equal(1, exists(id, "keybind"))
      killKey(id)
    end)

    it("rejects a non-string, non-function body", function()
      -- a table is not string-coercible (a number would be accepted as code)
      assert.has_error(function()
        tempKey(mudlet.key.F8, {})
      end)
    end)
  end)

  describe("permKey creation and validation", function()
    -- permanent keys cannot be removed with killKey, so use unique names and
    -- disable them after each test so they do not leak into other specs
    after_each(function()
      disableKey("SpecPermKeyOne")
      disableKey("SpecPermKeyTwo")
    end)

    it("creates a permanent key without a modifier (four-argument form)", function()
      local id = permKey("SpecPermKeyOne", "", mudlet.key.F9, [[echo("perm")]])
      assert.is_number(id)
      assert.is_true(id > 0)
      local keyCode, modifiers = getKeyCode(id)
      assert.are.equal(mudlet.key.F9, keyCode)
      assert.are.equal(mudlet.keymodifier.None, modifiers)
    end)

    it("creates a permanent key with a modifier (five-argument form)", function()
      local id = permKey("SpecPermKeyTwo", "", mudlet.keymodifier.Control, mudlet.key.F10, [[echo("perm")]])
      assert.is_number(id)
      assert.is_true(id > 0)
      local keyCode, modifiers = getKeyCode(id)
      assert.are.equal(mudlet.key.F10, keyCode)
      assert.are.equal(mudlet.keymodifier.Control, modifiers)
    end)

    it("errors when the lua code argument is not a string", function()
      assert.has_error(function()
        permKey("SpecPermKeyOne", "", mudlet.key.F9, 42)
      end)
    end)
  end)

  describe("enable, disable, kill, isActive and exists for keys", function()
    after_each(function()
      disableKey("SpecPermKeyKill")
      disableKey("SpecPermKeyToggle")
      disableKey("SpecDupKeys")
    end)

    it("killKey returns true for a temp key and false for a missing name", function()
      local id = tempKey(mudlet.key.F11, [[echo("x")]])
      assert.are.equal(1, exists(id, "keybind"))
      assert.is_true(killKey(id), "killing an existing temp key should return true")
      assert.is_false(killKey("no_such_key_name"), "killing a missing key should return false")
    end)

    it("killKey returns false the second time, as the key is already dead", function()
      local id = tempKey(mudlet.key.F11, [[echo("x")]])
      assert.is_true(killKey(id), "killing a live temporary key should report success")
      -- the key is still present here: only the deferred cleanup frees it, so the
      -- second kill really is being told about a corpse it can find
      assert.are.equal(1, exists(id, "keybind"), "the killed key is still present until cleanup runs")
      assert.are.equal(0, isActive(id, "keybind"), "a killed key is no longer active")
      assert.is_false(killKey(id), "killing an already killed key achieves nothing and has to say so")
      -- an incoming line runs every unit's deferred cleanup, which is what finally
      -- frees the key; the answer has to be the same after it. A key press cannot be
      -- synthesised headlessly, so there is no in-callback double kill to pin here -
      -- KeyUnit's depth and cleanup machinery matches AliasUnit's, whose spec has one
      feedTriggers("\nspec_key_kill_flush\n")
      assert.are.equal(0, exists(id, "keybind"), "the key should be gone after kill and cleanup")
      assert.is_false(killKey(id), "a freed key cannot be killed either")
    end)

    it("killKey returns false for a permanent key (they cannot be killed)", function()
      local id = permKey("SpecPermKeyKill", "", mudlet.key.F12, [[echo("x")]])
      assert.is_true(id > 0)
      assert.is_false(killKey("SpecPermKeyKill"), "permanent keys cannot be removed with killKey")
    end)

    it("enableKey and disableKey report whether a matching key was found", function()
      local id = permKey("SpecPermKeyToggle", "", mudlet.key.F12, [[echo("x")]])
      assert.is_true(id > 0)
      assert.is_true(disableKey("SpecPermKeyToggle"), "disableKey should report it found the key")
      assert.is_true(enableKey("SpecPermKeyToggle"), "enableKey should report it found the key")
      assert.is_false(disableKey("no_such_key_name"), "disableKey should return false when nothing matched")
      assert.is_false(enableKey("no_such_key_name"), "enableKey should return false when nothing matched")
    end)

    it("isActive reflects a key's enabled state", function()
      local id = tempKey(mudlet.key.F11, [[echo("x")]])
      assert.are.equal(1, isActive(id, "keybind"), "a fresh key should be active")
      disableKey(id)
      assert.are.equal(0, isActive(id, "keybind"), "a disabled key should not be active")
      enableKey(id)
      assert.are.equal(1, isActive(id, "keybind"), "a re-enabled key should be active")
      killKey(id)
    end)

    it("exists reports keys by ID and reports zero for an unknown name", function()
      local id = tempKey(mudlet.key.F11, [[echo("x")]])
      assert.are.equal(1, exists(id, "keybind"))
      assert.are.equal(0, exists("no_such_key_name", "keybind"))
      killKey(id)
    end)

    -- KeyUnit carries its own copy of the equal_range duplicate-name fix
    -- (PR #9366), so enable/disable by name must toggle every same-named key.
    -- Key firing cannot be synthesised headlessly, so isActive's count is the proxy.
    it("enableKey/disableKey by name toggle every duplicate-named key", function()
      local id1 = permKey("SpecDupKeys", "", mudlet.key.F11, [[echo("x")]])
      local id2 = permKey("SpecDupKeys", "", mudlet.key.F12, [[echo("x")]])
      assert.is_true(id1 > 0 and id2 > 0)
      -- invariants rather than exact counts: permanent keys from earlier local
      -- runs accumulate under this name in the saved profile
      assert.is_true(exists("SpecDupKeys", "keybind") >= 2, "at least the two duplicate-named keys exist")
      disableKey("SpecDupKeys")
      assert.are.equal(
        0,
        isActive("SpecDupKeys", "keybind"),
        "disabling by name must leave zero of the duplicates active"
      )
      enableKey("SpecDupKeys")
      assert.are.equal(
        exists("SpecDupKeys", "keybind"),
        isActive("SpecDupKeys", "keybind"),
        "enabling by name must reactivate every duplicate"
      )
    end)

    it("freeing a temporary key leaves a same-named permanent one reachable", function()
      -- tempKey names its key after its id, so a permanent key called after that
      -- number shares the name - and the name lookup table holds several keys per
      -- name
      local tempId = tempKey(mudlet.key.F11, [[echo("x")]])
      local sharedName = tostring(tempId)
      -- permanent keys cannot be deleted from Lua, so earlier local runs can leave
      -- same-named ones behind: work from a relative baseline
      local before = exists(sharedName, "keybind")
      assert.is_true(permKey(sharedName, "", mudlet.key.F12, [[echo("x")]]) > 0)
      finally(function()
        disableKey(sharedName)
      end)
      assert.are.equal(before + 1, exists(sharedName, "keybind"))

      assert.is_true(killKey(tempId), "the temporary key is the one that can be killed")
      -- an incoming line runs every unit's deferred cleanup, which frees it
      feedTriggers("\nspec_key_eviction_flush\n")

      assert.are.equal(before, exists(sharedName, "keybind"), "only the temporary key should leave the lookup table")
      assert.is_true(enableKey(sharedName), "the permanent key must still be reachable by name")
    end)

    it("killKey finds a temporary key behind a same-named permanent one", function()
      -- killKey walks the root node list in creation order, so a permanent key
      -- restored from the profile sits in front of this session's temporaries: it
      -- must be scanned past, not reported as a failure
      local seed = tempKey(mudlet.key.F9, [[echo("x")]])
      killKey(seed)
      -- permKey itself takes seed + 1, so the next temporary takes seed + 2
      local sharedName = tostring(seed + 2)
      assert.is_true(permKey(sharedName, "", mudlet.key.F10, [[echo("x")]]) > 0)
      finally(function()
        disableKey(sharedName)
      end)

      local tempId = tempKey(mudlet.key.F11, [[echo("x")]])
      assert.are.equal(seed + 2, tempId, "ids should still be handed out in sequence")
      assert.is_true(killKey(tempId), "killKey must scan past the permanent key")
    end)
  end)
end)
