package dispatcher

import (
	"log"
	"os"
	"path/filepath"
	"testing"

	"forge.secuaas.ovh/olivier/claude-failover/internal/config"
	"forge.secuaas.ovh/olivier/claude-failover/internal/state"
)

// mockTmux is a minimal in-memory tmux.Client for tests.
type mockTmux struct {
	sessions   map[string]bool
	paneOutput map[string]string
	sentKeys   []string
}

func newMockTmux() *mockTmux {
	return &mockTmux{
		sessions:   make(map[string]bool),
		paneOutput: make(map[string]string),
	}
}

func (m *mockTmux) HasSession(name string) bool        { return m.sessions[name] }
func (m *mockTmux) CreateSession(name, _ string) error { m.sessions[name] = true; return nil }
func (m *mockTmux) KillSession(_ string) error         { return nil }
func (m *mockTmux) SendKeys(_, keys string) error {
	m.sentKeys = append(m.sentKeys, keys)
	return nil
}
func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) {
	return m.paneOutput[session], nil
}

// TestParseFrontmatter verifies YAML frontmatter extraction.
func TestParseFrontmatter(t *testing.T) {
	input := "---\ntitle: Fix bug\npriority: critical\n---\nDo the fix."
	fm, body := parseFrontmatter([]byte(input))
	if fm.Title != "Fix bug" {
		t.Errorf("expected title 'Fix bug', got %q", fm.Title)
	}
	if fm.Priority != "critical" {
		t.Errorf("expected priority critical, got %q", fm.Priority)
	}
	if body != "Do the fix." {
		t.Errorf("expected body 'Do the fix.', got %q", body)
	}
}

// TestParseFrontmatterNoHeader handles files without a YAML header.
func TestParseFrontmatterNoHeader(t *testing.T) {
	input := "Just plain content."
	fm, body := parseFrontmatter([]byte(input))
	if fm.Title != "" {
		t.Errorf("expected empty title, got %q", fm.Title)
	}
	if body != "Just plain content." {
		t.Errorf("expected full body, got %q", body)
	}
}

// TestModelForPriority maps priority strings to model names.
func TestModelForPriority(t *testing.T) {
	cases := []struct{ priority, want string }{
		{"critical", "opus"},
		{"CRITICAL", "opus"},
		{"high", "sonnet"},
		{"default", "sonnet"},
		{"", "sonnet"},
	}
	for _, c := range cases {
		if got := modelForPriority(c.priority); got != c.want {
			t.Errorf("modelForPriority(%q) = %q, want %q", c.priority, got, c.want)
		}
	}
}

// TestFindFreeSessionSkipsFailed verifies that recently-failed sessions are skipped.
func TestFindFreeSessionSkipsFailed(t *testing.T) {
	tc := newMockTmux()
	tc.sessions["sess-1"] = true
	tc.sessions["sess-2"] = true

	s := state.New("")
	s.SetIdle("sess-1")
	s.SetFailed("sess-2")

	d := &Dispatcher{
		tmux:  tc,
		state: s,
		config: &config.Config{
			Pool: config.PoolConfig{
				Dedicated:  []config.DedicatedSession{{Name: "sess-1"}, {Name: "sess-2"}},
				Autonomous: config.AutonomousConfig{Max: 0},
			},
		},
		logger: log.Default(),
	}

	got := d.findFreeSession()
	if got != "sess-1" {
		t.Errorf("expected sess-1, got %q", got)
	}
}

// TestFindFreeSessionMissingTmux skips sessions not in tmux.
func TestFindFreeSessionMissingTmux(t *testing.T) {
	tc := newMockTmux()
	// sess-1 missing from tmux, sess-2 present and idle.
	tc.sessions["sess-2"] = true

	s := state.New("")
	s.SetIdle("sess-1")
	s.SetIdle("sess-2")

	d := &Dispatcher{
		tmux:  tc,
		state: s,
		config: &config.Config{
			Pool: config.PoolConfig{
				Dedicated:  []config.DedicatedSession{{Name: "sess-1"}, {Name: "sess-2"}},
				Autonomous: config.AutonomousConfig{Max: 0},
			},
		},
		logger: log.Default(),
	}

	got := d.findFreeSession()
	if got != "sess-2" {
		t.Errorf("expected sess-2, got %q", got)
	}
}

// TestDispatchProject creates a task file, dispatches it, and checks state + rename.
func TestDispatchProject(t *testing.T) {
	dir := t.TempDir()
	inbox := filepath.Join(dir, ".agent-queue", "inbox")
	os.MkdirAll(inbox, 0755)

	taskContent := "---\ntitle: My Task\npriority: high\n---\nDo the work."
	taskPath := filepath.Join(inbox, "task-001.md")
	os.WriteFile(taskPath, []byte(taskContent), 0644)

	tc := newMockTmux()
	tc.sessions["free-sess"] = true
	// Return ❯ prompt on first CapturePaneTail call (Claude is ready).
	tc.paneOutput["free-sess"] = "❯ "

	s := state.New("")
	s.SetIdle("free-sess")

	d := &Dispatcher{
		tmux:  tc,
		state: s,
		config: &config.Config{
			Pool: config.PoolConfig{
				Dedicated:  []config.DedicatedSession{{Name: "free-sess", Project: dir}},
				Autonomous: config.AutonomousConfig{Max: 0},
			},
		},
		logger: log.Default(),
	}

	d.dispatchProject(inbox)

	if st := s.GetSession("free-sess"); st == nil || st.State != "working" {
		t.Errorf("expected session working after dispatch, got %v", st)
	}

	// Original file renamed to .dispatched.
	if _, err := os.Stat(taskPath + ".dispatched"); os.IsNotExist(err) {
		t.Error("expected .dispatched marker")
	}
	if _, err := os.Stat(taskPath); !os.IsNotExist(err) {
		t.Error("expected original task file to be renamed")
	}
}

// TestDispatchProjectNoFreeSession leaves the task untouched when no session is available.
func TestDispatchProjectNoFreeSession(t *testing.T) {
	dir := t.TempDir()
	inbox := filepath.Join(dir, ".agent-queue", "inbox")
	os.MkdirAll(inbox, 0755)

	taskPath := filepath.Join(inbox, "task-002.md")
	os.WriteFile(taskPath, []byte("content"), 0644)

	tc := newMockTmux() // no sessions
	s := state.New("")

	d := &Dispatcher{
		tmux:  tc,
		state: s,
		config: &config.Config{
			Pool: config.PoolConfig{
				Autonomous: config.AutonomousConfig{Max: 0},
			},
		},
		logger: log.Default(),
	}

	d.dispatchProject(inbox)

	// File must remain unchanged.
	if _, err := os.Stat(taskPath); os.IsNotExist(err) {
		t.Error("task file should remain when no session is free")
	}
}
