package sqlite

import (
	"context"
	"testing"

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

// testEnv provides a test environment with common setup and helpers.
// Use newTestEnv(t) to create a test environment with automatic cleanup.
type testEnv struct {
	t     *testing.T
	Store *SQLiteStorage
	Ctx   context.Context
}

// newTestEnv creates a new test environment with a configured store.
// The store is automatically cleaned up when the test completes.
func newTestEnv(t *testing.T) *testEnv {
	t.Helper()
	store := newTestStore(t, "")
	return &testEnv{
		t:     t,
		Store: store,
		Ctx:   context.Background(),
	}
}

// CreateIssue creates a test issue with the given title and defaults.
// Returns the created issue with ID populated.
func (e *testEnv) CreateIssue(title string) *types.Issue {
	e.t.Helper()
	return e.CreateIssueWith(title, types.StatusOpen, 2, types.TypeTask)
}

// CreateIssueWith creates a test issue with specified attributes.
func (e *testEnv) CreateIssueWith(title string, status types.Status, priority int, issueType types.IssueType) *types.Issue {
	e.t.Helper()
	issue := &types.Issue{
		Title:     title,
		Status:    status,
		Priority:  priority,
		IssueType: issueType,
	}
	if err := e.Store.CreateIssue(e.Ctx, issue, "test-user"); err != nil {
		e.t.Fatalf("CreateIssue(%q) failed: %v", title, err)
	}
	return issue
}

// CreateIssueWithAssignee creates a test issue with an assignee.
func (e *testEnv) CreateIssueWithAssignee(title, assignee string) *types.Issue {
	e.t.Helper()
	issue := &types.Issue{
		Title:     title,
		Status:    types.StatusOpen,
		Priority:  2,
		IssueType: types.TypeTask,
		Assignee:  assignee,
	}
	if err := e.Store.CreateIssue(e.Ctx, issue, "test-user"); err != nil {
		e.t.Fatalf("CreateIssue(%q) failed: %v", title, err)
	}
	return issue
}

// CreateEpic creates an epic issue.
func (e *testEnv) CreateEpic(title string) *types.Issue {
	e.t.Helper()
	return e.CreateIssueWith(title, types.StatusOpen, 1, types.TypeEpic)
}

// CreateBug creates a bug issue.
func (e *testEnv) CreateBug(title string, priority int) *types.Issue {
	e.t.Helper()
	return e.CreateIssueWith(title, types.StatusOpen, priority, types.TypeBug)
}

// AddDep adds a blocking dependency (issue depends on dependsOn).
func (e *testEnv) AddDep(issue, dependsOn *types.Issue) {
	e.t.Helper()
	e.AddDepType(issue, dependsOn, types.DepBlocks)
}

// AddDepType adds a dependency with the specified type.
func (e *testEnv) AddDepType(issue, dependsOn *types.Issue, depType types.DependencyType) {
	e.t.Helper()
	dep := &types.Dependency{
		IssueID:     issue.ID,
		DependsOnID: dependsOn.ID,
		Type:        depType,
	}
	if err := e.Store.AddDependency(e.Ctx, dep, "test-user"); err != nil {
		e.t.Fatalf("AddDependency(%s -> %s) failed: %v", issue.ID, dependsOn.ID, err)
	}
}

// AddParentChild adds a parent-child dependency (child belongs to parent).
func (e *testEnv) AddParentChild(child, parent *types.Issue) {
	e.t.Helper()
	e.AddDepType(child, parent, types.DepParentChild)
}

// Close closes the issue with the given reason.
func (e *testEnv) Close(issue *types.Issue, reason string) {
	e.t.Helper()
	if err := e.Store.CloseIssue(e.Ctx, issue.ID, reason, "test-user"); err != nil {
		e.t.Fatalf("CloseIssue(%s) failed: %v", issue.ID, err)
	}
}

// GetReadyWork gets ready work with the given filter.
func (e *testEnv) GetReadyWork(filter types.WorkFilter) []*types.Issue {
	e.t.Helper()
	ready, err := e.Store.GetReadyWork(e.Ctx, filter)
	if err != nil {
		e.t.Fatalf("GetReadyWork failed: %v", err)
	}
	return ready
}

// GetReadyIDs returns a map of issue IDs that are ready (open status).
func (e *testEnv) GetReadyIDs() map[string]bool {
	e.t.Helper()
	ready := e.GetReadyWork(types.WorkFilter{Status: types.StatusOpen})
	ids := make(map[string]bool)
	for _, issue := range ready {
		ids[issue.ID] = true
	}
	return ids
}

// AssertReady asserts that the issue is in the ready work list.
func (e *testEnv) AssertReady(issue *types.Issue) {
	e.t.Helper()
	ids := e.GetReadyIDs()
	if !ids[issue.ID] {
		e.t.Errorf("expected %s (%s) to be ready, but it was blocked", issue.ID, issue.Title)
	}
}

// AssertBlocked asserts that the issue is NOT in the ready work list.
func (e *testEnv) AssertBlocked(issue *types.Issue) {
	e.t.Helper()
	ids := e.GetReadyIDs()
	if ids[issue.ID] {
		e.t.Errorf("expected %s (%s) to be blocked, but it was ready", issue.ID, issue.Title)
	}
}

// newTestStore creates a SQLiteStorage with issue_prefix configured (bd-166)
// This prevents "database not initialized" errors in tests
//
// Test Isolation Pattern (bd-2e80):
// By default, uses "file::memory:?mode=memory&cache=private" for proper test isolation.
// The standard ":memory:" creates a SHARED database across all tests in the same process,
// which can cause test interference and flaky behavior. The private mode ensures each
// test gets its own isolated in-memory database.
//
// To override (e.g., for file-based tests), pass a custom dbPath:
//   - For temp files: t.TempDir()+"/test.db"
//   - For shared memory (not recommended): ":memory:"
func newTestStore(t *testing.T, dbPath string) *SQLiteStorage {
	t.Helper()

	// Default to temp file for test isolation
	// File-based databases are more reliable than in-memory for connection pool scenarios
	if dbPath == "" {
		dbPath = t.TempDir() + "/test.db"
	}

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

	t.Cleanup(func() {
		if cerr := store.Close(); cerr != nil {
			t.Fatalf("Failed to close test database: %v", cerr)
		}
	})

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

	return store
}
