package main

import (
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

func TestTurnGate_NonBlockingSkipsWhenBusy(t *testing.T) {
	g := newTurnGate()
	if !g.acquire("s1", 0) {
		t.Fatal("first non-blocking acquire should succeed on a free session")
	}
	// A second non-blocking acquire (a background resume) must skip, not queue.
	if g.acquire("s1", 0) {
		t.Fatal("second non-blocking acquire should fail while a turn is active")
	}
	// A different session is independent.
	if !g.acquire("s2", 0) {
		t.Fatal("acquire on a different session should succeed")
	}
	g.release("s2")
	g.release("s1")
	// After release, the session is free again.
	if !g.acquire("s1", 0) {
		t.Fatal("acquire should succeed again after release")
	}
	g.release("s1")
}

func TestTurnGate_BlockingAcquireWaitsForRelease(t *testing.T) {
	g := newTurnGate()
	if !g.acquire("s1", 0) {
		t.Fatal("first acquire should succeed")
	}

	got := make(chan bool, 1)
	go func() { got <- g.acquire("s1", 2*time.Second) }()

	select {
	case <-got:
		t.Fatal("blocking acquire should wait, not return before release")
	case <-time.After(50 * time.Millisecond):
		// expected: still waiting
	}

	g.release("s1")
	select {
	case ok := <-got:
		if !ok {
			t.Fatal("blocking acquire should succeed after release")
		}
	case <-time.After(time.Second):
		t.Fatal("blocking acquire did not return after release")
	}
	g.release("s1")
}

func TestTurnGate_BlockingAcquireTimesOut(t *testing.T) {
	g := newTurnGate()
	g.acquire("s1", 0) // hold the permit

	start := time.Now()
	if g.acquire("s1", 60*time.Millisecond) {
		t.Fatal("acquire should time out while permit is held")
	}
	if elapsed := time.Since(start); elapsed < 50*time.Millisecond {
		t.Fatalf("acquire returned too fast (%v); expected to wait ~60ms", elapsed)
	}
	g.release("s1")
}

// TestTurnGate_SingleFlightConcurrent is the core F1 guarantee: many concurrent
// background acquirers on the SAME session, exactly one runs at a time. This is
// the property that prevents two turns interleaving tool calls.
func TestTurnGate_SingleFlightConcurrent(t *testing.T) {
	g := newTurnGate()
	const n = 50
	var inFlight, maxInFlight int64
	var runs int64
	var wg sync.WaitGroup
	wg.Add(n)
	start := make(chan struct{})
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			<-start
			if !g.acquire("shared", 0) { // background-style: skip if busy
				return
			}
			defer g.release("shared")
			cur := atomic.AddInt64(&inFlight, 1)
			for {
				m := atomic.LoadInt64(&maxInFlight)
				if cur <= m || atomic.CompareAndSwapInt64(&maxInFlight, m, cur) {
					break
				}
			}
			atomic.AddInt64(&runs, 1)
			time.Sleep(2 * time.Millisecond)
			atomic.AddInt64(&inFlight, -1)
		}()
	}
	close(start)
	wg.Wait()

	if maxInFlight != 1 {
		t.Fatalf("max in-flight turns = %d, want 1 (turns must not overlap)", maxInFlight)
	}
	if runs == 0 {
		t.Fatal("expected at least one turn to run")
	}
}
