package daemon

import (
	"context"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"testing"
	"time"

	"github.com/standardbeagle/go-cli-server/process"
	"github.com/stretchr/testify/require"
)

// cleanupTestDaemon aggressively cleans up a test daemon process.
// Tries graceful socket shutdown first, then finds and kills the process
// by matching its socket path in /proc cmdline.
func cleanupTestDaemon(t *testing.T, sockPath string) {
	t.Helper()

	// Try graceful shutdown first
	_ = StopDaemon(sockPath)
	time.Sleep(200 * time.Millisecond)

	// Force kill any process still using this socket path
	out, err := exec.Command("pgrep", "-f", sockPath).Output()
	if err != nil {
		return // No matching processes
	}
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		pid, err := strconv.Atoi(strings.TrimSpace(line))
		if err != nil || pid <= 0 {
			continue
		}
		if p, err := os.FindProcess(pid); err == nil {
			_ = p.Signal(syscall.SIGKILL)
			_, _ = p.Wait()
		}
	}
}

// findAgntBinary finds the agnt binary for testing.
// Returns the path to the binary or skips the test if not found.
func findAgntBinary(t *testing.T) string {
	wd, _ := os.Getwd()
	// Navigate from internal/daemon to project root
	projectRoot := filepath.Join(wd, "..", "..")
	daemonPath := filepath.Join(projectRoot, "agnt")

	// On Windows, add .exe extension
	if _, err := os.Stat(daemonPath); os.IsNotExist(err) {
		daemonPath += ".exe"
	}

	if _, err := os.Stat(daemonPath); os.IsNotExist(err) {
		t.Skipf("agnt binary not found at %s - run 'make build' first", daemonPath)
	}

	return daemonPath
}

// getBinaryVersion returns the version of the agnt binary.
func getBinaryVersion(t *testing.T, binaryPath string) string {
	cmd := exec.Command(binaryPath, "--version")
	output, err := cmd.Output()
	if err != nil {
		t.Fatalf("Failed to get binary version: %v", err)
	}
	// Parse "agnt vX.Y.Z" format (first line only - subsequent lines have daemon status)
	lines := strings.Split(strings.TrimSpace(string(output)), "\n")
	if len(lines) == 0 {
		t.Fatalf("No version output from binary")
	}
	version := strings.TrimSpace(lines[0])
	version = strings.TrimPrefix(version, "agnt ")
	version = strings.TrimPrefix(version, "v")
	return version
}

// TestDaemonUpgrade_FullCycle tests the complete upgrade cycle
func TestDaemonUpgrade_FullCycle(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	// Find the agnt binary for testing and get its version
	daemonPath := findAgntBinary(t)
	binaryVersion := getBinaryVersion(t, daemonPath)
	t.Logf("Binary version: %s", binaryVersion)

	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "test.sock")

	// Start daemon
	_ = NewForTest(t, DaemonConfig{
		SocketPath: sockPath,
		ProcessConfig: process.ManagerConfig{
			DefaultTimeout:    0,
			MaxOutputBuffer:   64 * 1024,
			GracefulTimeout:   2 * time.Second,
			HealthCheckPeriod: 0, // Disable health checks for test
		},
		MaxClients:   10,
		WriteTimeout: 5 * time.Second,
	})

	defer func() { cleanupTestDaemon(t, sockPath) }()

	// Wait for daemon to be ready
	require.Eventually(t, func() bool { return IsRunning(sockPath) },
		5*time.Second, 20*time.Millisecond, "daemon not running after start")

	// Get daemon info
	client := NewClient(WithSocketPath(sockPath))
	if err := client.Connect(); err != nil {
		t.Fatalf("Failed to connect to daemon: %v", err)
	}

	info, err := client.Info()
	if err != nil {
		client.Close()
		t.Fatalf("Failed to get daemon info: %v", err)
	}
	client.Close()

	t.Logf("Initial daemon version: %s", info.Version)
	initialUptime := info.Uptime

	// Create upgrader
	upgrader := NewDaemonUpgrader(UpgradeConfig{
		SocketPath:      sockPath,
		NewBinaryPath:   daemonPath, // Use actual agnt binary
		Timeout:         10 * time.Second,
		GracefulTimeout: 2 * time.Second,
		Force:           true, // Force upgrade even though version matches
		Verbose:         testing.Verbose(),
	})

	// Run upgrade
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	t.Log("Starting upgrade...")
	if err := upgrader.Upgrade(ctx); err != nil {
		t.Fatalf("Upgrade failed: %v", err)
	}

	// Verify new daemon is running
	if !IsRunning(sockPath) {
		t.Fatal("Daemon not running after upgrade")
	}

	// Connect to new daemon
	client2 := NewClient(WithSocketPath(sockPath))
	if err := client2.Connect(); err != nil {
		t.Fatalf("Failed to connect to new daemon: %v", err)
	}
	defer client2.Close()

	// Get new daemon info
	info2, err := client2.Info()
	if err != nil {
		t.Fatalf("Failed to get new daemon info: %v", err)
	}

	t.Logf("Post-upgrade daemon version: %s", info2.Version)

	// Version should match the binary version (not the initial in-memory daemon version)
	// During development, the in-memory daemon may have a different version than the binary
	if info2.Version != binaryVersion {
		t.Errorf("Version mismatch after upgrade: expected %s (from binary), got %s",
			binaryVersion, info2.Version)
	}

	// Version change already proves restart; uptime comparison is racy
	// (initial daemon may be measured at very low uptime, new daemon may exceed it
	// depending on upgrade duration). Log for observability only.
	t.Logf("Uptime after upgrade: %v (initial was %v)", info2.Uptime, initialUptime)
	// Cleanup is handled by defer
}

// TestUpgradeLock_ConcurrentAttempts tests that concurrent upgrade attempts are blocked
func TestUpgradeLock_ConcurrentAttempts(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	// Find the agnt binary for testing
	daemonPath := findAgntBinary(t)

	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "test.sock")

	// Start daemon
	_ = NewForTest(t, DaemonConfig{
		SocketPath: sockPath,
		ProcessConfig: process.ManagerConfig{
			DefaultTimeout:    0,
			MaxOutputBuffer:   64 * 1024,
			GracefulTimeout:   2 * time.Second,
			HealthCheckPeriod: 0,
		},
		MaxClients:   10,
		WriteTimeout: 5 * time.Second,
	})
	defer func() { cleanupTestDaemon(t, sockPath) }()

	require.Eventually(t, func() bool { return IsRunning(sockPath) },
		5*time.Second, 20*time.Millisecond, "daemon not running after start")

	// Create two upgraders
	upgrader1 := NewDaemonUpgrader(UpgradeConfig{
		SocketPath:      sockPath,
		NewBinaryPath:   daemonPath, // Use actual agnt binary
		Timeout:         10 * time.Second,
		GracefulTimeout: 2 * time.Second,
		Force:           true,
		Verbose:         false,
	})

	upgrader2 := NewDaemonUpgrader(UpgradeConfig{
		SocketPath:      sockPath,
		NewBinaryPath:   daemonPath, // Use actual agnt binary
		Timeout:         10 * time.Second,
		GracefulTimeout: 2 * time.Second,
		Force:           true,
		Verbose:         false,
	})

	// Start first upgrade in goroutine
	errCh1 := make(chan error, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		errCh1 <- upgrader1.Upgrade(ctx)
	}()

	// Give first upgrade time to acquire lock
	time.Sleep(100 * time.Millisecond)

	// Try second upgrade (should fail with lock error)
	ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel2()

	err2 := upgrader2.Upgrade(ctx2)
	if err2 == nil {
		t.Fatal("Second upgrade should have failed with lock error, but succeeded")
	}

	if !stringContains(err2.Error(), "lock") && !stringContains(err2.Error(), "progress") {
		t.Errorf("Expected lock error, got: %v", err2)
	}

	t.Logf("Second upgrade correctly blocked: %v", err2)

	// Wait for first upgrade to complete
	err1 := <-errCh1
	if err1 != nil {
		t.Errorf("First upgrade failed: %v", err1)
	}
}

// TestUpgradeStaleSocket tests upgrade when socket exists but daemon is not running
func TestUpgradeStaleSocket(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	// Find the agnt binary for testing
	daemonPath := findAgntBinary(t)

	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "test.sock")

	// Create a real stale Unix socket (not a regular file)
	// The daemon checks if the path is a socket, so we must create an actual socket
	listener, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("Failed to create stale socket: %v", err)
	}
	// Close the listener immediately to make it stale
	listener.Close()

	// Verify daemon is not running
	if IsRunning(sockPath) {
		t.Fatal("Daemon should not be running with stale socket")
	}

	defer func() { cleanupTestDaemon(t, sockPath) }()

	// Create upgrader
	upgrader := NewDaemonUpgrader(UpgradeConfig{
		SocketPath:      sockPath,
		NewBinaryPath:   daemonPath, // Use actual agnt binary
		Timeout:         10 * time.Second,
		GracefulTimeout: 2 * time.Second,
		Force:           false,
		Verbose:         testing.Verbose(),
	})

	// Run upgrade (should start new daemon since old one isn't running)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	t.Log("Starting upgrade with stale socket...")
	if err := upgrader.Upgrade(ctx); err != nil {
		t.Fatalf("Upgrade failed: %v", err)
	}

	// Verify new daemon is running
	if !IsRunning(sockPath) {
		t.Fatal("Daemon not running after upgrade")
	}
	// Cleanup is handled by defer
}

// TestUpgradeVersionCheck tests that upgrade respects version matching
func TestUpgradeVersionCheck(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	// Find the agnt binary for testing
	daemonPath := findAgntBinary(t)

	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "test.sock")

	// Start daemon
	_ = NewForTest(t, DaemonConfig{
		SocketPath: sockPath,
		ProcessConfig: process.ManagerConfig{
			DefaultTimeout:    0,
			MaxOutputBuffer:   64 * 1024,
			GracefulTimeout:   2 * time.Second,
			HealthCheckPeriod: 0,
		},
		MaxClients:   10,
		WriteTimeout: 5 * time.Second,
	})
	defer func() { cleanupTestDaemon(t, sockPath) }()

	require.Eventually(t, func() bool { return IsRunning(sockPath) },
		5*time.Second, 20*time.Millisecond, "daemon not running after start")

	// Create upgrader WITHOUT force flag
	upgrader := NewDaemonUpgrader(UpgradeConfig{
		SocketPath:      sockPath,
		NewBinaryPath:   daemonPath, // Use actual agnt binary
		Timeout:         10 * time.Second,
		GracefulTimeout: 2 * time.Second,
		Force:           false, // Don't force upgrade
		Verbose:         testing.Verbose(),
	})

	// Run upgrade (should be no-op since versions match)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	t.Log("Starting upgrade without force flag (versions match)...")
	if err := upgrader.Upgrade(ctx); err != nil {
		t.Fatalf("Upgrade failed: %v", err)
	}

	// Daemon should still be running
	if !IsRunning(sockPath) {
		t.Fatal("Daemon not running after upgrade")
	}

	t.Log("Upgrade correctly detected matching versions (no-op)")
}

// Helper function to check if a string contains a substring
func stringContains(s, substr string) bool {
	return strings.Contains(s, substr)
}
