package project

import (
	"context"
	"sync/atomic"
	"testing"
	"testing/synctest"
	"time"

	"github.com/microsoft/typescript-go/internal/bundled"
	"github.com/microsoft/typescript-go/internal/checker"
	"github.com/microsoft/typescript-go/internal/compiler"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/project/logging"
	"github.com/microsoft/typescript-go/internal/vfs/vfstest"
	"gotest.tools/v3/assert"
)

func setupCheckerPoolSession(t *testing.T, opts CheckerPoolOptions) (*Session, *checkerPool) {
	t.Helper()
	if !bundled.Embedded {
		t.Skip("bundled files are not embedded")
	}

	files := map[string]any{
		"/src/tsconfig.json": `{ "compilerOptions": { "noLib": true } }`,
		"/src/index.ts":      "export const x: number = 1;",
	}
	fs := bundled.WrapFS(vfstest.FromMap(files, false))
	session := NewSession(&SessionInit{
		BackgroundCtx: context.Background(),
		Options: &SessionOptions{
			CurrentDirectory:   "/",
			DefaultLibraryPath: bundled.LibPath(),
			PositionEncoding:   lsproto.PositionEncodingKindUTF8,
			WatchEnabled:       false,
			LoggingEnabled:     true,
			CheckerPoolOptions: opts,
		},
		FS:     fs,
		Logger: logging.NewTestLogger(),
	})
	session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, "export const x: number = 1;", lsproto.LanguageKindTypeScript)

	snapshot := session.Snapshot()
	project := snapshot.ProjectCollection.ConfiguredProject("/src/tsconfig.json")
	assert.Assert(t, project != nil, "expected configured project")
	assert.Assert(t, project.checkerPool != nil, "expected checker pool")
	return session, project.checkerPool
}

// newTestCheckerPool creates a checker pool inside the current goroutine context
// (suitable for use inside synctest.Test) using the given program.
func newTestCheckerPool(program *compiler.Program, opts CheckerPoolOptions) *checkerPool {
	return newCheckerPool(opts, program, func(string) {})
}

func TestCheckerPoolDiagnosticsRouting(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	// Diagnostics requests should get checker at index 0.
	ctx := core.WithRequestID(context.Background(), "diag-req-1")
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	c, release := pool.GetChecker(ctx, nil)
	assert.Assert(t, c != nil)
	assert.Assert(t, pool.checkers[0] == c, "diagnostics should use checker index 0")
	release()
}

func TestCheckerPoolQueryRouting(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	// Query requests should get a checker at index > 0.
	ctx := core.WithRequestID(context.Background(), "query-req-1")
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
	c, release := pool.GetChecker(ctx, nil)
	assert.Assert(t, c != nil)

	// Verify it's not the diagnostics checker slot.
	assert.Assert(t, pool.checkers[0] != c, "query should not use checker index 0")
	release()
}

func TestCheckerPoolRequestAffinity(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	ctx, cancel := context.WithCancel(t.Context())
	defer cancel()
	ctx = core.WithRequestID(ctx, "req-affinity")
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)

	// First call acquires.
	c1, release1 := pool.GetChecker(ctx, nil)

	// Second call with same request ID while still held returns same checker (noop release).
	c2, release2 := pool.GetChecker(ctx, nil)
	release2()
	release1()

	assert.Assert(t, c1 == c2, "same request ID should return the same checker while held")

	// After release, same request should still get the same checker (cross-release affinity).
	c3, release3 := pool.GetChecker(ctx, nil)
	release3()

	assert.Assert(t, c1 == c3, "same request ID should return the same checker after release")
}

func TestCheckerPoolIdleCleanup(t *testing.T) {
	t.Parallel()
	// Get a real program to use for checker creation, then test the pool
	// with fake time via synctest.
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 5 * time.Second})

		// Create a checker via a diagnostics request.
		ctx := core.WithRequestID(context.Background(), "diag-cleanup")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		// Create a query checker as well.
		ctx2 := core.WithRequestID(context.Background(), "query-cleanup")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != nil)
		release2()
		synctest.Wait()

		// Both checkers should exist.
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] != nil, "diagnostics checker should exist")
		var queryIdx int
		for i := 1; i < len(pool.checkers); i++ {
			if pool.checkers[i] != nil {
				queryIdx = i
				break
			}
		}
		assert.Assert(t, queryIdx > 0, "query checker should exist")
		pool.mu.Unlock()

		// Advance past idle timeout.
		time.Sleep(5 * time.Second)
		synctest.Wait()

		// After cleanup, both checkers should be disposed.
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] == nil, "diagnostics checker should be disposed after idle timeout")
		assert.Assert(t, pool.checkers[queryIdx] == nil, "query checker should be disposed after idle timeout")
		pool.mu.Unlock()
	})
}

func TestCheckerPoolFileAssociationCleanup(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()
	sourceFile := program.GetSourceFile("/src/index.ts")
	assert.Assert(t, sourceFile != nil)

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 5 * time.Second})

		// Create a query checker with file affinity.
		ctx := core.WithRequestID(context.Background(), "file-assoc-req")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, sourceFile)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		// File association should exist.
		pool.mu.Lock()
		_, hasAssoc := pool.fileAssociations[sourceFile]
		pool.mu.Unlock()
		assert.Assert(t, hasAssoc, "file should have a checker association")

		// Advance past idle timeout.
		time.Sleep(5 * time.Second)
		synctest.Wait()

		// File association should be cleared.
		pool.mu.Lock()
		_, hasAssoc = pool.fileAssociations[sourceFile]
		pool.mu.Unlock()
		assert.Assert(t, !hasAssoc, "file association should be cleared after checker disposal")
	})
}

func TestCheckerPoolMinCheckers(t *testing.T) {
	t.Parallel()
	// Requesting maxCheckers=1 should be clamped to 2.
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 1, IdleTimeout: 10 * time.Second})
	assert.Equal(t, pool.opts.MaxCheckers, 2)
	assert.Equal(t, len(pool.checkers), 2)
}

func TestCheckerPoolDefaultIdleTimeout(t *testing.T) {
	t.Parallel()
	// Zero idle timeout should default to 30s.
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4})
	assert.Equal(t, pool.opts.IdleTimeout, 30*time.Second)
}

func TestCheckerPoolQueryContention(t *testing.T) {
	t.Parallel()
	// maxCheckers=2 means 1 diagnostics + 1 query checker slot.
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 30 * time.Second})

		// Acquire the only query checker slot.
		ctx1 := core.WithRequestID(context.Background(), "query-hold")
		ctx1 = core.WithCheckerLifetime(ctx1, core.CheckerLifetimeTemporary)
		c1, release1 := pool.GetChecker(ctx1, nil)
		assert.Assert(t, c1 != nil)

		// A second query request from a different request ID should block.
		var c2Got atomic.Bool
		go func() {
			ctx2 := core.WithRequestID(context.Background(), "query-wait")
			ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
			c2, release2 := pool.GetChecker(ctx2, nil)
			_ = c2 // verified via c2Got flag
			c2Got.Store(c2 != nil)
			release2()
		}()

		// Wait for goroutine to reach the semaphore send.
		synctest.Wait()
		assert.Assert(t, !c2Got.Load(), "second query should be blocked while first holds the checker")

		// Release the first checker — second should unblock.
		release1()
		synctest.Wait()
		assert.Assert(t, c2Got.Load(), "second query should have acquired the checker after release")
	})
}

func TestCheckerPoolDiagnosticsContention(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 30 * time.Second})

		// Acquire the diagnostics checker.
		ctx1 := core.WithRequestID(context.Background(), "diag-hold")
		ctx1 = core.WithCheckerLifetime(ctx1, core.CheckerLifetimeDiagnostics)
		c1, release1 := pool.GetChecker(ctx1, nil)
		assert.Assert(t, c1 != nil)

		// A second diagnostics request should block since there's only one diag checker.
		var c2Got atomic.Bool
		go func() {
			ctx2 := core.WithRequestID(context.Background(), "diag-wait")
			ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeDiagnostics)
			c2, release2 := pool.GetChecker(ctx2, nil)
			_ = c2 // verified via c2Got flag
			c2Got.Store(c2 != nil)
			release2()
		}()

		synctest.Wait()
		assert.Assert(t, !c2Got.Load(), "second diagnostics request should be blocked")

		// A query request should NOT be blocked (separate slot).
		ctx3 := core.WithRequestID(context.Background(), "query-concurrent")
		ctx3 = core.WithCheckerLifetime(ctx3, core.CheckerLifetimeTemporary)
		c3, release3 := pool.GetChecker(ctx3, nil)
		assert.Assert(t, c3 != nil)
		assert.Assert(t, c3 != c1, "query checker should be different from diagnostics checker")
		release3()

		// Release the diagnostics checker — second diag request should unblock.
		release1()
		synctest.Wait()
		assert.Assert(t, c2Got.Load(), "second diagnostics request should have acquired the checker after release")
	})
}

func TestCheckerPoolCanceledCheckerDisposal(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()
	sourceFile := program.GetSourceFile("/src/index.ts")
	assert.Assert(t, sourceFile != nil)

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Acquire a query checker and cancel it.
		ctx := core.WithRequestID(context.Background(), "cancel-test")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)

		canceledCtx, cancel := context.WithCancel(context.Background())
		cancel()
		c.GetDiagnostics(canceledCtx, sourceFile)
		assert.Assert(t, c.WasCanceled())

		// Release should dispose the canceled checker.
		release()
		synctest.Wait()

		// Next request should get a fresh checker.
		ctx2 := core.WithRequestID(context.Background(), "after-cancel")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != c, "should get a new checker, not the canceled one")
		release2()
	})
}

func TestCheckerPoolRequestAssociationCleanupOnDisposal(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 5 * time.Second})

		// Create a query checker with a request association.
		reqCtx, reqCancel := context.WithCancel(context.Background())
		defer reqCancel()
		ctx := core.WithRequestID(reqCtx, "assoc-cleanup-req")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)

		// Cancel the checker to trigger disposal on release.
		canceledCtx, cancel := context.WithCancel(context.Background())
		cancel()
		sourceFile := program.GetSourceFile("/src/index.ts")
		c.GetDiagnostics(canceledCtx, sourceFile)
		assert.Assert(t, c.WasCanceled())

		release()
		synctest.Wait()

		// Request association should be cleared after checker disposal.
		pool.mu.Lock()
		_, hasReqAssoc := pool.requestAssociations["assoc-cleanup-req"]
		pool.mu.Unlock()
		assert.Assert(t, !hasReqAssoc, "request association should be cleared after checker disposal")
	})
}

func TestCheckerPoolRequestAssociationCleanupOnContextDone(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Create a cancellable context to simulate request lifecycle.
		reqCtx, reqCancel := context.WithCancel(context.Background())
		ctx := core.WithRequestID(reqCtx, "ctx-cleanup-req")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)

		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		// Association should still exist after release.
		pool.mu.Lock()
		_, hasAssoc := pool.requestAssociations["ctx-cleanup-req"]
		pool.mu.Unlock()
		assert.Assert(t, hasAssoc, "request association should persist after release")

		// Cancel the request context — association should be cleaned up.
		reqCancel()
		synctest.Wait()

		pool.mu.Lock()
		_, hasAssoc = pool.requestAssociations["ctx-cleanup-req"]
		pool.mu.Unlock()
		assert.Assert(t, !hasAssoc, "request association should be cleaned up after context cancellation")
	})
}

func TestCheckerPoolDiagnosticsRecreatedAfterIdleDisposal(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 5 * time.Second})

		// Create and release diagnostics checker.
		ctx := core.WithRequestID(context.Background(), "diag-recreate-1")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
		c1, release1 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c1 != nil)
		release1()
		synctest.Wait()

		// Advance past idle timeout — diagnostics checker should be disposed.
		time.Sleep(5 * time.Second)
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] == nil, "diagnostics checker should be disposed")
		pool.mu.Unlock()

		// Request diagnostics checker again — should get a fresh one.
		ctx2 := core.WithRequestID(context.Background(), "diag-recreate-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeDiagnostics)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != nil, "diagnostics checker should be re-created")
		assert.Assert(t, c2 != c1, "should be a new checker instance")
		release2()
	})
}

func TestCheckerPoolCrossReleaseAffinityWithContention(t *testing.T) {
	t.Parallel()
	// maxCheckers=2: 1 diagnostics + 1 query slot.
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 30 * time.Second})

		reqCtx, reqCancel := context.WithCancel(context.Background())
		defer reqCancel()

		// Request A acquires the only query slot.
		ctxA := core.WithRequestID(reqCtx, "req-A")
		ctxA = core.WithCheckerLifetime(ctxA, core.CheckerLifetimeTemporary)
		cA, releaseA := pool.GetChecker(ctxA, nil)
		assert.Assert(t, cA != nil)
		releaseA()
		synctest.Wait()

		// Request B takes the query slot while A is released.
		ctxB := core.WithRequestID(context.Background(), "req-B")
		ctxB = core.WithCheckerLifetime(ctxB, core.CheckerLifetimeTemporary)
		cB, releaseB := pool.GetChecker(ctxB, nil)
		assert.Assert(t, cB != nil)

		// Request A reacquires — should block because B holds the slot.
		var reacquired atomic.Bool
		cA2Ch := make(chan *checker.Checker, 1)
		go func() {
			c, release := pool.GetChecker(ctxA, nil)
			cA2Ch <- c
			reacquired.Store(true)
			release()
		}()

		synctest.Wait()
		assert.Assert(t, !reacquired.Load(), "request A should block while B holds the slot")

		// Release B — A should unblock and get the same checker.
		releaseB()
		synctest.Wait()
		assert.Assert(t, reacquired.Load(), "request A should unblock after B releases")
		select {
		case cA2 := <-cA2Ch:
			assert.Assert(t, cA2 == cA, "request A should get the same checker on reacquire")
		case <-t.Context().Done():
			t.Fatal("timed out waiting for reacquired checker")
		}
	})
}

func TestCheckerPoolLifetimeMismatchIgnoresAssociation(t *testing.T) {
	t.Parallel()
	// Verify that if a request first uses a diagnostics checker, then switches
	// to a temporary lifetime (or vice versa), the stale association is ignored
	// rather than returning a checker from the wrong category.
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		reqCtx, reqCancel := context.WithCancel(context.Background())
		defer reqCancel()

		// Acquire a diagnostics checker with request ID "mixed".
		ctxDiag := core.WithRequestID(reqCtx, "mixed")
		ctxDiag = core.WithCheckerLifetime(ctxDiag, core.CheckerLifetimeDiagnostics)
		cDiag, releaseDiag := pool.GetChecker(ctxDiag, nil)
		assert.Assert(t, cDiag != nil)
		assert.Assert(t, pool.checkers[0] == cDiag, "diagnostics checker should be at index 0")
		releaseDiag()
		synctest.Wait()

		// Now use the same request ID but with query purpose.
		// The old association points to index 0 (diagnostics), which should
		// be rejected — the returned checker must be a query checker (index > 0).
		ctxQuery := core.WithRequestID(reqCtx, "mixed")
		ctxQuery = core.WithCheckerLifetime(ctxQuery, core.CheckerLifetimeTemporary)
		cQuery, releaseQuery := pool.GetChecker(ctxQuery, nil)
		assert.Assert(t, cQuery != nil)
		assert.Assert(t, cQuery != cDiag, "query should not reuse the diagnostics checker")

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] != cQuery, "query checker should not be at diagnostics index 0")
		pool.mu.Unlock()
		releaseQuery()
	})
}

func TestCheckerPoolNoRequestID(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	// Calls without a request ID should still work (e.g., callhierarchy uses context.Background()).
	ctx := context.Background()

	c1, release1 := pool.GetChecker(ctx, nil)
	assert.Assert(t, c1 != nil)
	release1()

	c2, release2 := pool.GetChecker(ctx, nil)
	assert.Assert(t, c2 != nil)
	release2()

	// Without request ID, no affinity guarantee — just verify it doesn't crash.
}

func TestCheckerPoolDiagnosticsCrossReleaseAffinity(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		reqCtx, reqCancel := context.WithCancel(context.Background())
		defer reqCancel()
		ctx := core.WithRequestID(reqCtx, "diag-affinity")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)

		c1, release1 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c1 != nil)
		assert.Assert(t, pool.checkers[0] == c1, "should be the diagnostics checker")
		release1()
		synctest.Wait()

		// Same request reacquiring diagnostics should get the same checker.
		c2, release2 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c2 == c1, "same diagnostics request should get the same checker after release")
		release2()
	})
}

func TestCheckerPoolDiscardKeepsIdleCheckers(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Create both a diagnostics and a query checker.
		ctx1 := core.WithRequestID(context.Background(), "obs-diag")
		ctx1 = core.WithCheckerLifetime(ctx1, core.CheckerLifetimeDiagnostics)
		c1, release1 := pool.GetChecker(ctx1, nil)
		assert.Assert(t, c1 != nil)
		release1()
		synctest.Wait()

		ctx2 := core.WithRequestID(context.Background(), "obs-query")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != nil)
		release2()
		synctest.Wait()

		// Both checkers should exist before Discard.
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] != nil, "diagnostics checker should exist")
		pool.mu.Unlock()

		// Discard should keep idle checkers alive (they may be referenced by
		// API type handles) and just stop the cleanup timer.
		pool.Discard()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] == c1, "diagnostics checker should survive Discard")
		hasQuery := false
		for i := 1; i < len(pool.checkers); i++ {
			if pool.checkers[i] == c2 {
				hasQuery = true
				break
			}
		}
		assert.Assert(t, hasQuery, "query checker should survive Discard")
		assert.Assert(t, pool.cleanupTimer == nil, "cleanup timer should be stopped after Discard")
		pool.mu.Unlock()

		// Even after a long wait, checkers should not be disposed (no timer running).
		time.Sleep(60 * time.Second)
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] == c1, "diagnostics checker should persist indefinitely on discarded pool")
		pool.mu.Unlock()
	})
}

func TestCheckerPoolDiscardHeldCheckerSurvivesRelease(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Acquire a checker and hold it.
		ctx := core.WithRequestID(context.Background(), "held-obs")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)

		// Find which slot it's in.
		pool.mu.Lock()
		var heldIndex int
		for i := 1; i < len(pool.checkers); i++ {
			if pool.checkers[i] == c {
				heldIndex = i
				break
			}
		}
		pool.mu.Unlock()
		assert.Assert(t, heldIndex > 0, "should find the held checker")

		// Discard while checker is held — should NOT dispose it.
		pool.Discard()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[heldIndex] == c, "held checker should survive Discard")
		pool.mu.Unlock()

		// Release — checker should remain alive on a discarded pool.
		release()
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[heldIndex] == c, "checker should persist after release on discarded pool")
		pool.mu.Unlock()

		// Even after a long wait, checker persists (no cleanup timer running).
		time.Sleep(60 * time.Second)
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[heldIndex] == c, "checker should persist indefinitely on discarded pool")
		pool.mu.Unlock()
	})
}

func TestCheckerPoolDiscardStillFunctional(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})
		pool.Discard()

		// Pool should still work — GetChecker should create a fresh checker.
		ctx := core.WithRequestID(context.Background(), "post-obs")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil, "should still create checkers after Discard")

		// Find the slot.
		pool.mu.Lock()
		var idx int
		for i := 1; i < len(pool.checkers); i++ {
			if pool.checkers[i] == c {
				idx = i
				break
			}
		}
		pool.mu.Unlock()
		assert.Assert(t, idx > 0, "checker should be in a query slot")

		// Release — checker should persist on discarded pool (no cleanup timer).
		release()
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[idx] == c, "checker should persist after release on discarded pool")
		pool.mu.Unlock()

		// Re-acquire — should get the same checker back.
		ctx2 := core.WithRequestID(context.Background(), "post-obs-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 == c, "should get the same checker on discarded pool")
		release2()
	})
}

func TestCheckerPoolDiagnosticsCheckerStableIdentity(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Acquire the diagnostics checker.
		ctx := core.WithRequestID(context.Background(), "diag-stable-1")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
		c1, release1 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c1 != nil, "diagnostics checker should be created")
		release1()
		synctest.Wait()

		// Re-acquire before idle timeout — should be the same instance.
		ctx2 := core.WithRequestID(context.Background(), "diag-stable-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeDiagnostics)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 == c1, "diagnostics checker should be the same instance before idle timeout")
		release2()
	})
}

func TestCheckerPoolDiagnosticsCheckerSurvivesDiscard(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Create the diagnostics checker.
		ctx := core.WithRequestID(context.Background(), "diag-discard")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		pool.Discard()

		// Diagnostics checker should survive Discard.
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] == c, "diagnostics checker should survive Discard")
		pool.mu.Unlock()

		// Should still be acquirable and be the same instance.
		ctx2 := core.WithRequestID(context.Background(), "diag-discard-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeDiagnostics)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 == c, "diagnostics checker identity should be stable after Discard")
		release2()
	})
}

func TestCheckerPoolDiagnosticsCheckerIndependentFromQuery(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Acquire diagnostics and query checkers.
		diagCtx := core.WithRequestID(context.Background(), "diag-indep")
		diagCtx = core.WithCheckerLifetime(diagCtx, core.CheckerLifetimeDiagnostics)
		queryCtx := core.WithRequestID(context.Background(), "query-indep")
		queryCtx = core.WithCheckerLifetime(queryCtx, core.CheckerLifetimeTemporary)

		diagC, diagRelease := pool.GetChecker(diagCtx, nil)
		queryC, queryRelease := pool.GetChecker(queryCtx, nil)

		// They should be different checker instances.
		assert.Assert(t, diagC != queryC, "diagnostics and query checkers should be different")

		diagRelease()
		queryRelease()
	})
}

func TestCheckerPoolAPICheckerStableIdentity(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		ctx := core.WithCheckerLifetime(context.Background(), core.CheckerLifetimeAPI)
		c1, release1 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c1 != nil)
		release1()

		c2, release2 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c2 == c1, "API checker should be the same instance")
		release2()

		// Should survive idle timeout.
		time.Sleep(60 * time.Second)
		synctest.Wait()

		c3, release3 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c3 == c1, "API checker should survive idle timeout")
		release3()
	})
}

func TestCheckerPoolAPICheckerSurvivesDiscard(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		ctx := core.WithCheckerLifetime(context.Background(), core.CheckerLifetimeAPI)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()

		pool.Discard()

		pool.mu.Lock()
		assert.Assert(t, pool.persistentChecker == c, "API checker should survive Discard")
		pool.mu.Unlock()

		c2, release2 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c2 == c, "API checker identity should be stable after Discard")
		release2()
	})
}

func TestCheckerPoolAllThreeIndependent(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		dedCtx := core.WithRequestID(context.Background(), "ded-req")
		dedCtx = core.WithCheckerLifetime(dedCtx, core.CheckerLifetimeDiagnostics)
		tmpCtx := core.WithRequestID(context.Background(), "tmp-req")
		tmpCtx = core.WithCheckerLifetime(tmpCtx, core.CheckerLifetimeTemporary)
		perCtx := core.WithCheckerLifetime(context.Background(), core.CheckerLifetimeAPI)

		dedC, dedRelease := pool.GetChecker(dedCtx, nil)
		tmpC, tmpRelease := pool.GetChecker(tmpCtx, nil)
		perC, perRelease := pool.GetChecker(perCtx, nil)

		assert.Assert(t, dedC != tmpC, "diagnostics and temporary should be different")
		assert.Assert(t, dedC != perC, "diagnostics and API should be different")
		assert.Assert(t, tmpC != perC, "temporary and API should be different")

		dedRelease()
		tmpRelease()
		perRelease()
	})
}

func TestCheckerPoolFileAffinity(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()
	sourceFile := program.GetSourceFile("/src/index.ts")
	assert.Assert(t, sourceFile != nil)

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// First query with a file should create a checker and associate it.
		ctx1 := core.WithRequestID(context.Background(), "file-aff-1")
		ctx1 = core.WithCheckerLifetime(ctx1, core.CheckerLifetimeTemporary)
		c1, release1 := pool.GetChecker(ctx1, sourceFile)
		assert.Assert(t, c1 != nil)
		release1()
		synctest.Wait()

		// Second query with the same file (different request) should get the same checker via file affinity.
		ctx2 := core.WithRequestID(context.Background(), "file-aff-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, sourceFile)
		assert.Assert(t, c2 != nil)
		assert.Assert(t, c2 == c1, "same file should return the same checker via file affinity")
		release2()
	})
}

func TestCheckerPoolMultipleConcurrentQueryCheckers(t *testing.T) {
	t.Parallel()
	// maxCheckers=4: 1 diagnostics + 3 query slots.
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Acquire 3 query checkers concurrently (all slots).
		ctx1 := core.WithRequestID(context.Background(), "multi-q-1")
		ctx1 = core.WithCheckerLifetime(ctx1, core.CheckerLifetimeTemporary)
		c1, release1 := pool.GetChecker(ctx1, nil)
		assert.Assert(t, c1 != nil)

		ctx2 := core.WithRequestID(context.Background(), "multi-q-2")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != nil)

		ctx3 := core.WithRequestID(context.Background(), "multi-q-3")
		ctx3 = core.WithCheckerLifetime(ctx3, core.CheckerLifetimeTemporary)
		c3, release3 := pool.GetChecker(ctx3, nil)
		assert.Assert(t, c3 != nil)

		// All three should be distinct checkers.
		assert.Assert(t, c1 != c2, "concurrent query checkers should be distinct (1 vs 2)")
		assert.Assert(t, c1 != c3, "concurrent query checkers should be distinct (1 vs 3)")
		assert.Assert(t, c2 != c3, "concurrent query checkers should be distinct (2 vs 3)")

		// None should be the diagnostics checker at index 0.
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[0] != c1 && pool.checkers[0] != c2 && pool.checkers[0] != c3,
			"query checkers should not occupy the diagnostics slot")
		pool.mu.Unlock()

		// A 4th query request should block since all 3 slots are full.
		var c4Got atomic.Bool
		go func() {
			ctx4 := core.WithRequestID(context.Background(), "multi-q-4")
			ctx4 = core.WithCheckerLifetime(ctx4, core.CheckerLifetimeTemporary)
			c4, release4 := pool.GetChecker(ctx4, nil)
			_ = c4 // verified via c4Got flag
			c4Got.Store(c4 != nil)
			release4()
		}()

		synctest.Wait()
		assert.Assert(t, !c4Got.Load(), "4th query should block when all 3 query slots are held")

		release1()
		synctest.Wait()
		assert.Assert(t, c4Got.Load(), "4th query should unblock after one slot is released")

		release2()
		release3()
	})
}

func TestCheckerPoolDoubleReleaseSafe(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	ctx := core.WithRequestID(context.Background(), "double-release")
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
	c, release := pool.GetChecker(ctx, nil)
	assert.Assert(t, c != nil)

	// First release should work normally.
	release()
	// Second release should be a no-op (sync.OnceFunc).
	release()

	// Pool should still be functional after double release.
	ctx2 := core.WithRequestID(context.Background(), "after-double")
	ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
	c2, release2 := pool.GetChecker(ctx2, nil)
	assert.Assert(t, c2 != nil)
	release2()
}

func TestCheckerPoolDefaultMaxCheckers(t *testing.T) {
	t.Parallel()
	// Zero MaxCheckers should default to 4.
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 0, IdleTimeout: 10 * time.Second})
	assert.Equal(t, pool.opts.MaxCheckers, 4)
	assert.Equal(t, len(pool.checkers), 4)
	assert.Equal(t, cap(pool.querySem), 3, "querySem capacity should be MaxCheckers-1")
}

func TestCheckerPoolStaggeredIdleCleanup(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

		// Acquire checker A and hold it.
		ctxA := core.WithRequestID(context.Background(), "stagger-A")
		ctxA = core.WithCheckerLifetime(ctxA, core.CheckerLifetimeTemporary)
		cA, releaseA := pool.GetChecker(ctxA, nil)
		assert.Assert(t, cA != nil)

		// While A is held, acquire a second checker B.
		ctxB := core.WithRequestID(context.Background(), "stagger-B")
		ctxB = core.WithCheckerLifetime(ctxB, core.CheckerLifetimeTemporary)
		cB, releaseB := pool.GetChecker(ctxB, nil)
		assert.Assert(t, cB != nil)
		assert.Assert(t, cB != cA, "B should be a different checker since A is held")

		// Find their indices.
		pool.mu.Lock()
		var idxA, idxB int
		for i := 1; i < len(pool.checkers); i++ {
			if pool.checkers[i] == cA {
				idxA = i
			}
			if pool.checkers[i] == cB {
				idxB = i
			}
		}
		pool.mu.Unlock()
		assert.Assert(t, idxA > 0)
		assert.Assert(t, idxB > 0)

		// Release A first. Timer is set for t=10.
		releaseA()
		synctest.Wait()

		// Release B 6 seconds later. Timer is reset for t=16.
		time.Sleep(6 * time.Second)
		releaseB()
		synctest.Wait()

		// At t < 16, both should still exist (timer hasn't fired).
		pool.mu.Lock()
		assert.Assert(t, pool.checkers[idxA] != nil, "checker A should still exist before timer fires")
		assert.Assert(t, pool.checkers[idxB] != nil, "checker B should still exist before timer fires")
		pool.mu.Unlock()

		// Advance past t=16 (when the timer fires). Both should be disposed
		// because A has been idle 16s and B has been idle 10s.
		time.Sleep(11 * time.Second)
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.checkers[idxA] == nil, "checker A should be disposed after timer fires")
		assert.Assert(t, pool.checkers[idxB] == nil, "checker B should be disposed after timer fires")
		pool.mu.Unlock()
	})
}

func TestCheckerPoolDiscardIdempotent(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 2, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// Create a checker so there's something to discard.
		ctx := core.WithRequestID(context.Background(), "idem-q")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		// First discard should keep idle checkers alive.
		pool.Discard()
		pool.mu.Lock()
		hasChecker := false
		for _, c := range pool.checkers {
			if c != nil {
				hasChecker = true
				break
			}
		}
		pool.mu.Unlock()
		assert.Assert(t, hasChecker, "first Discard should keep idle checkers alive")

		// Second discard should be a no-op (no panic, no state corruption).
		pool.Discard()

		// Pool should still be functional after double Discard.
		ctx2 := core.WithRequestID(context.Background(), "post-idem")
		ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
		c2, release2 := pool.GetChecker(ctx2, nil)
		assert.Assert(t, c2 != nil, "pool should still work after double Discard")
		release2()
	})
}

func TestCheckerPoolGetGlobalDiagnosticsEmpty(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	// Before any checker is used, global diagnostics should be empty.
	diags := pool.GetGlobalDiagnostics()
	assert.Equal(t, len(diags), 0, "global diagnostics should be empty initially")
}

func TestCheckerPoolTakeNewGlobalDiagnostics(t *testing.T) {
	t.Parallel()
	_, pool := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})

	// Initially, no new globals.
	assert.Assert(t, !pool.TakeNewGlobalDiagnostics(), "should report no new globals initially")

	// Use a checker and trigger diagnostics, then release to run the merge.
	ctx := core.WithRequestID(context.Background(), "global-diag-req")
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
	sourceFile := pool.program.GetSourceFile("/src/index.ts")
	c, release := pool.GetChecker(ctx, sourceFile)
	assert.Assert(t, c != nil)
	c.GetDiagnostics(ctx, sourceFile)
	release()

	// Whether globals were produced depends on the program, but the flag
	// should reflect the merge result.
	firstTake := pool.TakeNewGlobalDiagnostics()

	// After taking, a second call should always return false (flag is reset).
	assert.Assert(t, !pool.TakeNewGlobalDiagnostics(), "TakeNewGlobalDiagnostics should reset after first call")

	// Releasing the same checker again with the same state should not set the flag.
	ctx2 := core.WithRequestID(context.Background(), "global-diag-req-2")
	ctx2 = core.WithCheckerLifetime(ctx2, core.CheckerLifetimeTemporary)
	c2, release2 := pool.GetChecker(ctx2, sourceFile)
	assert.Assert(t, c2 != nil)
	c2.GetDiagnostics(ctx2, sourceFile)
	release2()

	// If first call produced globals, the count is now stable, so no new change.
	// If first call produced no globals, still no change.
	_ = firstTake
	assert.Assert(t, !pool.TakeNewGlobalDiagnostics(), "should not report new globals when checker state is unchanged")
}

func TestCheckerPoolAPICheckerDisposedOnCancel(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()
	sourceFile := program.GetSourceFile("/src/index.ts")
	assert.Assert(t, sourceFile != nil)

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		ctx := core.WithCheckerLifetime(context.Background(), core.CheckerLifetimeAPI)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)

		// Cancel the API checker.
		canceledCtx, cancel := context.WithCancel(context.Background())
		cancel()
		c.GetDiagnostics(canceledCtx, sourceFile)
		assert.Assert(t, c.WasCanceled())

		// Releasing a canceled API checker must drop it so it isn't reused.
		release()
		synctest.Wait()

		pool.mu.Lock()
		assert.Assert(t, pool.persistentChecker == nil, "canceled API checker should be dropped on release")
		pool.mu.Unlock()

		// Next API acquisition gets a fresh, usable checker rather than panicking.
		c2, release2 := pool.GetChecker(ctx, nil)
		assert.Assert(t, c2 != c, "should get a fresh API checker after cancellation")
		release2()
	})
}

func TestCheckerPoolNonCancelableContextNoAffinity(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		// A context that carries a request ID but can never be canceled
		// (ctx.Done() == nil) must not register a request association, since
		// there would be no way to clean it up.
		ctx := core.WithRequestID(context.Background(), "uncancelable-req")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		assert.Assert(t, ctx.Done() == nil, "test precondition: context must be non-cancelable")

		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		pool.mu.Lock()
		assert.Equal(t, len(pool.requestAssociations), 0, "non-cancelable context must not grow requestAssociations")
		pool.mu.Unlock()
	})
}

func TestCheckerPoolCleanupAfterDiscardIsNoop(t *testing.T) {
	t.Parallel()
	session, _ := setupCheckerPoolSession(t, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 10 * time.Second})
	ls, err := session.GetLanguageService(context.Background(), "file:///src/index.ts")
	assert.NilError(t, err)
	program := ls.GetProgram()

	synctest.Test(t, func(t *testing.T) {
		pool := newTestCheckerPool(program, CheckerPoolOptions{MaxCheckers: 4, IdleTimeout: 30 * time.Second})

		ctx := core.WithRequestID(context.Background(), "discard-cleanup")
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeTemporary)
		c, release := pool.GetChecker(ctx, nil)
		assert.Assert(t, c != nil)
		release()
		synctest.Wait()

		pool.Discard()

		// Simulate the timer callback firing after Discard() (Stop() does not
		// guarantee the callback won't run). It must be a no-op and must not
		// re-arm the cleanup timer, which would keep the discarded pool alive.
		pool.cleanupIdleCheckers()

		pool.mu.Lock()
		assert.Assert(t, pool.cleanupTimer == nil, "cleanup must not reschedule a timer on a discarded pool")
		hasChecker := false
		for _, cc := range pool.checkers {
			if cc != nil {
				hasChecker = true
			}
		}
		assert.Assert(t, hasChecker, "idle checkers must survive cleanup on a discarded pool")
		pool.mu.Unlock()
	})
}
