package history

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// --- Helper: create a minimal Claude JSONL session file ---

func writeSessionJSONL(t *testing.T, dir, sessionID string, lines []map[string]interface{}) string {
	t.Helper()
	filePath := filepath.Join(dir, sessionID+".jsonl")
	f, err := os.Create(filePath) // #nosec G304
	if err != nil {
		t.Fatalf("create session file: %v", err)
	}
	defer func() { _ = f.Close() }()
	for _, line := range lines {
		data, _ := json.Marshal(line)
		_, _ = f.Write(data)
		_, _ = f.Write([]byte("\n"))
	}
	return filePath
}

func minimalSession(t *testing.T, dir, sessionID, userPrompt string, ts time.Time) string {
	t.Helper()
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": userPrompt},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "assistant",
				"content": []map[string]interface{}{
					{"type": "text", "text": "I'll help with that."},
				},
				"usage": map[string]interface{}{
					"input_tokens":  1500,
					"output_tokens": 300,
				},
			},
		},
	}
	return writeSessionJSONL(t, dir, sessionID, lines)
}

// --- Tests ---

func TestDecodeProjectDir(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"-Users-foo-myproject", "/Users-foo-myproject"},
		{"-Users-azaalouk-go-src-github-com-zanetworker-aimux", "/Users-azaalouk-go-src-github-com-zanetworker-aimux"},
		{"somedir", "somedir"},
		{"-", "/"},
	}
	for _, tt := range tests {
		got := decodeProjectDir(tt.input)
		if got != tt.want {
			t.Errorf("decodeProjectDir(%q) = %q, want %q", tt.input, got, tt.want)
		}
	}
}

func TestMetaPath(t *testing.T) {
	got := MetaPath("/path/to/abc123.jsonl")
	want := "/path/to/abc123.meta.json"
	if got != want {
		t.Errorf("MetaPath() = %q, want %q", got, want)
	}
}

func TestSaveAndLoadMeta(t *testing.T) {
	dir := t.TempDir()
	sessionFile := filepath.Join(dir, "test-session.jsonl")
	// Create a dummy session file so the path exists
	_ = os.WriteFile(sessionFile, []byte("{}"), 0o600)

	meta := Meta{
		Annotation: "failed",
		Note:       "agent looped for 20 turns",
		Tags:       []string{"loop-on-error", "wrong-file"},
	}

	if err := SaveMeta(sessionFile, meta); err != nil {
		t.Fatalf("SaveMeta: %v", err)
	}

	loaded := LoadMeta(sessionFile)
	if loaded.Annotation != "failed" {
		t.Errorf("Annotation = %q, want %q", loaded.Annotation, "failed")
	}
	if loaded.Note != "agent looped for 20 turns" {
		t.Errorf("Note = %q, want %q", loaded.Note, "agent looped for 20 turns")
	}
	if len(loaded.Tags) != 2 || loaded.Tags[0] != "loop-on-error" {
		t.Errorf("Tags = %v, want [loop-on-error wrong-file]", loaded.Tags)
	}
	if loaded.UpdatedAt == "" {
		t.Error("UpdatedAt should be set")
	}
}

func TestLoadMeta_NoFile(t *testing.T) {
	meta := LoadMeta("/nonexistent/path.jsonl")
	if meta.Annotation != "" || meta.Note != "" || len(meta.Tags) != 0 {
		t.Errorf("expected empty Meta for nonexistent file, got %+v", meta)
	}
}

func TestSaveMeta_AtomicWrite(t *testing.T) {
	dir := t.TempDir()
	sessionFile := filepath.Join(dir, "session.jsonl")
	_ = os.WriteFile(sessionFile, []byte("{}"), 0o600)

	// Write initial metadata
	if err := SaveMeta(sessionFile, Meta{Annotation: "achieved"}); err != nil {
		t.Fatalf("first SaveMeta: %v", err)
	}

	// Overwrite with new metadata
	if err := SaveMeta(sessionFile, Meta{Annotation: "failed", Tags: []string{"bug"}}); err != nil {
		t.Fatalf("second SaveMeta: %v", err)
	}

	loaded := LoadMeta(sessionFile)
	if loaded.Annotation != "failed" {
		t.Errorf("Annotation = %q, want %q after overwrite", loaded.Annotation, "failed")
	}
	if len(loaded.Tags) != 1 || loaded.Tags[0] != "bug" {
		t.Errorf("Tags = %v, want [bug] after overwrite", loaded.Tags)
	}
}

func TestCollectTags(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-project")
	_ = os.MkdirAll(projDir, 0o750)

	// Create two meta files with overlapping tags
	s1 := filepath.Join(projDir, "session1.jsonl")
	_ = os.WriteFile(s1, []byte("{}"), 0o600)
	_ = SaveMeta(s1, Meta{Tags: []string{"loop-on-error", "wrong-file"}})

	s2 := filepath.Join(projDir, "session2.jsonl")
	_ = os.WriteFile(s2, []byte("{}"), 0o600)
	_ = SaveMeta(s2, Meta{Tags: []string{"wrong-file", "hallucinated-api"}})

	tags := CollectTags(dir)
	if len(tags) != 3 {
		t.Fatalf("expected 3 unique tags, got %d: %v", len(tags), tags)
	}
	// Tags should be sorted
	if tags[0] != "hallucinated-api" || tags[1] != "loop-on-error" || tags[2] != "wrong-file" {
		t.Errorf("tags = %v, want [hallucinated-api loop-on-error wrong-file]", tags)
	}
}

func TestCollectTags_NoFiles(t *testing.T) {
	dir := t.TempDir()
	tags := CollectTags(dir)
	if len(tags) != 0 {
		t.Errorf("expected no tags, got %v", tags)
	}
}

func TestExtractUserText_BlockArray(t *testing.T) {
	content, _ := json.Marshal([]map[string]interface{}{
		{"type": "text", "text": "fix the bug in main.go"},
	})
	got := extractUserText(content)
	if got != "fix the bug in main.go" {
		t.Errorf("extractUserText = %q, want %q", got, "fix the bug in main.go")
	}
}

func TestExtractUserText_Truncation(t *testing.T) {
	longText := strings.Repeat("x", 200)
	content, _ := json.Marshal([]map[string]interface{}{
		{"type": "text", "text": longText},
	})
	got := extractUserText(content)
	if len(got) > 120 {
		t.Errorf("expected truncated text <= 120 chars, got %d", len(got))
	}
	if !strings.HasSuffix(got, "...") {
		t.Error("expected truncated text to end with ...")
	}
}

func TestExtractUserText_Nil(t *testing.T) {
	got := extractUserText(nil)
	if got != "" {
		t.Errorf("expected empty for nil content, got %q", got)
	}
}

func TestParseSessionLine_SkipsNoisePrompt(t *testing.T) {
	// First user message is all XML noise, second has real text.
	// The parser should skip the noise and pick up the second message.
	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "<system-reminder>some noise</system-reminder>"},
				},
			},
		},
		{
			"type":      "human",
			"timestamp": ts.Add(60 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "fix the login bug"},
				},
			},
		},
	}
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-proj")
	_ = os.MkdirAll(projDir, 0o750)
	writeSessionJSONL(t, projDir, "noise-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}
	if sessions[0].FirstPrompt != "fix the login bug" {
		t.Errorf("FirstPrompt = %q, want %q", sessions[0].FirstPrompt, "fix the login bug")
	}
}

func TestParseSessionLine_ImageOnlyMessage(t *testing.T) {
	// First user message has only an image block, second has text.
	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "image", "source": map[string]interface{}{"type": "base64", "data": "abc"}},
				},
			},
		},
		{
			"type":      "human",
			"timestamp": ts.Add(60 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "explain this screenshot"},
				},
			},
		},
	}
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-proj2")
	_ = os.MkdirAll(projDir, 0o750)
	writeSessionJSONL(t, projDir, "img-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}
	if sessions[0].FirstPrompt != "explain this screenshot" {
		t.Errorf("FirstPrompt = %q, want %q", sessions[0].FirstPrompt, "explain this screenshot")
	}
}

func TestDiscover_BasicSession(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-myproject")
	_ = os.MkdirAll(projDir, 0o750)

	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	minimalSession(t, projDir, "abc-123", "fix the markdown rendering", ts)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}

	s := sessions[0]
	if s.ID != "abc-123" {
		t.Errorf("ID = %q, want %q", s.ID, "abc-123")
	}
	if s.Provider != "claude" {
		t.Errorf("Provider = %q, want %q", s.Provider, "claude")
	}
	if s.FirstPrompt != "fix the markdown rendering" {
		t.Errorf("FirstPrompt = %q, want %q", s.FirstPrompt, "fix the markdown rendering")
	}
	if s.Resumable != true {
		t.Error("expected Resumable = true for Claude")
	}
	if s.StartTime.IsZero() {
		t.Error("expected non-zero StartTime")
	}
	if s.TokensIn != 1500 {
		t.Errorf("TokensIn = %d, want 1500", s.TokensIn)
	}
	if s.TokensOut != 300 {
		t.Errorf("TokensOut = %d, want 300", s.TokensOut)
	}
}

func TestDiscover_MultipleSessions_SortedByLastActive(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-project")
	_ = os.MkdirAll(projDir, 0o750)

	// Older session
	ts1 := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC)
	minimalSession(t, projDir, "old-session", "old task", ts1)

	// Newer session
	ts2 := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	minimalSession(t, projDir, "new-session", "new task", ts2)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 2 {
		t.Fatalf("expected 2 sessions, got %d", len(sessions))
	}

	// Most recent first
	if sessions[0].ID != "new-session" {
		t.Errorf("first session = %q, want %q (most recent)", sessions[0].ID, "new-session")
	}
	if sessions[1].ID != "old-session" {
		t.Errorf("second session = %q, want %q (older)", sessions[1].ID, "old-session")
	}
}

func TestEncodeProjectDir(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"/Users/foo/myproject", "-Users-foo-myproject"},
		{"/Users/foo/my.project", "-Users-foo-my-project"},
		{"/Users/azaalouk/go/src/github.com/zanetworker/aimux", "-Users-azaalouk-go-src-github-com-zanetworker-aimux"},
	}
	for _, tt := range tests {
		got := encodeProjectDir(tt.input)
		if got != tt.want {
			t.Errorf("encodeProjectDir(%q) = %q, want %q", tt.input, got, tt.want)
		}
	}
}

func TestDiscover_DirFilter(t *testing.T) {
	dir := t.TempDir()

	// Two projects with Claude-style encoded dir names
	proj1 := filepath.Join(dir, "-Users-test-project1")
	proj2 := filepath.Join(dir, "-Users-test-project2")
	_ = os.MkdirAll(proj1, 0o750)
	_ = os.MkdirAll(proj2, 0o750)

	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	minimalSession(t, proj1, "s1", "task one", ts)
	minimalSession(t, proj2, "s2", "task two", ts)

	// Filter using the real path (as the app would pass it)
	sessions, err := Discover(DiscoverOpts{Dir: "/Users/test/project1"}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session with dir filter, got %d", len(sessions))
	}
	if sessions[0].ID != "s1" {
		t.Errorf("filtered session = %q, want %q", sessions[0].ID, "s1")
	}
}

func TestDiscover_Limit(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-project")
	_ = os.MkdirAll(projDir, 0o750)

	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	for i := 0; i < 10; i++ {
		minimalSession(t, projDir, "session-"+string(rune('a'+i)), "task", ts.Add(time.Duration(i)*time.Hour))
	}

	sessions, err := Discover(DiscoverOpts{Limit: 3}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 3 {
		t.Errorf("expected 3 sessions with limit, got %d", len(sessions))
	}
}

func TestDiscover_NonexistentDir(t *testing.T) {
	sessions, err := Discover(DiscoverOpts{}, "/nonexistent/path")
	if err != nil {
		t.Fatalf("expected no error for nonexistent dir, got %v", err)
	}
	if len(sessions) != 0 {
		t.Errorf("expected 0 sessions, got %d", len(sessions))
	}
}

func TestDiscover_EmptyDir(t *testing.T) {
	dir := t.TempDir()
	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 0 {
		t.Errorf("expected 0 sessions, got %d", len(sessions))
	}
}

func TestDiscover_WithMetadata(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-project")
	_ = os.MkdirAll(projDir, 0o750)

	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	sessionPath := minimalSession(t, projDir, "annotated", "some task", ts)

	// Add metadata
	_ = SaveMeta(sessionPath, Meta{
		Annotation: "failed",
		Note:       "kept looping",
		Tags:       []string{"loop-on-error"},
	})

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}

	s := sessions[0]
	if s.Annotation != "failed" {
		t.Errorf("Annotation = %q, want %q", s.Annotation, "failed")
	}
	if s.Note != "kept looping" {
		t.Errorf("Note = %q, want %q", s.Note, "kept looping")
	}
	if len(s.Tags) != 1 || s.Tags[0] != "loop-on-error" {
		t.Errorf("Tags = %v, want [loop-on-error]", s.Tags)
	}
}

func TestDiscover_UnsupportedProvider(t *testing.T) {
	dir := t.TempDir()
	sessions, err := Discover(DiscoverOpts{Provider: "gemini"}, dir)
	if err != nil {
		t.Fatalf("expected no error for unsupported provider, got %v", err)
	}
	if len(sessions) != 0 {
		t.Errorf("expected 0 sessions for unsupported provider, got %d", len(sessions))
	}
}

func TestDiscover_CostCalculation(t *testing.T) {
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-project")
	_ = os.MkdirAll(projDir, 0o750)

	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "hello"},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"model": "claude-sonnet-4-5",
				"role":  "assistant",
				"content": []map[string]interface{}{
					{"type": "text", "text": "Hi!"},
				},
				"usage": map[string]interface{}{
					"input_tokens":                100000,
					"output_tokens":               10000,
					"cache_creation_input_tokens":  50000,
					"cache_read_input_tokens":      200000,
				},
			},
		},
	}
	writeSessionJSONL(t, projDir, "cost-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}

	s := sessions[0]
	if s.TokensIn != 100000 {
		t.Errorf("TokensIn = %d, want 100000", s.TokensIn)
	}
	if s.TokensOut != 10000 {
		t.Errorf("TokensOut = %d, want 10000", s.TokensOut)
	}
	if s.CacheReadTokens != 200000 {
		t.Errorf("CacheReadTokens = %d, want 200000", s.CacheReadTokens)
	}
	if s.CacheWriteTokens != 50000 {
		t.Errorf("CacheWriteTokens = %d, want 50000", s.CacheWriteTokens)
	}

	// claude-sonnet-4-5: $3/M in, $15/M out, $0.30/M cache read, $3.75/M cache write
	// Cost = 100000/1M * 3 + 10000/1M * 15 + 50000/1M * 3.75 + 200000/1M * 0.30
	// Cost = 0.30 + 0.15 + 0.1875 + 0.06 = 0.6975
	if s.CostUSD < 0.69 || s.CostUSD > 0.70 {
		t.Errorf("CostUSD = %.4f, want ~0.6975", s.CostUSD)
	}
}

func TestTitleForSessionFile(t *testing.T) {
	dir := t.TempDir()
	sessionFile := filepath.Join(dir, "test.jsonl")
	_ = os.WriteFile(sessionFile, []byte("{}"), 0o600)

	// No meta file — should return ""
	if got := TitleForSessionFile(sessionFile); got != "" {
		t.Errorf("expected empty title, got %q", got)
	}

	// With meta file
	_ = SaveMeta(sessionFile, Meta{Title: "Fix markdown rendering"})
	if got := TitleForSessionFile(sessionFile); got != "Fix markdown rendering" {
		t.Errorf("expected title, got %q", got)
	}

	// Empty path
	if got := TitleForSessionFile(""); got != "" {
		t.Errorf("expected empty for empty path, got %q", got)
	}
}

func TestScanSession_SubagentDetection(t *testing.T) {
	tests := map[string]struct {
		lines    []map[string]interface{}
		wantSub  bool
		wantPerm string
	}{
		"bypass_no_human_is_subagent": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "bypassPermissions",
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "tool_result", "tool_use_id": "t1", "content": "ok"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  true,
			wantPerm: "bypassPermissions",
		},
		"bypass_with_human_not_subagent": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "bypassPermissions",
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "text", "text": "Please fix the login page"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  false,
			wantPerm: "bypassPermissions",
		},
		"default_no_human_short_is_subagent": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "default",
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "tool_result", "tool_use_id": "t1", "content": "ok"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(2 * time.Minute).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  true,
			wantPerm: "default",
		},
		"default_no_human_long_not_subagent": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "default",
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "tool_result", "tool_use_id": "t1", "content": "ok"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(10 * time.Minute).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  false,
			wantPerm: "default",
		},
		"normal_interactive_session": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "default",
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "text", "text": "fix the bug in main.go"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "I'll look at it."}},
						},
					},
					{
						"type":      "human",
						"timestamp": ts.Add(2 * time.Minute).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role": "user",
							"content": []map[string]interface{}{
								{"type": "text", "text": "also check the tests"},
							},
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(3 * time.Minute).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Tests pass."}},
						},
					},
				}
			}(),
			wantSub:  false,
			wantPerm: "default",
		},
		"local_command_caveat_not_human": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "bypassPermissions",
						"message": map[string]interface{}{
							"role":    "user",
							"content": "<local-command-caveat>run ls</local-command-caveat>",
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  true,
			wantPerm: "bypassPermissions",
		},
		"command_name_not_human": {
			lines: func() []map[string]interface{} {
				ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
				return []map[string]interface{}{
					{
						"type":           "human",
						"timestamp":      ts.Format(time.RFC3339),
						"permissionMode": "bypassPermissions",
						"message": map[string]interface{}{
							"role":    "user",
							"content": "<command-name>bash</command-name>",
						},
					},
					{
						"type":      "assistant",
						"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
						"message": map[string]interface{}{
							"role":    "assistant",
							"content": []map[string]interface{}{{"type": "text", "text": "Done."}},
						},
					},
				}
			}(),
			wantSub:  true,
			wantPerm: "bypassPermissions",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			dir := t.TempDir()
			filePath := writeSessionJSONL(t, dir, "sub-"+name, tc.lines)

			s, err := scanSession("sub-"+name, filePath, "/test")
			if err != nil {
				t.Fatalf("scanSession: %v", err)
			}
			if s.IsSubagent != tc.wantSub {
				t.Errorf("IsSubagent = %v, want %v", s.IsSubagent, tc.wantSub)
			}
			if s.PermissionMode != tc.wantPerm {
				t.Errorf("PermissionMode = %q, want %q", s.PermissionMode, tc.wantPerm)
			}
		})
	}
}

func TestIsHumanMessage(t *testing.T) {
	tests := map[string]struct {
		content json.RawMessage
		want    bool
	}{
		"nil_content": {
			content: nil,
			want:    false,
		},
		"plain_text_human": {
			content: json.RawMessage(`"fix the bug in main.go"`),
			want:    true,
		},
		"local_command_caveat": {
			content: json.RawMessage(`"<local-command-caveat>run ls</local-command-caveat>"`),
			want:    false,
		},
		"command_name": {
			content: json.RawMessage(`"<command-name>bash</command-name>"`),
			want:    false,
		},
		"text_block_human": {
			content: json.RawMessage(`[{"type":"text","text":"please review this code"}]`),
			want:    true,
		},
		"tool_result_only": {
			content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]`),
			want:    false,
		},
		"mixed_tool_result_no_text": {
			content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"t1","content":"ok"},{"type":"tool_result","tool_use_id":"t2","content":"done"}]`),
			want:    false,
		},
		"empty_string": {
			content: json.RawMessage(`""`),
			want:    false,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := isHumanMessage(tc.content)
			if got != tc.want {
				t.Errorf("isHumanMessage = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestScanSession_EmptyFile(t *testing.T) {
	dir := t.TempDir()
	filePath := filepath.Join(dir, "empty.jsonl")
	_ = os.WriteFile(filePath, []byte(""), 0o600)

	s, err := scanSession("empty", filePath, "/test")
	if err != nil {
		t.Fatalf("scanSession: %v", err)
	}
	if s.ID != "empty" {
		t.Errorf("ID = %q, want %q", s.ID, "empty")
	}
	if s.TurnCount != 0 {
		t.Errorf("TurnCount = %d, want 0 for empty file", s.TurnCount)
	}
}

func TestParseSessionLine_ExtractsGitBranch(t *testing.T) {
	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"gitBranch": "feat/resize-handle",
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "fix the bug"},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
			"gitBranch": "feat/resize-handle",
			"message": map[string]interface{}{
				"role":  "assistant",
				"model": "claude-sonnet-4-6",
				"content": []map[string]interface{}{
					{"type": "text", "text": "I'll fix that."},
				},
				"usage": map[string]interface{}{
					"input_tokens":  1000,
					"output_tokens": 200,
				},
			},
		},
	}
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-proj")
	_ = os.MkdirAll(projDir, 0o750)
	writeSessionJSONL(t, projDir, "branch-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}
	if sessions[0].GitBranch != "feat/resize-handle" {
		t.Errorf("GitBranch = %q, want %q", sessions[0].GitBranch, "feat/resize-handle")
	}
	if sessions[0].Model != "claude-sonnet-4-6" {
		t.Errorf("Model = %q, want %q", sessions[0].Model, "claude-sonnet-4-6")
	}
}

func TestParseSessionLine_ExtractsLastPrompt(t *testing.T) {
	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "fix the markdown rendering"},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "assistant",
				"content": []map[string]interface{}{
					{"type": "text", "text": "Done."},
				},
				"usage": map[string]interface{}{
					"input_tokens":  1000,
					"output_tokens": 200,
				},
			},
		},
		{
			"type":      "human",
			"timestamp": ts.Add(60 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "now add table support"},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(90 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "assistant",
				"content": []map[string]interface{}{
					{"type": "text", "text": "Sure."},
				},
				"usage": map[string]interface{}{
					"input_tokens":  1200,
					"output_tokens": 400,
				},
			},
		},
	}
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-proj")
	_ = os.MkdirAll(projDir, 0o750)
	writeSessionJSONL(t, projDir, "lastprompt-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}
	if sessions[0].FirstPrompt != "fix the markdown rendering" {
		t.Errorf("FirstPrompt = %q, want %q", sessions[0].FirstPrompt, "fix the markdown rendering")
	}
	if sessions[0].LastPrompt != "now add table support" {
		t.Errorf("LastPrompt = %q, want %q", sessions[0].LastPrompt, "now add table support")
	}
}

func TestParseSessionLine_ExtractsLastAction(t *testing.T) {
	ts := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)
	lines := []map[string]interface{}{
		{
			"type":      "human",
			"timestamp": ts.Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "user",
				"content": []map[string]interface{}{
					{"type": "text", "text": "fix the bug"},
				},
			},
		},
		{
			"type":      "assistant",
			"timestamp": ts.Add(30 * time.Second).Format(time.RFC3339),
			"message": map[string]interface{}{
				"role": "assistant",
				"content": []map[string]interface{}{
					{"type": "text", "text": "I'll edit the file."},
					{
						"type": "tool_use",
						"name": "Read",
						"input": map[string]interface{}{
							"file_path": "/Users/test/main.go",
						},
					},
					{
						"type": "tool_use",
						"name": "Edit",
						"input": map[string]interface{}{
							"file_path": "/Users/test/config.go",
						},
					},
				},
				"usage": map[string]interface{}{
					"input_tokens":  1500,
					"output_tokens": 300,
				},
			},
		},
	}
	dir := t.TempDir()
	projDir := filepath.Join(dir, "-Users-test-proj")
	_ = os.MkdirAll(projDir, 0o750)
	writeSessionJSONL(t, projDir, "action-test", lines)

	sessions, err := Discover(DiscoverOpts{}, dir)
	if err != nil {
		t.Fatalf("Discover: %v", err)
	}
	if len(sessions) != 1 {
		t.Fatalf("expected 1 session, got %d", len(sessions))
	}
	if sessions[0].LastAction != "Ed config.go" {
		t.Errorf("LastAction = %q, want %q", sessions[0].LastAction, "Ed config.go")
	}
}

func TestScanSession_FilesystemFallback_NoTimestamps(t *testing.T) {
	dir := t.TempDir()
	filePath := filepath.Join(dir, "no-ts.jsonl")
	_ = os.WriteFile(filePath, []byte(`{"type":"permission-mode","permissionMode":"default"}
{"type":"last-prompt"}
`), 0o600)

	s, err := scanSession("no-ts", filePath, "/test")
	if err != nil {
		t.Fatalf("scanSession: %v", err)
	}
	if s.StartTime.IsZero() {
		t.Error("StartTime should not be zero; filesystem fallback should have filled it")
	}
	if s.LastActive.IsZero() {
		t.Error("LastActive should not be zero; filesystem fallback should have filled it")
	}
}

func TestScanSession_TimestampsFromJSONL(t *testing.T) {
	dir := t.TempDir()
	filePath := filepath.Join(dir, "with-ts.jsonl")
	start := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	end := time.Date(2026, 6, 1, 11, 30, 0, 0, time.UTC)
	data := `{"type":"user","timestamp":"` + start.Format(time.RFC3339) + `","message":{"role":"user","content":"hello"}}
{"type":"assistant","timestamp":"` + end.Format(time.RFC3339) + `","message":{"role":"assistant","content":"hi"}}
`
	_ = os.WriteFile(filePath, []byte(data), 0o600)

	s, err := scanSession("with-ts", filePath, "/test")
	if err != nil {
		t.Fatalf("scanSession: %v", err)
	}
	if !s.StartTime.Equal(start) {
		t.Errorf("StartTime = %v, want %v", s.StartTime, start)
	}
	if !s.LastActive.Equal(end) {
		t.Errorf("LastActive = %v, want %v", s.LastActive, end)
	}
}
