package watcher

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

	"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
}

func newTestWatcher(tc *mockTmux, s *state.State, signalDir string) *SessionWatcher {
	return &SessionWatcher{
		tmux:        tc,
		state:       s,
		done:        make(chan string, 32),
		interval:    time.Second,
		idleTimeout: 60 * time.Minute,
		signalDir:   signalDir,
		logger:      log.Default(),
	}
}

// TestSignalFileTriggersDone verifies that agent-done-<session> causes idle + done signal.
func TestSignalFileTriggersDone(t *testing.T) {
	dir := t.TempDir()
	tc := newMockTmux()
	tc.sessions["sess-a"] = true

	s := state.New("")
	s.SetWorking("sess-a", "task-1")

	w := newTestWatcher(tc, s, dir)

	sig := filepath.Join(dir, "agent-done-sess-a")
	if err := os.WriteFile(sig, []byte("done"), 0644); err != nil {
		t.Fatal(err)
	}

	w.poll()

	select {
	case got := <-w.done:
		if got != "sess-a" {
			t.Errorf("expected sess-a on done, got %q", got)
		}
	default:
		t.Fatal("expected done signal, got none")
	}

	if st := s.GetSession("sess-a"); st == nil || st.State != "idle" {
		t.Errorf("expected sess-a idle, got %v", st)
	}
	if _, err := os.Stat(sig); !os.IsNotExist(err) {
		t.Error("expected signal file to be deleted")
	}
}

// TestPromptDetectionTriggersDone verifies that ❯ in pane output signals completion.
func TestPromptDetectionTriggersDone(t *testing.T) {
	dir := t.TempDir()
	tc := newMockTmux()
	tc.sessions["sess-b"] = true
	tc.paneOutput["sess-b"] = "some output\n❯ "

	s := state.New("")
	s.SetWorking("sess-b", "task-2")

	w := newTestWatcher(tc, s, dir)
	w.poll()

	select {
	case got := <-w.done:
		if got != "sess-b" {
			t.Errorf("expected sess-b, got %q", got)
		}
	default:
		t.Fatal("expected done signal from prompt detection")
	}

	if st := s.GetSession("sess-b"); st == nil || st.State != "idle" {
		t.Errorf("expected sess-b idle, got %v", st)
	}
}

// TestSpinnerSuppressesCompletion verifies that an active spinner prevents false completion.
func TestSpinnerSuppressesCompletion(t *testing.T) {
	dir := t.TempDir()
	tc := newMockTmux()
	tc.sessions["sess-c"] = true
	tc.paneOutput["sess-c"] = "doing work 5s · \n❯ "

	s := state.New("")
	s.SetWorking("sess-c", "task-3")

	w := newTestWatcher(tc, s, dir)
	w.poll()

	select {
	case name := <-w.done:
		t.Errorf("unexpected done signal for %q (spinner should suppress)", name)
	default:
		// Correct: no signal while spinner is active.
	}

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

// TestIdleTimeoutTriggersDone verifies that a session exceeding idleTimeout is completed.
func TestIdleTimeoutTriggersDone(t *testing.T) {
	dir := t.TempDir()
	tc := newMockTmux()
	tc.sessions["sess-d"] = true
	tc.paneOutput["sess-d"] = "still running..."

	s := state.New("")
	s.SetWorking("sess-d", "task-4")

	w := &SessionWatcher{
		tmux:        tc,
		state:       s,
		done:        make(chan string, 32),
		interval:    time.Second,
		idleTimeout: 1 * time.Millisecond,
		signalDir:   dir,
		logger:      log.Default(),
	}

	time.Sleep(5 * time.Millisecond)
	w.poll()

	select {
	case got := <-w.done:
		if got != "sess-d" {
			t.Errorf("expected sess-d, got %q", got)
		}
	default:
		t.Fatal("expected done signal from idle timeout")
	}
}

// TestHasSpinnerPatterns verifies spinner pattern detection.
func TestHasSpinnerPatterns(t *testing.T) {
	cases := []struct {
		input string
		want  bool
	}{
		{"5s · running", true},
		{"12s ⠋ working", true},
		{"❯ prompt only", false},
		{"no spinner here", false},
	}
	for _, c := range cases {
		if got := hasSpinner(c.input); got != c.want {
			t.Errorf("hasSpinner(%q) = %v, want %v", c.input, got, c.want)
		}
	}
}
