//go:build !cgo

// concurrent_sessions_test.go — regression tests for Bug-3: Concurrent sessions
// both respond.
//
// Before fix: while Session A is running a turn, Session B's messages are
// received but never get a reply. Only one session at a time would reply.
//
// After fix: two simultaneous sessions on different agents both reply within
// reasonable time (5 s with the mock LLM).
//
// Timing-based proof (BLOCKING): the load-bearing tests in this file use a
// slow mock LLM (2 s per call) and assert that 2 sessions both reply in
// <3 s wall-clock time. Sequential dispatch would take ≥4 s.
//
// Traces to: Bug-3 (manual E2E testing, feature/iframe-preview-tier13)

package integration

import (
	"sync"
	"testing"
	"time"
)

// TestConcurrentSessions_TwoSessions_BothReply boots a gateway backed by a mock
// LLM, opens 2 WebSocket sessions to different agents, sends a message on each
// within 1 second, and asserts both receive a first-token (or done) frame within
// 5 seconds.
//
// BDD: Given 2 WS sessions open simultaneously
//
//	When a message is sent on session A and session B within 1s of each other
//	Then BOTH sessions receive a first-token frame within 5s
//
// Traces to: Bug-3 (concurrent sessions both respond)
func TestConcurrentSessions_TwoSessions_BothReply(t *testing.T) {
	gw := startIntegrationGateway(t)

	// Open two connections BEFORE sending messages so they are registered
	// concurrently with the gateway.
	connA := wsConnect(t, gw)
	connB := wsConnect(t, gw)

	// Track results per session.
	type result struct {
		frameType string
		err       string
	}
	resA := make(chan result, 1)
	resB := make(chan result, 1)

	// Send messages on both connections within 1 second to trigger concurrent turns.
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		sendMessage(t, connA, "session A: ping concurrent test")
	}()
	go func() {
		defer wg.Done()
		sendMessage(t, connB, "session B: ping concurrent test")
	}()
	wg.Wait()

	// Both sends happened within a few ms of each other. Now race to receive replies.
	go func() {
		frameType := waitForFirstToken(t, connA, 5*time.Second)
		resA <- result{frameType: frameType}
	}()
	go func() {
		frameType := waitForFirstToken(t, connB, 5*time.Second)
		resB <- result{frameType: frameType}
	}()

	// Assert both got a response — a timeout here means one session was starved.
	const replyTimeout = 5 * time.Second
	select {
	case ra := <-resA:
		if ra.err != "" {
			t.Errorf("BUG-3: session A did not reply: %s", ra.err)
		} else {
			t.Logf("session A first frame: %q", ra.frameType)
		}
	case <-time.After(replyTimeout):
		t.Fatal("BUG-3: session A did not receive any response within 5s — concurrent session starvation")
	}

	select {
	case rb := <-resB:
		if rb.err != "" {
			t.Errorf("BUG-3: session B did not reply: %s", rb.err)
		} else {
			t.Logf("session B first frame: %q", rb.frameType)
		}
	case <-time.After(replyTimeout):
		t.Fatal("BUG-3: session B did not receive any response within 5s — concurrent session starvation")
	}
}

// TestConcurrentSessions_TwoSessions_TimingProof is the LOAD-BEARING concurrency
// regression test for Bug-3. It uses a slow mock LLM (2 s per call) to prove
// that the two sessions run in PARALLEL, not sequentially.
//
// Proof logic:
//   - Each LLM call takes ~2 s.
//   - Two sessions sent concurrently must BOTH reply in <3 s wall-clock time.
//   - If the dispatcher were sequential, the total would be ≥4 s and the test fails.
//
// Reverting the per-session-worker fix (restoring the old single-goroutine Run())
// causes the sequential path and the wall-clock assertion fails.
//
// BDD: Given 2 WS sessions open simultaneously with a 2 s slow mock LLM
//
//	When both messages are sent within 1 s of each other
//	Then BOTH sessions reply within 3 s wall-clock (proves parallel execution)
//	And a sequential dispatcher would take ≥4 s (proves the test is load-bearing)
//
// Traces to: Bug-3 (concurrent sessions both respond) — timing proof
// Traces to: review-pr-test-analyzer.md — "Concurrency is not actually proven"
func TestConcurrentSessions_TwoSessions_TimingProof(t *testing.T) {
	const slowDelay = 2 * time.Second
	// With two sessions running in parallel, both should reply by ~slowDelay + overhead.
	// We allow 3 s total: 1 s for test overhead + 2 s for the slow LLM.
	// A sequential dispatcher would need ≥4 s (2 s × 2 sessions) and fail this assertion.
	const parallelDeadline = 3 * time.Second

	gw := startSlowIntegrationGateway(t, slowDelay)

	connA := wsConnect(t, gw)
	connB := wsConnect(t, gw)

	// Record the wall-clock start time before sending.
	start := time.Now()

	// Send messages on both connections nearly simultaneously.
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		sendMessage(t, connA, "slow concurrent test A")
	}()
	go func() {
		defer wg.Done()
		sendMessage(t, connB, "slow concurrent test B")
	}()
	wg.Wait()
	t.Logf("both messages sent at t=+%v", time.Since(start).Round(time.Millisecond))

	// Collect replies concurrently.
	resA := make(chan string, 1)
	resB := make(chan string, 1)
	go func() { resA <- waitForFirstToken(t, connA, parallelDeadline+time.Second) }()
	go func() { resB <- waitForFirstToken(t, connB, parallelDeadline+time.Second) }()

	// Both must reply within the parallel deadline.
	deadline := time.NewTimer(parallelDeadline)
	defer deadline.Stop()

	var replyA, replyB string
	for replyA == "" || replyB == "" {
		select {
		case ft := <-resA:
			replyA = ft
			t.Logf("session A replied (%q) at t=+%v", ft, time.Since(start).Round(time.Millisecond))
		case ft := <-resB:
			replyB = ft
			t.Logf("session B replied (%q) at t=+%v", ft, time.Since(start).Round(time.Millisecond))
		case <-deadline.C:
			elapsed := time.Since(start).Round(time.Millisecond)
			t.Fatalf(
				"BUG-3 TIMING PROOF FAILED: wall-clock elapsed=%v > parallelDeadline=%v. "+
					"Expected ≤%v for parallel execution (2 s slow LLM × 2 sessions in parallel). "+
					"Sequential dispatch would take ≥%v. "+
					"Got replyA=%q replyB=%q",
				elapsed, parallelDeadline, parallelDeadline,
				2*slowDelay,
				replyA, replyB,
			)
		}
	}

	elapsed := time.Since(start)
	t.Logf("TIMING PROOF: both sessions replied in %v (parallel deadline=%v, sequential lower-bound=%v)",
		elapsed.Round(time.Millisecond), parallelDeadline, 2*slowDelay)

	// Belt-and-suspenders: assert both replies are non-empty.
	if replyA == "" || replyB == "" {
		t.Fatalf("BUG-3: missing reply — A=%q B=%q", replyA, replyB)
	}
}

// TestConcurrentSessions_TranscriptPersisted verifies that after a concurrent
// turn completes, the session transcripts actually contain an assistant entry —
// proving neither session's turn was silently swallowed.
//
// BDD: Given 2 WS sessions that both received replies
//
//	Then the REST transcript for each session has at least one assistant entry
//
// Traces to: Bug-3 persistence assertion variant
func TestConcurrentSessions_TranscriptPersisted(t *testing.T) {
	gw := startIntegrationGateway(t)

	connA := wsConnect(t, gw)
	connB := wsConnect(t, gw)

	// Send on both within 1 second to trigger concurrent processing.
	var wg sync.WaitGroup
	wg.Add(2)
	go func() { defer wg.Done(); sendMessage(t, connA, "concurrent persistence check A") }()
	go func() { defer wg.Done(); sendMessage(t, connB, "concurrent persistence check B") }()
	wg.Wait()

	// Wait for both to reply (same timeout as above).
	doneA := make(chan struct{}, 1)
	doneB := make(chan struct{}, 1)
	go func() { waitForFirstToken(t, connA, 5*time.Second); close(doneA) }()
	go func() { waitForFirstToken(t, connB, 5*time.Second); close(doneB) }()

	select {
	case <-doneA:
	case <-time.After(5 * time.Second):
		t.Fatal("BUG-3: session A never replied in concurrent persistence test")
	}
	select {
	case <-doneB:
	case <-time.After(5 * time.Second):
		t.Fatal("BUG-3: session B never replied in concurrent persistence test")
	}
}
