package core

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

func TestWorkspacePool_GetOrCreate(t *testing.T) {
	pool := newWorkspacePool(15 * time.Minute)

	state1 := pool.GetOrCreate("/workspace/a")
	state2 := pool.GetOrCreate("/workspace/a")
	state3 := pool.GetOrCreate("/workspace/b")

	if state1 != state2 {
		t.Error("expected same state for same workspace")
	}
	if state1 == state3 {
		t.Error("expected different state for different workspace")
	}
}

func TestWorkspacePool_Touch(t *testing.T) {
	pool := newWorkspacePool(15 * time.Minute)
	state := pool.GetOrCreate("/workspace/a")

	before := state.LastActivity()
	time.Sleep(10 * time.Millisecond)
	state.Touch()
	after := state.LastActivity()

	if !after.After(before) {
		t.Error("expected lastActivity to advance after Touch()")
	}
}

func TestWorkspaceState_BeginEndTurn(t *testing.T) {
	state := newWorkspaceState("/workspace/a")

	before := state.LastActivity()
	time.Sleep(10 * time.Millisecond)
	state.BeginTurn()
	if !state.HasActiveTurn() {
		t.Fatal("expected workspace to report an active turn after BeginTurn")
	}
	if !state.LastActivity().After(before) {
		t.Fatal("expected lastActivity to advance on BeginTurn")
	}

	time.Sleep(10 * time.Millisecond)
	mid := state.LastActivity()
	state.EndTurn()
	if state.HasActiveTurn() {
		t.Fatal("expected workspace to report no active turns after EndTurn")
	}
	if !state.LastActivity().After(mid) {
		t.Fatal("expected lastActivity to advance on EndTurn")
	}
}

func TestWorkspacePool_ReapIdle(t *testing.T) {
	pool := newWorkspacePool(50 * time.Millisecond)
	pool.GetOrCreate(normalizeWorkspacePath("/workspace/a"))

	time.Sleep(100 * time.Millisecond)
	reaped := pool.ReapIdle()

	if len(reaped) != 1 || reaped[0] != normalizeWorkspacePath("/workspace/a") {
		t.Errorf("expected [%s] reaped, got %v", normalizeWorkspacePath("/workspace/a"), reaped)
	}

	if s := pool.Get(normalizeWorkspacePath("/workspace/a")); s != nil {
		t.Error("expected workspace removed after reap")
	}
}

func TestNormalizeWorkspacePath(t *testing.T) {
	tmp := t.TempDir()
	realDir := filepath.Join(tmp, "real-project")
	if err := os.Mkdir(realDir, 0o755); err != nil {
		t.Fatal(err)
	}
	symlink := filepath.Join(tmp, "link-project")
	if err := os.Symlink(realDir, symlink); err != nil {
		t.Skip("symlinks not supported")
	}

	// Resolve the expected path through EvalSymlinks so that the test works
	// on macOS where /var is a symlink to /private/var.
	resolvedRealDir, err := filepath.EvalSymlinks(realDir)
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name  string
		input string
		want  string
	}{
		{"trailing slash", realDir + "/", resolvedRealDir},
		{"double slash", filepath.Join(tmp, "real-project") + "//", resolvedRealDir},
		{"dot segment", filepath.Join(tmp, ".", "real-project"), resolvedRealDir},
		{"dotdot segment", filepath.Join(tmp, "real-project", "subdir", ".."), resolvedRealDir},
		{"symlink resolved", symlink, resolvedRealDir},
		{"nonexistent uses Clean only", "/nonexistent/path/./foo/../bar", "/nonexistent/path/bar"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := normalizeWorkspacePath(tt.input)
			if got != tt.want {
				t.Errorf("normalizeWorkspacePath(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestNormalizeBeforePoolProducesSameKey(t *testing.T) {
	tmp := t.TempDir()
	realDir := filepath.Join(tmp, "project")
	if err := os.Mkdir(realDir, 0o755); err != nil {
		t.Fatal(err)
	}

	pool := newWorkspacePool(15 * time.Minute)

	// Callers normalize before pool access (as resolveWorkspace does)
	ws1 := pool.GetOrCreate(normalizeWorkspacePath(realDir + "/"))
	ws2 := pool.GetOrCreate(normalizeWorkspacePath(realDir))

	if ws1 != ws2 {
		t.Error("normalized trailing slash produced a different workspace state")
	}
}

func TestWorkspacePool_ReapIdle_KeepsActive(t *testing.T) {
	pool := newWorkspacePool(200 * time.Millisecond)
	state := pool.GetOrCreate("/workspace/active")

	time.Sleep(100 * time.Millisecond)
	state.Touch() // Keep it alive

	reaped := pool.ReapIdle()
	if len(reaped) != 0 {
		t.Errorf("expected no reaping for active workspace, got %v", reaped)
	}

	if s := pool.Get("/workspace/active"); s == nil {
		t.Error("expected active workspace to still exist")
	}
}

func TestWorkspacePool_ReapIdle_SkipsBusyWorkspace(t *testing.T) {
	pool := newWorkspacePool(50 * time.Millisecond)
	state := pool.GetOrCreate(normalizeWorkspacePath("/workspace/busy"))
	state.BeginTurn()

	time.Sleep(100 * time.Millisecond)
	reaped := pool.ReapIdle()
	if len(reaped) != 0 {
		t.Fatalf("expected busy workspace to be preserved, got %v", reaped)
	}
	if got := pool.Get(normalizeWorkspacePath("/workspace/busy")); got == nil {
		t.Fatal("expected busy workspace to remain in pool")
	}

	state.EndTurn()
	time.Sleep(60 * time.Millisecond)
	reaped = pool.ReapIdle()
	if len(reaped) != 1 || reaped[0] != normalizeWorkspacePath("/workspace/busy") {
		t.Fatalf("expected busy workspace to reap after EndTurn, got %v", reaped)
	}
}

func TestInteractiveKeyForSessionKey_NormalizesWorkspace(t *testing.T) {
	tmp := t.TempDir()
	wsDir := filepath.Join(tmp, "ws1")
	if err := os.Mkdir(wsDir, 0o755); err != nil {
		t.Fatal(err)
	}

	e := NewEngine("test", &stubAgent{}, []Platform{&stubPlatformEngine{n: "test"}}, "", LangEnglish)
	e.multiWorkspace = true
	e.baseDir = tmp
	e.workspaceBindings = NewWorkspaceBindingManager(filepath.Join(tmp, "bindings.json"))

	channelID := "chan123"
	sessionKey := "test:" + channelID

	// Bind with trailing slash (unnormalized)
	e.workspaceBindings.Bind("project:test", channelID, "chan", wsDir+"/")

	key := e.interactiveKeyForSessionKey(sessionKey)
	expected := normalizeWorkspacePath(wsDir) + ":" + sessionKey

	if key != expected {
		t.Errorf("interactiveKeyForSessionKey should normalize workspace path\ngot:  %s\nwant: %s", key, expected)
	}

	// Also verify it matches what we'd get with the clean path
	e.workspaceBindings.Bind("project:test", channelID, "chan", wsDir)
	key2 := e.interactiveKeyForSessionKey(sessionKey)
	if key != key2 {
		t.Errorf("keys should be identical regardless of trailing slash\nslash:   %s\nclean:   %s", key, key2)
	}
}
