"""Concurrency tests: mmap thread safety and FastAPI multi-worker scenarios.

Tests cover two areas flagged in NEXT_SESSION.md / accordance_api.py:
  A — mmap thread safety: concurrent reads from read-only memory-mapped files
  B — FastAPI multi-worker: lazy-loading races, search-index init, rate limiter

Categories:
  CAT-A (12 tests): mmap thread safety (CommentaryModule, LexiconModule, TextModule)
  CAT-B (13 tests): FastAPI TestClient + ThreadPoolExecutor scenarios
"""
from __future__ import annotations
import pytest
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed

# =============================================================================
# IMPORT GUARDS
# =============================================================================

# --- Base module (mmap) ---
try:
    from accordance_parser.module import AccordanceModule
    _has_module = True
except Exception:
    _has_module = False

# --- Commentary ---
_has_commentary = False
_commentary_modules: dict[str, str] = {}
try:
    from accordance_parser.commentary_module import CommentaryModule
    from accordance_parser.discovery import discover_modules
    _mods = discover_modules()
    for abbr, info in _mods.items():
        if info.get("type") in (5, 6):
            _commentary_modules[abbr] = info["path"]
    if _commentary_modules:
        _has_commentary = True
except Exception:
    pass

# --- Lexicon ---
_has_lexicon = False
_lexicon_modules: dict[str, str] = {}
try:
    from accordance_parser.lexicon_module import LexiconModule
    for abbr, info in _mods.items():
        if info.get("type") in (2, 3, 4):
            _lexicon_modules[abbr] = info["path"]
    if _lexicon_modules:
        _has_lexicon = True
except Exception:
    pass

# --- Text ---
_has_text = False
_text_modules: dict[str, str] = {}
try:
    from accordance_parser.text_module import TextModule
    for abbr, info in _mods.items():
        if info.get("type") == 1:
            _text_modules[abbr] = info["path"]
    if _text_modules:
        _has_text = True
except Exception:
    pass

# --- FastAPI ---
_has_api = False
_api_import_error = ""
try:
    from accordance_api import (
        app, _modules,
    )
    from fastapi.testclient import TestClient
    _has_api = True
except Exception as e:
    _api_import_error = str(e)


# =============================================================================
# HELPERS
# =============================================================================

def _no_traceback(text: str) -> bool:
    """Ensure the text does not contain a Python traceback."""
    return "Traceback (most recent call last)" not in text


def _pick_first_commentary_abbr() -> str | None:
    """Return the abbreviation of the first available commentary module."""
    if not _commentary_modules:
        return None
    for k in sorted(_commentary_modules):
        return k
    return None


def _pick_first_lexicon_abbr() -> str | None:
    """Return the abbreviation of the first available lexicon module."""
    if not _lexicon_modules:
        return None
    for k in sorted(_lexicon_modules):
        return k
    return None


def _pick_first_text_abbr() -> str | None:
    """Return the abbreviation of the first available text module."""
    if not _text_modules:
        return None
    for k in sorted(_text_modules):
        return k
    return None


# =============================================================================
# CAT-A:  mmap Thread Safety
# =============================================================================

@pytest.mark.skipif(not _has_commentary, reason="No commentary modules available")
class TestMmapThreadSafety:
    """Concurrent read-only mmap access tests."""

    # ── helpers ──────────────────────────────────────────────────────────

    @staticmethod
    def _load_module(path: str):
        """Load a module and return its size. Closure-friendly."""
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            return None
        return mod

    @staticmethod
    def _read_section_worker(mod: CommentaryModule, ref: str,
                             results: list, idx: int):
        """Thread worker: read a section, store result."""
        try:
            result = mod.get_section(ref)
            results[idx] = result
        except Exception as e:
            results[idx] = f"ERROR: {e}"

    @staticmethod
    def _read_bytes_worker(mod: AccordanceModule, offset: int, length: int,
                           results: list, idx: int):
        """Thread worker: read bytes at offset, store result."""
        try:
            results[idx] = bytes(mod.mm[offset:offset + length])
        except Exception as e:
            results[idx] = f"ERROR: {e}"

    @staticmethod
    def _find_bytes_worker(mod: AccordanceModule, pattern: bytes,
                           results: list, idx: int):
        """Thread worker: find pattern, store result."""
        try:
            results[idx] = mod.find_bytes(pattern)
        except Exception as e:
            results[idx] = f"ERROR: {e}"

    # ── tests ────────────────────────────────────────────────────────────

    def test_mmap_concurrent_read_same_module(self):
        """Multiple threads read from same mmap at same offsets; all get identical data."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        n_threads = 8
        results: list = [None] * n_threads
        threads = []
        for i in range(n_threads):
            t = threading.Thread(
                target=self._read_bytes_worker,
                args=(mod, 0, 256, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        # All threads should return identical first 256 bytes
        first = results[0]
        assert isinstance(first, bytes), f"Thread 0 got {type(first)}"
        for i in range(1, n_threads):
            assert results[i] == first, (
                f"Thread {i} data differs from thread 0 at same offset"
            )
        mod.close()

    def test_mmap_concurrent_read_different_offsets(self):
        """Threads read different byte ranges concurrently; no cross-contamination."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        offsets = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]
        n_threads = len(offsets)
        results: list = [None] * n_threads
        threads = []
        for i, off in enumerate(offsets):
            t = threading.Thread(
                target=self._read_bytes_worker,
                args=(mod, off, 64, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], bytes), f"Thread {i} got {type(results[i])}"
            assert len(results[i]) == 64, (
                f"Thread {i} at offset {offsets[i]} returned {len(results[i])} bytes"
            )
        mod.close()

    def test_mmap_concurrent_find_bytes(self):
        """Multiple threads call find_bytes() on the same mmap concurrently."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        patterns = [b"\r\r", b"\r", b"\x00\x00", b"John", b"Genesis"]
        n_threads = len(patterns)
        results: list = [None] * n_threads
        threads = []
        for i, pat in enumerate(patterns):
            t = threading.Thread(
                target=self._find_bytes_worker,
                args=(mod, pat, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], int), (
                f"Thread {i} (pattern {patterns[i]!r}) got {type(results[i])}"
            )
            assert results[i] >= -1, (
                f"Thread {i}: find returned invalid position {results[i]}"
            )
        mod.close()

    def test_mmap_concurrent_read_cstring(self):
        """Multiple threads call _read_cstring at different offsets concurrently."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        def _cs_worker(mod, offset, maxlen, results, idx):
            try:
                results[idx] = mod._read_cstring(offset, maxlen)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        offsets = [0, 64, 128, 192, 256, 320]
        n_threads = len(offsets)
        results: list = [None] * n_threads
        threads = []
        for i, off in enumerate(offsets):
            t = threading.Thread(
                target=_cs_worker,
                args=(mod, off, 128, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], str), (
                f"Thread {i} at offset {offsets[i]} got {type(results[i])}: {results[i]!r}"
            )
            assert _no_traceback(results[i])
        mod.close()

    def test_concurrent_commentary_lookups_different_verses(self):
        """Threads call get_section() on same CommentaryModule for different verses."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        refs = [
            "Genesis 1:1", "Genesis 1:2", "Genesis 1:3", "Genesis 1:4",
            "Exodus 20:1", "Exodus 20:2", "Psalm 23:1", "Psalm 23:2",
        ]
        n_threads = len(refs)
        results: list = [None] * n_threads
        threads = []
        for i, ref in enumerate(refs):
            t = threading.Thread(
                target=self._read_section_worker,
                args=(mod, ref, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], str), (
                f"Thread {i} (ref {refs[i]}) got {type(results[i])}"
            )
            assert _no_traceback(results[i]), (
                f"Thread {i} returned traceback for {refs[i]}"
            )
        mod.close()

    def test_concurrent_commentary_lookups_same_verse(self):
        """Threads call get_section() on same module for the SAME verse."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        n_threads = 8
        same_ref = "Genesis 1:1"
        results: list = [None] * n_threads
        threads = []
        for i in range(n_threads):
            t = threading.Thread(
                target=self._read_section_worker,
                args=(mod, same_ref, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        first = results[0]
        for i in range(1, n_threads):
            assert results[i] == first, (
                f"Thread {i} returned different data for same verse '{same_ref}'"
            )
        assert _no_traceback(first)
        mod.close()

    def test_concurrent_commentary_list_books(self):
        """Multiple threads call list_books() on the same module simultaneously."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped by parser")

        def _list_books_worker(mod, results, idx):
            try:
                results[idx] = mod.list_books()
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        n_threads = 6
        results: list = [None] * n_threads
        threads = []
        for i in range(n_threads):
            t = threading.Thread(
                target=_list_books_worker,
                args=(mod, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        first = results[0]
        assert isinstance(first, list)
        for i in range(1, n_threads):
            assert results[i] == first, (
                f"Thread {i} list_books() differs from thread 0"
            )
        mod.close()

    def test_concurrent_module_load_multiple_instances(self):
        """Multiple threads each load their own module instances; verify no interference."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]

        def _load_and_read(path, ref, results, idx):
            try:
                mod = CommentaryModule(path)
                if mod.skipped:
                    mod.close()
                    results[idx] = "SKIPPED"
                    return
                result = mod.get_section(ref)
                results[idx] = result
                mod.close()
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        n_threads = 6
        results: list = [None] * n_threads
        threads = []
        for i in range(n_threads):
            t = threading.Thread(
                target=_load_and_read,
                args=(path, "Genesis 1:1", results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            if results[i] == "SKIPPED":
                continue
            assert isinstance(results[i], str), f"Thread {i} got {type(results[i])}"
            assert _no_traceback(results[i]), f"Thread {i} traceback in {results[i]}"
            assert len(results[i]) > 10, (
                f"Thread {i} returned short result: {results[i][:100]}"
            )

    def test_concurrent_module_close_independence(self):
        """Closing one module instance doesn't affect another thread's module."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]

        mod_a = CommentaryModule(path)
        mod_b = CommentaryModule(path)
        if mod_a.skipped:
            mod_a.close(); mod_b.close()
            pytest.skip(f"{abbr} skipped")

        # Verify both can read independently
        result_a = mod_a.get_section("Genesis 1:1")
        assert _no_traceback(result_a)

        # Close mod_a — mod_b must still work
        mod_a.close()
        result_b = mod_b.get_section("Genesis 1:1")
        assert isinstance(result_b, str)
        assert _no_traceback(result_b)
        assert len(result_b) > 10
        mod_b.close()

    @pytest.mark.skipif(not _has_lexicon, reason="No lexicon modules available")
    def test_concurrent_lexicon_lookup_same_module(self):
        """Multiple threads look up different headwords in the same LexiconModule."""
        abbr = _pick_first_lexicon_abbr()
        if abbr is None:
            pytest.skip("No lexicon modules")
        path = _lexicon_modules[abbr]
        mod = LexiconModule(path)

        headwords = mod.headwords()[:20] if mod.headwords() else []
        if len(headwords) < 4:
            mod.close()
            pytest.skip(f"Not enough headwords in {abbr}")

        # Use first few headwords for concurrent lookup
        hws = headwords[:8]
        n_threads = len(hws)
        results: list = [None] * n_threads

        def _lookup_worker(mod, hw, results, idx):
            try:
                results[idx] = mod.lookup(hw)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i, hw in enumerate(hws):
            t = threading.Thread(
                target=_lookup_worker,
                args=(mod, hw, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], str), (
                f"Thread {i} (hw={hws[i]}) got {type(results[i])}"
            )
            assert _no_traceback(results[i]), (
                f"Thread {i}: traceback for '{hws[i]}': {results[i][:200]}"
            )
        mod.close()

    def test_threaded_read_byte_identity(self):
        """Sequential vs concurrent mmap reads return identical bytes."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules")
        path = _commentary_modules[abbr]
        mod = CommentaryModule(path)
        if mod.skipped:
            mod.close()
            pytest.skip(f"{abbr} skipped")

        offsets = [0, 128, 256, 512, 1024, 2048, 4096]
        # Sequential reads
        seq_results = [bytes(mod.mm[off:off + 32]) for off in offsets]

        # Concurrent reads
        conc_results: list = [None] * len(offsets)
        threads = []
        for i, off in enumerate(offsets):
            t = threading.Thread(
                target=self._read_bytes_worker,
                args=(mod, off, 32, conc_results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(len(offsets)):
            assert conc_results[i] == seq_results[i], (
                f"Offset {offsets[i]}: concurrent read differs from sequential"
            )
        mod.close()

    @pytest.mark.skipif(not _has_text, reason="No text modules available")
    def test_concurrent_text_module_get_verse(self):
        """Multiple threads call get_verse() on the same TextModule concurrently."""
        abbr = _pick_first_text_abbr()
        if abbr is None:
            pytest.skip("No text modules")
        path = _text_modules[abbr]
        mod = TextModule(path, abbr)

        refs = [
            "Genesis 1:1", "Genesis 1:2", "Genesis 1:3",
            "Exodus 20:1", "Exodus 20:2",
            "Psalm 23:1", "Psalm 23:2", "Psalm 23:3",
        ]
        n_threads = len(refs)
        results: list = [None] * n_threads

        def _get_verse_worker(mod, ref, results, idx):
            try:
                results[idx] = mod.get_verse(ref)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i, ref in enumerate(refs):
            t = threading.Thread(
                target=_get_verse_worker,
                args=(mod, ref, results, i),
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert isinstance(results[i], str), f"Thread {i}: {type(results[i])}"
            assert _no_traceback(results[i])
            assert results[i] and len(results[i].strip()) > 0, (
                f"Thread {i}: empty result for {refs[i]}"
            )
        mod.close()


# =============================================================================
# CAT-B:  FastAPI Multi-Worker Scenarios
# =============================================================================


@pytest.mark.skipif(not _has_api, reason=f"FastAPI not available: {_api_import_error}")
class TestFastAPIConcurrency:
    """FastAPI code-path tests for patterns flagged as multi-worker-unsafe."""

    @pytest.fixture(autouse=True)
    def _reset_modules(self):
        """Reset lazy-loaded instances between tests to avoid cross-test pollution."""
        for entry in _modules.values():
            entry["instance"] = None
        # Reset search index singleton
        import accordance_api
        accordance_api._search_index = None
        yield
        for entry in _modules.values():
            entry["instance"] = None
        accordance_api._search_index = None

    # ── Lazy-loading code-path tests ─────────────────────────────────────

    def test_concurrent_get_lexicon_module_same_abbr(self):
        """Multiple threads call _get_lexicon_module() for the same abbrev concurrently."""
        abbr = _pick_first_lexicon_abbr()
        if abbr is None:
            pytest.skip("No lexicon modules available")

        from accordance_api import _get_lexicon_module
        n_threads = 6
        results: list = [None] * n_threads

        def _worker(abbr, results, idx):
            try:
                mod = _get_lexicon_module(abbr)
                results[idx] = mod
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i in range(n_threads):
            t = threading.Thread(target=_worker, args=(abbr, results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert not isinstance(results[i], str) or not results[i].startswith("ERROR"), (
                f"Thread {i} failed: {results[i]}"
            )
        # Known limitation: check-then-set in _get_lexicon_module can produce
        # multiple instances under concurrent load (extra mmap handles, not
        # data corruption). All instances MUST be functional.
        instances = [r for r in results if not (isinstance(r, str) and r.startswith("ERROR"))]
        for inst in instances:
            assert hasattr(inst, "abbrev"), f"Instance {inst} has no abbrev"
            assert inst.abbrev == abbr, f"Instance got wrong module: {inst.abbrev}"

    def test_concurrent_get_commentary_module_same_abbr(self):
        """Multiple threads call _get_commentary_module() for the same abbrev."""
        abbr = _pick_first_commentary_abbr()
        if abbr is None:
            pytest.skip("No commentary modules available")

        from accordance_api import _get_commentary_module
        n_threads = 6
        results: list = [None] * n_threads

        def _worker(abbr, results, idx):
            try:
                mod = _get_commentary_module(abbr)
                results[idx] = mod
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i in range(n_threads):
            t = threading.Thread(target=_worker, args=(abbr, results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert not isinstance(results[i], str) or not results[i].startswith("ERROR"), (
                f"Thread {i} failed: {results[i]}"
            )
        # Known check-then-set race: instances may differ (extra mmap handles
        # but no data corruption). Verify all are functional.
        instances = [r for r in results if not (isinstance(r, str) and r.startswith("ERROR"))]
        for inst in instances:
            assert hasattr(inst, "abbrev"), f"Instance {inst} has no abbrev"

    def test_concurrent_get_text_module_same_abbr(self):
        """Multiple threads call _get_text_module() for the same abbrev."""
        abbr = _pick_first_text_abbr()
        if abbr is None:
            pytest.skip("No text modules available")

        from accordance_api import _get_text_module
        n_threads = 4
        results: list = [None] * n_threads

        def _worker(abbr, results, idx):
            try:
                mod = _get_text_module(abbr)
                results[idx] = mod
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i in range(n_threads):
            t = threading.Thread(target=_worker, args=(abbr, results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert not isinstance(results[i], str) or not results[i].startswith("ERROR"), (
                f"Thread {i} failed: {results[i]}"
            )
        # Known check-then-set race: instances may differ (extra mmap handles
        # but no data corruption). Verify all are functional.
        instances = [r for r in results if not (isinstance(r, str) and r.startswith("ERROR"))]
        for inst in instances:
            assert hasattr(inst, "abbrev"), f"Instance {inst} has no abbrev"

    def test_concurrent_get_search_index(self):
        """Multiple threads call _get_search_index() simultaneously."""
        from accordance_api import _get_search_index
        n_threads = 4
        results: list = [None] * n_threads

        def _worker(results, idx):
            try:
                si = _get_search_index()
                results[idx] = si
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i in range(n_threads):
            t = threading.Thread(target=_worker, args=(results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            if isinstance(results[i], str) and results[i].startswith("ERROR"):
                # Search index may legitimately fail (no DB)
                if "unavailable" in str(results[i]).lower():
                    pytest.skip(f"Search index unavailable: {results[i]}")
                else:
                    pytest.fail(f"Thread {i} failed: {results[i]}")
            assert results[i] is not None
        # All instances must be functional SearchIndex objects (same
        # check-then-set race as module lazy-loading — multiple instances
        # possible but each functional).
        for inst in results:
            if not isinstance(inst, str):
                assert hasattr(inst, "search"), f"Instance {inst} has no search() method"

    def test_concurrent_get_lexicon_module_different(self):
        """Threads load DIFFERENT lexicons concurrently."""
        if len(_lexicon_modules) < 2:
            pytest.skip("Need at least 2 lexicon modules")
        abbrs = sorted(_lexicon_modules.keys())[:4]
        if len(abbrs) < 2:
            pytest.skip("Need at least 2 lexicon abbreviations")

        from accordance_api import _get_lexicon_module
        n_threads = len(abbrs)
        results: list = [None] * n_threads

        def _worker(abbr, results, idx):
            try:
                results[idx] = _get_lexicon_module(abbr)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i, abbr in enumerate(abbrs):
            t = threading.Thread(target=_worker, args=(abbr, results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert not isinstance(results[i], str) or not results[i].startswith("ERROR"), (
                f"Thread {i} ({abbrs[i]}) failed: {results[i]}"
            )
        # Each thread should get a different instance
        ids_seen = {id(r) for r in results}
        assert len(ids_seen) == n_threads, (
            f"Expected {n_threads} different instances, got {len(ids_seen)}"
        )

    def test_concurrent_get_commentary_module_different(self):
        """Threads load DIFFERENT commentaries concurrently."""
        if len(_commentary_modules) < 2:
            pytest.skip("Need at least 2 commentary modules")
        abbrs = sorted(_commentary_modules.keys())[:4]
        if len(abbrs) < 2:
            pytest.skip("Need at least 2 commentary abbreviations")

        from accordance_api import _get_commentary_module
        n_threads = len(abbrs)
        results: list = [None] * n_threads

        def _worker(abbr, results, idx):
            try:
                results[idx] = _get_commentary_module(abbr)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        threads = []
        for i, abbr in enumerate(abbrs):
            t = threading.Thread(target=_worker, args=(abbr, results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        for i in range(n_threads):
            assert not isinstance(results[i], str) or not results[i].startswith("ERROR"), (
                f"Thread {i} ({abbrs[i]}) failed: {results[i]}"
            )
        ids_seen = {id(r) for r in results}
        assert len(ids_seen) == n_threads, (
            f"Expected {n_threads} different instances, got {len(ids_seen)}"
        )

    # ── FastAPI TestClient + ThreadPoolExecutor tests ─────────────────────

    def test_fastapi_sequential_endpoints_no_corruption(self):
        """TestClient calls endpoints in rapid succession; verify no corruption."""
        client = TestClient(app)

        # Health
        r = client.get("/health")
        assert r.status_code == 200
        assert "modules_loaded" in r.json()

        # Bibles list
        r = client.get("/bibles")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

        # Lexicon list
        r = client.get("/lexicons")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

        # Commentary list
        r = client.get("/commentaries")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

        # Modules registry
        r = client.get("/modules")
        assert r.status_code == 200
        data = r.json()
        assert "total_count" in data

        # Rapid repeated calls to same endpoint
        for _ in range(5):
            r = client.get("/health")
            assert r.status_code == 200

    def test_fastapi_bible_verse_threaded(self):
        """ThreadPoolExecutor calls /bibles/{t}/verses concurrently."""
        abbr = _pick_first_text_abbr()
        if abbr is None:
            pytest.skip("No text modules available")

        client = TestClient(app)

        refs = [
            ("Genesis 1:1", abbr),
            ("Genesis 1:2", abbr),
            ("Exodus 20:1", abbr),
            ("Psalm 23:1", abbr),
            ("Psalm 23:2", abbr),
            ("John 3:16", abbr),
            ("John 3:17", abbr),
            ("Romans 8:28", abbr),
        ]

        def _call_endpoint(ref, translation):
            r = client.get(f"/bibles/{translation}/verses", params={"ref": ref})
            return r.status_code, r.json()

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {
                executor.submit(_call_endpoint, ref, trans): (ref, trans)
                for ref, trans in refs
            }
            for future in as_completed(futures):
                ref, trans = futures[future]
                status, data = future.result()
                assert status == 200, (
                    f"GET /bibles/{trans}/verses?ref={ref} returned {status}"
                )
                assert "text" in data, f"No 'text' in response for {ref}"
                assert len(data["text"]) > 0, f"Empty text for {ref}"

    def test_fastapi_lexicon_lookup_threaded(self):
        """ThreadPoolExecutor calls /lexicons/{m}/lookup concurrently."""
        if len(_lexicon_modules) < 1:
            pytest.skip("No lexicon modules available")

        abbr = _pick_first_lexicon_abbr()
        if abbr is None:
            pytest.skip("No lexicon modules")

        # Need to pre-warm lazy load to get headwords
        from accordance_api import _get_lexicon_module
        try:
            mod = _get_lexicon_module(abbr)
            headwords = mod.headwords()[:10]
        except Exception:
            pytest.skip(f"Cannot load {abbr}")
        if len(headwords) < 2:
            mod.close()
            pytest.skip(f"Not enough headwords in {abbr}")

        client = TestClient(app)

        def _call_endpoint(hw):
            r = client.get(f"/lexicons/{abbr}/lookup", params={"headword": hw})
            return r.status_code, r.json()

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {
                executor.submit(_call_endpoint, hw): hw
                for hw in headwords[:8]
            }
            for future in as_completed(futures):
                hw = futures[future]
                status, data = future.result()
                assert status == 200, (
                    f"GET /lexicons/{abbr}/lookup?headword={hw} returned {status}"
                )
                assert "entry" in data, f"No 'entry' in response for {hw}"

    def test_fastapi_mixed_endpoints_threaded(self):
        """ThreadPoolExecutor calls mixed endpoints concurrently."""
        client = TestClient(app)

        calls = [
            ("GET", "/health"),
            ("GET", "/bibles"),
            ("GET", "/lexicons"),
            ("GET", "/commentaries"),
            ("GET", "/modules"),
            ("GET", "/canon/books"),
        ]

        def _call_endpoint(method, path):
            if method == "GET":
                r = client.get(path)
            else:
                raise ValueError(f"Unknown method: {method}")
            return r.status_code

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {
                executor.submit(_call_endpoint, method, path): path
                for method, path in calls
            }
            for future in as_completed(futures):
                path = futures[future]
                status = future.result()
                assert status == 200, (
                    f"GET {path} returned {status}"
                )

    def test_rate_limiter_allows_localhost(self):
        """Rate limiter does not block localhost/TestClient requests."""
        client = TestClient(app)
        # Make 20 rapid requests — should all pass since rate limiter
        # skips 127.0.0.1 and testclient
        for _ in range(20):
            r = client.get("/health")
            assert r.status_code == 200

    def test_module_registry_concurrent_reads(self):
        """Multiple threads read from _modules dict concurrently."""
        def _read_registry(results, idx):
            try:
                count = len(_modules)
                bible_count = sum(1 for e in _modules.values() if e["type"] == 1)
                results[idx] = (count, bible_count)
            except Exception as e:
                results[idx] = f"ERROR: {e}"

        n_threads = 8
        results: list = [None] * n_threads
        threads = []
        for i in range(n_threads):
            t = threading.Thread(target=_read_registry, args=(results, i))
            threads.append(t); t.start()
        for t in threads:
            t.join()

        first = results[0]
        assert isinstance(first, tuple), f"Thread 0 got {results[0]}"
        for i in range(1, n_threads):
            assert results[i] == first, (
                f"Thread {i} registry data differs: {results[i]} vs {first}"
            )

    def test_threadpool_module_loading_no_deadlock(self):
        """Load 5+ different modules via ThreadPoolExecutor; verify no deadlock or hang."""
        from accordance_api import _get_lexicon_module, _get_commentary_module

        pairs: list[tuple[str, str]] = []  # (type, abbreviation)

        # Pick up to 3 lexicons
        for abbr in sorted(_lexicon_modules.keys())[:3]:
            pairs.append(("lexicon", abbr))
        # Pick up to 3 commentaries
        for abbr in sorted(_commentary_modules.keys())[:3]:
            pairs.append(("commentary", abbr))

        if len(pairs) < 3:
            pytest.skip(f"Need >=3 modules, got {len(pairs)}")

        def _load_one(kind, abbr):
            if kind == "lexicon":
                mod = _get_lexicon_module(abbr)
                return mod.abbrev, len(mod.headwords())
            else:
                mod = _get_commentary_module(abbr)
                return mod.abbrev, len(mod.list_books())

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {
                executor.submit(_load_one, kind, abbr): abbr
                for kind, abbr in pairs
            }
            for future in as_completed(futures, timeout=30):
                abbr = futures[future]
                abbrev, count = future.result()
                assert count > 0, (
                    f"Module {abbr} loaded with 0 entries/books"
                )

    def test_rapid_lazy_load_unload_cycle(self):
        """Rapidly reset and reload modules to stress check-then-set paths."""
        from accordance_api import _get_lexicon_module, _get_commentary_module
        import accordance_api

        abbr_lex = _pick_first_lexicon_abbr()
        abbr_com = _pick_first_commentary_abbr()

        if abbr_lex is None and abbr_com is None:
            pytest.skip("No modules available")

        for _ in range(10):
            # Reset all instances
            for entry in _modules.values():
                entry["instance"] = None
            accordance_api._search_index = None

            # Re-load
            if abbr_lex:
                mod = _get_lexicon_module(abbr_lex)
                assert mod is not None
            if abbr_com:
                mod = _get_commentary_module(abbr_com)
                assert mod is not None
