package luaplugin

import (
	"strings"
	"sync/atomic"
	"testing"
	"time"

	lua "github.com/yuin/gopher-lua"
)

func TestPluginBindAndEmit(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{"q": true, "ctrl+c": true})

	var fired atomic.Int64
	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		_G.result = p:bind("X", function(key) bump() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}

	p.mu.Lock()
	p.L.SetGlobal("bump", p.L.NewFunction(func(L *lua.LState) int {
		fired.Add(1)
		return 0
	}))
	p.mu.Unlock()

	if ok := m.EmitKey("x"); !ok {
		t.Fatal("EmitKey returned false for bound key")
	}

	waitAtomic(t, &fired, 1, 2*time.Second)
}

func TestPluginBindRejectsReservedKey(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{"q": true})

	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		_G.ok, _G.err = p:bind("q", function() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.L.GetGlobal("ok") != lua.LFalse {
		t.Fatal("expected ok=false for reserved key")
	}
	if s := p.L.GetGlobal("err").String(); !strings.Contains(s, "reserved") {
		t.Fatalf("err = %q, want reserved", s)
	}
}

func TestPluginBindRequiresKeymapPermission(t *testing.T) {
	m := newTestManager()

	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook"})
		_G.ok, _G.err = p:bind("x", function() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.L.GetGlobal("ok") != lua.LFalse {
		t.Fatal("expected ok=false without permission")
	}
}

func TestEmitKeyUnboundReturnsFalse(t *testing.T) {
	m := newTestManager()
	if m.EmitKey("nothing-bound-here") {
		t.Fatal("EmitKey should return false when nothing is bound")
	}
}

func TestPluginUnbind(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{})

	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		p:bind("x", function() end)
		p:unbind("x")
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	if m.EmitKey("x") {
		t.Fatal("key should be unbound")
	}
}

func TestPluginBindWithDescriptionSurfacesInKeyBindings(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{})

	loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		p:bind("x", "Extract chapters", function() end)
		p:bind("y", function() end)  -- no description → not surfaced
	`)

	bindings := m.KeyBindings()
	if len(bindings) != 1 {
		t.Fatalf("KeyBindings() returned %d entries, want 1: %+v", len(bindings), bindings)
	}
	if bindings[0].Key != "x" || bindings[0].Plugin != "kb" || bindings[0].Description != "Extract chapters" {
		t.Fatalf("unexpected binding: %+v", bindings[0])
	}
}

func TestKeyBindingsRemovedOnCleanup(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{})
	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		p:bind("x", "Do thing", function() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	if len(m.KeyBindings()) != 1 {
		t.Fatal("expected 1 binding before cleanup")
	}
	m.cleanupPlugin(p)
	if len(m.KeyBindings()) != 0 {
		t.Fatal("expected 0 bindings after cleanup")
	}
}

func TestCleanupPluginRemovesBinds(t *testing.T) {
	m := newTestManager()
	m.SetReservedKeys(map[string]bool{})

	p := loadTestPlugin(t, m, "kb", `
		local p = plugin.register({name = "kb", type = "hook", permissions = {"keymap"}})
		p:bind("x", function() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	if !m.EmitKey("x") {
		t.Fatal("expected x to be bound")
	}
	m.cleanupPlugin(p)
	if m.EmitKey("x") {
		t.Fatal("bind should be removed when plugin is cleaned up")
	}
}

func TestCommandRegisterAndEmit(t *testing.T) {
	m := newTestManager()
	p := loadTestPlugin(t, m, "cmd", `
		local p = plugin.register({name = "cmd", type = "hook"})
		p:command("hello", function(args)
			return "hi " .. (args[1] or "world")
		end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}

	out, err := m.EmitCommand("cmd", "hello", []string{"friend"})
	if err != nil {
		t.Fatal(err)
	}
	if out != "hi friend" {
		t.Fatalf("output = %q, want %q", out, "hi friend")
	}
}

func TestCommandNotFound(t *testing.T) {
	m := newTestManager()
	_, err := m.EmitCommand("nope", "nope", nil)
	if err == nil {
		t.Fatal("expected error for unknown command")
	}
	if !strings.Contains(err.Error(), "no such") {
		t.Fatalf("err = %q", err)
	}
}

func TestCommandListIncludesRegistered(t *testing.T) {
	m := newTestManager()
	loadTestPlugin(t, m, "cmd", `
		local p = plugin.register({name = "cmd", type = "hook"})
		p:command("a", function() end)
		p:command("b", function() end)
	`)
	list := m.CommandList()
	if len(list) != 2 {
		t.Fatalf("expected 2 commands, got %d: %v", len(list), list)
	}
}

func TestCleanupPluginRemovesCommands(t *testing.T) {
	m := newTestManager()
	p := loadTestPlugin(t, m, "cmd", `
		local p = plugin.register({name = "cmd", type = "hook"})
		p:command("a", function() end)
	`)
	if p == nil {
		t.Fatal("plugin failed to load")
	}
	m.cleanupPlugin(p)
	if len(m.CommandList()) != 0 {
		t.Fatal("commands should be removed when plugin is cleaned up")
	}
}

func waitAtomic(t *testing.T, counter *atomic.Int64, target int64, timeout time.Duration) {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if counter.Load() >= target {
			return
		}
		time.Sleep(5 * time.Millisecond)
	}
	t.Fatalf("counter reached %d, want %d", counter.Load(), target)
}
