package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/steveyegge/beads/cmd/bd/doctor"
	"github.com/steveyegge/beads/internal/git"
)

func TestDoctorNoBeadsDir(t *testing.T) {
	// Create temporary directory
	tmpDir := t.TempDir()

	// Run diagnostics
	result := runDiagnostics(tmpDir)

	// Should fail overall
	if result.OverallOK {
		t.Error("Expected OverallOK to be false when .beads/ directory is missing")
	}

	// Check installation check failed
	if len(result.Checks) == 0 {
		t.Fatal("Expected at least one check")
	}

	installCheck := result.Checks[0]
	if installCheck.Name != "Installation" {
		t.Errorf("Expected first check to be Installation, got %s", installCheck.Name)
	}
	if installCheck.Status != "error" {
		t.Errorf("Expected Installation status to be error, got %s", installCheck.Status)
	}
	if installCheck.Fix == "" {
		t.Error("Expected Installation check to have a fix")
	}
}

func TestDoctorWithBeadsDir(t *testing.T) {
	// Create temporary directory with .beads
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	// Run diagnostics
	result := runDiagnostics(tmpDir)

	// Should have installation check passing
	if len(result.Checks) == 0 {
		t.Fatal("Expected at least one check")
	}

	installCheck := result.Checks[0]
	if installCheck.Name != "Installation" {
		t.Errorf("Expected first check to be Installation, got %s", installCheck.Name)
	}
	if installCheck.Status != "ok" {
		t.Errorf("Expected Installation status to be ok, got %s", installCheck.Status)
	}
}

func TestDoctorJSONOutput(t *testing.T) {
	// Create temporary directory
	tmpDir := t.TempDir()

	// Run diagnostics
	result := runDiagnostics(tmpDir)

	// Marshal to JSON to verify structure
	jsonBytes, err := json.Marshal(result)
	if err != nil {
		t.Fatalf("Failed to marshal result to JSON: %v", err)
	}

	// Unmarshal back to verify structure
	var decoded doctorResult
	if err := json.Unmarshal(jsonBytes, &decoded); err != nil {
		t.Fatalf("Failed to unmarshal JSON: %v", err)
	}

	// Verify key fields
	if decoded.Path != result.Path {
		t.Errorf("Path mismatch: %s != %s", decoded.Path, result.Path)
	}
	if decoded.CLIVersion != result.CLIVersion {
		t.Errorf("CLIVersion mismatch: %s != %s", decoded.CLIVersion, result.CLIVersion)
	}
	if decoded.OverallOK != result.OverallOK {
		t.Errorf("OverallOK mismatch: %v != %v", decoded.OverallOK, result.OverallOK)
	}
	if len(decoded.Checks) != len(result.Checks) {
		t.Errorf("Checks length mismatch: %d != %d", len(decoded.Checks), len(result.Checks))
	}
}

// Note: isHashID is tested in migrate_hash_ids_test.go

func TestDetectHashBasedIDs(t *testing.T) {
	tests := []struct {
		name      string
		sampleIDs []string
		hasTable  bool
		expected  bool
	}{
		{
			name:      "hash IDs with letters",
			sampleIDs: []string{"bd-a3f8e9", "bd-b2c4d6"},
			hasTable:  false,
			expected:  true,
		},
		{
			name:      "hash IDs with mixed alphanumeric",
			sampleIDs: []string{"bd-0134cc5a", "bd-abc123"},
			hasTable:  false,
			expected:  true,
		},
		{
			name:      "hash IDs all numeric with variable length",
			sampleIDs: []string{"bd-0088", "bd-0134cc5a", "bd-02a4"},
			hasTable:  false,
			expected:  true, // Variable length indicates hash IDs
		},
		{
			name:      "hash IDs with leading zeros",
			sampleIDs: []string{"bd-0088", "bd-02a4", "bd-05a1"},
			hasTable:  false,
			expected:  true, // Leading zeros indicate hash IDs
		},
		{
			name:      "hash IDs all numeric non-sequential",
			sampleIDs: []string{"bd-0088", "bd-2312", "bd-0458"},
			hasTable:  false,
			expected:  true, // Non-sequential pattern
		},
		{
			name:      "sequential IDs",
			sampleIDs: []string{"bd-1", "bd-2", "bd-3", "bd-4"},
			hasTable:  false,
			expected:  false, // Sequential pattern
		},
		{
			name:      "sequential IDs with gaps",
			sampleIDs: []string{"bd-1", "bd-5", "bd-10", "bd-15"},
			hasTable:  false,
			expected:  false, // Still sequential pattern (small gaps allowed)
		},
		{
			name:      "database with child_counters table",
			sampleIDs: []string{"bd-1", "bd-2"},
			hasTable:  true,
			expected:  true, // child_counters table indicates hash IDs
		},
		{
			name:      "hash IDs with hierarchical children",
			sampleIDs: []string{"bd-a3f8e9.1", "bd-a3f8e9.2", "bd-b2c4d6"},
			hasTable:  false,
			expected:  true, // Base IDs have letters
		},
		{
			name:      "edge case: single ID with letters",
			sampleIDs: []string{"bd-abc"},
			hasTable:  false,
			expected:  true,
		},
		{
			name:      "edge case: single sequential ID",
			sampleIDs: []string{"bd-1"},
			hasTable:  false,
			expected:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create temporary database
			tmpDir := t.TempDir()
			dbPath := filepath.Join(tmpDir, "test.db")

			// Open database and create schema
			db, err := sql.Open("sqlite3", dbPath)
			if err != nil {
				t.Fatalf("Failed to open database: %v", err)
			}
			defer db.Close()

			// Create issues table
			_, err = db.Exec(`
				CREATE TABLE IF NOT EXISTS issues (
					id TEXT PRIMARY KEY,
					title TEXT,
					created_at TIMESTAMP
				)
			`)
			if err != nil {
				t.Fatalf("Failed to create issues table: %v", err)
			}

			// Create child_counters table if test requires it
			if tt.hasTable {
				_, err = db.Exec(`
					CREATE TABLE IF NOT EXISTS child_counters (
						parent_id TEXT PRIMARY KEY,
						last_child INTEGER NOT NULL DEFAULT 0
					)
				`)
				if err != nil {
					t.Fatalf("Failed to create child_counters table: %v", err)
				}
			}

			// Insert sample issues
			for _, id := range tt.sampleIDs {
				_, err = db.Exec("INSERT INTO issues (id, title, created_at) VALUES (?, ?, datetime('now'))",
					id, "Test issue")
				if err != nil {
					t.Fatalf("Failed to insert issue %s: %v", id, err)
				}
			}

			// Test detection
			result := doctor.DetectHashBasedIDs(db, tt.sampleIDs)
			if result != tt.expected {
				t.Errorf("detectHashBasedIDs() = %v, want %v", result, tt.expected)
			}
		})
	}
}

func TestCheckIDFormat(t *testing.T) {
	tests := []struct {
		name           string
		issueIDs       []string
		createTable    bool // create child_counters table
		expectedStatus string
	}{
		{
			name:           "hash IDs with letters",
			issueIDs:       []string{"bd-a3f8e9", "bd-b2c4d6", "bd-xyz123"},
			createTable:    false,
			expectedStatus: doctor.StatusOK,
		},
		{
			name:           "hash IDs all numeric with leading zeros",
			issueIDs:       []string{"bd-0088", "bd-02a4", "bd-05a1", "bd-0458"},
			createTable:    false,
			expectedStatus: doctor.StatusOK,
		},
		{
			name:           "hash IDs with child_counters table",
			issueIDs:       []string{"bd-123", "bd-456"},
			createTable:    true,
			expectedStatus: doctor.StatusOK,
		},
		{
			name:           "sequential IDs",
			issueIDs:       []string{"bd-1", "bd-2", "bd-3", "bd-4"},
			createTable:    false,
			expectedStatus: doctor.StatusWarning,
		},
		{
			name:           "mixed: mostly hash IDs",
			issueIDs:       []string{"bd-0088", "bd-0134cc5a", "bd-02a4"},
			createTable:    false,
			expectedStatus: doctor.StatusOK, // Variable length = hash IDs
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create temporary workspace
			tmpDir := t.TempDir()
			beadsDir := filepath.Join(tmpDir, ".beads")
			if err := os.Mkdir(beadsDir, 0750); err != nil {
				t.Fatal(err)
			}

			// Create database
			dbPath := filepath.Join(beadsDir, "beads.db")
			db, err := sql.Open("sqlite3", dbPath)
			if err != nil {
				t.Fatalf("Failed to open database: %v", err)
			}
			defer db.Close()

			// Create schema
			_, err = db.Exec(`
				CREATE TABLE IF NOT EXISTS issues (
					id TEXT PRIMARY KEY,
					title TEXT NOT NULL,
					created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
				)
			`)
			if err != nil {
				t.Fatalf("Failed to create issues table: %v", err)
			}

			if tt.createTable {
				_, err = db.Exec(`
					CREATE TABLE IF NOT EXISTS child_counters (
						parent_id TEXT PRIMARY KEY,
						last_child INTEGER NOT NULL DEFAULT 0
					)
				`)
				if err != nil {
					t.Fatalf("Failed to create child_counters table: %v", err)
				}
			}

			// Insert test issues
			for i, id := range tt.issueIDs {
				_, err = db.Exec(
					"INSERT INTO issues (id, title, created_at) VALUES (?, ?, datetime('now', ?||' seconds'))",
					id, "Test issue "+id, fmt.Sprintf("+%d", i))
				if err != nil {
					t.Fatalf("Failed to insert issue %s: %v", id, err)
				}
			}
			db.Close()

			// Run check
			check := doctor.CheckIDFormat(tmpDir)

			if check.Status != tt.expectedStatus {
				t.Errorf("Expected status %s, got %s (message: %s)", tt.expectedStatus, check.Status, check.Message)
			}

			if tt.expectedStatus == doctor.StatusOK && check.Status == doctor.StatusOK {
				if !strings.Contains(check.Message, "hash-based") {
					t.Errorf("Expected hash-based message, got: %s", check.Message)
				}
			}

			if tt.expectedStatus == doctor.StatusWarning && check.Status == doctor.StatusWarning {
				if check.Fix == "" {
					t.Error("Expected fix message for sequential IDs")
				}
			}
		})
	}
}

func TestCheckInstallation(t *testing.T) {
	// Test with missing .beads directory
	tmpDir := t.TempDir()
	check := doctor.CheckInstallation(tmpDir)

	if check.Status != doctor.StatusError {
		t.Errorf("Expected error status, got %s", check.Status)
	}
	if check.Fix == "" {
		t.Error("Expected fix to be provided")
	}

	// Test with existing .beads directory
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	check = doctor.CheckInstallation(tmpDir)
	if check.Status != doctor.StatusOK {
		t.Errorf("Expected ok status, got %s", check.Status)
	}
}

func TestCheckDatabaseVersionJSONLMode(t *testing.T) {
	// Create temporary directory with .beads but no database
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	// Create empty issues.jsonl to simulate --no-db mode
	jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
	if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
		t.Fatal(err)
	}

	// Create config.yaml with no-db: true to indicate intentional JSONL-only mode
	// Without this, doctor treats it as a fresh clone needing 'bd init' (bd-4ew)
	configPath := filepath.Join(beadsDir, "config.yaml")
	if err := os.WriteFile(configPath, []byte("no-db: true\n"), 0644); err != nil {
		t.Fatal(err)
	}

	check := doctor.CheckDatabaseVersion(tmpDir, Version)

	if check.Status != doctor.StatusOK {
		t.Errorf("Expected ok status for JSONL mode, got %s", check.Status)
	}
	if check.Message != "JSONL-only mode" {
		t.Errorf("Expected JSONL-only mode message, got %s", check.Message)
	}
	if check.Detail == "" {
		t.Error("Expected detail field to be set for JSONL mode")
	}
}

func TestCheckDatabaseVersionFreshClone(t *testing.T) {
	// Create temporary directory with .beads and JSONL but no database
	// This simulates a fresh clone that needs 'bd init'
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	// Create issues.jsonl with an issue (no config.yaml = not no-db mode)
	jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
	if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-1","title":"Test"}`+"\n"), 0644); err != nil {
		t.Fatal(err)
	}

	check := doctor.CheckDatabaseVersion(tmpDir, Version)

	if check.Status != doctor.StatusWarning {
		t.Errorf("Expected warning status for fresh clone, got %s", check.Status)
	}
	if check.Message != "Fresh clone detected (no database)" {
		t.Errorf("Expected fresh clone message, got %s", check.Message)
	}
	if check.Fix == "" {
		t.Error("Expected fix field to recommend 'bd init'")
	}
}

func TestCompareVersions(t *testing.T) {
	tests := []struct {
		v1       string
		v2       string
		expected int
	}{
		{"0.20.1", "0.20.1", 0},   // Equal
		{"0.20.1", "0.20.0", 1},   // v1 > v2
		{"0.20.0", "0.20.1", -1},  // v1 < v2
		{"0.10.0", "0.9.9", 1},    // Major.minor comparison
		{"1.0.0", "0.99.99", 1},   // Major version difference
		{"0.20.1", "0.3.0", 1},    // String comparison would fail this
		{"1.2", "1.2.0", 0},       // Different length, equal
		{"1.2.1", "1.2", 1},       // Different length, v1 > v2
	}

	for _, tc := range tests {
		result := doctor.CompareVersions(tc.v1, tc.v2)
		if result != tc.expected {
			t.Errorf("doctor.CompareVersions(%q, %q) = %d, expected %d", tc.v1, tc.v2, result, tc.expected)
		}
	}
}

func TestCheckMultipleDatabases(t *testing.T) {
	tests := []struct {
		name           string
		dbFiles        []string
		expectedStatus string
		expectWarning  bool
	}{
		{
			name:           "no databases",
			dbFiles:        []string{},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name:           "single database",
			dbFiles:        []string{"beads.db"},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name:           "multiple databases",
			dbFiles:        []string{"beads.db", "old.db"},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name:           "backup files ignored",
			dbFiles:        []string{"beads.db", "beads.backup.db"},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name:           "vc.db ignored",
			dbFiles:        []string{"beads.db", "vc.db"},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			beadsDir := filepath.Join(tmpDir, ".beads")
			if err := os.Mkdir(beadsDir, 0750); err != nil {
				t.Fatal(err)
			}

			// Create test database files
			for _, dbFile := range tc.dbFiles {
				path := filepath.Join(beadsDir, dbFile)
				if err := os.WriteFile(path, []byte{}, 0644); err != nil {
					t.Fatal(err)
				}
			}

			check := doctor.CheckMultipleDatabases(tmpDir)

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
			}

			if tc.expectWarning && check.Fix == "" {
				t.Error("Expected fix message for warning status")
			}
		})
	}
}

func TestCheckPermissions(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	check := doctor.CheckPermissions(tmpDir)

	if check.Status != doctor.StatusOK {
		t.Errorf("Expected ok status for writable directory, got %s: %s", check.Status, check.Message)
	}
}

func TestCheckDatabaseJSONLSync(t *testing.T) {
	tests := []struct {
		name           string
		hasDB          bool
		hasJSONL       bool
		expectedStatus string
	}{
		{
			name:           "no database",
			hasDB:          false,
			hasJSONL:       true,
			expectedStatus: doctor.StatusOK,
		},
		{
			name:           "no JSONL",
			hasDB:          true,
			hasJSONL:       false,
			expectedStatus: doctor.StatusOK,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			beadsDir := filepath.Join(tmpDir, ".beads")
			if err := os.Mkdir(beadsDir, 0750); err != nil {
				t.Fatal(err)
			}

			if tc.hasDB {
				dbPath := filepath.Join(beadsDir, "beads.db")
				// Skip database creation tests due to SQLite driver registration in tests
				// The real doctor command works fine with actual databases
				if tc.hasJSONL {
					t.Skip("Database creation in tests requires complex driver setup")
				}
				// For no-JSONL case, just create an empty file
				if err := os.WriteFile(dbPath, []byte{}, 0644); err != nil {
					t.Fatal(err)
				}
			}

			if tc.hasJSONL {
				jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
				if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
					t.Fatal(err)
				}
			}

			check := doctor.CheckDatabaseJSONLSync(tmpDir)

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
			}
		})
	}
}


func TestCountJSONLIssuesWithMalformedLines(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.Mkdir(beadsDir, 0750); err != nil {
		t.Fatal(err)
	}

	// Create JSONL file with mixed valid and invalid JSON
	jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
	jsonlContent := `{"id":"test-001","title":"Valid 1"}
invalid json line here
{"id":"test-002","title":"Valid 2"}
{"broken": incomplete
{"id":"test-003","title":"Valid 3"}
`
	if err := os.WriteFile(jsonlPath, []byte(jsonlContent), 0644); err != nil {
		t.Fatal(err)
	}

	count, prefixes, err := doctor.CountJSONLIssues(jsonlPath)

	// Should count valid issues (3)
	if count != 3 {
		t.Errorf("Expected 3 issues, got %d", count)
	}

	// Should have 1 error for malformed lines
	if err == nil {
		t.Error("Expected error for malformed lines, got nil")
	}
	if !strings.Contains(err.Error(), "skipped") {
		t.Errorf("Expected error about skipped lines, got: %v", err)
	}

	// Should have extracted prefix
	if prefixes["test"] != 3 {
		t.Errorf("Expected 3 'test' prefixes, got %d", prefixes["test"])
	}
}
func TestCheckGitHooks(t *testing.T) {
	tests := []struct {
		name           string
		hasGitDir      bool
		installedHooks []string
		expectedStatus string
		expectWarning  bool
	}{
		{
			name:           "not a git repository",
			hasGitDir:      false,
			installedHooks: []string{},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name:           "all hooks installed",
			hasGitDir:      true,
			installedHooks: []string{"pre-commit", "post-merge", "pre-push"},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name:           "no hooks installed",
			hasGitDir:      true,
			installedHooks: []string{},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name:           "some hooks installed",
			hasGitDir:      true,
			installedHooks: []string{"pre-commit"},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name:           "partial hooks installed",
			hasGitDir:      true,
			installedHooks: []string{"pre-commit", "post-merge"},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()

			// Always change to tmpDir to ensure GetGitDir detects the correct context
			oldDir, err := os.Getwd()
			if err != nil {
				t.Fatalf("Failed to get current directory: %v", err)
			}
			if err := os.Chdir(tmpDir); err != nil {
				t.Fatalf("Failed to change to test directory: %v", err)
			}
			defer func() {
				_ = os.Chdir(oldDir)
			}()

			if tc.hasGitDir {
				// Initialize a real git repository in the test directory
				cmd := exec.Command("git", "init")
				cmd.Dir = tmpDir
				if err := cmd.Run(); err != nil {
					t.Skipf("Skipping test: git init failed: %v", err)
				}

				gitDir, err := git.GetGitDir()
				if err != nil {
					t.Fatalf("git.GetGitDir() failed: %v", err)
				}
				hooksDir := filepath.Join(gitDir, "hooks")
				if err := os.MkdirAll(hooksDir, 0750); err != nil {
					t.Fatal(err)
				}

				// Create installed hooks
				for _, hookName := range tc.installedHooks {
					hookPath := filepath.Join(hooksDir, hookName)
					if err := os.WriteFile(hookPath, []byte("#!/bin/sh\n"), 0755); err != nil {
						t.Fatal(err)
					}
				}
			}

			check := doctor.CheckGitHooks()

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
			}

			if tc.expectWarning && check.Fix == "" {
				t.Error("Expected fix message for warning status")
			}

			if !tc.expectWarning && check.Fix != "" && tc.hasGitDir {
				t.Error("Expected no fix message for non-warning status")
			}
		})
	}
}

func TestCheckClaudePlugin(t *testing.T) {
	tests := []struct {
		name           string
		claudeCodeEnv  string
		expectedStatus string
		expectedMsg    string
	}{
		{
			name:           "not running in claude code",
			claudeCodeEnv:  "",
			expectedStatus: doctor.StatusOK,
			expectedMsg:    "N/A (not running in Claude Code)",
		},
		{
			name:           "not running in claude code (0)",
			claudeCodeEnv:  "0",
			expectedStatus: doctor.StatusOK,
			expectedMsg:    "N/A (not running in Claude Code)",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Save original env
			origEnv := os.Getenv("CLAUDECODE")
			defer func() {
				if origEnv == "" {
					os.Unsetenv("CLAUDECODE")
				} else {
					os.Setenv("CLAUDECODE", origEnv)
				}
			}()

			// Set test env
			if tc.claudeCodeEnv == "" {
				os.Unsetenv("CLAUDECODE")
			} else {
				os.Setenv("CLAUDECODE", tc.claudeCodeEnv)
			}

			check := doctor.CheckClaudePlugin()

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
			}

			if check.Message != tc.expectedMsg {
				t.Errorf("Expected message %q, got %q", tc.expectedMsg, check.Message)
			}
		})
	}
}

func TestGetClaudePluginVersion(t *testing.T) {
	tests := []struct {
		name            string
		pluginJSON      string
		expectInstalled bool
		expectVersion   string
		expectError     bool
	}{
		{
			name: "plugin installed v1 format",
			pluginJSON: `{
				"version": 1,
				"plugins": {
					"beads@beads-marketplace": {
						"version": "0.21.3"
					}
				}
			}`,
			expectInstalled: true,
			expectVersion:   "0.21.3",
			expectError:     false,
		},
		{
			name: "plugin installed v2 format (GH#741)",
			pluginJSON: `{
				"version": 2,
				"plugins": {
					"beads@beads-marketplace": [
						{
							"scope": "user",
							"installPath": "/path/to/plugin",
							"version": "1.0.0",
							"installedAt": "2025-11-25T19:20:27.889Z",
							"lastUpdated": "2025-11-25T19:20:27.889Z",
							"gitCommitSha": "abc123",
							"isLocal": true
						}
					]
				}
			}`,
			expectInstalled: true,
			expectVersion:   "1.0.0",
			expectError:     false,
		},
		{
			name: "plugin not installed v2 format",
			pluginJSON: `{
				"version": 2,
				"plugins": {
					"other-plugin@marketplace": [
						{
							"scope": "user",
							"version": "2.0.0"
						}
					]
				}
			}`,
			expectInstalled: false,
			expectVersion:   "",
			expectError:     false,
		},
		{
			name: "plugin not installed v1 format",
			pluginJSON: `{
				"version": 1,
				"plugins": {
					"other-plugin@marketplace": {
						"version": "1.0.0"
					}
				}
			}`,
			expectInstalled: false,
			expectVersion:   "",
			expectError:     false,
		},
		{
			name:            "invalid json",
			pluginJSON:      `{invalid json`,
			expectInstalled: false,
			expectVersion:   "",
			expectError:     true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Create temp dir with plugin file
			tmpHome := t.TempDir()
			pluginDir := filepath.Join(tmpHome, ".claude", "plugins")
			if err := os.MkdirAll(pluginDir, 0750); err != nil {
				t.Fatal(err)
			}
			pluginPath := filepath.Join(pluginDir, "installed_plugins.json")
			if err := os.WriteFile(pluginPath, []byte(tc.pluginJSON), 0600); err != nil {
				t.Fatal(err)
			}

			// Temporarily override home directory
			origHome := os.Getenv("HOME")
			os.Setenv("HOME", tmpHome)
			defer os.Setenv("HOME", origHome)

			version, installed, err := doctor.GetClaudePluginVersion()

			if tc.expectError && err == nil {
				t.Error("Expected error, got nil")
			}
			if !tc.expectError && err != nil {
				t.Errorf("Unexpected error: %v", err)
			}
			if installed != tc.expectInstalled {
				t.Errorf("Expected installed=%v, got %v", tc.expectInstalled, installed)
			}
			if version != tc.expectVersion {
				t.Errorf("Expected version %q, got %q", tc.expectVersion, version)
			}
		})
	}
}

func TestCheckMetadataVersionTracking(t *testing.T) {
	// GH#662: Tests updated to use .local_version file instead of metadata.json:LastBdVersion
	tests := []struct {
		name           string
		setupVersion   func(beadsDir string) error
		expectedStatus string
		expectWarning  bool
	}{
		{
			name: "valid current version",
			setupVersion: func(beadsDir string) error {
				return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(Version+"\n"), 0644)
			},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name: "slightly outdated version",
			setupVersion: func(beadsDir string) error {
				// Use a version that's less than 10 minor versions behind current
				return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.35.0\n"), 0644)
			},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name: "very old version",
			setupVersion: func(beadsDir string) error {
				// Use a version that's 10+ minor versions behind current (triggers warning)
				return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.29.0\n"), 0644)
			},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name: "empty version file",
			setupVersion: func(beadsDir string) error {
				return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(""), 0644)
			},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name: "invalid version format",
			setupVersion: func(beadsDir string) error {
				return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("invalid-version\n"), 0644)
			},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
		{
			name: "missing .local_version file",
			setupVersion: func(beadsDir string) error {
				// Don't create .local_version
				return nil
			},
			expectedStatus: doctor.StatusWarning,
			expectWarning:  true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			beadsDir := filepath.Join(tmpDir, ".beads")
			if err := os.Mkdir(beadsDir, 0750); err != nil {
				t.Fatal(err)
			}

			// Setup .local_version file
			if err := tc.setupVersion(beadsDir); err != nil {
				t.Fatal(err)
			}

			check := doctor.CheckMetadataVersionTracking(tmpDir, Version)

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s (message: %s)", tc.expectedStatus, check.Status, check.Message)
			}

			if tc.expectWarning && check.Status == doctor.StatusWarning && check.Fix == "" {
				t.Error("Expected fix message for warning status")
			}
		})
	}
}

func TestIsValidSemver(t *testing.T) {
	tests := []struct {
		version  string
		expected bool
	}{
		{"0.24.2", true},
		{"1.0.0", true},
		{"0.1", true},       // Major.minor is valid
		{"1", true},         // Just major is valid
		{"", false},         // Empty is invalid
		{"invalid", false},  // Non-numeric is invalid
		{"0.a.2", false},    // Letters in parts are invalid
		{"1.2.3.4", true},   // Extra parts are ok
	}

	for _, tc := range tests {
		result := doctor.IsValidSemver(tc.version)
		if result != tc.expected {
			t.Errorf("doctor.IsValidSemver(%q) = %v, expected %v", tc.version, result, tc.expected)
		}
	}
}

func TestParseVersionParts(t *testing.T) {
	tests := []struct {
		version  string
		expected []int
	}{
		{"0.24.2", []int{0, 24, 2}},
		{"1.0.0", []int{1, 0, 0}},
		{"0.1", []int{0, 1}},
		{"1", []int{1}},
		{"", []int{}},
		{"invalid", []int{}},
		{"1.a.3", []int{1}}, // Stops at first non-numeric part
	}

	for _, tc := range tests {
		result := doctor.ParseVersionParts(tc.version)
		if len(result) != len(tc.expected) {
			t.Errorf("doctor.ParseVersionParts(%q) returned %d parts, expected %d", tc.version, len(result), len(tc.expected))
			continue
		}
		for i := range result {
			if result[i] != tc.expected[i] {
				t.Errorf("doctor.ParseVersionParts(%q)[%d] = %d, expected %d", tc.version, i, result[i], tc.expected[i])
			}
		}
	}
}

func TestCheckSyncBranchConfig(t *testing.T) {
	tests := []struct {
		name           string
		setupFunc      func(t *testing.T, tmpDir string)
		expectedStatus string
		expectWarning  bool
	}{
		{
			name: "no beads directory",
			setupFunc: func(t *testing.T, tmpDir string) {
				// No .beads directory
			},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name: "not a git repo",
			setupFunc: func(t *testing.T, tmpDir string) {
				beadsDir := filepath.Join(tmpDir, ".beads")
				if err := os.Mkdir(beadsDir, 0750); err != nil {
					t.Fatal(err)
				}
			},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		{
			name: "sync.branch configured via env var",
			setupFunc: func(t *testing.T, tmpDir string) {
				// Initialize git repo
				cmd := exec.Command("git", "init")
				cmd.Dir = tmpDir
				if err := cmd.Run(); err != nil {
					t.Fatal(err)
				}

				// Create .beads directory
				beadsDir := filepath.Join(tmpDir, ".beads")
				if err := os.Mkdir(beadsDir, 0750); err != nil {
					t.Fatal(err)
				}

				// Set env var (simulates config.yaml or BEADS_SYNC_BRANCH)
				t.Setenv("BEADS_SYNC_BRANCH", "beads-sync")
			},
			expectedStatus: doctor.StatusOK,
			expectWarning:  false,
		},
		// Note: Tests for "not configured" scenarios are difficult because viper
		// reads config.yaml at startup from the test's working directory.
		// The env var tests above verify the core functionality.
		// For full integration testing, use actual fresh clones.
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			tc.setupFunc(t, tmpDir)

			result := doctor.CheckSyncBranchConfig(tmpDir)

			if result.Status != tc.expectedStatus {
				t.Errorf("Expected status %q, got %q", tc.expectedStatus, result.Status)
			}

			if tc.expectWarning && result.Fix == "" {
				t.Error("Expected Fix field to be set for warning status")
			}
		})
	}
}

// TestInteractiveFlagParsing verifies the --interactive flag is registered (bd-3xl)
func TestInteractiveFlagParsing(t *testing.T) {
	// Verify the flag exists and has the right short form
	flag := doctorCmd.Flags().Lookup("interactive")
	if flag == nil {
		t.Fatal("--interactive flag not found")
	}
	if flag.Shorthand != "i" {
		t.Errorf("Expected shorthand 'i', got %q", flag.Shorthand)
	}
	if flag.DefValue != "false" {
		t.Errorf("Expected default value 'false', got %q", flag.DefValue)
	}
}

// TestOutputFlagParsing verifies the --output flag is registered (bd-9cc)
func TestOutputFlagParsing(t *testing.T) {
	flag := doctorCmd.Flags().Lookup("output")
	if flag == nil {
		t.Fatal("--output flag not found")
	}
	if flag.Shorthand != "o" {
		t.Errorf("Expected shorthand 'o', got %q", flag.Shorthand)
	}
	if flag.DefValue != "" {
		t.Errorf("Expected default value '', got %q", flag.DefValue)
	}
}

// TestExportDiagnostics verifies the export functionality (bd-9cc)
func TestExportDiagnostics(t *testing.T) {
	tmpDir := t.TempDir()
	outputPath := filepath.Join(tmpDir, "diagnostics.json")

	// Create a test result
	result := doctorResult{
		Path:       "/test/path",
		CLIVersion: "0.29.0",
		OverallOK:  true,
		Timestamp:  "2025-01-01T00:00:00Z",
		Platform: map[string]string{
			"os_arch":        "darwin/arm64",
			"go_version":     "go1.21.0",
			"sqlite_version": "3.42.0",
		},
		Checks: []doctorCheck{
			{
				Name:    "Installation",
				Status:  "ok",
				Message: ".beads/ directory found",
			},
			{
				Name:    "Git Hooks",
				Status:  "warning",
				Message: "No hooks installed",
				Fix:     "Run 'bd hooks install'",
			},
		},
	}

	// Export to file
	if err := exportDiagnostics(result, outputPath); err != nil {
		t.Fatalf("exportDiagnostics failed: %v", err)
	}

	// Read the file back
	data, err := os.ReadFile(outputPath)
	if err != nil {
		t.Fatalf("Failed to read exported file: %v", err)
	}

	// Parse the JSON
	var decoded doctorResult
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("Failed to parse exported JSON: %v", err)
	}

	// Verify fields
	if decoded.Path != result.Path {
		t.Errorf("Path mismatch: got %q, want %q", decoded.Path, result.Path)
	}
	if decoded.CLIVersion != result.CLIVersion {
		t.Errorf("CLIVersion mismatch: got %q, want %q", decoded.CLIVersion, result.CLIVersion)
	}
	if decoded.OverallOK != result.OverallOK {
		t.Errorf("OverallOK mismatch: got %v, want %v", decoded.OverallOK, result.OverallOK)
	}
	if decoded.Timestamp != result.Timestamp {
		t.Errorf("Timestamp mismatch: got %q, want %q", decoded.Timestamp, result.Timestamp)
	}
	if decoded.Platform["os_arch"] != result.Platform["os_arch"] {
		t.Errorf("Platform.os_arch mismatch: got %q, want %q", decoded.Platform["os_arch"], result.Platform["os_arch"])
	}
	if len(decoded.Checks) != len(result.Checks) {
		t.Errorf("Checks length mismatch: got %d, want %d", len(decoded.Checks), len(result.Checks))
	}
}

// TestExportDiagnosticsInvalidPath verifies error handling (bd-9cc)
func TestExportDiagnosticsInvalidPath(t *testing.T) {
	result := doctorResult{
		Path:      "/test/path",
		OverallOK: true,
	}

	// Try to export to an invalid path
	err := exportDiagnostics(result, "/nonexistent/directory/diagnostics.json")
	if err == nil {
		t.Error("Expected error for invalid path, got nil")
	}
}

// TestCheckSyncBranchHookCompatibility tests the sync-branch hook compatibility check (issue #532)
// Note: We use BEADS_SYNC_BRANCH env var to control sync-branch detection because the config
// package reads from the actual beads repo's config.yaml. Only test cases with syncBranchEnv
// set to a non-empty value are reliable.
func TestCheckSyncBranchHookCompatibility(t *testing.T) {
	tests := []struct {
		name           string
		syncBranchEnv  string // BEADS_SYNC_BRANCH env var (must be non-empty to override config.yaml)
		hasGitDir      bool
		hookVersion    string // Empty means no hook, "custom" means non-bd hook
		expectedStatus string
	}{
		{
			name:           "sync-branch configured, no git repo",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      false,
			hookVersion:    "",
			expectedStatus: doctor.StatusOK, // N/A case
		},
		{
			name:           "sync-branch configured, no pre-push hook",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "",
			expectedStatus: doctor.StatusOK, // Covered by other check
		},
		{
			name:           "sync-branch configured, custom hook",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "custom",
			expectedStatus: doctor.StatusWarning,
		},
		{
			name:           "sync-branch configured, old hook (0.24.2)",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "0.24.2",
			expectedStatus: doctor.StatusError,
		},
		{
			name:           "sync-branch configured, old hook (0.28.0)",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "0.28.0",
			expectedStatus: doctor.StatusError,
		},
		{
			name:           "sync-branch configured, compatible hook (0.29.0)",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "0.29.0",
			expectedStatus: doctor.StatusOK,
		},
		{
			name:           "sync-branch configured, newer hook (0.30.0)",
			syncBranchEnv:  "beads-sync",
			hasGitDir:      true,
			hookVersion:    "0.30.0",
			expectedStatus: doctor.StatusOK,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()

			// Always set environment variable to control sync-branch detection
			// This overrides any config.yaml value in the actual beads repo
			t.Setenv("BEADS_SYNC_BRANCH", tc.syncBranchEnv)

			if tc.hasGitDir {
				// Initialize a real git repo (git rev-parse needs this)
				cmd := exec.Command("git", "init")
				cmd.Dir = tmpDir
				if err := cmd.Run(); err != nil {
					t.Fatal(err)
				}

				// Create pre-push hook if specified
				if tc.hookVersion != "" {
					hooksDir := filepath.Join(tmpDir, ".git", "hooks")
					hookPath := filepath.Join(hooksDir, "pre-push")
					var hookContent string
					if tc.hookVersion == "custom" {
						hookContent = "#!/bin/sh\n# Custom hook\nexit 0\n"
					} else {
						hookContent = fmt.Sprintf("#!/bin/sh\n# bd-hooks-version: %s\nexit 0\n", tc.hookVersion)
					}
					if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
						t.Fatal(err)
					}
				}
			}

			check := doctor.CheckSyncBranchHookCompatibility(tmpDir)

			if check.Status != tc.expectedStatus {
				t.Errorf("Expected status %s, got %s (message: %s)", tc.expectedStatus, check.Status, check.Message)
			}

			// Error case should have a fix message
			if tc.expectedStatus == doctor.StatusError && check.Fix == "" {
				t.Error("Expected fix message for error status")
			}
		})
	}
}

// TestCheckSyncBranchHookQuick tests the quick sync-branch hook check (issue #532)
// Note: We use BEADS_SYNC_BRANCH env var to control sync-branch detection.
func TestCheckSyncBranchHookQuick(t *testing.T) {
	tests := []struct {
		name          string
		syncBranchEnv string
		hasGitDir     bool
		hookVersion   string
		expectIssue   bool
	}{
		{
			name:          "old hook with sync-branch",
			syncBranchEnv: "beads-sync",
			hasGitDir:     true,
			hookVersion:   "0.24.0",
			expectIssue:   true,
		},
		{
			name:          "compatible hook with sync-branch",
			syncBranchEnv: "beads-sync",
			hasGitDir:     true,
			hookVersion:   "0.29.0",
			expectIssue:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tmpDir := t.TempDir()

			// Always set environment variable to control sync-branch detection
			// This overrides any config.yaml value in the actual beads repo
			t.Setenv("BEADS_SYNC_BRANCH", tc.syncBranchEnv)

			if tc.hasGitDir {
				// Initialize a real git repo (git rev-parse needs this)
				cmd := exec.Command("git", "init")
				cmd.Dir = tmpDir
				if err := cmd.Run(); err != nil {
					t.Fatal(err)
				}

				if tc.hookVersion != "" {
					hooksDir := filepath.Join(tmpDir, ".git", "hooks")
					hookPath := filepath.Join(hooksDir, "pre-push")
					hookContent := fmt.Sprintf("#!/bin/sh\n# bd-hooks-version: %s\nexit 0\n", tc.hookVersion)
					if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
						t.Fatal(err)
					}
				}
			}

			issue := doctor.CheckSyncBranchHookQuick(tmpDir)

			if tc.expectIssue && issue == "" {
				t.Error("Expected issue to be reported, got empty string")
			}
			if !tc.expectIssue && issue != "" {
				t.Errorf("Expected no issue, got: %s", issue)
			}
		})
	}
}
