"""The type facet, in both of its dialects.

The facet predicate is written twice by necessity — as SQL for rows the
database hands back, and as Python for candidates the vector search has
already fetched — plus a third time as the `is_task` pre-filter that narrows
the candidate set before it exists. Those three used to be hand-written and
agreed only by luck: adding `issue` to the SQL arm alone would have set the
pre-filter to is_task=False, handed the Python arm a candidate set with no
tasks in it, and returned an empty semantic half for the Issues facet forever
with nothing red anywhere (#3128).

They are now generated from one table. These tests pin the property that made
the trap possible, so it cannot come back by a different route.
"""
import pytest

from scribe.services.knowledge import (
    _FACETS,
    FACET_TYPES,
    NON_TASK_FACETS,
    _apply_type_filter,
    facet_is_task,
    matches_facet,
)
from tests.helpers import compiled_sql, fake_note, fake_snippet, fake_task


def _sql(note_type):
    from sqlalchemy import select

    from scribe.models.note import Note

    return compiled_sql(_apply_type_filter(select(Note.id), note_type))


# One representative row per shape the corpus actually holds.
ROWS = {
    "plain note": fake_note(note_type="note"),
    "process": fake_note(note_type="process"),
    "snippet": fake_snippet(),
    "work task": fake_task(task_kind="work", note_type="note"),
    "issue": fake_task(task_kind="issue", note_type="note"),
    "spike": fake_task(task_kind="spike", note_type="note"),
    "legacy plan": fake_task(task_kind="plan", note_type="note"),
}


@pytest.mark.parametrize("facet", sorted(FACET_TYPES))
def test_pre_filter_never_excludes_a_row_the_facet_wants(facet):
    """THE regression. `facet_is_task` narrows the semantic candidate set before
    `matches_facet` ever sees it, so a pre-filter that disagrees with the
    predicate doesn't return wrong rows — it returns NO rows, silently, on one
    half of a hybrid search."""
    want = facet_is_task(facet)
    for label, row in ROWS.items():
        if matches_facet(row, facet):
            assert want is None or want == row.is_task, (
                f"facet {facet!r} accepts the {label} row, but its pre-filter "
                f"asks for is_task={want} while the row has is_task={row.is_task} "
                f"— the semantic arm would never be handed this row"
            )


@pytest.mark.parametrize(
    "facet,expected",
    [
        ("task", {"work task", "issue", "spike", "legacy plan"}),
        ("issue", {"issue"}),
        ("spike", {"spike"}),
        ("work", {"work task"}),
        ("plan", {"legacy plan"}),
        ("note", {"plain note"}),
        ("process", {"process"}),
        ("snippet", {"snippet"}),
        ("", set(ROWS)),
    ],
)
def test_each_facet_selects_exactly_its_own_rows(facet, expected):
    assert {k for k, row in ROWS.items() if matches_facet(row, facet)} == expected


def test_a_plain_note_is_not_selected_by_its_own_type_when_it_is_a_task():
    """A task's `note_type` is 'note' — that column says nothing about task-ness.
    The hand-written Python arm omitted the `status IS NULL` half its SQL twin
    carried, so it only avoided returning every task under the Notes facet
    because the pre-filter had already dropped them."""
    assert matches_facet(fake_task(note_type="note"), "note") is False


def test_an_unknown_facet_matches_nothing_rather_than_everything():
    """A typo must return an empty list, never the whole corpus."""
    assert all(not matches_facet(row, "wrok") for row in ROWS.values())
    assert "notes.status IS NULL" in _sql("wrok")


def test_the_live_task_kinds_are_all_facets():
    """`issue` shipped in 0065 and `spike` in 0091; the browse vocabulary went
    three kinds without noticing either."""
    for kind in ("work", "issue", "spike"):
        assert kind in FACET_TYPES and _FACETS[kind][0] is True


def test_non_task_facets_are_the_note_types_and_only_those():
    assert set(NON_TASK_FACETS) == {"note", "process", "snippet"}


@pytest.mark.parametrize("facet", sorted(FACET_TYPES))
def test_sql_arm_constrains_the_axis_the_facet_lives_on(facet):
    """The SQL dialect of the same table. A task facet must pin `status` (and,
    for a single kind, `task_kind`); a record-type facet must pin `note_type`
    AND exclude tasks."""
    sql = _sql(facet)
    is_task, value = _FACETS[facet]
    assert "notes.deleted_at IS NULL" in sql
    if is_task:
        assert "notes.status IS NOT NULL" in sql
        assert (f"notes.task_kind = '{value}'" in sql) is (value is not None)
    else:
        assert "notes.status IS NULL" in sql
        assert f"notes.note_type = '{value}'" in sql
