// dual_mode_test.go - Test framework for ensuring commands work in both daemon and direct modes.
//
// PROBLEM:
// Multiple bugs have occurred where commands work in one mode but not the other:
// - GH#751: bd graph accessed nil store in daemon mode
// - GH#719: bd create -f bypassed daemon RPC
// - bd-fu83: relate/duplicate used direct store when daemon was running
//
// SOLUTION:
// This file provides a reusable test pattern that runs the same test logic
// in both direct mode (--no-daemon) and daemon mode, ensuring commands
// behave identically regardless of which mode they're running in.
//
// USAGE:
//
//	func TestCreateCommand(t *testing.T) {
//	    RunDualModeTest(t, "create basic issue", func(t *testing.T, env *DualModeTestEnv) {
//	        // Create an issue - this code runs twice: once in direct mode, once with daemon
//	        issue := &types.Issue{
//	            Title:     "Test issue",
//	            IssueType: types.TypeTask,
//	            Status:    types.StatusOpen,
//	            Priority:  2,
//	        }
//	        err := env.CreateIssue(issue)
//	        if err != nil {
//	            t.Fatalf("CreateIssue failed: %v", err)
//	        }
//
//	        // Verify issue was created
//	        got, err := env.GetIssue(issue.ID)
//	        if err != nil {
//	            t.Fatalf("GetIssue failed: %v", err)
//	        }
//	        if got.Title != "Test issue" {
//	            t.Errorf("expected title 'Test issue', got %q", got.Title)
//	        }
//	    })
//	}
//
// The test framework handles:
// - Setting up isolated test environments (temp dirs, databases)
// - Starting/stopping daemon for daemon mode tests
// - Saving/restoring global state between runs
// - Providing a unified API for common operations

//go:build integration
// +build integration

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"testing"
	"time"

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

// TestMode indicates which mode the test is running in
type TestMode string

const (
	// DirectMode: Commands access SQLite directly (--no-daemon)
	DirectMode TestMode = "direct"
	// DaemonMode: Commands communicate via RPC to a background daemon
	DaemonMode TestMode = "daemon"
)

// DualModeTestEnv provides a unified test environment that works in both modes.
// Tests should use this interface rather than accessing global state directly.
type DualModeTestEnv struct {
	t          *testing.T
	mode       TestMode
	tmpDir     string
	beadsDir   string
	dbPath     string
	socketPath string

	// Direct mode resources
	store *sqlite.SQLiteStorage

	// Daemon mode resources
	client     *rpc.Client
	server     *rpc.Server
	serverDone chan error

	// Context for operations
	ctx    context.Context
	cancel context.CancelFunc
}

// Mode returns the current test mode (direct or daemon)
func (e *DualModeTestEnv) Mode() TestMode {
	return e.mode
}

// Context returns the test context
func (e *DualModeTestEnv) Context() context.Context {
	return e.ctx
}

// Store returns the direct store (only valid in DirectMode)
// For mode-agnostic operations, use the helper methods instead.
func (e *DualModeTestEnv) Store() *sqlite.SQLiteStorage {
	if e.mode != DirectMode {
		e.t.Fatal("Store() called in daemon mode - use helper methods instead")
	}
	return e.store
}

// Client returns the RPC client (only valid in DaemonMode)
// For mode-agnostic operations, use the helper methods instead.
func (e *DualModeTestEnv) Client() *rpc.Client {
	if e.mode != DaemonMode {
		e.t.Fatal("Client() called in direct mode - use helper methods instead")
	}
	return e.client
}

// CreateIssue creates an issue in either mode
func (e *DualModeTestEnv) CreateIssue(issue *types.Issue) error {
	if e.mode == DirectMode {
		return e.store.CreateIssue(e.ctx, issue, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.CreateArgs{
		Title:       issue.Title,
		Description: issue.Description,
		IssueType:   string(issue.IssueType),
		Priority:    issue.Priority,
	}
	resp, err := e.client.Create(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("create failed: %s", resp.Error)
	}

	// Parse response to get the created issue ID
	// The RPC response contains the created issue as JSON
	var createdIssue types.Issue
	if err := json.Unmarshal(resp.Data, &createdIssue); err != nil {
		return fmt.Errorf("failed to parse created issue: %w", err)
	}
	issue.ID = createdIssue.ID
	return nil
}

// GetIssue retrieves an issue by ID in either mode
func (e *DualModeTestEnv) GetIssue(id string) (*types.Issue, error) {
	if e.mode == DirectMode {
		return e.store.GetIssue(e.ctx, id)
	}

	// Daemon mode: use RPC
	args := &rpc.ShowArgs{ID: id}
	resp, err := e.client.Show(args)
	if err != nil {
		return nil, err
	}
	if !resp.Success {
		return nil, fmt.Errorf("show failed: %s", resp.Error)
	}

	var issue types.Issue
	if err := json.Unmarshal(resp.Data, &issue); err != nil {
		return nil, fmt.Errorf("failed to parse issue: %w", err)
	}
	return &issue, nil
}

// UpdateIssue updates an issue in either mode
func (e *DualModeTestEnv) UpdateIssue(id string, updates map[string]interface{}) error {
	if e.mode == DirectMode {
		return e.store.UpdateIssue(e.ctx, id, updates, "test")
	}

	// Daemon mode: use RPC - convert map to UpdateArgs fields
	args := &rpc.UpdateArgs{ID: id}

	// Map common fields to their RPC counterparts
	if title, ok := updates["title"].(string); ok {
		args.Title = &title
	}
	if status, ok := updates["status"].(types.Status); ok {
		s := string(status)
		args.Status = &s
	}
	if statusStr, ok := updates["status"].(string); ok {
		args.Status = &statusStr
	}
	if priority, ok := updates["priority"].(int); ok {
		args.Priority = &priority
	}
	if desc, ok := updates["description"].(string); ok {
		args.Description = &desc
	}

	resp, err := e.client.Update(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("update failed: %s", resp.Error)
	}
	return nil
}

// DeleteIssue marks an issue as deleted (tombstoned) in either mode
func (e *DualModeTestEnv) DeleteIssue(id string, force bool) error {
	if e.mode == DirectMode {
		updates := map[string]interface{}{
			"status": types.StatusTombstone,
		}
		return e.store.UpdateIssue(e.ctx, id, updates, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.DeleteArgs{
		IDs:    []string{id},
		Force:  force,
		DryRun: false,
		Reason: "test deletion",
	}
	resp, err := e.client.Delete(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("delete failed: %s", resp.Error)
	}
	return nil
}

// AddDependency adds a dependency in either mode
func (e *DualModeTestEnv) AddDependency(issueID, dependsOnID string, depType types.DependencyType) error {
	if e.mode == DirectMode {
		dep := &types.Dependency{
			IssueID:     issueID,
			DependsOnID: dependsOnID,
			Type:        depType,
		}
		return e.store.AddDependency(e.ctx, dep, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.DepAddArgs{
		FromID:  issueID,
		ToID:    dependsOnID,
		DepType: string(depType),
	}
	resp, err := e.client.AddDependency(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("add dependency failed: %s", resp.Error)
	}
	return nil
}

// ListIssues returns issues matching the filter in either mode
func (e *DualModeTestEnv) ListIssues(filter types.IssueFilter) ([]*types.Issue, error) {
	if e.mode == DirectMode {
		return e.store.SearchIssues(e.ctx, "", filter)
	}

	// Daemon mode: use RPC - convert filter to ListArgs
	args := &rpc.ListArgs{}
	if filter.Status != nil {
		args.Status = string(*filter.Status)
	}
	if filter.Priority != nil {
		args.Priority = filter.Priority
	}
	if filter.IssueType != nil {
		args.IssueType = string(*filter.IssueType)
	}

	resp, err := e.client.List(args)
	if err != nil {
		return nil, err
	}
	if !resp.Success {
		return nil, fmt.Errorf("list failed: %s", resp.Error)
	}

	var issues []*types.Issue
	if err := json.Unmarshal(resp.Data, &issues); err != nil {
		return nil, fmt.Errorf("failed to parse issues: %w", err)
	}
	return issues, nil
}

// GetReadyWork returns issues ready for work in either mode
func (e *DualModeTestEnv) GetReadyWork() ([]*types.Issue, error) {
	if e.mode == DirectMode {
		return e.store.GetReadyWork(e.ctx, types.WorkFilter{})
	}

	// Daemon mode: use RPC
	args := &rpc.ReadyArgs{}
	resp, err := e.client.Ready(args)
	if err != nil {
		return nil, err
	}
	if !resp.Success {
		return nil, fmt.Errorf("ready failed: %s", resp.Error)
	}

	var issues []*types.Issue
	if err := json.Unmarshal(resp.Data, &issues); err != nil {
		return nil, fmt.Errorf("failed to parse issues: %w", err)
	}
	return issues, nil
}

// AddLabel adds a label to an issue in either mode
func (e *DualModeTestEnv) AddLabel(issueID, label string) error {
	if e.mode == DirectMode {
		return e.store.AddLabel(e.ctx, issueID, label, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.LabelAddArgs{
		ID:    issueID,
		Label: label,
	}
	resp, err := e.client.AddLabel(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("add label failed: %s", resp.Error)
	}
	return nil
}

// RemoveLabel removes a label from an issue in either mode
func (e *DualModeTestEnv) RemoveLabel(issueID, label string) error {
	if e.mode == DirectMode {
		return e.store.RemoveLabel(e.ctx, issueID, label, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.LabelRemoveArgs{
		ID:    issueID,
		Label: label,
	}
	resp, err := e.client.RemoveLabel(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("remove label failed: %s", resp.Error)
	}
	return nil
}

// AddComment adds a comment to an issue in either mode
func (e *DualModeTestEnv) AddComment(issueID, text string) error {
	if e.mode == DirectMode {
		return e.store.AddComment(e.ctx, issueID, "test", text)
	}

	// Daemon mode: use RPC
	args := &rpc.CommentAddArgs{
		ID:     issueID,
		Author: "test",
		Text:   text,
	}
	resp, err := e.client.AddComment(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("add comment failed: %s", resp.Error)
	}
	return nil
}

// CloseIssue closes an issue with a reason in either mode
func (e *DualModeTestEnv) CloseIssue(id, reason string) error {
	if e.mode == DirectMode {
		updates := map[string]interface{}{
			"status":       types.StatusClosed,
			"close_reason": reason,
		}
		return e.store.UpdateIssue(e.ctx, id, updates, "test")
	}

	// Daemon mode: use RPC
	args := &rpc.CloseArgs{
		ID:     id,
		Reason: reason,
	}
	resp, err := e.client.CloseIssue(args)
	if err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("close failed: %s", resp.Error)
	}
	return nil
}

// TmpDir returns the test temporary directory
func (e *DualModeTestEnv) TmpDir() string {
	return e.tmpDir
}

// BeadsDir returns the .beads directory path
func (e *DualModeTestEnv) BeadsDir() string {
	return e.beadsDir
}

// DBPath returns the database file path
func (e *DualModeTestEnv) DBPath() string {
	return e.dbPath
}

// DualModeTestFunc is the function signature for tests that run in both modes
type DualModeTestFunc func(t *testing.T, env *DualModeTestEnv)

// RunDualModeTest runs a test function in both direct mode and daemon mode.
// This ensures the tested behavior works correctly regardless of which mode
// the CLI is operating in.
func RunDualModeTest(t *testing.T, name string, testFn DualModeTestFunc) {
	t.Helper()

	// Run in direct mode
	t.Run(name+"_direct", func(t *testing.T) {
		if testing.Short() {
			t.Skip("skipping dual-mode test in short mode")
		}
		env := setupDirectModeEnv(t)
		testFn(t, env)
	})

	// Run in daemon mode
	t.Run(name+"_daemon", func(t *testing.T) {
		if testing.Short() {
			t.Skip("skipping dual-mode test in short mode")
		}
		env := setupDaemonModeEnv(t)
		testFn(t, env)
	})
}

// RunDirectModeOnly runs a test only in direct mode.
// Use sparingly - prefer RunDualModeTest for most tests.
func RunDirectModeOnly(t *testing.T, name string, testFn DualModeTestFunc) {
	t.Helper()
	t.Run(name, func(t *testing.T) {
		env := setupDirectModeEnv(t)
		testFn(t, env)
	})
}

// RunDaemonModeOnly runs a test only in daemon mode.
// Use sparingly - prefer RunDualModeTest for most tests.
func RunDaemonModeOnly(t *testing.T, name string, testFn DualModeTestFunc) {
	t.Helper()
	t.Run(name, func(t *testing.T) {
		if testing.Short() {
			t.Skip("skipping daemon test in short mode")
		}
		env := setupDaemonModeEnv(t)
		testFn(t, env)
	})
}

// setupDirectModeEnv creates a test environment for direct mode testing
func setupDirectModeEnv(t *testing.T) *DualModeTestEnv {
	t.Helper()

	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0755); err != nil {
		t.Fatalf("failed to create .beads dir: %v", err)
	}

	dbPath := filepath.Join(beadsDir, "beads.db")
	store := newTestStore(t, dbPath)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	t.Cleanup(cancel)

	env := &DualModeTestEnv{
		t:        t,
		mode:     DirectMode,
		tmpDir:   tmpDir,
		beadsDir: beadsDir,
		dbPath:   dbPath,
		store:    store,
		ctx:      ctx,
		cancel:   cancel,
	}

	return env
}

// setupDaemonModeEnv creates a test environment with a running daemon
func setupDaemonModeEnv(t *testing.T) *DualModeTestEnv {
	t.Helper()

	tmpDir := makeSocketTempDir(t)
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0755); err != nil {
		t.Fatalf("failed to create .beads dir: %v", err)
	}

	// Initialize git repo (required for daemon)
	initTestGitRepo(t, tmpDir)

	dbPath := filepath.Join(beadsDir, "beads.db")
	socketPath := filepath.Join(beadsDir, "bd.sock")
	store := newTestStore(t, dbPath)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

	// Create daemon logger
	log := daemonLogger{logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo}))}

	// Start RPC server
	server, serverErrChan, err := startRPCServer(ctx, socketPath, store, tmpDir, dbPath, log)
	if err != nil {
		cancel()
		t.Fatalf("failed to start RPC server: %v", err)
	}

	// Wait for server to be ready
	select {
	case <-server.WaitReady():
		// Server is ready
	case <-time.After(5 * time.Second):
		cancel()
		t.Fatal("server did not become ready within 5 seconds")
	}

	// Connect RPC client
	client, err := rpc.TryConnect(socketPath)
	if err != nil || client == nil {
		cancel()
		server.Stop()
		t.Fatalf("failed to connect RPC client: %v", err)
	}

	// Consume server errors in background
	serverDone := make(chan error, 1)
	go func() {
		select {
		case err := <-serverErrChan:
			serverDone <- err
		case <-ctx.Done():
			serverDone <- ctx.Err()
		}
	}()

	env := &DualModeTestEnv{
		t:          t,
		mode:       DaemonMode,
		tmpDir:     tmpDir,
		beadsDir:   beadsDir,
		dbPath:     dbPath,
		socketPath: socketPath,
		store:      store,
		client:     client,
		server:     server,
		serverDone: serverDone,
		ctx:        ctx,
		cancel:     cancel,
	}

	// Register cleanup
	t.Cleanup(func() {
		if client != nil {
			client.Close()
		}
		if server != nil {
			server.Stop()
		}
		cancel()
		os.RemoveAll(tmpDir)
	})

	return env
}

// ============================================================================
// Example dual-mode tests demonstrating the pattern
// ============================================================================

// TestDualMode_CreateAndRetrieveIssue demonstrates the basic dual-mode test pattern
func TestDualMode_CreateAndRetrieveIssue(t *testing.T) {
	RunDualModeTest(t, "create_and_retrieve", func(t *testing.T, env *DualModeTestEnv) {
		// This code runs twice: once in direct mode, once with daemon
		issue := &types.Issue{
			Title:       "Test issue",
			Description: "Test description",
			IssueType:   types.TypeTask,
			Status:      types.StatusOpen,
			Priority:    2,
		}

		// Create issue
		if err := env.CreateIssue(issue); err != nil {
			t.Fatalf("[%s] CreateIssue failed: %v", env.Mode(), err)
		}

		if issue.ID == "" {
			t.Fatalf("[%s] issue ID not set after creation", env.Mode())
		}

		// Retrieve issue
		got, err := env.GetIssue(issue.ID)
		if err != nil {
			t.Fatalf("[%s] GetIssue failed: %v", env.Mode(), err)
		}

		// Verify
		if got.Title != "Test issue" {
			t.Errorf("[%s] expected title 'Test issue', got %q", env.Mode(), got.Title)
		}
		if got.Status != types.StatusOpen {
			t.Errorf("[%s] expected status 'open', got %q", env.Mode(), got.Status)
		}
	})
}

// TestDualMode_UpdateIssue tests updating issues works in both modes
func TestDualMode_UpdateIssue(t *testing.T) {
	RunDualModeTest(t, "update_issue", func(t *testing.T, env *DualModeTestEnv) {
		// Create issue
		issue := &types.Issue{
			Title:     "Original title",
			IssueType: types.TypeTask,
			Status:    types.StatusOpen,
			Priority:  2,
		}
		if err := env.CreateIssue(issue); err != nil {
			t.Fatalf("[%s] CreateIssue failed: %v", env.Mode(), err)
		}

		// Update issue
		updates := map[string]interface{}{
			"title":  "Updated title",
			"status": types.StatusInProgress,
		}
		if err := env.UpdateIssue(issue.ID, updates); err != nil {
			t.Fatalf("[%s] UpdateIssue failed: %v", env.Mode(), err)
		}

		// Verify update
		got, err := env.GetIssue(issue.ID)
		if err != nil {
			t.Fatalf("[%s] GetIssue failed: %v", env.Mode(), err)
		}

		if got.Title != "Updated title" {
			t.Errorf("[%s] expected title 'Updated title', got %q", env.Mode(), got.Title)
		}
		if got.Status != types.StatusInProgress {
			t.Errorf("[%s] expected status 'in_progress', got %q", env.Mode(), got.Status)
		}
	})
}

// TestDualMode_Dependencies tests dependency operations in both modes
func TestDualMode_Dependencies(t *testing.T) {
	RunDualModeTest(t, "dependencies", func(t *testing.T, env *DualModeTestEnv) {
		// Create two issues
		blocker := &types.Issue{
			Title:     "Blocker issue",
			IssueType: types.TypeTask,
			Status:    types.StatusOpen,
			Priority:  1,
		}
		blocked := &types.Issue{
			Title:     "Blocked issue",
			IssueType: types.TypeTask,
			Status:    types.StatusOpen,
			Priority:  2,
		}

		if err := env.CreateIssue(blocker); err != nil {
			t.Fatalf("[%s] CreateIssue(blocker) failed: %v", env.Mode(), err)
		}
		if err := env.CreateIssue(blocked); err != nil {
			t.Fatalf("[%s] CreateIssue(blocked) failed: %v", env.Mode(), err)
		}

		// Add blocking dependency
		if err := env.AddDependency(blocked.ID, blocker.ID, types.DepBlocks); err != nil {
			t.Fatalf("[%s] AddDependency failed: %v", env.Mode(), err)
		}

		// Verify blocked issue is not in ready queue
		ready, err := env.GetReadyWork()
		if err != nil {
			t.Fatalf("[%s] GetReadyWork failed: %v", env.Mode(), err)
		}

		for _, r := range ready {
			if r.ID == blocked.ID {
				t.Errorf("[%s] blocked issue should not be in ready queue", env.Mode())
			}
		}

		// Verify blocker is in ready queue (it has no blockers)
		found := false
		for _, r := range ready {
			if r.ID == blocker.ID {
				found = true
				break
			}
		}
		if !found {
			t.Errorf("[%s] blocker issue should be in ready queue", env.Mode())
		}
	})
}

// TestDualMode_ListIssues tests listing issues works in both modes
func TestDualMode_ListIssues(t *testing.T) {
	RunDualModeTest(t, "list_issues", func(t *testing.T, env *DualModeTestEnv) {
		// Create multiple issues
		for i := 0; i < 3; i++ {
			issue := &types.Issue{
				Title:     fmt.Sprintf("Issue %d", i),
				IssueType: types.TypeTask,
				Status:    types.StatusOpen,
				Priority:  i + 1,
			}
			if err := env.CreateIssue(issue); err != nil {
				t.Fatalf("[%s] CreateIssue failed: %v", env.Mode(), err)
			}
		}

		// List all issues
		issues, err := env.ListIssues(types.IssueFilter{})
		if err != nil {
			t.Fatalf("[%s] ListIssues failed: %v", env.Mode(), err)
		}

		if len(issues) != 3 {
			t.Errorf("[%s] expected 3 issues, got %d", env.Mode(), len(issues))
		}
	})
}

// TestDualMode_Labels tests label operations in both modes
func TestDualMode_Labels(t *testing.T) {
	RunDualModeTest(t, "labels", func(t *testing.T, env *DualModeTestEnv) {
		// Create issue
		issue := &types.Issue{
			Title:     "Issue with labels",
			IssueType: types.TypeBug,
			Status:    types.StatusOpen,
			Priority:  1,
		}
		if err := env.CreateIssue(issue); err != nil {
			t.Fatalf("[%s] CreateIssue failed: %v", env.Mode(), err)
		}

		// Add label
		if err := env.AddLabel(issue.ID, "critical"); err != nil {
			t.Fatalf("[%s] AddLabel failed: %v", env.Mode(), err)
		}

		// Verify label was added by fetching the issue
		got, err := env.GetIssue(issue.ID)
		if err != nil {
			t.Fatalf("[%s] GetIssue failed: %v", env.Mode(), err)
		}

		// Note: Label verification depends on whether the Show RPC returns labels
		// This test primarily verifies the AddLabel operation doesn't error
		_ = got // Use the retrieved issue for future label verification
	})
}
