package main

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/steveyegge/beads/internal/config"
	"github.com/steveyegge/beads/internal/storage/sqlite"
	"github.com/steveyegge/beads/internal/types"
)

// TestMultiWorkspaceDeletionSync simulates the bd-hv01 bug scenario:
// Clone A deletes an issue, Clone B still has it, and after sync it should stay deleted
func TestMultiWorkspaceDeletionSync(t *testing.T) {
	// Setup two separate workspaces simulating two git clones
	cloneADir := t.TempDir()
	cloneBDir := t.TempDir()

	cloneAJSONL := filepath.Join(cloneADir, "issues.jsonl")
	cloneBJSONL := filepath.Join(cloneBDir, "issues.jsonl")

	cloneADB := filepath.Join(cloneADir, "beads.db")
	cloneBDB := filepath.Join(cloneBDir, "beads.db")

	ctx := context.Background()

	// Create stores for both clones
	storeA, err := sqlite.New(context.Background(), cloneADB)
	if err != nil {
		t.Fatalf("Failed to create store A: %v", err)
	}
	defer storeA.Close()

	if err := storeA.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		t.Fatalf("Failed to set issue_prefix for store A: %v", err)
	}

	storeB, err := sqlite.New(context.Background(), cloneBDB)
	if err != nil {
		t.Fatalf("Failed to create store B: %v", err)
	}
	defer storeB.Close()

	if err := storeB.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		t.Fatalf("Failed to set issue_prefix for store B: %v", err)
	}

	// Step 1: Both clones start with the same two issues
	issueToDelete := &types.Issue{
		ID:          "bd-delete-me",
		Title:       "Issue to be deleted",
		Description: "This will be deleted in clone A",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "bug",
	}

	issueToKeep := &types.Issue{
		ID:          "bd-keep-me",
		Title:       "Issue to keep",
		Description: "This should remain",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "feature",
	}

	// Create in both stores (using "test" as actor)
	if err := storeA.CreateIssue(ctx, issueToDelete, "test"); err != nil {
		t.Fatalf("Failed to create issue in store A: %v", err)
	}
	if err := storeA.CreateIssue(ctx, issueToKeep, "test"); err != nil {
		t.Fatalf("Failed to create issue in store A: %v", err)
	}

	if err := storeB.CreateIssue(ctx, issueToDelete, "test"); err != nil {
		t.Fatalf("Failed to create issue in store B: %v", err)
	}
	if err := storeB.CreateIssue(ctx, issueToKeep, "test"); err != nil {
		t.Fatalf("Failed to create issue in store B: %v", err)
	}

	// Export from both
	if err := exportToJSONLWithStore(ctx, storeA, cloneAJSONL); err != nil {
		t.Fatalf("Failed to export from store A: %v", err)
	}
	if err := exportToJSONLWithStore(ctx, storeB, cloneBJSONL); err != nil {
		t.Fatalf("Failed to export from store B: %v", err)
	}

	// Initialize base snapshots for both (simulating first sync)
	if err := initializeSnapshotsIfNeeded(cloneAJSONL); err != nil {
		t.Fatalf("Failed to initialize snapshots for A: %v", err)
	}
	if err := initializeSnapshotsIfNeeded(cloneBJSONL); err != nil {
		t.Fatalf("Failed to initialize snapshots for B: %v", err)
	}

	// Step 2: Clone A deletes the issue
	if err := storeA.DeleteIssue(ctx, "bd-delete-me"); err != nil {
		t.Fatalf("Failed to delete issue in store A: %v", err)
	}

	// Step 3: Clone A exports and captures left snapshot (simulating pre-pull)
	if err := exportToJSONLWithStore(ctx, storeA, cloneAJSONL); err != nil {
		t.Fatalf("Failed to export from store A after deletion: %v", err)
	}
	if err := captureLeftSnapshot(cloneAJSONL); err != nil {
		t.Fatalf("Failed to capture left snapshot for A: %v", err)
	}

	// Simulate git push/pull: Copy Clone A's JSONL to Clone B's "remote" state
	remoteJSONL := cloneAJSONL

	// Step 4: Clone B exports (still has both issues) and captures left snapshot
	if err := exportToJSONLWithStore(ctx, storeB, cloneBJSONL); err != nil {
		t.Fatalf("Failed to export from store B: %v", err)
	}
	if err := captureLeftSnapshot(cloneBJSONL); err != nil {
		t.Fatalf("Failed to capture left snapshot for B: %v", err)
	}

	// Step 5: Simulate Clone B pulling from remote (copy remote JSONL)
	remoteData, err := os.ReadFile(remoteJSONL)
	if err != nil {
		t.Fatalf("Failed to read remote JSONL: %v", err)
	}
	if err := os.WriteFile(cloneBJSONL, remoteData, 0644); err != nil {
		t.Fatalf("Failed to write pulled JSONL to clone B: %v", err)
	}

	// Step 6: Clone B applies 3-way merge and prunes deletions
	// This is the key fix - it should detect that bd-delete-me was deleted remotely
	merged, err := merge3WayAndPruneDeletions(ctx, storeB, cloneBJSONL)
	if err != nil {
		t.Fatalf("Failed to apply deletions from merge: %v", err)
	}

	if !merged {
		t.Error("Expected 3-way merge to run, but it was skipped")
	}

	// Step 7: Verify the deletion was applied to Clone B's database
	deletedIssue, err := storeB.GetIssue(ctx, "bd-delete-me")
	if err == nil && deletedIssue != nil {
		t.Errorf("Issue bd-delete-me should have been deleted from Clone B, but still exists")
	}

	// Verify the kept issue still exists
	keptIssue, err := storeB.GetIssue(ctx, "bd-keep-me")
	if err != nil || keptIssue == nil {
		t.Errorf("Issue bd-keep-me should still exist in Clone B")
	}

	// Verify Clone A still has only one issue
	issuesA, err := storeA.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		t.Fatalf("Failed to search issues in store A: %v", err)
	}
	if len(issuesA) != 1 {
		t.Errorf("Clone A should have 1 issue after deletion, got %d", len(issuesA))
	}

	// Verify Clone B now matches Clone A (both have 1 issue)
	issuesB, err := storeB.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		t.Fatalf("Failed to search issues in store B: %v", err)
	}
	if len(issuesB) != 1 {
		t.Errorf("Clone B should have 1 issue after merge, got %d", len(issuesB))
	}
}

// TestDeletionWithLocalModification tests the conflict scenario:
// Remote deletes an issue, but local has modified it
func TestDeletionWithLocalModification(t *testing.T) {
	dir := t.TempDir()
	jsonlPath := filepath.Join(dir, "issues.jsonl")
	dbPath := filepath.Join(dir, "beads.db")

	ctx := context.Background()

	store, err := sqlite.New(context.Background(), dbPath)
	if err != nil {
		t.Fatalf("Failed to create store: %v", err)
	}
	defer store.Close()

	if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		t.Fatalf("Failed to set issue_prefix: %v", err)
	}

	// Create an issue
	issue := &types.Issue{
		ID:          "bd-conflict",
		Title:       "Original title",
		Description: "Original description",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "bug",
	}

	if err := store.CreateIssue(ctx, issue, "test"); err != nil {
		t.Fatalf("Failed to create issue: %v", err)
	}

	// Export and create base snapshot
	if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
		t.Fatalf("Failed to export: %v", err)
	}
	if err := initializeSnapshotsIfNeeded(jsonlPath); err != nil {
		t.Fatalf("Failed to initialize snapshots: %v", err)
	}

	// Modify the issue locally
	updates := map[string]interface{}{
		"title": "Modified title locally",
	}
	if err := store.UpdateIssue(ctx, "bd-conflict", updates, "test"); err != nil {
		t.Fatalf("Failed to update issue: %v", err)
	}

	// Export modified state and capture left snapshot
	if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
		t.Fatalf("Failed to export after modification: %v", err)
	}
	if err := captureLeftSnapshot(jsonlPath); err != nil {
		t.Fatalf("Failed to capture left snapshot: %v", err)
	}

	// Simulate remote deletion (write empty JSONL)
	if err := os.WriteFile(jsonlPath, []byte(""), 0644); err != nil {
		t.Fatalf("Failed to simulate remote deletion: %v", err)
	}

	// Try to merge - deletion now wins over modification (bd-pq5k)
	// This should succeed and delete the issue
	_, err = merge3WayAndPruneDeletions(ctx, store, jsonlPath)
	if err != nil {
		t.Errorf("Expected merge to succeed (deletion wins), but got error: %v", err)
	}

	// The issue should be deleted (deletion wins over modification)
	conflictIssue, err := store.GetIssue(ctx, "bd-conflict")
	if err == nil && conflictIssue != nil {
		t.Error("Issue should be deleted after merge (deletion wins)")
	}
}

// TestComputeAcceptedDeletions tests the deletion detection logic
func TestComputeAcceptedDeletions(t *testing.T) {
	dir := t.TempDir()

	jsonlPath := filepath.Join(dir, "issues.jsonl")
	sm := NewSnapshotManager(jsonlPath)
	basePath, leftPath := sm.GetSnapshotPaths()
	mergedPath := filepath.Join(dir, "merged.jsonl")

	// Base has 3 issues
	baseContent := `{"id":"bd-1","title":"Issue 1"}
{"id":"bd-2","title":"Issue 2"}
{"id":"bd-3","title":"Issue 3"}
`

	// Left has 3 issues (unchanged from base)
	leftContent := baseContent

	// Merged has only 2 issues (bd-2 was deleted remotely)
	mergedContent := `{"id":"bd-1","title":"Issue 1"}
{"id":"bd-3","title":"Issue 3"}
`

	if err := os.WriteFile(basePath, []byte(baseContent), 0644); err != nil {
		t.Fatalf("Failed to write base: %v", err)
	}
	if err := os.WriteFile(leftPath, []byte(leftContent), 0644); err != nil {
		t.Fatalf("Failed to write left: %v", err)
	}
	if err := os.WriteFile(mergedPath, []byte(mergedContent), 0644); err != nil {
		t.Fatalf("Failed to write merged: %v", err)
	}

	deletions, err := sm.ComputeAcceptedDeletions(mergedPath)
	if err != nil {
		t.Fatalf("Failed to compute deletions: %v", err)
	}

	if len(deletions) != 1 {
		t.Errorf("Expected 1 deletion, got %d", len(deletions))
	}

	if len(deletions) > 0 && deletions[0] != "bd-2" {
		t.Errorf("Expected deletion of bd-2, got %s", deletions[0])
	}
}

// TestComputeAcceptedDeletions_LocallyModified tests that deletion wins even for locally modified issues (bd-pq5k)
func TestComputeAcceptedDeletions_LocallyModified(t *testing.T) {
	dir := t.TempDir()

	jsonlPath := filepath.Join(dir, "issues.jsonl")
	sm := NewSnapshotManager(jsonlPath)
	basePath, leftPath := sm.GetSnapshotPaths()
	mergedPath := filepath.Join(dir, "merged.jsonl")

	// Base has 2 issues
	baseContent := `{"id":"bd-1","title":"Original 1"}
{"id":"bd-2","title":"Original 2"}
`

	// Left has bd-2 modified locally
	leftContent := `{"id":"bd-1","title":"Original 1"}
{"id":"bd-2","title":"Modified locally"}
`

	// Merged has only bd-1 (bd-2 deleted remotely, we modified it locally, but deletion wins per bd-pq5k)
	mergedContent := `{"id":"bd-1","title":"Original 1"}
`

	if err := os.WriteFile(basePath, []byte(baseContent), 0644); err != nil {
		t.Fatalf("Failed to write base: %v", err)
	}
	if err := os.WriteFile(leftPath, []byte(leftContent), 0644); err != nil {
		t.Fatalf("Failed to write left: %v", err)
	}
	if err := os.WriteFile(mergedPath, []byte(mergedContent), 0644); err != nil {
		t.Fatalf("Failed to write merged: %v", err)
	}

	deletions, err := sm.ComputeAcceptedDeletions(mergedPath)
	if err != nil {
		t.Fatalf("Failed to compute deletions: %v", err)
	}

	// bd-pq5k: bd-2 SHOULD be in accepted deletions even though modified locally (deletion wins)
	if len(deletions) != 1 {
		t.Errorf("Expected 1 deletion (deletion wins over local modification), got %d: %v", len(deletions), deletions)
	}
	if len(deletions) == 1 && deletions[0] != "bd-2" {
		t.Errorf("Expected deletion of bd-2, got %v", deletions)
	}
}

// TestSnapshotManagement tests the snapshot file lifecycle
func TestSnapshotManagement(t *testing.T) {
	dir := t.TempDir()
	jsonlPath := filepath.Join(dir, "issues.jsonl")

	// Write initial JSONL
	content := `{"id":"bd-1","title":"Test"}
`
	if err := os.WriteFile(jsonlPath, []byte(content), 0644); err != nil {
		t.Fatalf("Failed to write JSONL: %v", err)
	}

	// Initialize snapshots
	if err := initializeSnapshotsIfNeeded(jsonlPath); err != nil {
		t.Fatalf("Failed to initialize snapshots: %v", err)
	}

	sm := NewSnapshotManager(jsonlPath)
	basePath, leftPath := sm.GetSnapshotPaths()

	// Base should exist, left should not
	if !fileExists(basePath) {
		t.Error("Base snapshot should exist after initialization")
	}
	if fileExists(leftPath) {
		t.Error("Left snapshot should not exist yet")
	}

	// Capture left snapshot
	if err := captureLeftSnapshot(jsonlPath); err != nil {
		t.Fatalf("Failed to capture left snapshot: %v", err)
	}

	if !fileExists(leftPath) {
		t.Error("Left snapshot should exist after capture")
	}

	// Update base snapshot
	if err := updateBaseSnapshot(jsonlPath); err != nil {
		t.Fatalf("Failed to update base snapshot: %v", err)
	}

	// Both should exist now
	baseCount, leftCount, baseExists, leftExists := getSnapshotStats(jsonlPath)
	if !baseExists || !leftExists {
		t.Error("Both snapshots should exist")
	}
	if baseCount != 1 || leftCount != 1 {
		t.Errorf("Expected 1 issue in each snapshot, got base=%d left=%d", baseCount, leftCount)
	}
}

// TestMultiRepoDeletionTracking tests deletion tracking with multi-repo mode
// This is the test for bd-4oob: snapshot files need to be created per-JSONL file
func TestMultiRepoDeletionTracking(t *testing.T) {
	// Setup workspace directories
	primaryDir := t.TempDir()
	additionalDir := t.TempDir()

	// Setup .beads directories
	primaryBeadsDir := filepath.Join(primaryDir, ".beads")
	additionalBeadsDir := filepath.Join(additionalDir, ".beads")
	if err := os.MkdirAll(primaryBeadsDir, 0755); err != nil {
		t.Fatalf("Failed to create primary .beads dir: %v", err)
	}
	if err := os.MkdirAll(additionalBeadsDir, 0755); err != nil {
		t.Fatalf("Failed to create additional .beads dir: %v", err)
	}

	// Create database in primary dir
	dbPath := filepath.Join(primaryBeadsDir, "beads.db")
	ctx := context.Background()

	store, err := sqlite.New(context.Background(), dbPath)
	if err != nil {
		t.Fatalf("Failed to create store: %v", err)
	}
	defer store.Close()

	if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		t.Fatalf("Failed to set issue_prefix: %v", err)
	}

	// Setup multi-repo config
	config.Set("repos.primary", primaryDir)
	config.Set("repos.additional", []string{additionalDir})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	// Create issues in different repos
	primaryIssue := &types.Issue{
		ID:          "bd-primary",
		Title:       "Primary repo issue",
		Description: "This belongs to primary",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "task",
		SourceRepo:  ".", // Primary repo
	}

	additionalIssue := &types.Issue{
		ID:          "bd-additional",
		Title:       "Additional repo issue",
		Description: "This belongs to additional",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "task",
		SourceRepo:  additionalDir,
	}

	if err := store.CreateIssue(ctx, primaryIssue, "test"); err != nil {
		t.Fatalf("Failed to create primary issue: %v", err)
	}
	if err := store.CreateIssue(ctx, additionalIssue, "test"); err != nil {
		t.Fatalf("Failed to create additional issue: %v", err)
	}

	// Export to multi-repo (this creates multiple JSONL files)
	results, err := store.ExportToMultiRepo(ctx)
	if err != nil {
		t.Fatalf("ExportToMultiRepo failed: %v", err)
	}
	if results == nil {
		t.Fatal("Expected multi-repo results, got nil")
	}
	if results["."] != 1 {
		t.Errorf("Expected 1 issue in primary repo, got %d", results["."])
	}
	if results[additionalDir] != 1 {
		t.Errorf("Expected 1 issue in additional repo, got %d", results[additionalDir])
	}

	// Verify JSONL files exist
	primaryJSONL := filepath.Join(primaryBeadsDir, "issues.jsonl")
	additionalJSONL := filepath.Join(additionalBeadsDir, "issues.jsonl")

	if !fileExists(primaryJSONL) {
		t.Fatalf("Primary JSONL not created: %s", primaryJSONL)
	}
	if !fileExists(additionalJSONL) {
		t.Fatalf("Additional JSONL not created: %s", additionalJSONL)
	}

	// THIS IS THE BUG: Initialize snapshots - currently only works for single JSONL
	// Should create snapshots for BOTH JSONL files
	if err := initializeSnapshotsIfNeeded(primaryJSONL); err != nil {
		t.Fatalf("Failed to initialize primary snapshots: %v", err)
	}
	if err := initializeSnapshotsIfNeeded(additionalJSONL); err != nil {
		t.Fatalf("Failed to initialize additional snapshots: %v", err)
	}

	// Verify snapshot files exist for both repos
	primarySM := NewSnapshotManager(primaryJSONL)
	primaryBasePath, primaryLeftPath := primarySM.GetSnapshotPaths()
	additionalSM := NewSnapshotManager(additionalJSONL)
	additionalBasePath, additionalLeftPath := additionalSM.GetSnapshotPaths()

	if !fileExists(primaryBasePath) {
		t.Errorf("Primary base snapshot not created: %s", primaryBasePath)
	}
	if !fileExists(additionalBasePath) {
		t.Errorf("Additional base snapshot not created: %s", additionalBasePath)
	}

	// Capture left snapshot BEFORE simulating git pull
	// This represents our local state before the pull
	if err := captureLeftSnapshot(primaryJSONL); err != nil {
		t.Fatalf("Failed to capture primary left snapshot: %v", err)
	}
	if err := captureLeftSnapshot(additionalJSONL); err != nil {
		t.Fatalf("Failed to capture additional left snapshot: %v", err)
	}

	// Simulate remote deletion: replace additional repo JSONL with empty file
	// This simulates what happens after a git pull where remote deleted the issue
	emptyFile, err := os.Create(additionalJSONL)
	if err != nil {
		t.Fatalf("Failed to create empty JSONL: %v", err)
	}
	emptyFile.Close()

	// Verify left snapshots exist
	if !fileExists(primaryLeftPath) {
		t.Errorf("Primary left snapshot not created: %s", primaryLeftPath)
	}
	if !fileExists(additionalLeftPath) {
		t.Errorf("Additional left snapshot not created: %s", additionalLeftPath)
	}

	// Now apply deletion tracking for additional repo
	// This should detect that bd-additional was deleted remotely and remove it from DB
	merged, err := merge3WayAndPruneDeletions(ctx, store, additionalJSONL)
	if err != nil {
		t.Fatalf("merge3WayAndPruneDeletions failed for additional repo: %v", err)
	}
	if !merged {
		t.Error("Expected merge to be performed (base snapshot exists)")
	}

	// Verify the issue was deleted from the database
	issue, err := store.GetIssue(ctx, "bd-additional")
	if err != nil {
		t.Errorf("Unexpected error getting issue: %v", err)
	}
	if issue != nil {
		t.Errorf("Expected bd-additional to be deleted from database, but it still exists: %+v", issue)
	}

	// Verify primary issue still exists
	primaryResult, err := store.GetIssue(ctx, "bd-primary")
	if err != nil {
		t.Errorf("Primary issue should still exist: %v", err)
	}
	if primaryResult == nil {
		t.Error("Primary issue should not be nil")
	}
}

// TestGetMultiRepoJSONLPaths_EmptyPaths tests handling of empty path configs
func TestGetMultiRepoJSONLPaths_EmptyPaths(t *testing.T) {
	// Test with empty primary (should default to ".")
	config.Set("repos.primary", "")
	config.Set("repos.additional", []string{})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	paths := getMultiRepoJSONLPaths()
	if paths != nil {
		t.Errorf("Expected nil (single-repo mode) for empty primary, got %v", paths)
	}
}

// TestGetMultiRepoJSONLPaths_Duplicates tests deduplication of paths
func TestGetMultiRepoJSONLPaths_Duplicates(t *testing.T) {
	// Setup temp dirs
	primaryDir := t.TempDir()
	
	// Create .beads directories
	if err := os.MkdirAll(filepath.Join(primaryDir, ".beads"), 0755); err != nil {
		t.Fatalf("Failed to create .beads dir: %v", err)
	}

	// Test with duplicate paths (., ./, and absolute path to same location)
	config.Set("repos.primary", primaryDir)
	config.Set("repos.additional", []string{primaryDir, primaryDir}) // Duplicates
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	paths := getMultiRepoJSONLPaths()
	
	// Current implementation doesn't dedupe - just verify it returns all entries
	// (This documents current behavior; future improvement could dedupe)
	expectedCount := 3 // primary + 2 duplicates
	if len(paths) != expectedCount {
		t.Errorf("Expected %d paths, got %d: %v", expectedCount, len(paths), paths)
	}
	
	// All should point to same JSONL location
	expectedJSONL := filepath.Join(primaryDir, ".beads", "issues.jsonl")
	for i, p := range paths {
		if p != expectedJSONL {
			t.Errorf("Path[%d] = %s, want %s", i, p, expectedJSONL)
		}
	}
}

// TestGetMultiRepoJSONLPaths_PathsWithSpaces tests handling of paths containing spaces
func TestGetMultiRepoJSONLPaths_PathsWithSpaces(t *testing.T) {
	// Create temp dir with space in name
	baseDir := t.TempDir()
	primaryDir := filepath.Join(baseDir, "my project")
	additionalDir := filepath.Join(baseDir, "other repo")
	
	// Create .beads directories
	if err := os.MkdirAll(filepath.Join(primaryDir, ".beads"), 0755); err != nil {
		t.Fatalf("Failed to create primary .beads: %v", err)
	}
	if err := os.MkdirAll(filepath.Join(additionalDir, ".beads"), 0755); err != nil {
		t.Fatalf("Failed to create additional .beads: %v", err)
	}

	config.Set("repos.primary", primaryDir)
	config.Set("repos.additional", []string{additionalDir})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	paths := getMultiRepoJSONLPaths()
	
	if len(paths) != 2 {
		t.Fatalf("Expected 2 paths, got %d", len(paths))
	}
	
	// Verify paths are constructed correctly
	expectedPrimary := filepath.Join(primaryDir, ".beads", "issues.jsonl")
	expectedAdditional := filepath.Join(additionalDir, ".beads", "issues.jsonl")
	
	if paths[0] != expectedPrimary {
		t.Errorf("Primary path = %s, want %s", paths[0], expectedPrimary)
	}
	if paths[1] != expectedAdditional {
		t.Errorf("Additional path = %s, want %s", paths[1], expectedAdditional)
	}
}

// TestGetMultiRepoJSONLPaths_RelativePaths tests handling of relative paths
func TestGetMultiRepoJSONLPaths_RelativePaths(t *testing.T) {
	// Note: Current implementation takes paths as-is without normalization
	// This test documents current behavior
	config.Set("repos.primary", ".")
	config.Set("repos.additional", []string{"../other", "./foo/../bar"})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	paths := getMultiRepoJSONLPaths()
	
	if len(paths) != 3 {
		t.Fatalf("Expected 3 paths, got %d", len(paths))
	}
	
	// Current implementation: relative paths are NOT expanded to absolute
	// They're used as-is with filepath.Join
	expectedPrimary := filepath.Join(".", ".beads", "issues.jsonl")
	expectedOther := filepath.Join("../other", ".beads", "issues.jsonl")
	expectedBar := filepath.Join("./foo/../bar", ".beads", "issues.jsonl")
	
	if paths[0] != expectedPrimary {
		t.Errorf("Primary path = %s, want %s", paths[0], expectedPrimary)
	}
	if paths[1] != expectedOther {
		t.Errorf("Additional[0] path = %s, want %s", paths[1], expectedOther)
	}
	if paths[2] != expectedBar {
		t.Errorf("Additional[1] path = %s, want %s", paths[2], expectedBar)
	}
}

// TestGetMultiRepoJSONLPaths_TildeExpansion tests that tilde is NOT expanded
func TestGetMultiRepoJSONLPaths_TildeExpansion(t *testing.T) {
	// Current implementation does NOT expand tilde - it's used literally
	config.Set("repos.primary", "~/repos/main")
	config.Set("repos.additional", []string{"~/repos/other"})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	paths := getMultiRepoJSONLPaths()
	
	if len(paths) != 2 {
		t.Fatalf("Expected 2 paths, got %d", len(paths))
	}
	
	// Tilde should be literal (NOT expanded) in current implementation
	expectedPrimary := filepath.Join("~/repos/main", ".beads", "issues.jsonl")
	expectedAdditional := filepath.Join("~/repos/other", ".beads", "issues.jsonl")
	
	if paths[0] != expectedPrimary {
		t.Errorf("Primary path = %s, want %s", paths[0], expectedPrimary)
	}
	if paths[1] != expectedAdditional {
		t.Errorf("Additional path = %s, want %s", paths[1], expectedAdditional)
	}
}

// TestMultiRepoSnapshotIsolation verifies that snapshot operations on one repo
// don't interfere with another repo's snapshots
func TestMultiRepoSnapshotIsolation(t *testing.T) {
	// Setup two repo directories
	repo1Dir := t.TempDir()
	repo2Dir := t.TempDir()

	// Create .beads/issues.jsonl in each
	repo1JSONL := filepath.Join(repo1Dir, ".beads", "issues.jsonl")
	repo2JSONL := filepath.Join(repo2Dir, ".beads", "issues.jsonl")

	if err := os.MkdirAll(filepath.Dir(repo1JSONL), 0755); err != nil {
		t.Fatalf("Failed to create repo1 .beads: %v", err)
	}
	if err := os.MkdirAll(filepath.Dir(repo2JSONL), 0755); err != nil {
		t.Fatalf("Failed to create repo2 .beads: %v", err)
	}

	// Write test issues
	issue1 := map[string]interface{}{
		"id":    "bd-repo1-issue",
		"title": "Repo 1 Issue",
	}
	issue2 := map[string]interface{}{
		"id":    "bd-repo2-issue",
		"title": "Repo 2 Issue",
	}

	// Write to repo1
	f1, err := os.Create(repo1JSONL)
	if err != nil {
		t.Fatalf("Failed to create repo1 JSONL: %v", err)
	}
	json.NewEncoder(f1).Encode(issue1)
	f1.Close()

	// Write to repo2
	f2, err := os.Create(repo2JSONL)
	if err != nil {
		t.Fatalf("Failed to create repo2 JSONL: %v", err)
	}
	json.NewEncoder(f2).Encode(issue2)
	f2.Close()

	// Initialize snapshots for both
	if err := initializeSnapshotsIfNeeded(repo1JSONL); err != nil {
		t.Fatalf("Failed to init repo1 snapshots: %v", err)
	}
	if err := initializeSnapshotsIfNeeded(repo2JSONL); err != nil {
		t.Fatalf("Failed to init repo2 snapshots: %v", err)
	}

	// Get snapshot paths for both
	repo1SM := NewSnapshotManager(repo1JSONL)
	repo1Base, repo1Left := repo1SM.GetSnapshotPaths()
	repo2SM := NewSnapshotManager(repo2JSONL)
	repo2Base, repo2Left := repo2SM.GetSnapshotPaths()

	// Verify isolation: snapshots should be in different directories
	if filepath.Dir(repo1Base) == filepath.Dir(repo2Base) {
		t.Error("Snapshot directories should be different for different repos")
	}

	// Verify each snapshot contains only its own issue
	repo1IDs, err := repo1SM.BuildIDSet(repo1Base)
	if err != nil {
		t.Fatalf("Failed to read repo1 base snapshot: %v", err)
	}
	repo2IDs, err := repo2SM.BuildIDSet(repo2Base)
	if err != nil {
		t.Fatalf("Failed to read repo2 base snapshot: %v", err)
	}

	if !repo1IDs["bd-repo1-issue"] {
		t.Error("Repo1 snapshot should contain bd-repo1-issue")
	}
	if repo1IDs["bd-repo2-issue"] {
		t.Error("Repo1 snapshot should NOT contain bd-repo2-issue")
	}

	if !repo2IDs["bd-repo2-issue"] {
		t.Error("Repo2 snapshot should contain bd-repo2-issue")
	}
	if repo2IDs["bd-repo1-issue"] {
		t.Error("Repo2 snapshot should NOT contain bd-repo1-issue")
	}

	// Capture left snapshots for both
	if err := captureLeftSnapshot(repo1JSONL); err != nil {
		t.Fatalf("Failed to capture repo1 left: %v", err)
	}
	if err := captureLeftSnapshot(repo2JSONL); err != nil {
		t.Fatalf("Failed to capture repo2 left: %v", err)
	}

	// Verify left snapshots are isolated
	if !fileExists(repo1Left) || !fileExists(repo2Left) {
		t.Error("Both left snapshots should exist")
	}

	repo1LeftIDs, err := repo1SM.BuildIDSet(repo1Left)
	if err != nil {
		t.Fatalf("Failed to read repo1 left snapshot: %v", err)
	}
	repo2LeftIDs, err := repo2SM.BuildIDSet(repo2Left)
	if err != nil {
		t.Fatalf("Failed to read repo2 left snapshot: %v", err)
	}

	if !repo1LeftIDs["bd-repo1-issue"] || repo1LeftIDs["bd-repo2-issue"] {
		t.Error("Repo1 left snapshot has wrong issues")
	}
	if !repo2LeftIDs["bd-repo2-issue"] || repo2LeftIDs["bd-repo1-issue"] {
		t.Error("Repo2 left snapshot has wrong issues")
	}
}

// TestMultiRepoFlushPrefixFiltering tests that non-primary repos only flush issues
// with their own prefix (GH #437 fix).
//
// The bug: In multi-repo mode, when a non-primary repo flushes, it incorrectly writes
// ALL issues (including from primary) to its local issues.jsonl. This causes prefix
// mismatch errors on subsequent imports.
//
// Expected behavior:
// - Primary repo: writes all issues (from all repos)
// - Non-primary repos: only writes issues matching their prefix
func TestMultiRepoFlushPrefixFiltering(t *testing.T) {
	// Setup workspace directories
	primaryDir := t.TempDir()
	additionalDir := t.TempDir()

	// Setup .beads directories
	primaryBeadsDir := filepath.Join(primaryDir, ".beads")
	additionalBeadsDir := filepath.Join(additionalDir, ".beads")
	if err := os.MkdirAll(primaryBeadsDir, 0755); err != nil {
		t.Fatalf("Failed to create primary .beads dir: %v", err)
	}
	if err := os.MkdirAll(additionalBeadsDir, 0755); err != nil {
		t.Fatalf("Failed to create additional .beads dir: %v", err)
	}

	// Create database in additional (non-primary) dir
	dbPath := filepath.Join(additionalBeadsDir, "beads.db")
	ctx := context.Background()

	store, err := sqlite.New(context.Background(), dbPath)
	if err != nil {
		t.Fatalf("Failed to create store: %v", err)
	}
	defer store.Close()

	// Set prefix for additional repo (different from primary)
	if err := store.SetConfig(ctx, "issue_prefix", "foo-b"); err != nil {
		t.Fatalf("Failed to set issue_prefix: %v", err)
	}

	// Setup multi-repo config
	config.Set("repos.primary", primaryDir)
	config.Set("repos.additional", []string{additionalDir})
	defer func() {
		config.Set("repos.primary", "")
		config.Set("repos.additional", nil)
	}()

	// Create issues with different prefixes (simulating hydrated multi-repo)
	// foo-a prefix = primary repo issues (hydrated from remote)
	// foo-b prefix = additional repo issues (local)
	primaryIssue := &types.Issue{
		ID:          "foo-a-001",
		Title:       "Primary repo issue",
		Description: "This belongs to primary",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "task",
		SourceRepo:  ".",
	}

	additionalIssue := &types.Issue{
		ID:          "foo-b-001",
		Title:       "Additional repo issue",
		Description: "This belongs to additional",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   "task",
		SourceRepo:  additionalDir,
	}

	// Use batch create with SkipPrefixValidation to simulate multi-repo hydration
	// (in real multi-repo mode, issues from other repos are imported with prefix validation skipped)
	if err := store.CreateIssuesWithFullOptions(ctx, []*types.Issue{primaryIssue, additionalIssue}, "test", sqlite.BatchCreateOptions{SkipPrefixValidation: true}); err != nil {
		t.Fatalf("Failed to batch create issues: %v", err)
	}

	// Build issues slice (simulating what flushToJSONLWithState does)
	allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		t.Fatalf("Failed to search issues: %v", err)
	}
	if len(allIssues) != 2 {
		t.Fatalf("Expected 2 issues, got %d", len(allIssues))
	}

	// Get configured prefix for this repo (additional)
	prefix, err := store.GetConfig(ctx, "issue_prefix")
	if err != nil {
		t.Fatalf("Failed to get prefix: %v", err)
	}

	// Determine if we're primary (we're not - we're in additional)
	cwd := additionalDir // Simulate being in additional repo
	primaryPath := config.GetMultiRepoConfig().Primary
	absCwd, _ := filepath.Abs(cwd)
	absPrimary, _ := filepath.Abs(primaryPath)
	isPrimary := absCwd == absPrimary

	if isPrimary {
		t.Fatal("Expected to be non-primary repo")
	}

	// Filter issues by prefix (the fix)
	filtered := make([]*types.Issue, 0, len(allIssues))
	prefixWithDash := prefix + "-"
	for _, issue := range allIssues {
		if len(issue.ID) >= len(prefixWithDash) && issue.ID[:len(prefixWithDash)] == prefixWithDash {
			filtered = append(filtered, issue)
		}
	}

	// Verify filtering worked
	if len(filtered) != 1 {
		t.Errorf("Expected 1 filtered issue, got %d", len(filtered))
	}
	if len(filtered) > 0 && filtered[0].ID != "foo-b-001" {
		t.Errorf("Expected filtered issue to be foo-b-001, got %s", filtered[0].ID)
	}

	// Write filtered issues to additional repo's JSONL
	jsonlPath := filepath.Join(additionalBeadsDir, "issues.jsonl")
	f, err := os.Create(jsonlPath)
	if err != nil {
		t.Fatalf("Failed to create JSONL: %v", err)
	}
	encoder := json.NewEncoder(f)
	for _, issue := range filtered {
		if err := encoder.Encode(issue); err != nil {
			f.Close()
			t.Fatalf("Failed to encode issue: %v", err)
		}
	}
	f.Close()

	// Read back and verify only foo-b issue is present
	data, err := os.ReadFile(jsonlPath)
	if err != nil {
		t.Fatalf("Failed to read JSONL: %v", err)
	}

	var readIssue types.Issue
	if err := json.Unmarshal(data, &readIssue); err != nil {
		t.Fatalf("Failed to parse JSONL: %v", err)
	}

	if readIssue.ID != "foo-b-001" {
		t.Errorf("JSONL contains wrong issue: expected foo-b-001, got %s", readIssue.ID)
	}

	// Verify foo-a issue is NOT in the file
	if string(data[:10]) == `{"id":"foo-a` {
		t.Error("JSONL incorrectly contains foo-a issue (GH #437 bug)")
	}
}
