//go:build e2e

// Package dispatcher contains e2e tests for the core Dispatcher, slash command
// InstructionHandler, agent overrides, and profile persistence.
package dispatcher

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"

	"github.com/pingcap/failpoint"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/e2e/helpers"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/dispatcher"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

func setupDispatcherTest(t *testing.T) (*dispatcher.Dispatcher, string) {
	t.Helper()
	cfgDir := t.TempDir()
	failpoint.Enable("agentteam-config-dir", fmt.Sprintf(`return("%s")`, cfgDir))
	t.Cleanup(func() { failpoint.Disable("agentteam-config-dir") })
	_ = profile.Default()
	d := newE2EDispatcher(t, "")
	t.Cleanup(d.Close)
	return d, cfgDir
}

// ---------------------------------------------------------------------------
// Tests: Agent overrides
// ---------------------------------------------------------------------------

func TestAgentOverrides_Concurrent(t *testing.T) {
	setupDispatcherTest(t)
	overrides := dispatcher.NewAgentOverrides()

	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			key := fmt.Sprintf("ch-%d", i%5)
			conv := fmt.Sprintf("conv-%d", i)
			overrides.Set(key, conv, fmt.Sprintf("agent-%d", i))
		}(i)
	}
	wg.Wait()

	for i := 0; i < 100; i++ {
		key := fmt.Sprintf("ch-%d", i%5)
		conv := fmt.Sprintf("conv-%d", i)
		if got := overrides.Get(key, conv); got != fmt.Sprintf("agent-%d", i) {
			t.Errorf("override[%s:%s]=%q, want agent-%d", key, conv, got, i)
		}
	}
}

func TestAgentOverrides_MissingKey(t *testing.T) {
	setupDispatcherTest(t)
	overrides := dispatcher.NewAgentOverrides()
	for _, tc := range []struct{ ch, conv string }{
		{"ch-1", "conv-1"}, {"ch-2", "conv-2"}, {"", "conv-1"}, {"ch-1", ""},
	} {
		if got := overrides.Get(tc.ch, tc.conv); got != "" {
			t.Errorf("expected empty for (%q, %q), got %q", tc.ch, tc.conv, got)
		}
	}
}

// ---------------------------------------------------------------------------
// Tests: Dispatch routing
// ---------------------------------------------------------------------------

func TestDispatch_NilMessageIgnored(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	d.Dispatch(context.Background(), ch, nil)
	if len(ch.SentMsgs) != 0 {
		t.Fatalf("nil message should not produce replies, got %d", len(ch.SentMsgs))
	}
}

func TestDispatch_NilContextUsesBackground(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/help")
	d.Dispatch(nil, ch, msg)
	if len(ch.SentMsgs) == 0 {
		t.Fatal("expected reply even with nil context")
	}
}

func TestDispatch_NonSlashNotHandledByInstructions(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{
		ID: "non-slash-agent", Runtime: "opencode", Name: "Non Slash",
	})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("non-slash-agent", "#general"), "hello world")
	d.Dispatch(context.Background(), ch, msg)
	if len(ch.SentMsgs) != 0 {
		t.Fatalf("expected no immediate channel reply, got %d messages", len(ch.SentMsgs))
	}
}

// ---------------------------------------------------------------------------
// Tests: Slash command dispatch
// ---------------------------------------------------------------------------

func TestDispatch_SlashHelp(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/help")
	d.Dispatch(context.Background(), ch, msg)
	if len(ch.SentMsgs) == 0 {
		t.Fatal("expected reply from /help")
	}
	if !strings.Contains(ch.LastReply(), "/info") {
		t.Errorf("/help reply should list /info command: %s", ch.LastReply())
	}
}

func TestDispatch_SlashInfo(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{ID: "info-ag", Runtime: "opencode", Name: "Info Agent"})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/info")
	d.Dispatch(context.Background(), ch, msg)
	if !strings.Contains(ch.LastReply(), "test-server") || !strings.Contains(ch.LastReply(), "Info Agent") {
		t.Errorf("info should mention server connection and current agent: %s", ch.LastReply())
	}
}

func TestDispatch_SlashList(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{ID: "list-a1", Runtime: "opencode", Name: "Agent One"})
	profile.Default().Agents().Add(agent.Config{ID: "list-a2", Runtime: "claude", Name: "Agent Two"})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/list")
	d.Dispatch(context.Background(), ch, msg)
	reply := ch.LastReply()
	if !strings.Contains(reply, "list-a1") {
		t.Errorf("list should contain 'list-a1': %s", reply)
	}
	if !strings.Contains(reply, "list-a2") {
		t.Errorf("list should contain 'list-a2': %s", reply)
	}
}

func TestDispatch_SlashAgentSwitchByName(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{ID: "sw-a", Runtime: "opencode", Name: "Switch Agent"})
	profile.Default().Agents().Add(agent.Config{ID: "sw-b", Runtime: "claude", Name: "Other Agent"})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/agent sw-b")
	d.Dispatch(context.Background(), ch, msg)
	if !strings.Contains(ch.LastReply(), "Other Agent") {
		t.Errorf("switch should mention 'Other Agent': %s", ch.LastReply())
	}
}

func TestDispatch_SlashAgentSwitchByIndex(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{ID: "idx-a", Runtime: "opencode", Name: "Agent A"})
	profile.Default().Agents().Add(agent.Config{ID: "idx-b", Runtime: "claude", Name: "Agent B"})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/agent 2")
	d.Dispatch(context.Background(), ch, msg)
	if !strings.Contains(ch.LastReply(), "Agent B") {
		t.Errorf("switch by index should mention 'Agent B': %s", ch.LastReply())
	}
}

func TestDispatch_SlashAgentSwitchOutOfRange(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	profile.Default().Agents().Add(agent.Config{ID: "oor-a", Runtime: "opencode", Name: "A"})
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/agent 99")
	d.Dispatch(context.Background(), ch, msg)
	if !strings.Contains(ch.LastReply(), "99") {
		t.Errorf("out of range should mention index: %s", ch.LastReply())
	}
}

func TestDispatch_SlashAgentSwitchNonexistent(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/agent nonexistent")
	d.Dispatch(context.Background(), ch, msg)
	if !strings.Contains(ch.LastReply(), "nonexistent") {
		t.Errorf("not found should mention name: %s", ch.LastReply())
	}
}

func TestDispatch_SlashEcho(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/echo Hello World")
	d.Dispatch(context.Background(), ch, msg)
	if len(ch.SentMsgs) < 2 {
		t.Fatal("expected echo text + timing report")
	}
	echoText := ch.SentMsgs[len(ch.SentMsgs)-2].Content
	if !strings.Contains(echoText, "Hello World") {
		t.Errorf("echo should contain 'Hello World': %s", echoText)
	}
}

func TestDispatch_SlashNew(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/new")
	d.Dispatch(context.Background(), ch, msg)
	reply := ch.LastReply()
	if !strings.Contains(reply, "idle") && !strings.Contains(reply, "new") && !strings.Contains(reply, "当前没有活跃的会话") {
		t.Errorf("new should confirm session reset: %s", reply)
	}
}

func TestDispatch_SlashStatus(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/status")
	d.Dispatch(context.Background(), ch, msg)
	if ch.LastReply() == "" {
		t.Fatal("expected non-empty status reply")
	}
}

func TestDispatch_SlashToggleDebug(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/toggle-debug")
	d.Dispatch(context.Background(), ch, msg)
	if !ch.DebugOnVal {
		t.Error("debug should be toggled on")
	}
}

func TestDispatch_SlashSendMediaNoArgs(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/sendmedia")
	d.Dispatch(context.Background(), ch, msg)
	reply := ch.LastReply()
	if !strings.Contains(reply, "usage") && !strings.Contains(reply, "Usage") && !strings.Contains(reply, "用法") {
		t.Errorf("sendmedia without args should show usage: %s", reply)
	}
}

func TestDispatch_SlashReset(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/reset")
	d.Dispatch(context.Background(), ch, msg)
	if ch.LastReply() == "" {
		t.Fatal("expected non-empty reset reply")
	}
}

func TestDispatch_UnknownSlashNotHandled(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	h := dispatcher.NewInstructionHandler(d, dispatcher.NewAgentOverrides())
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "/unknown-command")
	if handled, _ := h.Handle(context.Background(), ch, msg); handled {
		t.Error("unknown slash command should not be handled")
	}
}

func TestDispatch_NonSlashNotHandled(t *testing.T) {
	d, _ := setupDispatcherTest(t)
	ch := &helpers.MockChannel{IDVal: "test-server", EnabledVal: true}
	h := dispatcher.NewInstructionHandler(d, dispatcher.NewAgentOverrides())
	msg := helpers.MakeMessage("test-server", helpers.MakeAgentTeamMeta("agent-1", "#general"), "hello world")
	if handled, _ := h.Handle(context.Background(), ch, msg); handled {
		t.Error("non-slash message should not be handled")
	}
}

// ---------------------------------------------------------------------------
// Tests: Profile persistence
// ---------------------------------------------------------------------------

func TestProfilePersistence_Agent(t *testing.T) {
	cfgDir := t.TempDir()
	failpoint.Enable("agentteam-config-dir", fmt.Sprintf(`return("%s")`, cfgDir))
	t.Cleanup(func() { failpoint.Disable("agentteam-config-dir") })

	mgr := profile.Default()
	mgr.Agents().Add(agent.Config{ID: "persist-ag", Runtime: "opencode", Name: "Persist Test"})
	mgr.Reload()
	ag, ok := mgr.Agents().Get("persist-ag")
	if !ok {
		t.Fatal("agent not found after reload")
	}
	if ag.Name != "Persist Test" {
		t.Errorf("name=%q, want 'Persist Test'", ag.Name)
	}
}

func newE2EDispatcher(t *testing.T, url string) *dispatcher.Dispatcher {
	t.Helper()
	d, err := dispatcher.New(url, dispatcher.Dependencies{})
	require.NoError(t, err)
	return d
}
