package main

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// Helper function to create test database with issues
func createTestDBWithIssues(t *testing.T, issues []*types.Issue) (string, *sqlite.SQLiteStorage) {
	t.Helper()
	tmpDir, err := os.MkdirTemp("", "bd-collision-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	t.Cleanup(func() { os.RemoveAll(tmpDir) })

	dbPath := filepath.Join(tmpDir, "test.db")
	testStore, err := sqlite.New(dbPath)
	if err != nil {
		t.Fatalf("Failed to create storage: %v", err)
	}
	t.Cleanup(func() { testStore.Close() })

	ctx := context.Background()
	for _, issue := range issues {
		if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
			t.Fatalf("Failed to create issue %s: %v", issue.ID, err)
		}
	}

	return tmpDir, testStore
}

// Helper function to write JSONL file
func writeJSONLFile(t *testing.T, dir string, issues []*types.Issue) string {
	t.Helper()
	jsonlPath := filepath.Join(dir, "issues.jsonl")
	f, err := os.Create(jsonlPath)
	if err != nil {
		t.Fatalf("Failed to create JSONL file: %v", err)
	}
	defer f.Close()

	encoder := json.NewEncoder(f)
	for _, issue := range issues {
		if err := encoder.Encode(issue); err != nil {
			t.Fatalf("Failed to encode issue %s: %v", issue.ID, err)
		}
	}

	return jsonlPath
}

// Helper function to capture stderr output
func captureStderr(t *testing.T, fn func()) string {
	t.Helper()
	oldStderr := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	fn()

	w.Close()
	os.Stderr = oldStderr

	var buf bytes.Buffer
	io.Copy(&buf, r)
	return buf.String()
}

// Helper function to setup auto-import test environment
func setupAutoImportTest(t *testing.T, testStore *sqlite.SQLiteStorage, tmpDir string) {
	t.Helper()
	store = testStore
	dbPath = filepath.Join(tmpDir, "test.db")

	storeMutex.Lock()
	storeActive = true
	storeMutex.Unlock()

	t.Cleanup(func() {
		storeMutex.Lock()
		storeActive = false
		storeMutex.Unlock()
	})
}

// TestAutoImportMultipleCollisionsRemapped tests that multiple collisions are auto-resolved
func TestAutoImportMultipleCollisionsRemapped(t *testing.T) {
	// Create 5 issues in DB with local modifications
	now := time.Now().UTC()
	closedTime := now.Add(-1 * time.Hour)

	dbIssues := []*types.Issue{
		{
			ID:        "test-mc-1",
			Title:     "Local version 1",
			Status:    types.StatusClosed,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
			ClosedAt:  &closedTime,
		},
		{
			ID:        "test-mc-2",
			Title:     "Local version 2",
			Status:    types.StatusInProgress,
			Priority:  2,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-3",
			Title:     "Local version 3",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeFeature,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-4",
			Title:     "Exact match",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-5",
			Title:     "Another exact match",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// Create JSONL with 3 colliding issues, 2 exact matches, and 1 new issue
	jsonlIssues := []*types.Issue{
		{
			ID:        "test-mc-1",
			Title:     "Remote version 1 (conflict)",
			Status:    types.StatusOpen,
			Priority:  3,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-2",
			Title:     "Remote version 2 (conflict)",
			Status:    types.StatusClosed,
			Priority:  1,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now.Add(-30 * time.Minute),
			ClosedAt:  &closedTime,
		},
		{
			ID:        "test-mc-3",
			Title:     "Remote version 3 (conflict)",
			Status:    types.StatusBlocked,
			Priority:  3,
			IssueType: types.TypeFeature,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-4",
			Title:     "Exact match",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-5",
			Title:     "Another exact match",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-mc-6",
			Title:     "Brand new issue",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Capture stderr and run auto-import
	stderrOutput := captureStderr(t, autoImportIfNewer)

	ctx := context.Background()

	// Verify local versions are preserved (original IDs still have local content)
	local1, _ := testStore.GetIssue(ctx, "test-mc-1")
	if local1.Title != "Local version 1" {
		t.Errorf("Expected local version preserved for test-mc-1, got: %s", local1.Title)
	}

	local2, _ := testStore.GetIssue(ctx, "test-mc-2")
	if local2.Title != "Local version 2" {
		t.Errorf("Expected local version preserved for test-mc-2, got: %s", local2.Title)
	}

	local3, _ := testStore.GetIssue(ctx, "test-mc-3")
	if local3.Title != "Local version 3" {
		t.Errorf("Expected local version preserved for test-mc-3, got: %s", local3.Title)
	}

	// Verify new issue was imported
	newIssue, _ := testStore.GetIssue(ctx, "test-mc-6")
	if newIssue == nil {
		t.Fatal("Expected new issue test-mc-6 to be imported")
	}
	if newIssue.Title != "Brand new issue" {
		t.Errorf("Expected new issue title 'Brand new issue', got: %s", newIssue.Title)
	}

	// Verify remapping message was printed
	if !strings.Contains(stderrOutput, "remapped") {
		t.Errorf("Expected remapping message in stderr, got: %s", stderrOutput)
	}
	if !strings.Contains(stderrOutput, "test-mc-1") {
		t.Errorf("Expected test-mc-1 in remapping message, got: %s", stderrOutput)
	}

	// Verify colliding issues were created with new IDs
	// They should appear in the database with different IDs
	allIssues, err := testStore.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		t.Fatalf("Failed to get all issues: %v", err)
	}

	// Should have: 5 original + 1 new + 3 remapped = 9 total
	if len(allIssues) < 8 {
		t.Errorf("Expected at least 8 issues (5 original + 1 new + 3 remapped), got %d", len(allIssues))
	}
}

// TestAutoImportAllCollisionsRemapped tests when every issue has a collision
func TestAutoImportAllCollisionsRemapped(t *testing.T) {
	now := time.Now().UTC()
	closedTime := now.Add(-1 * time.Hour)

	dbIssues := []*types.Issue{
		{
			ID:        "test-ac-1",
			Title:     "Local 1",
			Status:    types.StatusClosed,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
			ClosedAt:  &closedTime,
		},
		{
			ID:        "test-ac-2",
			Title:     "Local 2",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// JSONL with all conflicts (different content for same IDs)
	jsonlIssues := []*types.Issue{
		{
			ID:        "test-ac-1",
			Title:     "Remote 1 (conflict)",
			Status:    types.StatusOpen,
			Priority:  3,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-ac-2",
			Title:     "Remote 2 (conflict)",
			Status:    types.StatusClosed,
			Priority:  1,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now,
			ClosedAt:  &closedTime,
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Capture stderr and run auto-import
	stderrOutput := captureStderr(t, autoImportIfNewer)

	ctx := context.Background()

	// Verify all local versions preserved
	local1, _ := testStore.GetIssue(ctx, "test-ac-1")
	if local1.Title != "Local 1" {
		t.Errorf("Expected local version preserved, got: %s", local1.Title)
	}

	local2, _ := testStore.GetIssue(ctx, "test-ac-2")
	if local2.Title != "Local 2" {
		t.Errorf("Expected local version preserved, got: %s", local2.Title)
	}

	// Verify remapping message mentions both
	if !strings.Contains(stderrOutput, "remapped 2") {
		t.Errorf("Expected '2' in remapping count, got: %s", stderrOutput)
	}
}

// TestAutoImportExactMatchesOnly tests happy path with no conflicts
func TestAutoImportExactMatchesOnly(t *testing.T) {
	now := time.Now().UTC()

	dbIssues := []*types.Issue{
		{
			ID:        "test-em-1",
			Title:     "Exact match issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// JSONL with exact match + new issue
	jsonlIssues := []*types.Issue{
		{
			ID:        "test-em-1",
			Title:     "Exact match issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-em-2",
			Title:     "New issue",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Run auto-import (should not print collision warnings)
	stderrOutput := captureStderr(t, autoImportIfNewer)

	ctx := context.Background()

	// Verify new issue imported
	newIssue, _ := testStore.GetIssue(ctx, "test-em-2")
	if newIssue == nil {
		t.Fatal("Expected new issue to be imported")
	}
	if newIssue.Title != "New issue" {
		t.Errorf("Expected title 'New issue', got: %s", newIssue.Title)
	}

	// Verify no collision warnings
	if strings.Contains(stderrOutput, "remapped") {
		t.Errorf("Expected no remapping message, got: %s", stderrOutput)
	}
}

// TestAutoImportHashUnchanged tests fast path when JSONL hasn't changed
func TestAutoImportHashUnchanged(t *testing.T) {
	now := time.Now().UTC()

	dbIssues := []*types.Issue{
		{
			ID:        "test-hu-1",
			Title:     "Test issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	writeJSONLFile(t, tmpDir, dbIssues)

	// Run auto-import first time
	os.Setenv("BD_DEBUG", "1")
	defer os.Unsetenv("BD_DEBUG")

	stderrOutput1 := captureStderr(t, autoImportIfNewer)

	// Should trigger import on first run
	if !strings.Contains(stderrOutput1, "auto-import triggered") && !strings.Contains(stderrOutput1, "hash changed") {
		t.Logf("First run: %s", stderrOutput1)
	}

	// Run auto-import second time (JSONL unchanged)
	stderrOutput2 := captureStderr(t, autoImportIfNewer)

	// Verify fast path was taken (hash match)
	if !strings.Contains(stderrOutput2, "JSONL unchanged") {
		t.Errorf("Expected 'JSONL unchanged' in debug output, got: %s", stderrOutput2)
	}
}

// TestAutoImportParseError tests that parse errors are handled gracefully
func TestAutoImportParseError(t *testing.T) {
	now := time.Now().UTC()

	dbIssues := []*types.Issue{
		{
			ID:        "test-pe-1",
			Title:     "Test issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// Create malformed JSONL
	jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
	os.WriteFile(jsonlPath, []byte(`{"id":"test-pe-1","title":"Good issue","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-16T00:00:00Z","updated_at":"2025-10-16T00:00:00Z"}
{invalid json here}
`), 0644)

	// Run auto-import (should skip due to parse error)
	stderrOutput := captureStderr(t, autoImportIfNewer)

	// Verify parse error was reported
	if !strings.Contains(stderrOutput, "parse error") {
		t.Errorf("Expected parse error message, got: %s", stderrOutput)
	}
}

// TestAutoImportEmptyJSONL tests behavior with empty JSONL file
func TestAutoImportEmptyJSONL(t *testing.T) {
	now := time.Now().UTC()

	dbIssues := []*types.Issue{
		{
			ID:        "test-ej-1",
			Title:     "Existing issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// Create empty JSONL
	jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
	os.WriteFile(jsonlPath, []byte(""), 0644)

	// Run auto-import
	autoImportIfNewer()

	ctx := context.Background()

	// Verify existing issue still exists (not deleted)
	existing, _ := testStore.GetIssue(ctx, "test-ej-1")
	if existing == nil {
		t.Fatal("Expected existing issue to remain after empty JSONL import")
	}
}

// TestAutoImportNewIssuesOnly tests importing only new issues
func TestAutoImportNewIssuesOnly(t *testing.T) {
	now := time.Now().UTC()

	dbIssues := []*types.Issue{
		{
			ID:        "test-ni-1",
			Title:     "Existing issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// JSONL with only new issues (no collisions, no exact matches)
	jsonlIssues := []*types.Issue{
		{
			ID:        "test-ni-2",
			Title:     "New issue 1",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: now,
			UpdatedAt: now,
		},
		{
			ID:        "test-ni-3",
			Title:     "New issue 2",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeBug,
			CreatedAt: now,
			UpdatedAt: now,
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Run auto-import
	stderrOutput := captureStderr(t, autoImportIfNewer)

	ctx := context.Background()

	// Verify new issues imported
	issue2, _ := testStore.GetIssue(ctx, "test-ni-2")
	if issue2 == nil || issue2.Title != "New issue 1" {
		t.Error("Expected new issue 1 to be imported")
	}

	issue3, _ := testStore.GetIssue(ctx, "test-ni-3")
	if issue3 == nil || issue3.Title != "New issue 2" {
		t.Error("Expected new issue 2 to be imported")
	}

	// Verify no collision warnings
	if strings.Contains(stderrOutput, "remapped") {
		t.Errorf("Expected no collision messages, got: %s", stderrOutput)
	}
}

// TestAutoImportUpdatesExactMatches tests that exact matches update the DB
func TestAutoImportUpdatesExactMatches(t *testing.T) {
	now := time.Now().UTC()
	oldTime := now.Add(-24 * time.Hour)

	dbIssues := []*types.Issue{
		{
			ID:        "test-um-1",
			Title:     "Exact match",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: oldTime,
			UpdatedAt: oldTime,
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	// JSONL with exact match (same content, newer timestamp)
	jsonlIssues := []*types.Issue{
		{
			ID:        "test-um-1",
			Title:     "Exact match",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
			CreatedAt: oldTime,
			UpdatedAt: now, // Newer timestamp
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Run auto-import
	autoImportIfNewer()

	ctx := context.Background()

	// Verify issue was updated (UpdatedAt should be newer)
	updated, _ := testStore.GetIssue(ctx, "test-um-1")
	if updated.UpdatedAt.Before(now.Add(-1 * time.Second)) {
		t.Errorf("Expected UpdatedAt to be updated to %v, got %v", now, updated.UpdatedAt)
	}
}

// TestAutoImportJSONLNotFound tests behavior when JSONL doesn't exist
func TestAutoImportJSONLNotFound(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "bd-test-notfound-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	dbPath = filepath.Join(tmpDir, "test.db")
	// Don't create JSONL file

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

	store = testStore
	storeMutex.Lock()
	storeActive = true
	storeMutex.Unlock()
	defer func() {
		storeMutex.Lock()
		storeActive = false
		storeMutex.Unlock()
	}()

	// Enable debug mode to see skip message
	os.Setenv("BD_DEBUG", "1")
	defer os.Unsetenv("BD_DEBUG")

	// Run auto-import (should skip silently)
	stderrOutput := captureStderr(t, autoImportIfNewer)

	// Verify it skipped due to missing JSONL
	if !strings.Contains(stderrOutput, "JSONL not found") {
		t.Logf("Expected 'JSONL not found' message, got: %s", stderrOutput)
	}
}

// TestAutoImportCollisionRemapMultipleFields tests remapping with different field conflicts
func TestAutoImportCollisionRemapMultipleFields(t *testing.T) {
	now := time.Now().UTC()

	// Create issue with many fields set
	dbIssues := []*types.Issue{
		{
			ID:                 "test-fields-1",
			Title:              "Local title",
			Description:        "Local description",
			Status:             types.StatusOpen,
			Priority:           1,
			IssueType:          types.TypeTask,
			CreatedAt:          now,
			UpdatedAt:          now,
			Notes:              "Local notes",
			Design:             "Local design",
			AcceptanceCriteria: "Local acceptance",
		},
	}

	tmpDir, testStore := createTestDBWithIssues(t, dbIssues)
	setupAutoImportTest(t, testStore, tmpDir)

	ctx := context.Background()

	// JSONL with conflicts in multiple fields
	jsonlIssues := []*types.Issue{
		{
			ID:                 "test-fields-1",
			Title:              "Remote title (conflict)",
			Description:        "Remote description (conflict)",
			Status:             types.StatusClosed,
			Priority:           3,
			IssueType:          types.TypeBug,
			CreatedAt:          now,
			UpdatedAt:          now,
			ClosedAt:           &now,
			Notes:              "Remote notes (conflict)",
			Design:             "Remote design (conflict)",
			AcceptanceCriteria: "Remote acceptance (conflict)",
		},
	}

	writeJSONLFile(t, tmpDir, jsonlIssues)

	// Run auto-import
	stderrOutput := captureStderr(t, autoImportIfNewer)

	// Verify remapping occurred
	if !strings.Contains(stderrOutput, "test-fields-1") {
		t.Logf("Expected remapping message for test-fields-1: %s", stderrOutput)
	}

	// Verify local version of issue is preserved with all fields
	local, _ := testStore.GetIssue(ctx, "test-fields-1")
	if local.Title != "Local title" {
		t.Errorf("Expected local title preserved, got: %s", local.Title)
	}
	if local.Description != "Local description" {
		t.Errorf("Expected local description preserved, got: %s", local.Description)
	}
	if local.Status != types.StatusOpen {
		t.Errorf("Expected local status preserved, got: %s", local.Status)
	}
	if local.Priority != 1 {
		t.Errorf("Expected local priority preserved, got: %d", local.Priority)
	}
}

// TestAutoImportMetadataReadError tests error handling when metadata can't be read
func TestAutoImportMetadataReadError(t *testing.T) {
	// This test is difficult to implement without mocking since metadata
	// should always work in SQLite. We can document that this error path
	// is defensive but hard to trigger in practice.
	t.Skip("Metadata read error is defensive code path, hard to test without mocking")
}
