package profile

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
)

func TestManager_NewWithEmptyDir(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)
	if m == nil {
		t.Fatal("expected non-nil Manager")
	}
	if m.Config() == nil {
		t.Fatal("expected non-nil Config")
	}
	if m.configDir != dir {
		t.Fatalf("expected configDir %q, got %q", dir, m.configDir)
	}
}

func TestManager_AgentCRUD(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	agents := m.Agents().All()
	if len(agents) != 0 {
		t.Fatalf("expected 0 agents, got %d", len(agents))
	}

	_, ok := m.Agents().Get("test-agent")
	if ok {
		t.Fatal("expected agent not found")
	}

	a := makeAgentConfig("test-agent", "Test Agent")
	if err := m.Agents().Add(a); err != nil {
		t.Fatalf("AddAgent: %v", err)
	}

	got, ok := m.Agents().Get("test-agent")
	if !ok {
		t.Fatal("expected agent found after AddAgent")
	}
	if got.Name != "Test Agent" {
		t.Fatalf("expected name %q, got %q", "Test Agent", got.Name)
	}

	if err := m.Agents().Add(a); err == nil {
		t.Fatal("expected duplicate error")
	}

	a.Name = "Updated Agent"
	if err := m.Agents().Update(a); err != nil {
		t.Fatalf("UpdateAgent: %v", err)
	}
	got, _ = m.Agents().Get("test-agent")
	if got.Name != "Updated Agent" {
		t.Fatalf("expected updated name %q, got %q", "Updated Agent", got.Name)
	}

	if err := m.Agents().Delete("test-agent"); err != nil {
		t.Fatalf("RemoveAgent: %v", err)
	}
	_, ok = m.Agents().Get("test-agent")
	if ok {
		t.Fatal("expected agent not found after RemoveAgent")
	}

	if err := m.Agents().Delete("test-agent"); err == nil {
		t.Fatal("expected not-found error on double remove")
	}
}

func TestManager_AgentDefaultElection(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	a1 := makeAgentConfig("alpha", "Alpha")
	if err := m.Agents().Add(a1); err != nil {
		t.Fatalf("AddAgent alpha: %v", err)
	}
	if m.Agents().Default() != "alpha" {
		t.Fatalf("expected default agent %q, got %q", "alpha", m.Agents().Default())
	}

	a2 := makeAgentConfig("beta", "Beta")
	if err := m.Agents().Add(a2); err != nil {
		t.Fatalf("AddAgent beta: %v", err)
	}
	if m.Agents().Default() != "alpha" {
		t.Fatal("default should remain alpha (first added)")
	}

	if err := m.Agents().Delete("alpha"); err != nil {
		t.Fatalf("RemoveAgent alpha: %v", err)
	}
	if m.Agents().Default() != "beta" {
		t.Fatalf("expected default agent to re-elect to %q, got %q", "beta", m.Agents().Default())
	}

	if err := m.Agents().Delete("beta"); err != nil {
		t.Fatalf("RemoveAgent beta: %v", err)
	}
	if got := m.Agents().Default(); got != "" {
		t.Fatalf("expected default agent to clear after removing all agents, got %q", got)
	}
	loaded, err := Load(filepath.Join(dir, "config.yaml"))
	if err != nil {
		t.Fatalf("Load config: %v", err)
	}
	if got := loaded.DefaultAgent; got != "" {
		t.Fatalf("persisted DefaultAgent = %q, want empty", got)
	}
}

func TestManager_AgentDeleteUsesLatestPersistedDefault(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	stale := New(dir)

	alpha := makeAgentConfig("alpha", "Alpha")
	if err := stale.Agents().Add(alpha); err != nil {
		t.Fatalf("AddAgent alpha: %v", err)
	}
	beta := makeAgentConfig("beta", "Beta")
	if err := stale.Agents().Add(beta); err != nil {
		t.Fatalf("AddAgent beta: %v", err)
	}

	latest := New(dir)
	if err := latest.Agents().SetDefault("beta"); err != nil {
		t.Fatalf("SetDefault beta: %v", err)
	}
	if err := stale.Agents().Delete("beta"); err != nil {
		t.Fatalf("Delete beta: %v", err)
	}
	loaded, err := Load(filepath.Join(dir, "config.yaml"))
	if err != nil {
		t.Fatalf("Load config: %v", err)
	}
	if got := loaded.DefaultAgent; got != "alpha" {
		t.Fatalf("persisted DefaultAgent = %q, want alpha", got)
	}
}

func TestManager_ChannelCRUD(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	channels := m.Servers().All()
	if len(channels) != 0 {
		t.Fatalf("expected 0 channels, got %d", len(channels))
	}

	ch := &AgentTeamServer{Name: "My AgentTeam"}
	if err := m.Servers().Add(ch); err != nil {
		t.Fatalf("AddChannel: %v", err)
	}

	got, ok := m.Servers().Get(ch.ID)
	if !ok {
		t.Fatal("expected channel found after AddChannel")
	}
	if got.Name != "My AgentTeam" {
		t.Fatalf("expected name %q, got %q", "My AgentTeam", got.Name)
	}

	if err := m.Servers().Delete(ch.ID); err != nil {
		t.Fatalf("RemoveChannel: %v", err)
	}
	_, ok = m.Servers().Get(ch.ID)
	if ok {
		t.Fatal("expected channel not found after RemoveChannel")
	}
}

func TestAgentPageUsesRevisionBoundedContinuation(t *testing.T) {
	t.Parallel()

	m := New(t.TempDir())
	for _, item := range []agent.Config{
		makeAgentConfig("alpha", "Alpha"),
		makeAgentConfig("beta", "Beta"),
		makeAgentConfig("gamma", "Gamma"),
	} {
		if err := m.Agents().Add(item); err != nil {
			t.Fatalf("add %s: %v", item.ID, err)
		}
	}
	first, hasMore, revision, err := m.Agents().Page("", "", "", 2)
	if err != nil || !hasMore || revision == "" {
		t.Fatalf("first page = %#v, hasMore=%v, revision=%q, err=%v", first, hasMore, revision, err)
	}
	if got := []string{first[0].ID, first[1].ID}; got[0] != "alpha" || got[1] != "beta" {
		t.Fatalf("first page IDs = %v", got)
	}
	second, hasMore, sameRevision, err := m.Agents().Page("", revision, first[1].ID, 2)
	if err != nil || hasMore || sameRevision != revision || len(second) != 1 || second[0].ID != "gamma" {
		t.Fatalf("second page = %#v, hasMore=%v, revision=%q, err=%v", second, hasMore, sameRevision, err)
	}

	if err := m.Agents().Add(makeAgentConfig("delta", "Delta")); err != nil {
		t.Fatal(err)
	}
	if _, _, _, err := m.Agents().Page("", revision, first[1].ID, 2); err == nil {
		t.Fatal("expected insertion to stale the continuation")
	}

	fresh, _, revision, err := m.Agents().Page("", "", "", 10)
	if err != nil || len(fresh) != 4 {
		t.Fatalf("fresh page = %#v, err=%v", fresh, err)
	}
	if err := m.Agents().Delete("alpha"); err != nil {
		t.Fatal(err)
	}
	if _, _, _, err := m.Agents().Page("", revision, "beta", 2); err == nil {
		t.Fatal("expected deletion to stale the continuation")
	}
	_, _, revision, err = m.Agents().Page("", "", "", 10)
	if err != nil || revision == "" {
		t.Fatalf("page after deletion = revision %q, err=%v", revision, err)
	}

	updated := makeAgentConfig("beta", "Special Beta")
	if err := m.Agents().Update(updated); err != nil {
		t.Fatal(err)
	}
	if _, _, _, err := m.Agents().Page("", revision, "beta", 2); err == nil {
		t.Fatal("expected rename to stale the continuation")
	}
	_, _, revision, err = m.Agents().Page("special", "", "", 2)
	if err != nil || revision == "" {
		t.Fatalf("filtered page after rename err=%v revision=%q", err, revision)
	}
	m.Reload()
	if _, _, _, err := m.Agents().Page("special", revision, "", 2); err == nil {
		t.Fatal("expected reload to stale the continuation")
	}
}

func TestAgentPageConcurrentReadersAndMutation(t *testing.T) {
	t.Parallel()

	m := New(t.TempDir())
	for i := 0; i < 12; i++ {
		id := "agent-" + string(rune('a'+i))
		if err := m.Agents().Add(makeAgentConfig(id, id)); err != nil {
			t.Fatal(err)
		}
	}
	_, _, revision, err := m.Agents().Page("", "", "", 3)
	if err != nil {
		t.Fatal(err)
	}
	var group sync.WaitGroup
	for i := 0; i < 8; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			_, _, _, _ = m.Agents().Page("", revision, "", 3)
		}()
	}
	group.Add(1)
	go func() {
		defer group.Done()
		updated := makeAgentConfig("agent-a", "renamed")
		_ = m.Agents().Update(updated)
	}()
	group.Wait()
	if _, _, _, err := m.Agents().Page("", revision, "", 3); err == nil {
		t.Fatal("expected concurrent mutation to invalidate the old revision")
	}
}

func TestAgentPageBoundsSparseAndAbsentFilterWork(t *testing.T) {
	t.Parallel()

	m := New(t.TempDir())
	m.mu.Lock()
	m.agents = make(map[string]*agent.Config, 1200)
	for i := 0; i < 1200; i++ {
		id := fmt.Sprintf("agent-%04d", i)
		name := "ordinary"
		if i == 900 {
			name = "needle"
		}
		item := makeAgentConfig(id, name)
		m.agents[id] = &item
	}
	m.agentMgr.rebuildAgentIndexLocked()
	m.mu.Unlock()

	dense, err := m.Agents().PageResult("agent-", "", "", 10)
	if err != nil {
		t.Fatal(err)
	}
	if dense.Examined > 11 || len(dense.Agents) != 10 || !dense.HasMore {
		t.Fatalf("dense page examined=%d len=%d hasMore=%v", dense.Examined, len(dense.Agents), dense.HasMore)
	}

	page, err := m.Agents().PageResult("needle", "", "", 1)
	if err != nil {
		t.Fatal(err)
	}
	if page.Examined != agentPageScanBudget || !page.HasMore || page.NextAnchor == "" || len(page.Agents) != 0 {
		t.Fatalf("sparse first page=%+v", page)
	}
	found := false
	for calls := 0; page.HasMore && calls < 10; calls++ {
		page, err = m.Agents().PageResult("needle", page.Revision, page.NextAnchor, 1)
		if err != nil {
			t.Fatal(err)
		}
		if page.Examined > agentPageScanBudget {
			t.Fatalf("sparse continuation examined=%d", page.Examined)
		}
		if len(page.Agents) == 1 && page.Agents[0].ID == "agent-0900" {
			found = true
		}
	}
	if !found || page.HasMore {
		t.Fatalf("sparse terminal page=%+v found=%v", page, found)
	}

	absent, err := m.Agents().PageResult("does-not-exist", "", "", 1)
	if err != nil {
		t.Fatal(err)
	}
	if absent.Examined != agentPageScanBudget || !absent.HasMore || absent.NextAnchor == "" {
		t.Fatalf("absent first page=%+v", absent)
	}
	for calls := 0; absent.HasMore && calls < 10; calls++ {
		absent, err = m.Agents().PageResult("does-not-exist", absent.Revision, absent.NextAnchor, 1)
		if err != nil {
			t.Fatal(err)
		}
		if absent.Examined > agentPageScanBudget {
			t.Fatalf("absent continuation examined=%d", absent.Examined)
		}
	}
	if absent.HasMore || len(absent.Agents) != 0 {
		t.Fatalf("absent terminal page=%+v", absent)
	}

	middle, err := m.Agents().PageResult("ordinary", dense.Revision, "agent-0599", 5)
	if err != nil || middle.Examined > 6 || len(middle.Agents) != 5 {
		t.Fatalf("middle page=%+v err=%v", middle, err)
	}
	terminal, err := m.Agents().PageResult("ordinary", dense.Revision, "agent-1199", 5)
	if err != nil || terminal.Examined != 0 || terminal.HasMore || len(terminal.Agents) != 0 {
		t.Fatalf("terminal page=%+v err=%v", terminal, err)
	}
}

func TestAgentPageErrorsAreTyped(t *testing.T) {
	t.Parallel()

	m := New(t.TempDir())
	item := makeAgentConfig("alpha", "Alpha")
	m.mu.Lock()
	m.agents[item.ID] = &item
	m.agentMgr.rebuildAgentIndexLocked()
	m.mu.Unlock()
	_, err := m.Agents().PageResult("", "", "alpha", 1)
	pageErr, ok := err.(*agent.PageError)
	if !ok || pageErr.Kind != agent.PageErrorMalformed {
		t.Fatalf("malformed error=%T %#v", err, err)
	}
	_, err = m.Agents().PageResult("", "stale", "", 1)
	pageErr, ok = err.(*agent.PageError)
	if !ok || pageErr.Kind != agent.PageErrorStaleRevision {
		t.Fatalf("revision error=%T %#v", err, err)
	}
	_, err = m.Agents().PageResult("", "", "", 101)
	pageErr, ok = err.(*agent.PageError)
	if !ok || pageErr.Kind != agent.PageErrorLimit {
		t.Fatalf("limit error=%T %#v", err, err)
	}
}

func TestManager_AgentPersistedToDisk(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	a := makeAgentConfig("disk-agent", "Disk Agent")
	if err := m.Agents().Add(a); err != nil {
		t.Fatalf("AddAgent: %v", err)
	}

	m2 := New(dir)
	got, ok := m2.Agents().Get("disk-agent")
	if !ok {
		t.Fatal("agent not found after reload from disk")
	}
	if got.Name != "Disk Agent" {
		t.Fatalf("expected name %q, got %q", "Disk Agent", got.Name)
	}
}

func TestManager_ChannelPersistedToDisk(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	ch := &AgentTeamServer{Name: "Disk AgentTeam"}
	if err := m.Servers().Add(ch); err != nil {
		t.Fatalf("AddChannel: %v", err)
	}

	m2 := New(dir)
	got, ok := m2.Servers().Get(ch.ID)
	if !ok {
		t.Fatal("channel not found after reload from disk")
	}
	if got.Name != "Disk AgentTeam" {
		t.Fatalf("expected name %q, got %q", "Disk AgentTeam", got.Name)
	}
}

func TestManagerConfigReturnsSnapshot(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	cfg := m.Config()
	cfg.Port = 19090

	if got := m.Config().Port; got == 19090 {
		t.Fatalf("Config returned mutable cached pointer; Port = %d", got)
	}
}

func TestManagerReloadWaitsForLockedReaders(t *testing.T) {
	dir := t.TempDir()
	m := New(dir)

	m.mu.RLock()
	reloadDone := make(chan struct{})
	go func() {
		m.Reload()
		close(reloadDone)
	}()

	select {
	case <-reloadDone:
		t.Fatal("Reload completed while a reader held Manager.mu")
	default:
	}

	cfg := DefaultConfig()
	cfg.Port = 19091
	if err := Save(cfg, filepath.Join(dir, "config.yaml")); err != nil {
		t.Fatalf("Save config error = %v", err)
	}

	m.mu.RUnlock()
	<-reloadDone

	if got := m.Config().Port; got != 19091 {
		t.Fatalf("Config().Port after Reload = %d, want 19091", got)
	}
}

func TestManager_SetChannelEnabled(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	m := New(dir)

	ch := &AgentTeamServer{Name: "Toggle AgentTeamServer"}
	if err := m.Servers().Add(ch); err != nil {
		t.Fatalf("AddChannel: %v", err)
	}

	if err := m.Servers().SetEnabled(ch.ID, false); err != nil {
		t.Fatalf("SetChannelEnabled: %v", err)
	}
	got, _ := m.Servers().Get(ch.ID)
	if got.Enabled {
		t.Fatal("expected channel to be disabled")
	}
}

func TestDefaultServerName(t *testing.T) {
	t.Parallel()

	tests := []struct {
		chType string
		want   string
	}{
		{"agentteam", ""},
		{"unknown", "unknown"},
	}
	for _, tt := range tests {
		got := defaultServerName(tt.chType)
		if got != tt.want {
			t.Errorf("defaultServerName(%q) = %q, want %q", tt.chType, got, tt.want)
		}
	}
}

func TestLoadYAMLFile_MissingFile(t *testing.T) {
	t.Parallel()

	var v map[string]interface{}
	err := loadYAMLFile(filepath.Join(t.TempDir(), "nonexistent.yaml"), &v)
	if err != nil {
		t.Fatalf("expected nil error for missing file, got %v", err)
	}
}

func TestLoadYAMLFile_InvalidYAML(t *testing.T) {
	t.Parallel()

	f := filepath.Join(t.TempDir(), "bad.yaml")
	mustWriteFile(t, f, []byte(": invalid: yaml: content: :"))

	var v map[string]interface{}
	err := loadYAMLFile(f, &v)
	if err == nil {
		t.Fatal("expected error for invalid YAML")
	}
}

func TestSaveYAMLFile(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	f := filepath.Join(dir, "test.yaml")

	data := map[string]string{"key": "value"}
	if err := saveYAMLFile(data, f, nil); err != nil {
		t.Fatalf("saveYAMLFile: %v", err)
	}

	got, err := os.ReadFile(f)
	if err != nil {
		t.Fatalf("ReadFile: %v", err)
	}
	if string(got) == "" {
		t.Fatal("expected non-empty file content")
	}
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

func makeAgentConfig(id, name string) agent.Config {
	return agent.Config{
		ID:      id,
		Name:    name,
		Runtime: "opencode",
		Source:  agent.NewLocalSource(),
	}
}
