package sqlite

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/steveyegge/beads/internal/types"
	_ "github.com/ncruces/go-sqlite3/driver"
	_ "github.com/ncruces/go-sqlite3/embed"
)

func setupTestDB(t *testing.T) (*SQLiteStorage, func()) {
	t.Helper()

	// Create temporary directory
	tmpDir, err := os.MkdirTemp("", "beads-test-*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}

	dbPath := filepath.Join(tmpDir, "test.db")
	ctx := context.Background()

	store, err := New(ctx, dbPath)
	if err != nil {
		os.RemoveAll(tmpDir)
		t.Fatalf("failed to create storage: %v", err)
	}

	// CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors
	ctx = context.Background()
	if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		store.Close()
		os.RemoveAll(tmpDir)
		t.Fatalf("failed to set issue_prefix: %v", err)
	}

	cleanup := func() {
		store.Close()
		os.RemoveAll(tmpDir)
	}

	return store, cleanup
}

func TestCreateIssue(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	issue := &types.Issue{
		Title:       "Test issue",
		Description: "Test description",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   types.TypeTask,
	}

	err := store.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	if issue.ID == "" {
		t.Error("Issue ID should be set")
	}

	if !issue.CreatedAt.After(time.Time{}) {
		t.Error("CreatedAt should be set")
	}

	if !issue.UpdatedAt.After(time.Time{}) {
		t.Error("UpdatedAt should be set")
	}
}

func TestCreateIssueValidation(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	tests := []struct {
		name    string
		issue   *types.Issue
		wantErr bool
	}{
		{
			name: "valid issue",
			issue: &types.Issue{
				Title:     "Valid",
				Status:    types.StatusOpen,
				Priority:  2,
				IssueType: types.TypeTask,
			},
			wantErr: false,
		},
		{
			name: "missing title",
			issue: &types.Issue{
				Status:    types.StatusOpen,
				Priority:  2,
				IssueType: types.TypeTask,
			},
			wantErr: true,
		},
		{
			name: "invalid priority",
			issue: &types.Issue{
				Title:     "Test",
				Status:    types.StatusOpen,
				Priority:  10,
				IssueType: types.TypeTask,
			},
			wantErr: true,
		},
		{
			name: "invalid status",
			issue: &types.Issue{
				Title:     "Test",
				Status:    "invalid",
				Priority:  2,
				IssueType: types.TypeTask,
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := store.CreateIssue(ctx, tt.issue, "test-user")
			if (err != nil) != tt.wantErr {
				t.Errorf("CreateIssue() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestGetIssue(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	original := &types.Issue{
		Title:              "Test issue",
		Description:        "Description",
		Design:             "Design notes",
		AcceptanceCriteria: "Acceptance",
		Notes:              "Notes",
		Status:             types.StatusOpen,
		Priority:           1,
		IssueType:          types.TypeFeature,
		Assignee:           "alice",
	}

	err := store.CreateIssue(ctx, original, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	// Retrieve the issue
	retrieved, err := store.GetIssue(ctx, original.ID)
	if err != nil {
		t.Fatalf("GetIssue failed: %v", err)
	}

	if retrieved == nil {
		t.Fatal("GetIssue returned nil")
	}

	if retrieved.ID != original.ID {
		t.Errorf("ID mismatch: got %v, want %v", retrieved.ID, original.ID)
	}

	if retrieved.Title != original.Title {
		t.Errorf("Title mismatch: got %v, want %v", retrieved.Title, original.Title)
	}

	if retrieved.Description != original.Description {
		t.Errorf("Description mismatch: got %v, want %v", retrieved.Description, original.Description)
	}

	if retrieved.Assignee != original.Assignee {
		t.Errorf("Assignee mismatch: got %v, want %v", retrieved.Assignee, original.Assignee)
	}
}

func TestGetIssueNotFound(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	issue, err := store.GetIssue(ctx, "bd-999")
	if err != nil {
		t.Fatalf("GetIssue failed: %v", err)
	}

	if issue != nil {
		t.Errorf("Expected nil for non-existent issue, got %v", issue)
	}
}

// createIssuesTestHelper provides test setup and assertion methods
type createIssuesTestHelper struct {
	t     *testing.T
	ctx   context.Context
	store *SQLiteStorage
}

func newCreateIssuesHelper(t *testing.T, store *SQLiteStorage) *createIssuesTestHelper {
	return &createIssuesTestHelper{t: t, ctx: context.Background(), store: store}
}

func (h *createIssuesTestHelper) newIssue(id, title string, status types.Status, priority int, issueType types.IssueType, closedAt *time.Time) *types.Issue {
	return &types.Issue{
		ID:        id,
		Title:     title,
		Status:    status,
		Priority:  priority,
		IssueType: issueType,
		ClosedAt:  closedAt,
	}
}

func (h *createIssuesTestHelper) createIssues(issues []*types.Issue) error {
	return h.store.CreateIssues(h.ctx, issues, "test-user")
}

func (h *createIssuesTestHelper) assertNoError(err error) {
	if err != nil {
		h.t.Errorf("CreateIssues() unexpected error: %v", err)
	}
}

func (h *createIssuesTestHelper) assertError(err error) {
	if err == nil {
		h.t.Error("CreateIssues() expected error, got nil")
	}
}

func (h *createIssuesTestHelper) assertCount(issues []*types.Issue, expected int) {
	if len(issues) != expected {
		h.t.Errorf("expected %d issues, got %d", expected, len(issues))
	}
}

func (h *createIssuesTestHelper) assertIDSet(issue *types.Issue, index int) {
	if issue.ID == "" {
		h.t.Errorf("issue %d: ID should be set", index)
	}
}

func (h *createIssuesTestHelper) assertTimestampSet(ts time.Time, field string, index int) {
	if !ts.After(time.Time{}) {
		h.t.Errorf("issue %d: %s should be set", index, field)
	}
}

func (h *createIssuesTestHelper) assertUniqueIDs(issues []*types.Issue) {
	ids := make(map[string]bool)
	for _, issue := range issues {
		if ids[issue.ID] {
			h.t.Errorf("duplicate ID found: %s", issue.ID)
		}
		ids[issue.ID] = true
	}
}

func (h *createIssuesTestHelper) assertEqual(expected, actual interface{}, field string) {
	if expected != actual {
		h.t.Errorf("expected %s %v, got %v", field, expected, actual)
	}
}

func (h *createIssuesTestHelper) assertNotNil(value interface{}, field string) {
	if value == nil {
		h.t.Errorf("%s should be set", field)
	}
}

func (h *createIssuesTestHelper) assertNoAutoGenID(issues []*types.Issue, wantErr bool) {
	if !wantErr {
		return
	}
	for i, issue := range issues {
		if issue == nil {
			continue
		}
		hasCustomID := issue.ID != "" && (issue.ID == "bd-100" || issue.ID == "bd-200" || 
			issue.ID == "bd-999" || issue.ID == "bd-existing")
		if !hasCustomID && issue.ID != "" {
			h.t.Errorf("issue %d: ID should not be auto-generated on error, got %s", i, issue.ID)
		}
	}
}

func TestCreateIssues(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	h := newCreateIssuesHelper(t, store)

	tests := []struct {
		name       string
		issues     []*types.Issue
		wantErr    bool
		checkFunc  func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue)
	}{
		{
			name:    "empty batch",
			issues:  []*types.Issue{},
			wantErr: false,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
				h.assertCount(issues, 0)
			},
		},
		{
			name: "single issue",
			issues: []*types.Issue{
				h.newIssue("", "Single issue", types.StatusOpen, 1, types.TypeTask, nil),
			},
			wantErr: false,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
				h.assertCount(issues, 1)
				h.assertIDSet(issues[0], 0)
				h.assertTimestampSet(issues[0].CreatedAt, "CreatedAt", 0)
				h.assertTimestampSet(issues[0].UpdatedAt, "UpdatedAt", 0)
			},
		},
		{
			name: "multiple issues",
			issues: []*types.Issue{
				h.newIssue("", "Issue 1", types.StatusOpen, 1, types.TypeTask, nil),
				h.newIssue("", "Issue 2", types.StatusInProgress, 2, types.TypeBug, nil),
				h.newIssue("", "Issue 3", types.StatusOpen, 3, types.TypeFeature, nil),
			},
			wantErr: false,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
				h.assertCount(issues, 3)
				for i, issue := range issues {
					h.assertIDSet(issue, i)
					h.assertTimestampSet(issue.CreatedAt, "CreatedAt", i)
					h.assertTimestampSet(issue.UpdatedAt, "UpdatedAt", i)
				}
				h.assertUniqueIDs(issues)
			},
		},
		{
		name: "mixed ID assignment - explicit and auto-generated",
		issues: []*types.Issue{
		h.newIssue("bd-100", "Custom ID 1", types.StatusOpen, 1, types.TypeTask, nil),
		h.newIssue("", "Auto ID", types.StatusOpen, 1, types.TypeTask, nil),
		h.newIssue("bd-200", "Custom ID 2", types.StatusOpen, 1, types.TypeTask, nil),
		},
		wantErr: false,
		checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
		h.assertCount(issues, 3)
		h.assertEqual("bd-100", issues[0].ID, "ID")
		if issues[1].ID == "" || issues[1].ID == "bd-100" || issues[1].ID == "bd-200" {
		t.Errorf("expected auto-generated ID, got %s", issues[1].ID)
		}
		h.assertEqual("bd-200", issues[2].ID, "ID")
		},
		},
		{
			name: "validation error - missing title",
			issues: []*types.Issue{
				h.newIssue("", "Valid issue", types.StatusOpen, 1, types.TypeTask, nil),
				h.newIssue("", "", types.StatusOpen, 1, types.TypeTask, nil),
			},
			wantErr: true,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
			name:    "validation error - invalid priority",
			issues:  []*types.Issue{h.newIssue("", "Test", types.StatusOpen, 10, types.TypeTask, nil)},
			wantErr: true,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
			name:    "validation error - invalid status",
			issues:  []*types.Issue{h.newIssue("", "Test", "invalid", 1, types.TypeTask, nil)},
			wantErr: true,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
		name: "duplicate ID error",
		issues: []*types.Issue{
		h.newIssue("bd-999", "First issue", types.StatusOpen, 1, types.TypeTask, nil),
		h.newIssue("bd-999", "Second issue", types.StatusOpen, 1, types.TypeTask, nil),
		},
		wantErr: true,
		checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
			name: "closed_at invariant - open status with closed_at",
			issues: []*types.Issue{
				h.newIssue("", "Invalid closed_at", types.StatusOpen, 1, types.TypeTask, &time.Time{}),
			},
			wantErr: true,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
			name: "closed_at invariant - closed status without closed_at auto-sets it (GH#523)",
			issues: []*types.Issue{
				h.newIssue("", "Missing closed_at", types.StatusClosed, 1, types.TypeTask, nil),
			},
			wantErr: false, // Defensive fix auto-sets closed_at instead of rejecting
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
				h.assertCount(issues, 1)
				h.assertEqual(types.StatusClosed, issues[0].Status, "status")
				if issues[0].ClosedAt == nil {
					t.Error("ClosedAt should be auto-set for closed issues (GH#523 defensive fix)")
				}
			},
		},
		{
			name: "nil item in batch",
			issues: []*types.Issue{
				h.newIssue("", "Valid issue", types.StatusOpen, 1, types.TypeTask, nil),
				nil,
			},
			wantErr: true,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {},
		},
		{
			name: "valid closed issue with closed_at",
			issues: []*types.Issue{
				h.newIssue("", "Properly closed", types.StatusClosed, 1, types.TypeTask, func() *time.Time { t := time.Now(); return &t }()),
			},
			wantErr: false,
			checkFunc: func(t *testing.T, h *createIssuesTestHelper, issues []*types.Issue) {
				h.assertCount(issues, 1)
				h.assertEqual(types.StatusClosed, issues[0].Status, "status")
				h.assertNotNil(issues[0].ClosedAt, "ClosedAt")
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := h.createIssues(tt.issues)
			if tt.wantErr {
				h.assertError(err)
				h.assertNoAutoGenID(tt.issues, tt.wantErr)
			} else {
				h.assertNoError(err)
			}
			if !tt.wantErr && tt.checkFunc != nil {
				tt.checkFunc(t, h, tt.issues)
			}
		})
	}
}

func TestCreateIssuesRollback(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	t.Run("rollback on validation error", func(t *testing.T) {
		// Create a valid issue first
		validIssue := &types.Issue{
			Title:     "Valid issue",
			Status:    types.StatusOpen,
			Priority:  1,
			IssueType: types.TypeTask,
		}
		err := store.CreateIssue(ctx, validIssue, "test-user")
		if err != nil {
			t.Fatalf("failed to create valid issue: %v", err)
		}

		// Try to create batch with one valid and one invalid issue
		issues := []*types.Issue{
			{
				Title:     "Another valid issue",
				Status:    types.StatusOpen,
				Priority:  1,
				IssueType: types.TypeTask,
			},
			{
				Status:    types.StatusOpen,
				Priority:  1,
				IssueType: types.TypeTask,
			},
		}

		err = store.CreateIssues(ctx, issues, "test-user")
		if err == nil {
			t.Fatal("expected error for invalid batch, got nil")
		}

		// Verify the "Another valid issue" was rolled back by searching all issues
		filter := types.IssueFilter{}
		allIssues, err := store.SearchIssues(ctx, "", filter)
		if err != nil {
			t.Fatalf("failed to search issues: %v", err)
		}

		// Should only have the first valid issue, not the rolled-back one
		if len(allIssues) != 1 {
			t.Errorf("expected 1 issue after rollback, got %d", len(allIssues))
		}

		if len(allIssues) > 0 && allIssues[0].ID != validIssue.ID {
			t.Errorf("expected only the first valid issue, got %s", allIssues[0].ID)
		}
	})

	// Note: "rollback on conflict with existing ID" test removed - CreateIssues
	// uses INSERT OR IGNORE which silently skips duplicates (needed for JSONL import)
}

func TestUpdateIssue(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	issue := &types.Issue{
		Title:     "Original",
		Status:    types.StatusOpen,
		Priority:  2,
		IssueType: types.TypeTask,
	}

	err := store.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	// Update the issue
	updates := map[string]interface{}{
		"title":    "Updated",
		"status":   string(types.StatusInProgress),
		"priority": 1,
		"assignee": "bob",
	}

	err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
	if err != nil {
		t.Fatalf("UpdateIssue failed: %v", err)
	}

	// Verify updates
	updated, err := store.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("GetIssue failed: %v", err)
	}

	if updated.Title != "Updated" {
		t.Errorf("Title not updated: got %v, want Updated", updated.Title)
	}

	if updated.Status != types.StatusInProgress {
		t.Errorf("Status not updated: got %v, want %v", updated.Status, types.StatusInProgress)
	}

	if updated.Priority != 1 {
		t.Errorf("Priority not updated: got %v, want 1", updated.Priority)
	}

	if updated.Assignee != "bob" {
		t.Errorf("Assignee not updated: got %v, want bob", updated.Assignee)
	}
}

func TestUpdateIssueValidation(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	issue := &types.Issue{
		Title:     "Test Issue",
		Status:    types.StatusOpen,
		Priority:  2,
		IssueType: types.TypeTask,
	}

	err := store.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	// Test invalid issue type
	updates := map[string]interface{}{
		"issue_type": "invalid-type",
	}
	err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
	if err == nil {
		t.Error("Expected error for invalid issue_type, got nil")
	}

	// Test negative estimated_minutes
	updates = map[string]interface{}{
		"estimated_minutes": -10,
	}
	err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
	if err == nil {
		t.Error("Expected error for negative estimated_minutes, got nil")
	}

	// Test valid issue type
	updates = map[string]interface{}{
		"issue_type": string(types.TypeBug),
	}
	err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
	if err != nil {
		t.Errorf("Valid issue_type should not error: %v", err)
	}

	// Test valid estimated_minutes
	updates = map[string]interface{}{
		"estimated_minutes": 60,
	}
	err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
	if err != nil {
		t.Errorf("Valid estimated_minutes should not error: %v", err)
	}
}

func TestCloseIssue(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	issue := &types.Issue{
		Title:     "Test",
		Status:    types.StatusOpen,
		Priority:  2,
		IssueType: types.TypeTask,
	}

	err := store.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
	if err != nil {
		t.Fatalf("CloseIssue failed: %v", err)
	}

	// Verify closure
	closed, err := store.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("GetIssue failed: %v", err)
	}

	if closed.Status != types.StatusClosed {
		t.Errorf("Status not closed: got %v, want %v", closed.Status, types.StatusClosed)
	}

	if closed.ClosedAt == nil {
		t.Error("ClosedAt should be set")
	}

	if closed.CloseReason != "Done" {
		t.Errorf("CloseReason not set: got %q, want %q", closed.CloseReason, "Done")
	}
}

func TestClosedAtInvariant(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	t.Run("UpdateIssue auto-sets closed_at when closing", func(t *testing.T) {
		issue := &types.Issue{
			Title:     "Test",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
		}
		err := store.CreateIssue(ctx, issue, "test-user")
		if err != nil {
			t.Fatalf("CreateIssue failed: %v", err)
		}

		// Update to closed without providing closed_at
		updates := map[string]interface{}{
			"status": string(types.StatusClosed),
		}
		err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
		if err != nil {
			t.Fatalf("UpdateIssue failed: %v", err)
		}

		// Verify closed_at was auto-set
		updated, err := store.GetIssue(ctx, issue.ID)
		if err != nil {
			t.Fatalf("GetIssue failed: %v", err)
		}
		if updated.Status != types.StatusClosed {
			t.Errorf("Status should be closed, got %v", updated.Status)
		}
		if updated.ClosedAt == nil {
			t.Error("ClosedAt should be auto-set when changing to closed status")
		}
	})

	t.Run("UpdateIssue clears closed_at when reopening", func(t *testing.T) {
		issue := &types.Issue{
			Title:     "Test",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
		}
		err := store.CreateIssue(ctx, issue, "test-user")
		if err != nil {
			t.Fatalf("CreateIssue failed: %v", err)
		}

		// Close the issue
		err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
		if err != nil {
			t.Fatalf("CloseIssue failed: %v", err)
		}

		// Verify it's closed with closed_at and close_reason set
		closed, err := store.GetIssue(ctx, issue.ID)
		if err != nil {
			t.Fatalf("GetIssue failed: %v", err)
		}
		if closed.ClosedAt == nil {
			t.Fatal("ClosedAt should be set after closing")
		}
		if closed.CloseReason != "Done" {
			t.Errorf("CloseReason should be 'Done', got %q", closed.CloseReason)
		}

		// Reopen the issue
		updates := map[string]interface{}{
			"status": string(types.StatusOpen),
		}
		err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
		if err != nil {
			t.Fatalf("UpdateIssue failed: %v", err)
		}

		// Verify closed_at and close_reason were cleared
		reopened, err := store.GetIssue(ctx, issue.ID)
		if err != nil {
			t.Fatalf("GetIssue failed: %v", err)
		}
		if reopened.Status != types.StatusOpen {
			t.Errorf("Status should be open, got %v", reopened.Status)
		}
		if reopened.ClosedAt != nil {
			t.Error("ClosedAt should be cleared when reopening issue")
		}
		if reopened.CloseReason != "" {
			t.Errorf("CloseReason should be cleared when reopening issue, got %q", reopened.CloseReason)
		}
	})

	t.Run("CreateIssue auto-sets closed_at for closed issue (GH#523)", func(t *testing.T) {
		issue := &types.Issue{
			Title:     "Test",
			Status:    types.StatusClosed,
			Priority:  2,
			IssueType: types.TypeTask,
			ClosedAt:  nil, // Defensive fix should auto-set this
		}
		err := store.CreateIssue(ctx, issue, "test-user")
		if err != nil {
			t.Errorf("CreateIssue should auto-set closed_at (GH#523 defensive fix), got error: %v", err)
		}
		if issue.ClosedAt == nil {
			t.Error("ClosedAt should be auto-set for closed issues (GH#523 defensive fix)")
		}
	})

	t.Run("CreateIssue rejects open issue with closed_at", func(t *testing.T) {
		now := time.Now()
		issue := &types.Issue{
			Title:     "Test",
			Status:    types.StatusOpen,
			Priority:  2,
			IssueType: types.TypeTask,
			ClosedAt:  &now, // Invalid: open with closed_at
		}
		err := store.CreateIssue(ctx, issue, "test-user")
		if err == nil {
			t.Error("CreateIssue should reject open issue with closed_at")
		}
	})
}

func TestSearchIssues(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create test issues
	issues := []*types.Issue{
		{Title: "Bug in login", Status: types.StatusOpen, Priority: 0, IssueType: types.TypeBug},
		{Title: "Feature request", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeFeature},
		{Title: "Another bug", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeBug},
	}

	for _, issue := range issues {
		err := store.CreateIssue(ctx, issue, "test-user")
		if err != nil {
			t.Fatalf("CreateIssue failed: %v", err)
		}
		// Close the third issue
		if issue.Title == "Another bug" {
			err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
			if err != nil {
				t.Fatalf("CloseIssue failed: %v", err)
			}
		}
	}

	// Test query search
	results, err := store.SearchIssues(ctx, "bug", types.IssueFilter{})
	if err != nil {
		t.Fatalf("SearchIssues failed: %v", err)
	}

	if len(results) != 2 {
		t.Errorf("Expected 2 results, got %d", len(results))
	}

	// Test status filter
	openStatus := types.StatusOpen
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{Status: &openStatus})
	if err != nil {
		t.Fatalf("SearchIssues failed: %v", err)
	}

	if len(results) != 2 {
		t.Errorf("Expected 2 open issues, got %d", len(results))
	}

	// Test type filter
	bugType := types.TypeBug
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{IssueType: &bugType})
	if err != nil {
		t.Fatalf("SearchIssues failed: %v", err)
	}

	if len(results) != 2 {
		t.Errorf("Expected 2 bugs, got %d", len(results))
	}

	// Test priority filter (P0)
	priority0 := 0
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{Priority: &priority0})
	if err != nil {
		t.Fatalf("SearchIssues failed: %v", err)
	}

	if len(results) != 1 {
		t.Errorf("Expected 1 P0 issue, got %d", len(results))
	}

	// Test label filtering (AND semantics)
	err = store.AddLabel(ctx, issues[0].ID, "backend", "test-user")
	if err != nil {
		t.Fatalf("AddLabel failed: %v", err)
	}
	err = store.AddLabel(ctx, issues[0].ID, "urgent", "test-user")
	if err != nil {
		t.Fatalf("AddLabel failed: %v", err)
	}
	err = store.AddLabel(ctx, issues[1].ID, "backend", "test-user")
	if err != nil {
		t.Fatalf("AddLabel failed: %v", err)
	}

	// Filter by single label
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{Labels: []string{"backend"}})
	if err != nil {
		t.Fatalf("SearchIssues with label filter failed: %v", err)
	}
	if len(results) != 2 {
		t.Errorf("Expected 2 issues with 'backend' label, got %d", len(results))
	}

	// Filter by multiple labels (AND semantics - must have ALL)
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{Labels: []string{"backend", "urgent"}})
	if err != nil {
		t.Fatalf("SearchIssues with multiple labels failed: %v", err)
	}
	if len(results) != 1 {
		t.Errorf("Expected 1 issue with both 'backend' AND 'urgent' labels, got %d", len(results))
	}

	// Test label filtering (OR semantics)
	err = store.AddLabel(ctx, issues[2].ID, "frontend", "test-user")
	if err != nil {
		t.Fatalf("AddLabel failed: %v", err)
	}

	results, err = store.SearchIssues(ctx, "", types.IssueFilter{LabelsAny: []string{"frontend", "urgent"}})
	if err != nil {
		t.Fatalf("SearchIssues with LabelsAny filter failed: %v", err)
	}
	if len(results) != 2 {
		t.Errorf("Expected 2 issues with 'frontend' OR 'urgent' labels, got %d", len(results))
	}

	// Test combined AND + OR filtering
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{
		Labels:    []string{"backend"},
		LabelsAny: []string{"urgent", "frontend"},
	})
	if err != nil {
		t.Fatalf("SearchIssues with combined Labels and LabelsAny failed: %v", err)
	}
	// Should return issue[0] (has backend AND urgent)
	// issue[1] has backend but not urgent/frontend, so excluded
	if len(results) != 1 {
		t.Errorf("Expected 1 issue with 'backend' AND ('urgent' OR 'frontend'), got %d", len(results))
	}
	if len(results) > 0 && results[0].ID != issues[0].ID {
		t.Errorf("Expected issue %s, got %s", issues[0].ID, results[0].ID)
	}

	// Test whitespace trimming in labels
	results, err = store.SearchIssues(ctx, "", types.IssueFilter{Labels: []string{" backend ", "  urgent  "}})
	if err != nil {
		t.Fatalf("SearchIssues with whitespace labels failed: %v", err)
	}
	// This won't match because storage layer doesn't trim - that's CLI's job
	// But let's verify the storage layer accepts it without error
	if len(results) != 0 {
		t.Logf("Note: Storage layer doesn't auto-trim labels (expected - trimming is CLI responsibility)")
	}
}

func TestGetStatistics(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Test statistics on empty database (regression test for NULL handling)
	stats, err := store.GetStatistics(ctx)
	if err != nil {
		t.Fatalf("GetStatistics failed on empty database: %v", err)
	}

	if stats.TotalIssues != 0 {
		t.Errorf("Expected 0 total issues, got %d", stats.TotalIssues)
	}
	if stats.OpenIssues != 0 {
		t.Errorf("Expected 0 open issues, got %d", stats.OpenIssues)
	}
	if stats.InProgressIssues != 0 {
		t.Errorf("Expected 0 in-progress issues, got %d", stats.InProgressIssues)
	}
	if stats.ClosedIssues != 0 {
		t.Errorf("Expected 0 closed issues, got %d", stats.ClosedIssues)
	}

	// Create some issues to verify statistics work with data
	issues := []*types.Issue{
		{Title: "Open task", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
		{Title: "In progress task", Status: types.StatusInProgress, Priority: 1, IssueType: types.TypeTask},
		{Title: "Closed task", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
		{Title: "Another open task", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
	}

	for _, issue := range issues {
		err := store.CreateIssue(ctx, issue, "test-user")
		if err != nil {
			t.Fatalf("CreateIssue failed: %v", err)
		}
		// Close the one that should be closed
		if issue.Title == "Closed task" {
			err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
			if err != nil {
				t.Fatalf("CloseIssue failed: %v", err)
			}
		}
	}

	// Get statistics with data
	stats, err = store.GetStatistics(ctx)
	if err != nil {
		t.Fatalf("GetStatistics failed with data: %v", err)
	}

	if stats.TotalIssues != 4 {
		t.Errorf("Expected 4 total issues, got %d", stats.TotalIssues)
	}
	if stats.OpenIssues != 2 {
		t.Errorf("Expected 2 open issues, got %d", stats.OpenIssues)
	}
	if stats.InProgressIssues != 1 {
		t.Errorf("Expected 1 in-progress issue, got %d", stats.InProgressIssues)
	}
	if stats.ClosedIssues != 1 {
		t.Errorf("Expected 1 closed issue, got %d", stats.ClosedIssues)
	}
	if stats.ReadyIssues != 2 {
		t.Errorf("Expected 2 ready issues (open with no blockers), got %d", stats.ReadyIssues)
	}
}

// Note: High-concurrency stress tests were removed as the pure Go SQLite driver
// (modernc.org/sqlite) can experience "database is locked" errors under extreme
// parallel load (100+ simultaneous operations). This is a known limitation and
// does not affect normal usage where WAL mode handles typical concurrent operations.
// For very high concurrency needs, consider using CGO-enabled sqlite3 driver or PostgreSQL.

// TestParallelIssueCreation verifies that parallel issue creation works correctly with hash IDs
// This is a regression test for bd-89 (GH-6). With hash-based IDs, parallel creation works
// naturally since each issue gets a unique random hash - no coordination needed.
func TestParallelIssueCreation(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	const numIssues = 20

	// Create issues in parallel using goroutines
	errors := make(chan error, numIssues)
	ids := make(chan string, numIssues)

	for i := 0; i < numIssues; i++ {
		go func() {
			issue := &types.Issue{
				Title:     "Parallel test issue",
				Status:    types.StatusOpen,
				Priority:  2,
				IssueType: types.TypeTask,
			}
			err := store.CreateIssue(ctx, issue, "test-user")
			if err != nil {
				errors <- err
				return
			}
			ids <- issue.ID
			errors <- nil
		}()
	}

	// Collect results
	var collectedIDs []string
	var failureCount int
	for i := 0; i < numIssues; i++ {
		if err := <-errors; err != nil {
			t.Errorf("CreateIssue failed in parallel test: %v", err)
			failureCount++
		}
	}

	close(ids)
	for id := range ids {
		collectedIDs = append(collectedIDs, id)
	}

	// Verify no failures occurred
	if failureCount > 0 {
		t.Fatalf("Expected 0 failures, got %d", failureCount)
	}

	// Verify we got the expected number of IDs
	if len(collectedIDs) != numIssues {
		t.Fatalf("Expected %d IDs, got %d", numIssues, len(collectedIDs))
	}

	// Verify all IDs are unique (no duplicates from race conditions)
	seen := make(map[string]bool)
	for _, id := range collectedIDs {
		if seen[id] {
			t.Errorf("Duplicate ID detected: %s", id)
		}
		seen[id] = true
	}

	// Verify all issues can be retrieved (they actually exist in the database)
	for _, id := range collectedIDs {
		issue, err := store.GetIssue(ctx, id)
		if err != nil {
			t.Errorf("Failed to retrieve issue %s: %v", id, err)
		}
		if issue == nil {
			t.Errorf("Issue %s not found in database", id)
		}
	}
}

func TestSetAndGetMetadata(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Set metadata
	err := store.SetMetadata(ctx, "import_hash", "abc123def456")
	if err != nil {
		t.Fatalf("SetMetadata failed: %v", err)
	}

	// Get metadata
	value, err := store.GetMetadata(ctx, "import_hash")
	if err != nil {
		t.Fatalf("GetMetadata failed: %v", err)
	}

	if value != "abc123def456" {
		t.Errorf("Expected 'abc123def456', got '%s'", value)
	}
}

func TestGetMetadataNotFound(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Get non-existent metadata
	value, err := store.GetMetadata(ctx, "nonexistent")
	if err != nil {
		t.Fatalf("GetMetadata failed: %v", err)
	}

	if value != "" {
		t.Errorf("Expected empty string for non-existent key, got '%s'", value)
	}
}

func TestSetMetadataUpdate(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Set initial value
	err := store.SetMetadata(ctx, "test_key", "initial_value")
	if err != nil {
		t.Fatalf("SetMetadata failed: %v", err)
	}

	// Update value
	err = store.SetMetadata(ctx, "test_key", "updated_value")
	if err != nil {
		t.Fatalf("SetMetadata update failed: %v", err)
	}

	// Verify updated value
	value, err := store.GetMetadata(ctx, "test_key")
	if err != nil {
		t.Fatalf("GetMetadata failed: %v", err)
	}

	if value != "updated_value" {
		t.Errorf("Expected 'updated_value', got '%s'", value)
	}
}

func TestMetadataMultipleKeys(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Set multiple metadata keys
	keys := map[string]string{
		"key1": "value1",
		"key2": "value2",
		"key3": "value3",
	}

	for key, value := range keys {
		err := store.SetMetadata(ctx, key, value)
		if err != nil {
			t.Fatalf("SetMetadata failed for %s: %v", key, err)
		}
	}

	// Verify all keys
	for key, expectedValue := range keys {
		value, err := store.GetMetadata(ctx, key)
		if err != nil {
			t.Fatalf("GetMetadata failed for %s: %v", key, err)
		}
		if value != expectedValue {
			t.Errorf("For key %s, expected '%s', got '%s'", key, expectedValue, value)
		}
	}
}

func TestPath(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "beads-test-path-*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	// Test with relative path
	relPath := filepath.Join(tmpDir, "test.db")
	ctx := context.Background()

	store, err := New(ctx, relPath)
	if err != nil {
		t.Fatalf("failed to create storage: %v", err)
	}
	defer store.Close()

	// Path() should return absolute path
	path := store.Path()
	if !filepath.IsAbs(path) {
		t.Errorf("Path() should return absolute path, got: %s", path)
	}

	// Path should match the temp directory
	expectedPath, _ := filepath.Abs(relPath)
	if path != expectedPath {
		t.Errorf("Path() returned %s, expected %s", path, expectedPath)
	}
}

func TestMultipleStorageDistinctPaths(t *testing.T) {
	tmpDir1, err := os.MkdirTemp("", "beads-test-path1-*")
	if err != nil {
		t.Fatalf("failed to create temp dir 1: %v", err)
	}
	defer os.RemoveAll(tmpDir1)

	tmpDir2, err := os.MkdirTemp("", "beads-test-path2-*")
	if err != nil {
		t.Fatalf("failed to create temp dir 2: %v", err)
	}
	defer os.RemoveAll(tmpDir2)

	ctx := context.Background()
	store1, err := New(ctx, filepath.Join(tmpDir1, "db1.db"))
	if err != nil {
		t.Fatalf("failed to create storage 1: %v", err)
	}
	defer store1.Close()

	store2, err := New(ctx, filepath.Join(tmpDir2, "db2.db"))
	if err != nil {
		t.Fatalf("failed to create storage 2: %v", err)
	}
	defer store2.Close()

	// Paths should be distinct
	path1 := store1.Path()
	path2 := store2.Path()

	if path1 == path2 {
		t.Errorf("Multiple storage instances should have distinct paths, both returned: %s", path1)
	}

	// Both should be absolute
	if !filepath.IsAbs(path1) || !filepath.IsAbs(path2) {
		t.Errorf("Both paths should be absolute: path1=%s, path2=%s", path1, path2)
	}
}

func TestInMemoryDatabase(t *testing.T) {
	ctx := context.Background()

	// Test that :memory: database works
	ctx = context.Background()

	store, err := New(ctx, ":memory:")
	if err != nil {
		t.Fatalf("failed to create in-memory storage: %v", err)
	}
	defer store.Close()

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

	// Verify we can create and retrieve an issue
	issue := &types.Issue{
		Title:       "Test in-memory issue",
		Description: "Testing :memory: database",
		Status:      types.StatusOpen,
		Priority:    1,
		IssueType:   types.TypeTask,
	}

	err = store.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed in memory database: %v", err)
	}

	// Retrieve the issue
	retrieved, err := store.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("GetIssue failed in memory database: %v", err)
	}

	if retrieved == nil {
		t.Fatal("GetIssue returned nil for in-memory issue")
	}

	if retrieved.Title != issue.Title {
		t.Errorf("Title mismatch: got %v, want %v", retrieved.Title, issue.Title)
	}

	if retrieved.Description != issue.Description {
		t.Errorf("Description mismatch: got %v, want %v", retrieved.Description, issue.Description)
	}
}

func TestInMemorySharedCache(t *testing.T) {
	t.Skip("Multiple separate New(\":memory:\") calls create independent databases - this is expected SQLite behavior")
	ctx := context.Background()

	// Create first connection
	ctx = context.Background()

	store1, err := New(ctx, ":memory:")
	if err != nil {
		t.Fatalf("failed to create first in-memory storage: %v", err)
	}
	defer store1.Close()

	// Set issue_prefix (bd-166)
	if err := store1.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
		t.Fatalf("failed to set issue_prefix: %v", err)
	}

	// Create an issue in the first connection
	issue := &types.Issue{
		Title:       "Shared memory test",
		Description: "Testing shared cache behavior",
		Status:      types.StatusOpen,
		Priority:    2,
		IssueType:   types.TypeBug,
	}

	err = store1.CreateIssue(ctx, issue, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed: %v", err)
	}

	// Create second connection - Note: this creates a SEPARATE database
	// Shared cache only works within a single sql.DB connection pool
	ctx = context.Background()

	store2, err := New(ctx, ":memory:")
	if err != nil {
		t.Fatalf("failed to create second in-memory storage: %v", err)
	}
	defer store2.Close()

	// Retrieve the issue from the second connection
	retrieved, err := store2.GetIssue(ctx, issue.ID)
	if err != nil {
		t.Fatalf("GetIssue failed from second connection: %v", err)
	}

	if retrieved == nil {
		t.Fatal("Shared memory cache not working: second connection can't see first connection's data")
	}

	if retrieved.Title != issue.Title {
		t.Errorf("Title mismatch: got %v, want %v", retrieved.Title, issue.Title)
	}

	// Verify both connections can see each other's changes
	issue2 := &types.Issue{
		Title:     "Second issue",
		Status:    types.StatusOpen,
		Priority:  1,
		IssueType: types.TypeTask,
	}

	err = store2.CreateIssue(ctx, issue2, "test-user")
	if err != nil {
		t.Fatalf("CreateIssue failed in second connection: %v", err)
	}

	// First connection should see the issue created by second connection
	retrieved2, err := store1.GetIssue(ctx, issue2.ID)
	if err != nil {
		t.Fatalf("GetIssue failed from first connection: %v", err)
	}

	if retrieved2 == nil {
		t.Fatal("First connection can't see second connection's data")
	}

	if retrieved2.Title != issue2.Title {
		t.Errorf("Title mismatch: got %v, want %v", retrieved2.Title, issue2.Title)
	}
}

func TestGetAllConfig(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Set multiple config values
	err := store.SetConfig(ctx, "key1", "value1")
	if err != nil {
		t.Fatalf("SetConfig key1 failed: %v", err)
	}

	err = store.SetConfig(ctx, "key2", "value2")
	if err != nil {
		t.Fatalf("SetConfig key2 failed: %v", err)
	}

	// Get all config
	allConfig, err := store.GetAllConfig(ctx)
	if err != nil {
		t.Fatalf("GetAllConfig failed: %v", err)
	}

	if len(allConfig) < 2 {
		t.Errorf("Expected at least 2 config entries, got %d", len(allConfig))
	}

	if allConfig["key1"] != "value1" {
		t.Errorf("Expected key1=value1, got %s", allConfig["key1"])
	}

	if allConfig["key2"] != "value2" {
		t.Errorf("Expected key2=value2, got %s", allConfig["key2"])
	}
}

func TestDeleteConfig(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()

	// Set a config value
	err := store.SetConfig(ctx, "test-key", "test-value")
	if err != nil {
		t.Fatalf("SetConfig failed: %v", err)
	}

	// Verify it exists
	value, err := store.GetConfig(ctx, "test-key")
	if err != nil {
		t.Fatalf("GetConfig failed: %v", err)
	}
	if value != "test-value" {
		t.Errorf("Expected test-value, got %s", value)
	}

	// Delete it
	err = store.DeleteConfig(ctx, "test-key")
	if err != nil {
		t.Fatalf("DeleteConfig failed: %v", err)
	}

	// Verify it's gone
	value, err = store.GetConfig(ctx, "test-key")
	if err != nil {
		t.Fatalf("GetConfig failed: %v", err)
	}
	if value != "" {
		t.Errorf("Expected empty value after deletion, got: %s", value)
	}
}

func TestIsClosed(t *testing.T) {
	store, cleanup := setupTestDB(t)
	defer cleanup()

	// Store should not be closed initially
	if store.IsClosed() {
		t.Error("Store should not be closed initially")
	}

	// Close the store
	err := store.Close()
	if err != nil {
		t.Fatalf("Close failed: %v", err)
	}

	// Store should be closed now
	if !store.IsClosed() {
		t.Error("Store should be closed after calling Close()")
	}
}
