//go:build integration
// +build integration

package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"
)

func TestDaemonLockPreventsMultipleInstances(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

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

	// Acquire lock
	lock1, err := acquireDaemonLock(beadsDir, dbPath)
	if err != nil {
		t.Fatalf("Failed to acquire first lock: %v", err)
	}
	defer lock1.Close()

	// Try to acquire lock again - should fail
	lock2, err := acquireDaemonLock(beadsDir, dbPath)
	if err != ErrDaemonLocked {
		if lock2 != nil {
			lock2.Close()
		}
		t.Fatalf("Expected ErrDaemonLocked, got: %v", err)
	}

	// Release first lock
	lock1.Close()

	// Now should be able to acquire lock
	lock3, err := acquireDaemonLock(beadsDir, dbPath)
	if err != nil {
		t.Fatalf("Failed to acquire lock after release: %v", err)
	}
	lock3.Close()
}

func TestTryDaemonLockDetectsRunning(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

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

	// Initially no daemon running
	running, _ := tryDaemonLock(beadsDir)
	if running {
		t.Fatal("Expected no daemon running initially")
	}

	// Acquire lock
	lock, err := acquireDaemonLock(beadsDir, dbPath)
	if err != nil {
		t.Fatalf("Failed to acquire lock: %v", err)
	}
	defer lock.Close()

	// Now should detect daemon running
	running, pid := tryDaemonLock(beadsDir)
	if !running {
		t.Fatal("Expected daemon to be detected as running")
	}
	if pid != os.Getpid() {
		t.Errorf("Expected PID %d, got %d", os.Getpid(), pid)
	}
}

func TestBackwardCompatibilityWithOldDaemon(t *testing.T) {
	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

	// Simulate old daemon: PID file exists but no lock file
	pidFile := filepath.Join(beadsDir, "daemon.pid")
	currentPID := os.Getpid()
	if err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", currentPID)), 0600); err != nil {
		t.Fatalf("Failed to write PID file: %v", err)
	}

	// tryDaemonLock should detect the old daemon via PID file fallback
	running, pid := tryDaemonLock(beadsDir)
	if !running {
		t.Fatal("Expected old daemon to be detected via PID file")
	}
	if pid != currentPID {
		t.Errorf("Expected PID %d, got %d", currentPID, pid)
	}

	// Clean up PID file
	os.Remove(pidFile)

	// Now should report no daemon running
	running, _ = tryDaemonLock(beadsDir)
	if running {
		t.Fatal("Expected no daemon running after PID file removed")
	}
}

func TestDaemonLockJSONFormat(t *testing.T) {
	// Skip on Windows - file locking prevents reading lock file while locked
	if runtime.GOOS == "windows" {
		t.Skip("Windows file locking prevents reading locked files")
	}

	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

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

	// Acquire lock
	lock, err := acquireDaemonLock(beadsDir, dbPath)
	if err != nil {
		t.Fatalf("Failed to acquire lock: %v", err)
	}
	defer lock.Close()

	// Read the lock file and verify JSON format
	lockInfo, err := readDaemonLockInfo(beadsDir)
	if err != nil {
		t.Fatalf("Failed to read lock info: %v", err)
	}

	if lockInfo.PID != os.Getpid() {
		t.Errorf("Expected PID %d, got %d", os.Getpid(), lockInfo.PID)
	}

	if lockInfo.Database != dbPath {
		t.Errorf("Expected database %s, got %s", dbPath, lockInfo.Database)
	}

	if lockInfo.Version != Version {
		t.Errorf("Expected version %s, got %s", Version, lockInfo.Version)
	}

	if lockInfo.StartedAt.IsZero() {
		t.Error("Expected non-zero StartedAt timestamp")
	}
}

func TestValidateDaemonLock(t *testing.T) {
	// Skip on Windows - file locking prevents reading lock file while locked
	if runtime.GOOS == "windows" {
		t.Skip("Windows file locking prevents reading locked files")
	}

	tmpDir := t.TempDir()
	beadsDir := filepath.Join(tmpDir, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

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

	// No lock file - validation should pass
	if err := validateDaemonLock(beadsDir, dbPath); err != nil {
		t.Errorf("Expected no error when no lock file exists, got: %v", err)
	}

	// Acquire lock with correct database
	lock, err := acquireDaemonLock(beadsDir, dbPath)
	if err != nil {
		t.Fatalf("Failed to acquire lock: %v", err)
	}
	defer lock.Close()

	// Validation should pass with matching database
	if err := validateDaemonLock(beadsDir, dbPath); err != nil {
		t.Errorf("Expected no error with matching database, got: %v", err)
	}

	// Validation should fail with different database
	wrongDB := filepath.Join(beadsDir, "wrong.db")
	if err := validateDaemonLock(beadsDir, wrongDB); err == nil {
		t.Error("Expected error with mismatched database")
	}
}

func TestMultipleDaemonProcessesRace(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping race condition test in short mode")
	}

	// Find the bd binary
	bdBinary, err := exec.LookPath("bd")
	if err != nil {
		// Try local build (platform-specific)
		localBinary := "./bd"
		if runtime.GOOS == "windows" {
			localBinary = "./bd.exe"
		}
		if _, err := os.Stat(localBinary); err == nil {
			bdBinary = localBinary
		} else {
			t.Skip("bd binary not found, skipping race test")
		}
	}

	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
	beadsDir := filepath.Dir(dbPath)

	// Initialize a test database with git repo
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		t.Fatal(err)
	}

	// Create git repo
	cmd := exec.Command("git", "init")
	cmd.Dir = tmpDir
	if err := cmd.Run(); err != nil {
		t.Fatalf("Failed to init git repo: %v", err)
	}

	// Initialize bd
	cmd = exec.Command(bdBinary, "init", "--prefix", "test")
	cmd.Dir = tmpDir
	cmd.Env = append(os.Environ(), "BEADS_DB="+dbPath)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("Failed to init bd: %v\nOutput: %s", err, out)
	}

	// Try to start 5 daemons simultaneously
	numAttempts := 5
	results := make(chan error, numAttempts)

	for i := 0; i < numAttempts; i++ {
		go func() {
			cmd := exec.Command(bdBinary, "daemon", "--interval", "10m")
			cmd.Dir = tmpDir
			cmd.Env = append(os.Environ(), "BEADS_DB="+dbPath)
			err := cmd.Start()
			if err != nil {
				results <- err
				return
			}

			// Wait a bit for daemon to start
			time.Sleep(200 * time.Millisecond)

			// Check if it's still running
			if cmd.Process != nil {
				cmd.Process.Kill()
			}
			results <- cmd.Wait()
		}()
	}

	// Wait for all attempts
	var successCount int
	var alreadyRunning int
	timeout := time.After(5 * time.Second)

	for i := 0; i < numAttempts; i++ {
		select {
		case err := <-results:
			if err == nil {
				successCount++
			} else if strings.Contains(err.Error(), "exit status 1") {
				// Could be "already running" error
				alreadyRunning++
			}
		case <-timeout:
			t.Fatal("Test timed out waiting for daemon processes")
		}
	}

	// Clean up any remaining daemon files
	os.Remove(filepath.Join(beadsDir, "daemon.pid"))
	os.Remove(filepath.Join(beadsDir, "daemon.lock"))
	os.Remove(filepath.Join(beadsDir, "bd.sock"))

	t.Logf("Results: %d success, %d already running", successCount, alreadyRunning)

	// At most one should have succeeded in holding the lock
	// (though timing means even the first might have exited by the time we checked)
	if alreadyRunning < numAttempts-1 {
		t.Logf("Warning: Expected at least %d processes to fail with 'already running', got %d", 
			numAttempts-1, alreadyRunning)
		t.Log("This could indicate a race condition, but may also be timing-related in tests")
	}
}
