package main

import (
	"bytes"
	"context"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/config"
)

func tempSockDir(t *testing.T) string {
	t.Helper()

	base := "/tmp"
	if runtime.GOOS == windowsOS {
		base = os.TempDir()
	} else if _, err := os.Stat(base); err != nil {
		base = os.TempDir()
	}

	d, err := os.MkdirTemp(base, "bd-sock-*")
	if err != nil {
		t.Fatalf("MkdirTemp: %v", err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(d) })
	return d
}

func startTestRPCServer(t *testing.T) (socketPath string, cleanup func()) {
	t.Helper()

	tmpDir := tempSockDir(t)
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0o750); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}

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

	ctx, cancel := context.WithCancel(context.Background())
	log := newTestLogger()

	server, _, err := startRPCServer(ctx, socketPath, store, tmpDir, db, log)
	if err != nil {
		cancel()
		t.Fatalf("startRPCServer: %v", err)
	}

	cleanup = func() {
		cancel()
		if server != nil {
			_ = server.Stop()
		}
	}

	return socketPath, cleanup
}

func captureStderr(t *testing.T, fn func()) string {
	t.Helper()

	old := os.Stderr
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("os.Pipe: %v", err)
	}
	os.Stderr = w

	var buf bytes.Buffer
	done := make(chan struct{})
	go func() {
		_, _ = io.Copy(&buf, r)
		close(done)
	}()

	fn()
	_ = w.Close()
	os.Stderr = old
	<-done
	_ = r.Close()

	return buf.String()
}

func TestDaemonAutostart_AcquireStartLock_CreatesAndCleansStale(t *testing.T) {
	tmpDir := t.TempDir()
	lockPath := filepath.Join(tmpDir, "bd.sock.startlock")
	pid, err := readPIDFromFile(lockPath)
	if err == nil || pid != 0 {
		// lock doesn't exist yet; expect read to fail.
	}

	if !acquireStartLock(lockPath, filepath.Join(tmpDir, "bd.sock")) {
		t.Fatalf("expected acquireStartLock to succeed")
	}
	got, err := readPIDFromFile(lockPath)
	if err != nil {
		t.Fatalf("readPIDFromFile: %v", err)
	}
	if got != os.Getpid() {
		t.Fatalf("expected lock PID %d, got %d", os.Getpid(), got)
	}

	// Stale lock: dead/unreadable PID should be removed and recreated.
	if err := os.WriteFile(lockPath, []byte("0\n"), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
	if !acquireStartLock(lockPath, filepath.Join(tmpDir, "bd.sock")) {
		t.Fatalf("expected acquireStartLock to succeed on stale lock")
	}
	got, err = readPIDFromFile(lockPath)
	if err != nil {
		t.Fatalf("readPIDFromFile: %v", err)
	}
	if got != os.Getpid() {
		t.Fatalf("expected recreated lock PID %d, got %d", os.Getpid(), got)
	}
}

func TestDaemonAutostart_SocketHealthAndReadiness(t *testing.T) {
	socketPath, cleanup := startTestRPCServer(t)
	defer cleanup()

	if !canDialSocket(socketPath, 500*time.Millisecond) {
		t.Fatalf("expected canDialSocket to succeed")
	}
	if !isDaemonHealthy(socketPath) {
		t.Fatalf("expected isDaemonHealthy to succeed")
	}
	if !waitForSocketReadiness(socketPath, 500*time.Millisecond) {
		t.Fatalf("expected waitForSocketReadiness to succeed")
	}

	missing := filepath.Join(tempSockDir(t), "missing.sock")
	if canDialSocket(missing, 50*time.Millisecond) {
		t.Fatalf("expected canDialSocket to fail")
	}
	if waitForSocketReadiness(missing, 200*time.Millisecond) {
		t.Fatalf("expected waitForSocketReadiness to time out")
	}
}

func TestDaemonAutostart_HandleExistingSocket(t *testing.T) {
	socketPath, cleanup := startTestRPCServer(t)
	defer cleanup()

	if !handleExistingSocket(socketPath) {
		t.Fatalf("expected handleExistingSocket true for running daemon")
	}
}

func TestDaemonAutostart_HandleExistingSocket_StaleCleansUp(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0o750); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}

	socketPath := filepath.Join(beadsDir, "bd.sock")
	pidFile := filepath.Join(beadsDir, "daemon.pid")
	if err := os.WriteFile(socketPath, []byte("not-a-socket"), 0o600); err != nil {
		t.Fatalf("WriteFile socket: %v", err)
	}
	if err := os.WriteFile(pidFile, []byte("0\n"), 0o600); err != nil {
		t.Fatalf("WriteFile pid: %v", err)
	}

	if handleExistingSocket(socketPath) {
		t.Fatalf("expected false for stale socket")
	}
	if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
		t.Fatalf("expected socket removed")
	}
	if _, err := os.Stat(pidFile); !os.IsNotExist(err) {
		t.Fatalf("expected pidfile removed")
	}
}

func TestDaemonAutostart_TryAutoStartDaemon_EarlyExits(t *testing.T) {
	oldFailures := daemonStartFailures
	oldLast := lastDaemonStartAttempt
	defer func() {
		daemonStartFailures = oldFailures
		lastDaemonStartAttempt = oldLast
	}()

	daemonStartFailures = 1
	lastDaemonStartAttempt = time.Now()
	if tryAutoStartDaemon(filepath.Join(t.TempDir(), "bd.sock")) {
		t.Fatalf("expected tryAutoStartDaemon to skip due to backoff")
	}

	daemonStartFailures = 0
	lastDaemonStartAttempt = time.Time{}
	socketPath, cleanup := startTestRPCServer(t)
	defer cleanup()
	if !tryAutoStartDaemon(socketPath) {
		t.Fatalf("expected tryAutoStartDaemon true when daemon already healthy")
	}
}

func TestDaemonAutostart_MiscHelpers(t *testing.T) {
	if determineSocketPath("/x") != "/x" {
		t.Fatalf("determineSocketPath should be identity")
	}

	if err := config.Initialize(); err != nil {
		t.Fatalf("config.Initialize: %v", err)
	}
	old := config.GetDuration("flush-debounce")
	defer config.Set("flush-debounce", old)

	config.Set("flush-debounce", 0)
	if got := getDebounceDuration(); got != 5*time.Second {
		t.Fatalf("expected default debounce 5s, got %v", got)
	}
	config.Set("flush-debounce", 2*time.Second)
	if got := getDebounceDuration(); got != 2*time.Second {
		t.Fatalf("expected debounce 2s, got %v", got)
	}
}

func TestDaemonAutostart_EmitVerboseWarning(t *testing.T) {
	old := daemonStatus
	defer func() { daemonStatus = old }()

	daemonStatus.SocketPath = "/tmp/bd.sock"
	for _, tt := range []struct {
		reason      string
		shouldWrite bool
	}{
		{FallbackConnectFailed, true},
		{FallbackHealthFailed, true},
		{FallbackAutoStartDisabled, true},
		{FallbackAutoStartFailed, true},
		{FallbackDaemonUnsupported, true},
		{FallbackWorktreeSafety, false},
		{FallbackFlagNoDaemon, false},
	} {
		t.Run(tt.reason, func(t *testing.T) {
			daemonStatus.FallbackReason = tt.reason
			out := captureStderr(t, emitVerboseWarning)
			if tt.shouldWrite && out == "" {
				t.Fatalf("expected output")
			}
			if !tt.shouldWrite && out != "" {
				t.Fatalf("expected no output, got %q", out)
			}
		})
	}
}

func TestDaemonAutostart_StartDaemonProcess_Stubbed(t *testing.T) {
	oldExec := execCommandFn
	oldWait := waitForSocketReadinessFn
	oldCfg := configureDaemonProcessFn
	defer func() {
		execCommandFn = oldExec
		waitForSocketReadinessFn = oldWait
		configureDaemonProcessFn = oldCfg
	}()

	execCommandFn = func(string, ...string) *exec.Cmd {
		return exec.Command(os.Args[0], "-test.run=^$")
	}
	waitForSocketReadinessFn = func(string, time.Duration) bool { return true }
	configureDaemonProcessFn = func(*exec.Cmd) {}

	if !startDaemonProcess(filepath.Join(t.TempDir(), "bd.sock")) {
		t.Fatalf("expected startDaemonProcess true when readiness stubbed")
	}
}

func TestDaemonAutostart_RestartDaemonForVersionMismatch_Stubbed(t *testing.T) {
	oldExec := execCommandFn
	oldWait := waitForSocketReadinessFn
	oldRun := isDaemonRunningFn
	oldCfg := configureDaemonProcessFn
	defer func() {
		execCommandFn = oldExec
		waitForSocketReadinessFn = oldWait
		isDaemonRunningFn = oldRun
		configureDaemonProcessFn = oldCfg
	}()

	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0o750); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}
	oldDB := dbPath
	defer func() { dbPath = oldDB }()
	dbPath = filepath.Join(beadsDir, "test.db")

	pidFile, err := getPIDFilePath()
	if err != nil {
		t.Fatalf("getPIDFilePath: %v", err)
	}
	sock := getSocketPath()
	if err := os.WriteFile(pidFile, []byte("999999\n"), 0o600); err != nil {
		t.Fatalf("WriteFile pid: %v", err)
	}
	if err := os.WriteFile(sock, []byte("stale"), 0o600); err != nil {
		t.Fatalf("WriteFile sock: %v", err)
	}

	execCommandFn = func(string, ...string) *exec.Cmd {
		return exec.Command(os.Args[0], "-test.run=^$")
	}
	waitForSocketReadinessFn = func(string, time.Duration) bool { return true }
	isDaemonRunningFn = func(string) (bool, int) { return false, 0 }
	configureDaemonProcessFn = func(*exec.Cmd) {}

	if !restartDaemonForVersionMismatch() {
		t.Fatalf("expected restartDaemonForVersionMismatch true when stubbed")
	}
	if _, err := os.Stat(pidFile); !os.IsNotExist(err) {
		t.Fatalf("expected pidfile removed")
	}
	if _, err := os.Stat(sock); !os.IsNotExist(err) {
		t.Fatalf("expected socket removed")
	}
}

// TestIsWispOperation tests the wisp operation detection for auto-daemon-bypass (bd-ta4r)
func TestIsWispOperation(t *testing.T) {
	// Helper to create a command with parent hierarchy
	makeCmd := func(names ...string) *cobra.Command {
		var current *cobra.Command
		for i, name := range names {
			cmd := &cobra.Command{Use: name}
			if i == 0 {
				current = cmd
			} else {
				current.AddCommand(cmd)
				current = cmd
			}
		}
		return current
	}

	tests := []struct {
		name     string
		cmdNames []string // hierarchy: root, child, grandchild...
		args     []string
		want     bool
	}{
		// Wisp subcommands
		{
			name:     "mol wisp (direct)",
			cmdNames: []string{"bd", "mol", "wisp"},
			args:     []string{},
			want:     true,
		},
		{
			name:     "mol wisp create",
			cmdNames: []string{"bd", "mol", "wisp", "create"},
			args:     []string{"some-proto"},
			want:     true,
		},
		{
			name:     "mol wisp list",
			cmdNames: []string{"bd", "mol", "wisp", "list"},
			args:     []string{},
			want:     true,
		},
		{
			name:     "mol wisp gc",
			cmdNames: []string{"bd", "mol", "wisp", "gc"},
			args:     []string{},
			want:     true,
		},
		// mol burn and squash (wisp-only operations)
		{
			name:     "mol burn",
			cmdNames: []string{"bd", "mol", "burn"},
			args:     []string{"bd-eph-abc"},
			want:     true,
		},
		{
			name:     "mol squash",
			cmdNames: []string{"bd", "mol", "squash"},
			args:     []string{"bd-eph-abc"},
			want:     true,
		},
		// Ephemeral issue IDs in args
		{
			name:     "close with bd-eph ID",
			cmdNames: []string{"bd", "close"},
			args:     []string{"bd-eph-abc123"},
			want:     true,
		},
		{
			name:     "show with gt-eph ID",
			cmdNames: []string{"bd", "show"},
			args:     []string{"gt-eph-xyz"},
			want:     true,
		},
		{
			name:     "update with eph- prefix",
			cmdNames: []string{"bd", "update"},
			args:     []string{"eph-test", "--status=closed"},
			want:     true,
		},
		// Non-wisp operations (should NOT bypass)
		{
			name:     "regular show",
			cmdNames: []string{"bd", "show"},
			args:     []string{"bd-abc123"},
			want:     false,
		},
		{
			name:     "regular close",
			cmdNames: []string{"bd", "close"},
			args:     []string{"bd-xyz"},
			want:     false,
		},
		{
			name:     "mol pour (persistent)",
			cmdNames: []string{"bd", "mol", "pour"},
			args:     []string{"some-formula"},
			want:     false,
		},
		{
			name:     "list command",
			cmdNames: []string{"bd", "list"},
			args:     []string{},
			want:     false,
		},
		// Edge cases
		{
			name:     "flag that looks like eph ID should be ignored",
			cmdNames: []string{"bd", "show"},
			args:     []string{"--format=bd-eph-style", "bd-regular"},
			want:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd := makeCmd(tt.cmdNames...)
			got := isWispOperation(cmd, tt.args)
			if got != tt.want {
				t.Errorf("isWispOperation() = %v, want %v", got, tt.want)
			}
		})
	}
}
