"""
Audit: Type 2 English dictionaries exposed through REST API and MCP server.

Covers:
  1. Discovery returns 12+ type-2 entries
  2. API /lexicons endpoint includes type-2 modules
  3. API /lexicons/{m}/lookup returns English text (Easton: "Jerusalem")
  4. API /lexicons/{m}/metadata reports correct format
  5. MCP list_lexicons includes type-2 modules (direct import)
  6. search_dictionary works across dictionaries
  7. Language filter: type-2 modules report language="English"
  8. Bible Words module: 300+ entries, lookup returns definitions
"""

import os

import pytest

# ---------------------------------------------------------------------------
# Direct parser imports
# ---------------------------------------------------------------------------

from accordance_parser.discovery import discover_modules
from accordance_parser.lexicon_module import LexiconModule

# ---------------------------------------------------------------------------
# Expected type-2 English dictionary modules
# (verified by Info.plist: com.oaktree.module.moduletype == 2)
# ---------------------------------------------------------------------------

_TYPE2_EXPECTED = {
    "Bible Words",
    "Easton",
    "ISBE",
    "NAS Topical Index",
    "NI Bible Dictionary",   # also appears as "NI Bible"
    "Names",
    "Nave\u2019s",           # Nave's Topical Bible
    "Place Names",
    "Sites",
    "Webster",
    # PhotoGuide Sampler is type-2 but not a dictionary; still valid
    # Read Me-Modules is type-2 but just docs; still valid
}

# Exclude these type-2 modules from dictionary-specific tests
# (they're informational/tools, not dictionaries with headwords)
_NON_DICTIONARY_TYPE2 = {
    "PhotoGuide Sampler",
    "Read Me-Modules",
    "Sites",  # Atlas site info, thin content
}


# =============================================================================
# Helpers
# =============================================================================

def _load_module(abbr: str):
    """Load a LexiconModule by abbreviation, or skip if unavailable."""
    reg = discover_modules()
    info = reg.get(abbr)
    if info is None:
        pytest.skip(f"Module '{abbr}' not found in discovery registry")
    path = info.get("path", "")
    if not path or not os.path.exists(path):
        pytest.skip(f"Module '{abbr}' data file missing: {path}")
    return LexiconModule(path)


def _type2_modules():
    """Return {abbr: info} for all type-2 modules in the discovery registry."""
    reg = discover_modules()
    return {abbr: info for abbr, info in reg.items() if info.get("type") == 2}


# =============================================================================
# Test 1: Discovery includes type 2
# =============================================================================

def test_discovery_includes_type2():
    """discover_modules() returns type-2 entries (12+ expected)."""
    reg = discover_modules()
    assert len(reg) > 0, "Discovery returned empty registry — no modules found"
    type2 = _type2_modules()
    assert len(type2) >= 8, (
        f"Only {len(type2)} type-2 modules found (expected 12+). "
        f"Keys: {sorted(type2.keys())}"
    )
    # Verify known dictionaries are present
    for expected in _TYPE2_EXPECTED:
        if expected in type2:
            continue
        # NI Bible Dictionary may appear without a trailing space — check
        if expected == "NI Bible Dictionary":
            if any(abbr.startswith("NI Bible") for abbr in type2):
                continue
        assert expected in type2, f"Expected type-2 module '{expected}' not found"


def test_type2_language_is_english():
    """All type-2 modules report language='English' via LexiconModule (authoritative).

    NOTE: The discovery-level 'language' field comes directly from the plist.
    Some modules (e.g., Bible Words) have a plist language of 'el' (Greek) but
    actually contain English content.  The LexiconModule correctly detects this
    and reports 'English' via its .language property.  The API updates the
    language cache on first load (_get_lexicon_module), so the API endpoint
    will show the correct language.
    """
    type2 = _type2_modules()
    failures = []
    for abbr in sorted(type2.keys()):
        try:
            mod = _load_module(abbr)
        except Exception:
            continue  # Some type-2 modules (PhotoGuide, Sites) may fail to parse
        if mod.language != "English":
            failures.append(f"{abbr}: LexiconModule.language={mod.language!r}")
    assert not failures, (
        "Type-2 modules with wrong LexiconModule language:\n"
        + "\n".join(failures)
    )


# =============================================================================
# Test 2: API /lexicons endpoint includes type-2 modules
# =============================================================================

@pytest.fixture(scope="module")
def _api_modules():
    """Return _modules dict from accordance_api (already populated at import)."""
    import accordance_api
    return accordance_api._modules


def test_api_modules_includes_type2(_api_modules):
    """The API module registry includes type-2 modules."""
    type2_api = {abbr: v for abbr, v in _api_modules.items() if v["type"] == 2}
    assert len(type2_api) >= 8, (
        f"API registry has only {len(type2_api)} type-2 modules. "
        f"Keys: {sorted(type2_api.keys())}"
    )


def test_api_lexicons_endpoint_type2():
    """GET /lexicons returns type-2 modules (tested via TestClient)."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons")
    assert resp.status_code == 200, f"GET /lexicons returned {resp.status_code}"
    body = resp.json()
    assert isinstance(body, list), f"/lexicons didn't return list: {type(body)}"

    # Find type-2 abbreviations in the response
    type2_abbrs = set(_type2_modules().keys())
    returned_abbrs = {item["abbreviation"] for item in body}

    # At least Easton (a canonical type-2 dictionary) must be present
    assert "Easton" in returned_abbrs, (
        f"Easton not in /lexicons response. Got: {sorted(returned_abbrs)}"
    )
    # Most type-2 modules should appear
    common = type2_abbrs & returned_abbrs
    assert len(common) >= 5, (
        f"Only {len(common)} type-2 modules in /lexicons response: {sorted(common)}"
    )


# =============================================================================
# Test 3: API /lexicons/{m}/lookup — Easton "Jerusalem"
# =============================================================================

def test_api_easton_lookup_jerusalem():
    """GET /lexicons/Easton/lookup?headword=Jerusalem returns English text."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons/Easton/lookup", params={"headword": "Jerusalem"})
    assert resp.status_code == 200, (
        f"Easton lookup 'Jerusalem' returned {resp.status_code}: {resp.text[:200]}"
    )

    body = resp.json()
    assert body["module"] == "Easton"
    assert body["language"] == "English", (
        f"Easton language={body['language']!r}, expected 'English'"
    )
    assert body["headword"] == "Jerusalem"
    entry = body["entry"]
    assert isinstance(entry, str) and len(entry) > 50, (
        f"Easton Jerusalem entry too short ({len(entry)} chars): {entry[:200]!r}"
    )
    assert "jerusalem" in entry.lower(), (
        f"Easton Jerusalem entry doesn't mention Jerusalem: {entry[:300]!r}"
    )


def test_api_easton_lookup_nonexistent():
    """GET /lexicons/Easton/lookup for nonexistent headword returns 404."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons/Easton/lookup",
                      params={"headword": "XYZZY_NOT_FOUND_99999"})
    assert resp.status_code == 404, (
        f"Expected 404 for nonexistent headword, got {resp.status_code}"
    )


# =============================================================================
# Test 4: API /lexicons/{m}/metadata
# =============================================================================

def test_api_easton_metadata():
    """GET /lexicons/Easton/metadata?headword=Jerusalem reports format."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons/Easton/metadata",
                      params={"headword": "Jerusalem"})
    assert resp.status_code == 200, (
        f"Easton metadata returned {resp.status_code}: {resp.text[:200]}"
    )
    body = resp.json()
    assert body["module"] == "Easton"
    assert body["has_entry"] is True, "has_entry=False for Easton/Jerusalem"
    assert body["headword"] == "Jerusalem"
    assert isinstance(body["keys"], dict), f"keys is not dict: {type(body['keys'])}"
    assert body["homograph_count"] >= 0


def test_api_easton_metadata_nonexistent():
    """GET /lexicons/Easton/metadata for nonexistent returns 404."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons/Easton/metadata",
                      params={"headword": "XYZZY_NOT_FOUND_99999"})
    assert resp.status_code == 404, (
        f"Expected 404 for nonexistent, got {resp.status_code}"
    )


# =============================================================================
# Test 5: LexiconModule type-2 entries load correctly (direct parser)
# =============================================================================

# Known type-2 modules with reasonable entry counts (verified empirically)
_TYPE2_KNOWN_COUNTS = {
    "Easton": 3900,
    "ISBE": 2500,
    "Nave\u2019s": 5000,
    "Names": 2600,
    "NI Bible Dictionary": 4500,
    "Place Names": 100,
}

_MODULE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "modules", "Tools")


@pytest.mark.parametrize("abbr,min_count", [
    ("Easton", 1000),
    ("ISBE", 300),
    ("Nave\u2019s", 500),
    ("Names", 20),       # Hitchcock's has only 26 name-initial entries
    ("Place Names", 30),
])
def test_type2_module_loads(abbr, min_count):
    """Each major type-2 dictionary loads with a reasonable entry count."""
    mod = _load_module(abbr)
    count = len(mod.headwords())
    assert count >= min_count, (
        f"{abbr} has only {count} entries, expected >= {min_count}"
    )
    assert mod.language == "English", (
        f"{abbr}.language={mod.language!r}, expected 'English'"
    )
    assert mod.format, f"{abbr}.format is empty"


@pytest.mark.parametrize("abbr", ["Easton", "ISBE", "Names"])
def test_type2_module_format_is_standard(abbr):
    """Most type-2 dictionaries use 'standard' format."""
    mod = _load_module(abbr)
    assert mod.format == "standard", (
        f"{abbr}.format={mod.format!r}, expected 'standard'"
    )


def test_easton_lookup_known_entry():
    """Easton can look up well-known entries like 'olive' and 'temple'."""
    mod = _load_module("Easton")
    for headword in ["olive", "temple", "tabernacle", "jerusalem"]:
        result = mod.lookup(headword)
        assert result is not None, (
            f"Easton.lookup({headword!r}) returned None"
        )
        assert len(result) > 20, (
            f"Easton {headword!r} entry too short: {len(result)} chars"
        )
        # Entry must contain printable English text (could be definition
        # text that doesn't repeat the headword)
        english_chars = sum(1 for c in result if 0x20 <= ord(c) <= 0x7E)
        assert english_chars > 20, (
            f"Easton {headword!r} entry has only {english_chars} printable ASCII: "
            f"{result[:200]!r}"
        )


# =============================================================================
# Test 6: search_dictionary (via MCP direct call or simulated)
# =============================================================================

def test_search_dictionary_easton():
    """Search headwords in Easton for 'temple' returns results."""
    mod = _load_module("Easton")
    results = mod.find("temple", max_results=10)
    assert len(results) >= 1, f"Easton find('temple') returned {len(results)} results"
    # Verify at least one result contains 'temple' (case-insensitive)
    found = any("temple" in r.lower() for r in results)
    assert found, (
        f"No result in Easton find('temple') contains 'temple': {results[:5]}"
    )


def test_search_dictionary_across_all():
    """find() works across all type-2 dictionaries."""
    type2 = _type2_modules()
    searchable = sorted(type2.keys() - _NON_DICTIONARY_TYPE2)
    if not searchable:
        pytest.skip("No searchable type-2 modules found")

    tested = 0
    for abbr in searchable[:6]:  # Test up to 6 to keep runtime reasonable
        try:
            mod = _load_module(abbr)
            results = mod.find("god", max_results=5)
            assert isinstance(results, list), f"{abbr}.find() didn't return list"
            # Some modules may have 0 results for "god", that's OK (e.g. Sites)
            tested += 1
        except Exception:
            # Some type-2 modules (PhotoGuide, Sites) may fail — skip
            continue
    assert tested >= 2, f"Only tested {tested} type-2 modules for search"


# =============================================================================
# Test 7: Language filter — type-2 modules are English, not Greek/Hebrew
# =============================================================================

def test_lexicons_filter_by_language_english():
    """GET /lexicons returns only type-2 modules with language='en'."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/lexicons")
    assert resp.status_code == 200
    body = resp.json()

    type2_abbrs = set(_type2_modules().keys())
    for item in body:
        abbr = item["abbreviation"]
        if abbr in type2_abbrs:
            assert item["language"] == "English", (
                f"{abbr}.language={item['language']!r} in /lexicons, "
                f"expected 'English'"
            )


def test_search_by_language_en():
    """GET /search?language=en returns lexicon results from type-2 modules."""
    from starlette.testclient import TestClient
    from accordance_api import app

    client = TestClient(app)
    resp = client.get("/search", params={
        "q": "temple",
        "type": "lexicon",
        "language": "en",
        "max_results": 10,
        "fuzzy": False,
    })
    # Search index may not be built — this may return 500
    if resp.status_code == 500 and "index" in resp.text.lower():
        pytest.skip("Search index not available")
    assert resp.status_code == 200, (
        f"Search returned {resp.status_code}: {resp.text[:200]}"
    )
    body = resp.json()
    assert body["query"] == "temple"
    assert body["total"] >= 0
    for item in body["results"]:
        assert item["source_type"] == "lexicon"


# =============================================================================
# Test 8: Bible Words module (300+ entries, English definitions)
# =============================================================================

def test_bible_words_loads():
    """Bible Words module loads with 300+ entries."""
    mod = _load_module("Bible Words")
    assert mod.language == "English", (
        f"Bible Words language={mod.language!r}, expected 'English'"
    )
    count = len(mod.headwords())
    assert count >= 300, (
        f"Bible Words has only {count} entries, expected >= 300"
    )
    assert mod.format == "biblewords", (
        f"Bible Words format={mod.format!r}, expected 'biblewords'"
    )


def test_bible_words_lookup_english_text():
    """Bible Words lookup returns English definitions."""
    mod = _load_module("Bible Words")
    # Sample a few headwords and verify they produce genuine English text
    hws = mod.headwords()
    assert len(hws) > 0, "Bible Words has 0 headwords"

    # Try some well-known words
    tested = 0
    for test_word in ["abandon", "faith", "love", "grace", "hope", "peace"]:
        result = mod.lookup(test_word)
        if result is None:
            continue
        tested += 1
        assert len(result) > 30, (
            f"Bible Words '{test_word}' entry too short: {len(result)} chars"
        )
        # Must contain English text (Latin-1 printable range)
        ascii_chars = sum(1 for c in result if 0x20 <= ord(c) <= 0x7E)
        assert ascii_chars > 10, (
            f"Bible Words '{test_word}' has only {ascii_chars} printable ASCII chars"
        )
    assert tested >= 2, (
        f"Bible Words: only {tested} of 6 test words returned definitions"
    )


def test_bible_words_first_entry_lookup():
    """Bible Words first headword returns non-truncated entry."""
    mod = _load_module("Bible Words")
    hws = mod.headwords()
    first = hws[0]
    result = mod.lookup(first)
    assert result is not None, (
        f"Bible Words.lookup({first!r}) returned None"
    )
    assert len(result) >= 20, (
        f"Bible Words first entry too short ({len(result)} chars): {result[:200]!r}"
    )


# =============================================================================
# Test 5b: MCP list_lexicons / search_dictionary via direct import
# =============================================================================

def test_mcp_lexicons_include_type2():
    """accordance_mcp_server._available_lexicons includes type-2 modules."""
    try:
        import accordance_mcp_server as mcp_mod
    except ImportError:
        pytest.skip("mcp package not installed — skipping MCP server import test")

    lexicons = getattr(mcp_mod, "_available_lexicons", {})
    if not lexicons:
        pytest.skip("MCP server has no available lexicons (parsers may have failed)")

    # At least one type-2 module should appear
    type2_abbrs = set(_type2_modules().keys())
    in_mcp = type2_abbrs & set(lexicons.keys())
    assert len(in_mcp) >= 1, (
        f"No type-2 modules in MCP _available_lexicons. "
        f"Type-2: {sorted(type2_abbrs)[:10]}  "
        f"MCP keys: {sorted(lexicons.keys())[:20]}"
    )
    # Specifically, verify Easton (the canonical type-2 dict) is present
    if "Easton" in type2_abbrs:
        assert "Easton" in lexicons, "Easton missing from MCP available_lexicons"


def test_mcp_list_lexicons_function():
    """MCP list_lexicons() function returns type-2 modules."""
    try:
        import accordance_mcp_server as mcp_mod
        list_fn = getattr(mcp_mod, "list_lexicons", None)
    except ImportError:
        pytest.skip("mcp package not installed")

    if list_fn is None:
        pytest.skip("list_lexicons not found on MCP server module")

    result = list_fn()
    assert isinstance(result, str), f"list_lexicons returned {type(result)}, not str"
    # The result should mention at least one type-2 module
    type2_abbrs = set(_type2_modules().keys())
    found = any(abbr in result for abbr in type2_abbrs)
    if not found:
        # Not a hard fail: modules may not have loaded at MCP server init time
        # But if Easton IS in _available_lexicons, it SHOULD appear
        lexicons = getattr(mcp_mod, "_available_lexicons", {})
        if lexicons:
            pytest.fail(
                f"list_lexicons() output missing all type-2 modules. "
                f"Expected: {sorted(type2_abbrs & set(lexicons.keys()))}"
            )
        else:
            pytest.skip("No lexicons loaded in MCP server at all")


def test_mcp_search_dictionary_across():
    """MCP search_dictionary() finds results across dictionaries."""
    try:
        import accordance_mcp_server as mcp_mod
        search_fn = getattr(mcp_mod, "search_dictionary", None)
    except ImportError:
        pytest.skip("mcp package not installed")

    if search_fn is None:
        pytest.skip("search_dictionary not found on MCP server module")

    result = search_fn("temple")
    assert isinstance(result, str), f"search_dictionary returned {type(result)}"
    # Should contain results from at least one module
    assert len(result) > 20, f"search_dictionary('temple') result too short: {result[:200]!r}"
    # Should include type-2 modules
    type2_abbrs = _type2_modules().keys()
    type2_in_result = [abbr for abbr in type2_abbrs if abbr in result]
    assert len(type2_in_result) >= 1, (
        f"No type-2 modules in search_dictionary('temple') output. "
        f"Results preview: {result[:300]!r}"
    )


# =============================================================================
# Test 5c: get_dictionary_entry for type-2 modules
# =============================================================================

def test_mcp_get_dictionary_entry_easton():
    """MCP get_dictionary_entry('Easton', 'Jerusalem') returns English text."""
    try:
        import accordance_mcp_server as mcp_mod
        get_fn = getattr(mcp_mod, "get_dictionary_entry", None)
    except ImportError:
        pytest.skip("mcp package not installed")

    if get_fn is None:
        pytest.skip("get_dictionary_entry not found on MCP server module")

    result = get_fn("Easton", "Jerusalem")
    assert isinstance(result, str), f"get_dictionary_entry returned {type(result)}"
    assert len(result) > 50, (
        f"get_dictionary_entry('Easton','Jerusalem') too short: {result[:200]!r}"
    )
    assert "jerusalem" in result.lower(), (
        f"Result doesn't mention Jerusalem: {result[:300]!r}"
    )
    # Must not be an error message
    assert not result.startswith("[Lexicon"), f"Error from get_dictionary_entry: {result}"


def test_mcp_search_dictionary_content_type2():
    """MCP search_dictionary_content() with language='en' finds English dict content."""
    try:
        import accordance_mcp_server as mcp_mod
        search_fn = getattr(mcp_mod, "search_dictionary_content", None)
    except ImportError:
        pytest.skip("mcp package not installed")

    if search_fn is None:
        pytest.skip("search_dictionary_content not found on MCP server module")

    result = search_fn("righteousness", language="en", max_results=5)
    assert isinstance(result, str), f"search_dictionary_content returned {type(result)}"
    # Should find something in English dictionaries
    assert len(result) > 20, (
        f"search_dictionary_content('righteousness', en) result too short: {result[:300]!r}"
    )
    # Not an error string
    if result.startswith("(Search") or result.startswith("(No"):
        pytest.skip(f"MCP search index not available: {result[:100]}")
