package viewer

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

func TestSessionsRoot(t *testing.T) {
	root, err := SessionsRoot()
	if err != nil {
		t.Fatal(err)
	}
	home, _ := os.UserHomeDir()
	expected := filepath.Join(home, ".opencodereview", "sessions")
	if root != expected {
		t.Errorf("SessionsRoot() = %q, want %q", root, expected)
	}
}

func TestLoadSession_FullParse(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "sess1.jsonl"),
		`{"type":"session_start","timestamp":"2025-06-10T08:00:00Z","cwd":"/home/dev/proj","gitBranch":"feat","model":"claude-3","reviewMode":"commit","diffFrom":"aaa","diffTo":"bbb","diffCommit":"ccc"}`,
		`{"type":"llm_request","filePath":"main.go","taskType":"main_task","request_no":1,"messages":[{"role":"user","content":"review this"}]}`,
		`{"type":"llm_response","filePath":"main.go","taskType":"main_task","content":"Code looks good","duration_ms":1500,"model":"claude-3","usage":{"prompt_tokens":100,"completion_tokens":50,"cache_read_tokens":10,"cache_write_tokens":5},"tool_calls":[{"name":"search","arguments":"query"}]}`,
		`{"type":"tool_call","filePath":"main.go","taskType":"main_task","result":"found 3 results","ok":true,"duration_ms":20}`,
		`{"type":"llm_request","filePath":"util.go","taskType":"plan_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"util.go","taskType":"plan_task","content":"planning","duration_ms":800,"model":"claude-3","usage":{"prompt_tokens":200,"completion_tokens":80,"cache_read_tokens":0,"cache_write_tokens":0}}`,
		`{"type":"session_end","duration_seconds":120.5,"files_reviewed":["main.go","util.go"],"llm_failures":1}`,
	)

	vs, err := LoadSession(root, "repo", "sess1")
	if err != nil {
		t.Fatal(err)
	}

	// Check summary
	if vs.Summary.SessionID != "sess1" {
		t.Errorf("SessionID = %q", vs.Summary.SessionID)
	}
	if vs.Summary.CWD != "/home/dev/proj" {
		t.Errorf("CWD = %q", vs.Summary.CWD)
	}
	if vs.Summary.GitBranch != "feat" {
		t.Errorf("GitBranch = %q", vs.Summary.GitBranch)
	}
	if vs.Summary.Model != "claude-3" {
		t.Errorf("Model = %q", vs.Summary.Model)
	}
	if vs.Summary.ReviewMode != "commit" {
		t.Errorf("ReviewMode = %q", vs.Summary.ReviewMode)
	}
	if vs.Summary.DiffFrom != "aaa" {
		t.Errorf("DiffFrom = %q", vs.Summary.DiffFrom)
	}
	if vs.Summary.DiffTo != "bbb" {
		t.Errorf("DiffTo = %q", vs.Summary.DiffTo)
	}
	if vs.Summary.DiffCommit != "ccc" {
		t.Errorf("DiffCommit = %q", vs.Summary.DiffCommit)
	}
	if vs.Summary.DurationSec != 120.5 {
		t.Errorf("DurationSec = %f", vs.Summary.DurationSec)
	}
	if vs.Summary.FileCount != 2 {
		t.Errorf("FileCount = %d", vs.Summary.FileCount)
	}
	if vs.Summary.LLMFailures != 1 {
		t.Errorf("LLMFailures = %d", vs.Summary.LLMFailures)
	}

	// Check files are sorted
	if len(vs.Files) != 2 {
		t.Fatalf("Files count = %d, want 2", len(vs.Files))
	}
	if vs.Files[0].FilePath != "main.go" {
		t.Errorf("Files[0] = %q, want main.go", vs.Files[0].FilePath)
	}
	if vs.Files[1].FilePath != "util.go" {
		t.Errorf("Files[1] = %q, want util.go", vs.Files[1].FilePath)
	}

	// Check main.go task card
	mainCards := vs.Files[0].Tasks[MainTask]
	if len(mainCards) != 1 {
		t.Fatalf("main.go main_task cards = %d", len(mainCards))
	}
	card := mainCards[0]
	if card.RequestNo != 1 {
		t.Errorf("RequestNo = %d", card.RequestNo)
	}
	if card.ResponseContent != "Code looks good" {
		t.Errorf("ResponseContent = %q", card.ResponseContent)
	}
	if card.DurationMs != 1500 {
		t.Errorf("DurationMs = %d", card.DurationMs)
	}
	if card.Model != "claude-3" {
		t.Errorf("Model = %q", card.Model)
	}
	if card.PromptTokens != 100 {
		t.Errorf("PromptTokens = %d", card.PromptTokens)
	}
	if card.CompletionTokens != 50 {
		t.Errorf("CompletionTokens = %d", card.CompletionTokens)
	}
	if card.CacheReadTokens != 10 {
		t.Errorf("CacheReadTokens = %d", card.CacheReadTokens)
	}
	if card.CacheWriteTokens != 5 {
		t.Errorf("CacheWriteTokens = %d", card.CacheWriteTokens)
	}

	// Check tool calls
	if len(card.ToolCalls) != 1 {
		t.Fatalf("ToolCalls = %d", len(card.ToolCalls))
	}
	tc := card.ToolCalls[0]
	if tc.Name != "search" {
		t.Errorf("ToolCall.Name = %q", tc.Name)
	}
	if !tc.Ok {
		t.Error("ToolCall.Ok = false, want true")
	}
	if tc.Result != "found 3 results" {
		t.Errorf("ToolCall.Result = %q", tc.Result)
	}
	if tc.DurationMs != 20 {
		t.Errorf("ToolCall.DurationMs = %d", tc.DurationMs)
	}

	// Check token usage
	if vs.TokenUsage.TotalPromptTokens != 300 {
		t.Errorf("TotalPromptTokens = %d, want 300", vs.TokenUsage.TotalPromptTokens)
	}
	if vs.TokenUsage.TotalCompletionTokens != 130 {
		t.Errorf("TotalCompletionTokens = %d, want 130", vs.TokenUsage.TotalCompletionTokens)
	}
	if vs.TokenUsage.TotalCacheReadTokens != 10 {
		t.Errorf("TotalCacheReadTokens = %d", vs.TokenUsage.TotalCacheReadTokens)
	}
	if vs.TokenUsage.TotalCacheWriteTokens != 5 {
		t.Errorf("TotalCacheWriteTokens = %d", vs.TokenUsage.TotalCacheWriteTokens)
	}
	if vs.TokenUsage.RequestCount != 2 {
		t.Errorf("RequestCount = %d, want 2", vs.TokenUsage.RequestCount)
	}

	// Check file token breakdown
	if len(vs.TokenUsage.FileTokenBreakdown) != 2 {
		t.Fatalf("FileTokenBreakdown count = %d", len(vs.TokenUsage.FileTokenBreakdown))
	}
	// Sorted by total tokens (descending), util.go (200+80=280) > main.go (100+50=150)
	if vs.TokenUsage.FileTokenBreakdown[0].FilePath != "util.go" {
		t.Errorf("top token file = %q, want util.go", vs.TokenUsage.FileTokenBreakdown[0].FilePath)
	}
}

func TestLoadSession_MissingFile(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	_, err := LoadSession(root, "repo", "nonexistent")
	if err == nil {
		t.Error("expected error for missing session file")
	}
}

func TestLoadSession_MalformedLines(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "bad.jsonl"),
		`not json at all`,
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{broken json`,
		`{"type":"session_end","duration_seconds":10,"files_reviewed":[],"llm_failures":0}`,
	)

	vs, err := LoadSession(root, "repo", "bad")
	if err != nil {
		t.Fatal(err)
	}
	if vs.Summary.CWD != "/x" {
		t.Errorf("CWD = %q, want /x (should skip malformed lines)", vs.Summary.CWD)
	}
}

func TestLoadSession_LLMError(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "errs.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"main_task","request_no":1,"messages":[]}`,
		`{"type":"llm_error","filePath":"a.go","taskType":"main_task","error":"rate limit exceeded","duration_ms":500}`,
		`{"type":"session_end","duration_seconds":5,"files_reviewed":["a.go"],"llm_failures":1}`,
	)

	vs, err := LoadSession(root, "repo", "errs")
	if err != nil {
		t.Fatal(err)
	}

	cards := vs.Files[0].Tasks[MainTask]
	if len(cards) != 1 {
		t.Fatalf("cards count = %d", len(cards))
	}
	if cards[0].Error != "rate limit exceeded" {
		t.Errorf("Error = %q", cards[0].Error)
	}
	if cards[0].DurationMs != 500 {
		t.Errorf("DurationMs = %d", cards[0].DurationMs)
	}
}

func TestLoadSession_ToolCallWithOkFalse(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "tc.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"main_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"main_task","content":"result","duration_ms":100,"model":"m","usage":{"prompt_tokens":10,"completion_tokens":5},"tool_calls":[{"name":"search","arguments":"query"}]}`,
		`{"type":"tool_call","filePath":"a.go","taskType":"main_task","result":"error: not found","ok":false,"duration_ms":50}`,
		`{"type":"session_end","duration_seconds":1,"files_reviewed":["a.go"]}`,
	)

	vs, err := LoadSession(root, "repo", "tc")
	if err != nil {
		t.Fatal(err)
	}

	cards := vs.Files[0].Tasks[MainTask]
	if len(cards) != 1 {
		t.Fatalf("cards = %d", len(cards))
	}
	if len(cards[0].ToolCalls) != 1 {
		t.Fatalf("tool calls = %d", len(cards[0].ToolCalls))
	}
	tc := cards[0].ToolCalls[0]
	if tc.Ok {
		t.Error("expected Ok=false")
	}
	if tc.Result != "error: not found" {
		t.Errorf("Result = %q", tc.Result)
	}
	if tc.DurationMs != 50 {
		t.Errorf("DurationMs = %d", tc.DurationMs)
	}
}

func TestLoadSession_MultipleRequests(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "multi.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"main_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"main_task","content":"first pass","duration_ms":100,"model":"m","usage":{"prompt_tokens":50,"completion_tokens":20}}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"main_task","request_no":2,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"main_task","content":"second pass","duration_ms":200,"model":"m","usage":{"prompt_tokens":60,"completion_tokens":30}}`,
		`{"type":"session_end","duration_seconds":10,"files_reviewed":["a.go"]}`,
	)

	vs, err := LoadSession(root, "repo", "multi")
	if err != nil {
		t.Fatal(err)
	}

	cards := vs.Files[0].Tasks[MainTask]
	if len(cards) != 2 {
		t.Fatalf("cards = %d, want 2", len(cards))
	}
	if cards[0].ResponseContent != "first pass" {
		t.Errorf("cards[0] content = %q", cards[0].ResponseContent)
	}
	if cards[1].ResponseContent != "second pass" {
		t.Errorf("cards[1] content = %q", cards[1].ResponseContent)
	}
	if vs.TokenUsage.RequestCount != 2 {
		t.Errorf("RequestCount = %d", vs.TokenUsage.RequestCount)
	}
}

func TestLoadSession_EmptyFile(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}
	writeJSONL(t, filepath.Join(repoDir, "empty.jsonl"))

	vs, err := LoadSession(root, "repo", "empty")
	if err != nil {
		t.Fatal(err)
	}
	if len(vs.Files) != 0 {
		t.Errorf("Files = %d, want 0", len(vs.Files))
	}
}

func TestLoadSession_ResponseWithoutRequest(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	// llm_response for a file that has no prior request - should not panic
	writeJSONL(t, filepath.Join(repoDir, "orphan.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_response","filePath":"unknown.go","taskType":"main_task","content":"orphan","duration_ms":100,"model":"m"}`,
		`{"type":"session_end","duration_seconds":1,"files_reviewed":[]}`,
	)

	vs, err := LoadSession(root, "repo", "orphan")
	if err != nil {
		t.Fatal(err)
	}
	// No file groups should be created for orphan responses (no request created the fileIndex entry)
	if len(vs.Files) != 0 {
		t.Errorf("Files = %d, want 0 (orphan response has no request)", len(vs.Files))
	}
}

func TestLoadSession_LLMErrorWithoutRequest(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "orphanerr.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_error","filePath":"unknown.go","taskType":"main_task","error":"fail","duration_ms":10}`,
		`{"type":"session_end","duration_seconds":1,"files_reviewed":[]}`,
	)

	// Should not panic
	vs, err := LoadSession(root, "repo", "orphanerr")
	if err != nil {
		t.Fatal(err)
	}
	if len(vs.Files) != 0 {
		t.Errorf("Files = %d", len(vs.Files))
	}
}

func TestLoadSession_ToolCallWithoutRequest(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "orphantc.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"tool_call","filePath":"unknown.go","taskType":"main_task","result":"x","ok":true}`,
		`{"type":"session_end","duration_seconds":1,"files_reviewed":[]}`,
	)

	// Should not panic
	vs, err := LoadSession(root, "repo", "orphantc")
	if err != nil {
		t.Fatal(err)
	}
	if len(vs.Files) != 0 {
		t.Errorf("Files = %d", len(vs.Files))
	}
}

func TestDiscoverRepos_SkipsUnreadableSubdir(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("permission checks are bypassed for root")
	}
	root := t.TempDir()
	badRepo := filepath.Join(root, "unreadable-repo")
	if err := os.MkdirAll(badRepo, 0755); err != nil {
		t.Fatal(err)
	}
	writeJSONL(t, filepath.Join(badRepo, "s.jsonl"), `{"type":"session_start"}`)
	// Remove read permission so os.ReadDir(repoDir) fails
	if err := os.Chmod(badRepo, 0000); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = os.Chmod(badRepo, 0755) })

	repos, err := DiscoverRepos(root)
	if err != nil {
		t.Fatal(err)
	}
	// Unreadable repo is skipped (continue)
	if len(repos) != 0 {
		t.Errorf("repos = %d, want 0 (unreadable dir skipped)", len(repos))
	}
}

func TestListSessions_SkipsUnreadableFiles(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("permission checks are bypassed for root")
	}
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	// Create a valid session file
	writeJSONL(t, filepath.Join(repoDir, "good.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`)

	// Create an unreadable jsonl file
	badPath := filepath.Join(repoDir, "bad.jsonl")
	if err := os.WriteFile(badPath, []byte(`{"type":"session_start"}`), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.Chmod(badPath, 0000); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = os.Chmod(badPath, 0644) })

	sessions, err := ListSessions(root, "repo")
	if err != nil {
		t.Fatal(err)
	}
	// Should get 1 session (the good one), bad one is skipped
	if len(sessions) != 1 {
		t.Errorf("sessions = %d, want 1 (bad file skipped)", len(sessions))
	}
}

func TestLoadSession_MultipleTaskTypes(t *testing.T) {
	root := t.TempDir()
	repoDir := filepath.Join(root, "repo")
	if err := os.MkdirAll(repoDir, 0755); err != nil {
		t.Fatal(err)
	}

	writeJSONL(t, filepath.Join(repoDir, "tasks.jsonl"),
		`{"type":"session_start","timestamp":"2025-01-01T00:00:00Z","cwd":"/x","model":"m"}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"plan_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"plan_task","content":"planning","duration_ms":100,"model":"m","usage":{"prompt_tokens":10,"completion_tokens":5}}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"main_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"main_task","content":"reviewing","duration_ms":200,"model":"m","usage":{"prompt_tokens":20,"completion_tokens":10}}`,
		`{"type":"llm_request","filePath":"a.go","taskType":"memory_compression_task","request_no":1,"messages":[]}`,
		`{"type":"llm_response","filePath":"a.go","taskType":"memory_compression_task","content":"compressed","duration_ms":50,"model":"m","usage":{"prompt_tokens":5,"completion_tokens":3}}`,
		`{"type":"session_end","duration_seconds":5,"files_reviewed":["a.go"]}`,
	)

	vs, err := LoadSession(root, "repo", "tasks")
	if err != nil {
		t.Fatal(err)
	}

	if len(vs.Files) != 1 {
		t.Fatalf("Files = %d", len(vs.Files))
	}
	fg := vs.Files[0]
	if len(fg.Tasks) != 3 {
		t.Errorf("task types = %d, want 3", len(fg.Tasks))
	}
	if len(fg.Tasks[PlanTask]) != 1 {
		t.Errorf("plan_task cards = %d", len(fg.Tasks[PlanTask]))
	}
	if len(fg.Tasks[MainTask]) != 1 {
		t.Errorf("main_task cards = %d", len(fg.Tasks[MainTask]))
	}
	if len(fg.Tasks[MemoryCompressionTask]) != 1 {
		t.Errorf("memory_compression_task cards = %d", len(fg.Tasks[MemoryCompressionTask]))
	}
}
