"""Tests for the citation + term alias mechanisms (#000041 + #000042).

Pure unit + small-DB tests over the helpers in arborist/qa/aliases.py.
The end-to-end resolver-uses-alias path is exercised separately via
the `arborist warrant-resolve --use-aliases` CLI on real shards.
"""

from __future__ import annotations

import sqlite3

import pytest

from arborist.qa.aliases import (
    add_citation_alias,
    add_term_alias,
    domain_for_pillar,
    expand_query_with_term_aliases,
    list_citation_aliases,
    list_term_aliases,
    lookup_citation_aliases,
    lookup_term_aliases,
    remove_citation_alias,
    remove_term_alias,
)
from arborist.store import SCHEMA_SQL


@pytest.fixture
def db(tmp_path):
    """Fresh SQLite DB with the alias schema applied."""
    p = tmp_path / "aliases.db"
    conn = sqlite3.connect(str(p))
    try:
        conn.executescript(SCHEMA_SQL)
        conn.commit()
    finally:
        conn.close()
    return str(p)


# --- pillar domain mapping -------------------------------------------


@pytest.mark.parametrize(
    "pillar,expected",
    [
        ("I", "logic"),
        ("IV", "geometry"),
        ("VII", "combinatorics"),
        ("IX", "lambda-calculus"),
        ("UNKNOWN", ""),
        ("", ""),
    ],
)
def test_domain_for_pillar(pillar, expected):
    assert domain_for_pillar(pillar) == expected


# --- citation aliases -----------------------------------------------


def test_citation_alias_add_lookup_remove(db):
    inserted = add_citation_alias(
        db,
        original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
        substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
        substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
        substitute_title="Principles of Mathematical Logic",
        decision_by="fox 2026-05-09",
        decision_rationale="Hilbert-Ackermann 1928 is PD; covers same propositional axioms",
    )
    assert inserted is True

    aliases = lookup_citation_aliases(
        db, "Introduction to Mathematical Logic by Elliott Mendelson"
    )
    assert len(aliases) == 1
    a = aliases[0]
    assert a.substitute_title == "Principles of Mathematical Logic"
    assert "David Hilbert" in a.substitute_authors

    # Idempotent — second add returns False.
    inserted_again = add_citation_alias(
        db,
        original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
        substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
        substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
        substitute_title="Principles of Mathematical Logic",
        decision_by="fox 2026-05-09",
    )
    assert inserted_again is False

    # Remove + verify gone.
    removed = remove_citation_alias(
        db,
        "Introduction to Mathematical Logic by Elliott Mendelson",
        "Principles of Mathematical Logic by Hilbert and Ackermann",
    )
    assert removed is True
    assert lookup_citation_aliases(
        db, "Introduction to Mathematical Logic by Elliott Mendelson"
    ) == []


def test_citation_alias_audit_discipline(db):
    """Empty `decision_by` raises — fail-closed audit."""
    with pytest.raises(ValueError, match="decision_by must be non-empty"):
        add_citation_alias(
            db,
            original_ref="A",
            substitute_ref="B",
            substitute_authors=[],
            substitute_title="",
            decision_by="",
        )


def test_citation_alias_list_filter(db):
    add_citation_alias(
        db,
        original_ref="Mendelson 1997",
        substitute_ref="HA 1928",
        substitute_authors=["Hilbert"],
        substitute_title="Principles",
        decision_by="fox",
    )
    add_citation_alias(
        db,
        original_ref="Stanley §1.2",
        substitute_ref="Brualdi §3.5",
        substitute_authors=["Brualdi"],
        substitute_title="Combinatorics",
        decision_by="fox",
    )
    all_a = list_citation_aliases(db)
    assert len(all_a) == 2
    filtered = list_citation_aliases(db, original_filter="Mendelson")
    assert len(filtered) == 1
    assert filtered[0].original_ref == "Mendelson 1997"


# --- term aliases ----------------------------------------------------


def test_term_alias_add_lookup(db):
    inserted = add_term_alias(
        db,
        term="incidence",
        alternate_term="connection",
        domain="geometry",
        decision_by="fox 2026-05-09",
        decision_rationale="Hilbert 1902 Townsend uses 'connection'",
    )
    assert inserted is True

    # Forward lookup
    alts = lookup_term_aliases(db, "incidence", "geometry")
    assert "connection" in alts

    # Bidirectional — registered (incidence, connection) also returns
    # "incidence" when looking up by "connection".
    reverse = lookup_term_aliases(db, "connection", "geometry")
    assert "incidence" in reverse


def test_term_alias_domain_isolation(db):
    """Same term in different domains stay distinct."""
    add_term_alias(
        db, term="set", alternate_term="class",
        domain="set-theory", decision_by="fox",
    )
    add_term_alias(
        db, term="set", alternate_term="ensemble",
        domain="combinatorics", decision_by="fox",
    )
    assert lookup_term_aliases(db, "set", "set-theory") == ["class"]
    assert lookup_term_aliases(db, "set", "combinatorics") == ["ensemble"]


def test_term_alias_lowercase_normalization(db):
    """Term + alternate + domain stored lowercased; lookup is
    case-insensitive."""
    add_term_alias(
        db, term="Incidence", alternate_term="Connection",
        domain="GEOMETRY", decision_by="fox",
    )
    assert lookup_term_aliases(db, "incidence", "geometry") == ["connection"]
    assert lookup_term_aliases(db, "INCIDENCE", "Geometry") == ["connection"]


def test_term_alias_remove(db):
    add_term_alias(
        db, term="x", alternate_term="y",
        domain="z", decision_by="fox",
    )
    removed = remove_term_alias(db, "x", "y", "z")
    assert removed is True
    assert lookup_term_aliases(db, "x", "z") == []


def test_term_alias_audit_discipline(db):
    """Empty fields raise — fail-closed."""
    with pytest.raises(ValueError, match="decision_by must be non-empty"):
        add_term_alias(
            db, term="a", alternate_term="b",
            domain="c", decision_by="",
        )
    with pytest.raises(ValueError, match="domain must be non-empty"):
        add_term_alias(
            db, term="a", alternate_term="b",
            domain="", decision_by="fox",
        )


# --- query expansion -------------------------------------------------


def test_expand_query_with_term_aliases_basic(db):
    add_term_alias(
        db, term="incidence", alternate_term="connection",
        domain="geometry", decision_by="fox",
    )
    expanded = expand_query_with_term_aliases(
        "incidence axiom", "geometry", db
    )
    assert "(incidence OR connection)" in expanded
    # Other tokens pass through.
    assert "axiom" in expanded


def test_expand_query_preserves_phrase_syntax(db):
    add_term_alias(
        db, term="line incidence", alternate_term="line connection",
        domain="geometry", decision_by="fox",
    )
    expanded = expand_query_with_term_aliases(
        '"line incidence"', "geometry", db
    )
    assert '"line incidence"' in expanded
    assert '"line connection"' in expanded
    assert " OR " in expanded


def test_expand_query_no_aliases_passes_through(db):
    """No registered aliases → query unchanged."""
    q = "modus ponens"
    expanded = expand_query_with_term_aliases(q, "logic", db)
    assert expanded == q


def test_expand_query_unreachable_db_returns_original():
    """Bad DB path → query unchanged (fail-closed; aliases are opt-in)."""
    expanded = expand_query_with_term_aliases(
        "incidence", "geometry", "/nonexistent/path/aliases.db"
    )
    assert expanded == "incidence"


# --- list_term_aliases (gap-fill 2026-05-10) -------------------------


def test_list_term_aliases_filters_by_domain(db):
    """`list_term_aliases(domain=X)` returns only rows in that
    domain. Same pattern as list_citation_aliases filter."""
    add_term_alias(
        db, term="set", alternate_term="class",
        domain="set-theory", decision_by="fox",
    )
    add_term_alias(
        db, term="line", alternate_term="ray",
        domain="geometry", decision_by="fox",
    )
    add_term_alias(
        db, term="incidence", alternate_term="connection",
        domain="geometry", decision_by="fox",
    )
    from arborist.qa.aliases import list_term_aliases

    geom = list_term_aliases(db, domain="geometry")
    assert len(geom) == 2
    assert all(a.domain == "geometry" for a in geom)
    set_th = list_term_aliases(db, domain="set-theory")
    assert len(set_th) == 1
    assert set_th[0].term == "set"


def test_list_term_aliases_filters_by_term_substring(db):
    """`term_filter` is a substring match (LIKE %X%) on term."""
    add_term_alias(db, term="incidence", alternate_term="connection",
                   domain="geometry", decision_by="fox")
    add_term_alias(db, term="line incidence", alternate_term="line connection",
                   domain="geometry", decision_by="fox")
    add_term_alias(db, term="parallel", alternate_term="parallels",
                   domain="geometry", decision_by="fox")
    from arborist.qa.aliases import list_term_aliases

    inc = list_term_aliases(db, term_filter="incidence")
    assert len(inc) == 2
    assert all("incidence" in a.term for a in inc)


def test_list_term_aliases_unreachable_db_returns_empty():
    """Same fail-closed contract as expand_query_with_term_aliases:
    nonexistent DB → empty list, no exception."""
    from arborist.qa.aliases import list_term_aliases

    assert list_term_aliases("/nonexistent/path/aliases.db") == []


def test_list_term_aliases_handles_missing_table(tmp_path):
    """A SQLite file without the term_aliases table → empty list,
    not an OperationalError."""
    import sqlite3

    bad_db = tmp_path / "no-schema.db"
    sqlite3.connect(str(bad_db)).execute("CREATE TABLE foo (x TEXT)").connection.close()
    from arborist.qa.aliases import list_term_aliases

    assert list_term_aliases(str(bad_db)) == []


# --- internal helper coverage (small surfaces, easy regression risk) -


def test_tokenize_fts_query_preserves_quoted_phrase():
    from arborist.qa.aliases import _tokenize_fts_query

    assert _tokenize_fts_query('"line incidence"') == ['"line incidence"']
    assert _tokenize_fts_query('foo "bar baz" qux') == [
        "foo", '"bar baz"', "qux"
    ]


def test_tokenize_fts_query_handles_parentheses_as_tokens():
    """OR-expansion produces queries like `(a OR b)`; the tokenizer
    must preserve `(` and `)` as standalone tokens (round-trip)."""
    from arborist.qa.aliases import _tokenize_fts_query

    assert _tokenize_fts_query("(a OR b)") == ["(", "a", "OR", "b", ")"]


def test_tokenize_fts_query_unterminated_quote_falls_back():
    """A user query with a stray opening quote shouldn't lose data —
    the unmatched-quote tail becomes one token."""
    from arborist.qa.aliases import _tokenize_fts_query

    out = _tokenize_fts_query('foo "bar baz')
    assert out[0] == "foo"
    assert "bar baz" in out[-1]


def test_quote_for_fts_passes_through_already_quoted():
    from arborist.qa.aliases import _quote_for_fts

    assert _quote_for_fts('"line incidence"') == '"line incidence"'
    assert _quote_for_fts("incidence") == "incidence"


def test_quote_for_fts_quotes_multiword_bare_token():
    """Defense-in-depth: a bare token that contains a space gets
    quoted (shouldn't happen via _tokenize_fts_query but the helper
    is defensive)."""
    from arborist.qa.aliases import _quote_for_fts

    assert _quote_for_fts("two words") == '"two words"'


def test_match_quoting_quoted_reference():
    from arborist.qa.aliases import _match_quoting

    # Quoted reference → quoted alt.
    assert _match_quoting('"line incidence"', "line connection") == '"line connection"'
    # Bare reference → bare alt.
    assert _match_quoting("incidence", "connection") == "connection"
