package store

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

func TestAcquireLockfile_FreshDir(t *testing.T) {
	dir := t.TempDir()
	release, err := AcquireLockfile(dir)
	if err != nil {
		t.Fatalf("AcquireLockfile: %v", err)
	}
	defer release()

	// Lockfile should exist with our PID.
	data, err := os.ReadFile(filepath.Join(dir, "tinyforge.lock"))
	if err != nil {
		t.Fatalf("read lockfile: %v", err)
	}
	want := fmt.Sprintf("%d\n", os.Getpid())
	if string(data) != want {
		t.Errorf("lockfile content = %q, want %q", data, want)
	}
}

func TestAcquireLockfile_HeldByLivePID_Refused(t *testing.T) {
	dir := t.TempDir()
	// Plant a lockfile holding the current PID (which is obviously alive).
	if err := os.WriteFile(filepath.Join(dir, "tinyforge.lock"),
		[]byte(fmt.Sprintf("%d\n", os.Getpid())), 0o600); err != nil {
		t.Fatalf("plant lockfile: %v", err)
	}
	release, err := AcquireLockfile(dir)
	if err == nil {
		release()
		t.Fatal("expected ErrLockHeld, got nil")
	}
	if !errors.Is(err, ErrLockHeld) {
		t.Errorf("error = %v, want wrap of ErrLockHeld", err)
	}
}

func TestAcquireLockfile_StalePID_Reclaimed(t *testing.T) {
	dir := t.TempDir()
	// PID 1 is init/launchd/systemd on POSIX and the System Idle Process
	// on Windows — never our process, and very unlikely to be dead. We
	// use a deliberately-impossible PID instead: a 31-bit value far
	// above any plausible system maximum.
	stalePID := 2147483640
	if err := os.WriteFile(filepath.Join(dir, "tinyforge.lock"),
		[]byte(fmt.Sprintf("%d\n", stalePID)), 0o600); err != nil {
		t.Fatalf("plant stale lockfile: %v", err)
	}
	release, err := AcquireLockfile(dir)
	if err != nil {
		t.Fatalf("expected reclaim of stale lock, got: %v", err)
	}
	defer release()

	// Verify it now holds OUR pid, not the stale one.
	data, err := os.ReadFile(filepath.Join(dir, "tinyforge.lock"))
	if err != nil {
		t.Fatalf("read lockfile after reclaim: %v", err)
	}
	want := fmt.Sprintf("%d\n", os.Getpid())
	if string(data) != want {
		t.Errorf("lockfile content after reclaim = %q, want %q", data, want)
	}
}

func TestAcquireLockfile_ConcurrentReclaim_SingleWinner(t *testing.T) {
	dir := t.TempDir()
	// Plant a stale lockfile (impossibly high, certainly-dead PID), then have
	// many goroutines race to reclaim it. Exactly one must win; the rest must
	// be refused with ErrLockHeld. A "last-writer-wins" reclaim would let
	// several goroutines all believe they own the lock.
	stalePID := 2147483640
	if err := os.WriteFile(filepath.Join(dir, "tinyforge.lock"),
		[]byte(fmt.Sprintf("%d\n", stalePID)), 0o600); err != nil {
		t.Fatalf("plant stale lockfile: %v", err)
	}

	const n = 16
	var (
		wg       sync.WaitGroup
		mu       sync.Mutex
		winners  int
		releases []func()
	)
	start := make(chan struct{})
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			release, err := AcquireLockfile(dir)
			if err != nil {
				if !errors.Is(err, ErrLockHeld) {
					t.Errorf("loser error = %v, want wrap of ErrLockHeld", err)
				}
				return
			}
			mu.Lock()
			winners++
			releases = append(releases, release)
			mu.Unlock()
		}()
	}
	close(start)
	wg.Wait()

	for _, r := range releases {
		r()
	}
	if winners != 1 {
		t.Fatalf("concurrent reclaim winners = %d, want exactly 1", winners)
	}
}

func TestAcquireLockfile_ReleaseRemovesFile(t *testing.T) {
	dir := t.TempDir()
	release, err := AcquireLockfile(dir)
	if err != nil {
		t.Fatalf("AcquireLockfile: %v", err)
	}
	release()

	path := filepath.Join(dir, "tinyforge.lock")
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Errorf("lockfile still present after release: %v", err)
	}
}
