"""The admin's featured pin as a ledger decision (Stream AW2.2, D5 and D6). What it prevents: a
pin written as a bare column update that no undo, no replay and no drift check would ever
see. Pinned here: pin and unpin write one batch, one row per target, the column following;
named twice is one decision, already so is not recorded, nothing to record opens no batch;
every refusal code (`PAGE_NOT_FOUND`, `PAGE_HIDDEN`, `VALUE_INVALID` before any batch opens,
`DECISION_HELD` for a bulk pin over a person's individual one); undo releases to false; a
hand-set column is drift, detected not prevented; a merge and a line fold carry the pin; an
old slug and a merged variant forward; `list` marks inert and drift; the CLI grammar exits 1
before any database call. SQLite kit with the ledger's append-only guard."""
from __future__ import annotations

import pytest
from sqlalchemy import func, select

from app import cli
from app.models import Decision, DecisionBatch, ProductLine, ProductVariant, Redirect
from app.services import featured_pins, overrides
from app.services.decisions import undo as undo_service, writer
from tests.featured_kit import RIAN, build


@pytest.fixture
def db():
    s = build()
    yield s
    s.close()


def pin(db, *targets, featured=True, by="rian", reason=None):
    result = featured_pins.set_featured(db, [featured_pins.parse_target(t) for t in targets], featured, by, reason=reason)
    db.commit()
    return result


def test_pin_and_unpin_write_one_batch_and_the_column_follows(db):
    result = pin(db, "variant:1", "line:glen-public")
    assert result["counts"] == {"changed": 2, "unchanged": 0, "refused": 0} and result["mode"] == "bulk"
    assert db.get(ProductVariant, 1).featured and db.get(ProductLine, 11).featured
    batch = db.scalar(select(DecisionBatch))
    assert str(batch.uid) == result["batch_uid"] and batch.kind == "cli" and batch.closed_at is not None
    rows = list(db.scalars(select(Decision).where(Decision.field == "featured")))
    assert {(r.entity_type, r.entity_id, r.value) for r in rows} == {("product_variant", 1, True), ("product_line", 11, True)}
    assert [t["path"] for t in result["targets"]] == ["/products/glen-bare?variant=1", "/products/glen-public"]
    # Unpin: a second batch, the column back to false.
    result = pin(db, "variant:1", featured=False)
    assert result["counts"]["changed"] == 1 and not db.get(ProductVariant, 1).featured and result["mode"] == "individual"
    assert db.scalar(select(func.count()).select_from(DecisionBatch)) == 2


def test_named_twice_is_one_decision_already_so_is_not_recorded_and_nothing_opens_no_batch(db):
    result = pin(db, "line:glen-public", "line:11")
    assert result["counts"] == {"changed": 1, "unchanged": 1, "refused": 0}
    again = pin(db, "line:glen-public")
    assert again["batch_uid"] is None and again["counts"] == {"changed": 0, "unchanged": 1, "refused": 0}
    assert db.scalar(select(func.count()).select_from(DecisionBatch)) == 1


def test_every_refusal_code(db):
    result = pin(db, "line:nothing", "variant:999", "line:bare-hidden", "variant:9")
    assert [(r["target"], r["error_code"]) for r in result["refusals"]] == [
        ("line:nothing", "PAGE_NOT_FOUND"), ("variant:999", "PAGE_NOT_FOUND"),
        ("line:bare-hidden", "PAGE_HIDDEN"), ("variant:9", "PAGE_HIDDEN"),
    ]
    assert result["batch_uid"] is None and db.scalar(select(func.count()).select_from(DecisionBatch)) == 0
    # The value's shape is checked before any batch opens: the writer bool-checks hidden,
    # indexed and ignored only (an issue for its owner).
    with pytest.raises(writer.Refused) as refused:
        featured_pins.set_featured(db, [("variant", "1")], "yes", "rian")
    assert refused.value.code == "VALUE_INVALID" and db.scalar(select(func.count()).select_from(DecisionBatch)) == 0
    # An unpin of a hidden line is allowed: it releases, it never leads.
    db.get(ProductLine, 15).featured = True; db.flush()
    assert pin(db, "line:bare-hidden", featured=False)["counts"]["changed"] == 1


def test_a_bulk_pin_never_overrules_a_person_s_individual_decision(db):
    pin(db, "variant:1")  # rian, individual
    result = pin(db, "variant:1", "variant:2", featured=False, by="adi")  # bulk
    assert result["refusals"] == [{"target": "variant:1", "error_code": "DECISION_HELD",
                                   "summary": result["refusals"][0]["summary"]}]
    assert "rian" in result["refusals"][0]["summary"]
    assert db.get(ProductVariant, 1).featured and result["counts"]["changed"] == 0  # variant 2 was already unpinned


def test_undo_releases_to_false_and_verify_sees_no_drift(db):
    result = pin(db, "variant:1", "line:glen-public")
    assert undo_service.verify(db)["drift"] == 0
    with_undo = undo_service.undo_batch(db, result["batch_uid"], RIAN, "changed my mind")
    db.commit()
    assert with_undo.kind == "undo"
    assert not db.get(ProductVariant, 1).featured and not db.get(ProductLine, 11).featured
    assert undo_service.verify(db)["drift"] == 0


def test_a_hand_set_column_is_drift_detected_not_prevented(db):
    db.get(ProductVariant, 2).featured = True  # outside the ledger
    db.commit()
    pins = featured_pins.current(db)
    assert [(p["kind"], p["id"], p["drift"], p["by"]) for p in pins] == [("variant", 2, True, None)]
    # The other way: a decision the column no longer shows.
    pin(db, "variant:1")
    db.get(ProductVariant, 1).featured = False
    db.commit()
    assert {p["id"]: p["drift"] for p in featured_pins.current(db)} == {1: True, 2: True}
    assert undo_service.verify(db)["drift"] == 1


def test_a_merge_carries_the_pin_to_the_survivor(db):
    pin(db, "variant:1")
    with writer.batch(db, "route", "individual", RIAN, commit=True) as b:
        writer.record(b, "product_variant", db.get(ProductVariant, 1), "merged_into", db.get(ProductVariant, 2))
    survivor = db.get(ProductVariant, 2)
    assert survivor.featured and db.get(ProductVariant, 1).merged_into_id == 2
    carried = db.scalar(select(Decision).where(Decision.entity_type == "product_variant", Decision.entity_id == 2, Decision.field == "featured"))
    assert carried is not None and carried.detail.get("carried_from")
    # The merged id forwards to its survivor: pinning it again is "already so" on the survivor.
    assert featured_pins.find(db, "variant", 1).id == 2
    assert pin(db, "variant:1")["counts"] == {"changed": 0, "unchanged": 1, "refused": 0}


def test_a_line_fold_carries_the_pin(db):
    pin(db, "line:glen-public")
    moved = overrides.move(db, "line", "11", "12")
    db.commit()
    assert moved["moved"] == ["featured"] and db.get(ProductLine, 12).featured


def test_an_old_slug_and_an_alias_line_forward(db):
    db.add(Redirect(from_slug="glen-old", kind="product_line", to_slug="glen-public"))
    db.get(ProductLine, 12).alias_of_id = 11
    db.commit()
    assert featured_pins.find(db, "line", "glen-old").id == 11
    assert featured_pins.find(db, "line", "glen-bare").id == 11, "an alias line resolves to its fold row"
    assert pin(db, "line:glen-old", "line:glen-bare")["counts"] == {"changed": 1, "unchanged": 1, "refused": 0}


def test_list_marks_inert_and_carries_the_decision(db):
    pin(db, "variant:1", "line:glen-public")
    db.get(ProductLine, 15).featured = True  # hidden, set by hand: inert and drift
    db.commit()
    pins = {(p["kind"], p["id"]): p for p in featured_pins.current(db)}
    assert not pins[("variant", 1)]["inert"] and pins[("variant", 1)]["by"] == "rian" and pins[("variant", 1)]["decided_at"] is not None
    assert pins[("line", 15)]["inert"] and pins[("line", 15)]["drift"] and pins[("line", 15)]["path"] == "/products/bare-hidden"
    assert featured_pins.comparison_state(db, "variant", db.get(ProductVariant, 1)) == "comparison: 2 airports, $1.00 / 1%"
    assert featured_pins.comparison_state(db, "variant", db.get(ProductVariant, 8)) == "comparison: none: shows only in the full list"
    assert featured_pins.comparison_state(db, "line", db.get(ProductLine, 12)).startswith("comparison: 2 airports, $3.00")


def test_the_cli_grammar_exits_1_before_any_database_call(monkeypatch, capsys):
    import app.db

    monkeypatch.setattr(app.db, "SessionLocal", lambda: (_ for _ in ()).throw(AssertionError("a session was opened")))
    assert cli.main(["featured", "pin", "brand:glen", "--by", "rian"]) == 1
    assert "is not line:<slug or id> or variant:<id>" in capsys.readouterr().out
    assert cli.main(["featured", "unpin", "variant:abc", "--by", "rian"]) == 1
    assert "named by its id" in capsys.readouterr().out


def test_the_cli_pins_commits_and_prints_the_undo_line_and_the_comparison_state(db, monkeypatch, capsys):
    import app.db

    monkeypatch.setattr(app.db, "SessionLocal", lambda: db)
    monkeypatch.setattr(db, "close", lambda: None)
    assert cli.main(["featured", "pin", "variant:1", "variant:8", "line:nothing", "--by", "rian"]) == 0
    out = capsys.readouterr().out
    assert "featured pin: 2 pinned, 0 already so, 1 refused; undo with `decisions undo-batch " in out
    assert "comparison: 2 airports, $1.00 / 1%" in out and "comparison: none: shows only in the full list" in out
    assert "refused line:nothing: PAGE_NOT_FOUND" in out
    assert db.get(ProductVariant, 1).featured and db.get(ProductVariant, 8).featured
    assert cli.main(["featured", "list"]) == 0
    listed = capsys.readouterr().out
    assert "variant:1" in listed and "variant:8" in listed and "rian" in listed
    assert cli.main(["featured", "pin", "line:nothing", "--by", "rian"]) == 2
