"""Real-Postgres tests for WHICH rules reach a session (milestone 307 step 5).

What mocks can't prove, and what this milestone must not get wrong:

1. **Nothing stops binding.** A rule with no tier, no areas and no edges
   behaves exactly as it did before tiers existed. That is the one failure this
   whole design must not produce, and it is asserted first.
2. A conditional rule is invisible to a project that doesn't work in its area,
   and arrives — binding, not suggested — to one that does.
3. A `co_surfaces` partner arrives with its other half, which is the failure
   that made merging rule 144 into rule 46 look like the only fix.
4. An explicit suppression outranks an edge.
"""
import pytest
import pytest_asyncio

from scribe.models import async_session
from scribe.models.project import Project
from scribe.models.rulebook import Rulebook
from scribe.services import canonical_systems as canonical_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from tests.helpers import ensure_user

pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]


@pytest_asyncio.fixture
async def world():
    """A project with TWO rulebooks, because the two payloads are different sets.

    `list_always_on_rules` covers always-on rulebooks; `get_applicable_rules`
    covers SUBSCRIBED ones. Conflating them is easy and would make these tests
    assert nothing, so the fixture carries one of each and every test says
    which payload it is about.
    """
    async with async_session() as s:
        owner = await ensure_user(s, "surfacing_owner")
        project = Project(user_id=owner.id, title="Surfacing target")
        s.add(project)
        await s.flush()
        ids = {"owner": owner.id, "pid": project.id}
        await s.commit()

    always = await rulebooks_svc.create_rulebook(ids["owner"], "Family standards")
    async with async_session() as s:
        rb = await s.get(Rulebook, always.id)
        rb.always_on = True
        await s.commit()
    always_topic = await rulebooks_svc.create_topic(always.id, ids["owner"], "git")
    await rulebooks_svc.create_rule(
        always_topic.id, ids["owner"], "dev is home", "Work on dev.",
    )

    book = await rulebooks_svc.create_rulebook(ids["owner"], "Subscribed practices")
    topic = await rulebooks_svc.create_topic(book.id, ids["owner"], "release")
    plain = await rulebooks_svc.create_rule(
        topic.id, ids["owner"], "Between batches, keep stacking", "Keep going.",
    )
    await rulebooks_svc.subscribe_project(
        project_id=ids["pid"], rulebook_id=book.id, user_id=ids["owner"],
    )
    ids.update({
        "always": always.id, "always_topic": always_topic.id,
        "book": book.id, "topic": topic.id, "plain": plain.id,
    })
    return ids


async def _titles(ids) -> set[str]:
    applicable = await rulebooks_svc.get_applicable_rules(ids["pid"], ids["owner"])
    return {r["title"] for r in applicable["rules"]}


@pytest.mark.integration
async def test_a_rule_with_no_tier_no_areas_and_no_edges_binds_exactly_as_before(world):
    """THE compatibility guarantee. An install upgrades and every rule it
    already had keeps arriving — no tier set, no areas, no edges, still bound.
    Getting this wrong would silently stop enforcing rules people rely on,
    which is worse than any amount of payload bloat."""
    always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
    assert "dev is home" in {r.title for r in always_on}
    assert "Between batches, keep stacking" in await _titles(world)


@pytest.mark.integration
async def test_a_conditional_rule_is_reachable_not_resident(world):
    """It leaves the session-start payload entirely — that is the point of the
    tier — and it does NOT reach a project with no matching area."""
    # In the ALWAYS-ON book: the tier alone keeps it out of the session-start
    # payload, which is the whole point of the tier.
    resident = await rulebooks_svc.create_rule(
        world["always_topic"], world["owner"], "Release tagging", "Derive the tag.",
        when_to_apply="when cutting a release", tier="conditional",
    )
    assert resident.tier == "conditional"
    always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
    assert "Release tagging" not in {r.title for r in always_on}

    # In the SUBSCRIBED book, untagged: the project has no area to reach it by,
    # so it stays out of the project payload too. Absent for a DIFFERENT reason
    # than above, which is why both are asserted.
    await rulebooks_svc.create_rule(
        world["topic"], world["owner"], "Untagged conditional", "No area yet.",
        when_to_apply="sometime", tier="conditional",
    )
    assert "Untagged conditional" not in await _titles(world)


@pytest.mark.integration
async def test_a_conditional_rule_binds_a_project_that_works_in_its_area(world):
    """The payoff: the tag match carries it in deterministically. The project
    reaches the area through its own System's canonical_id — its local NAME is
    irrelevant, which is the whole reason the catalog exists."""
    area = await canonical_svc.find_by_name("CI & Release")
    assert area is not None, "migration 0087 seeds the standard vocabulary"

    rule = await rulebooks_svc.create_rule(
        world["topic"], world["owner"], "Release tagging", "Derive the tag.",
        when_to_apply="when cutting a release", tier="conditional",
    )
    await rulebooks_svc.set_rule_systems(rule.id, world["owner"], [area.id])

    # Still absent: the project has no Systems at all yet.
    assert "Release tagging" not in await _titles(world)

    # The project names the area with its OWN word, mapped to the same canon.
    local = await systems_svc.create_system(
        world["owner"], world["pid"], "CI & runners", description="ours",
    )
    await canonical_svc.set_system_canonical(world["owner"], local.id, area.id)

    surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
    hit = [r for r in surfaced["rules"] if r["title"] == "Release tagging"]
    assert hit, "a tagged conditional rule must bind a project working in that area"
    assert [s["name"] for s in hit[0]["systems"]] == ["CI & Release"]


@pytest.mark.integration
async def test_co_surfaces_drags_in_the_half_that_would_have_been_missed(world):
    """Rule 144 was split off rule 46 and folded back the same day because
    "either rule could surface without the other". This is the edge that makes
    that unnecessary: the partner arrives even though nothing else selected
    it, and says why it is here."""
    partner = await rulebooks_svc.create_rule(
        world["topic"], world["owner"], "Version names are labels",
        "A name decides nothing.",
        when_to_apply="when naming a build", tier="conditional",
    )
    await rulebooks_svc.add_rule_relation(
        world["owner"], world["plain"], partner.id, "co_surfaces",
        note="they fail together",
    )
    surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
    hit = [r for r in surfaced["rules"] if r["title"] == "Version names are labels"]
    assert hit, "a co_surfaces partner must arrive with its other half"
    assert hit[0]["via"] == "co_surfaces"


@pytest.mark.integration
async def test_a_suppression_outranks_an_edge(world):
    """The edge says these belong together; the suppression says this project
    does not want that one. An explicit decision beats an inferred one."""
    partner = await rulebooks_svc.create_rule(
        world["topic"], world["owner"], "Muted partner", "Should not arrive.",
        tier="conditional",
    )
    await rulebooks_svc.add_rule_relation(
        world["owner"], world["plain"], partner.id, "co_surfaces",
    )
    await rulebooks_svc.suppress_rule_for_project(
        world["pid"], partner.id, world["owner"],
    )
    surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
    assert "Muted partner" not in {r["title"] for r in surfaced["rules"]}
