"""Decided beats rule (Stream L, LT3): an override survives every rekey and backfill, a human
product merge holds across the next collection, a pin holds, an ignore hides.

What it cost: `overrides` had zero rows, zero writers and no reader in the keying path; a
survivor renamed at confirm to "1 Million" re-keyed `rabanne|1-million||100ml` on the next
`rederive` and the next collection created a new product beside it; `_resolve_product` looked
up live rows only, so a confirmed merge of the bare "1 Million 10cl" into the Eau de Toilette
came undone on the next sighting (576 `variation_unknown` pairs on staging were that class).
The SQLite kit with the ONE_MILLION fixture; no network.
"""

from __future__ import annotations

import inspect
from datetime import UTC, datetime

import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app import cli
from app.models import (
    Account, Award, Base, Brand, CollectionRun, Listing, Shop, Suggestion, PriceObservation, ProductVariant,
    ProductLine, Merge, RawRecord, Retailer, Source, AttributeAlias,
)
from app.models.decisions import LEDGER_TABLES
from app.models.places import Place, ShopPlace
from app.models.catalog import IDENTITY_RULES_VERSION
from app.services import catalog_queries as cq
from app.services import collector_view, ingest, keying, merges, overrides
from app.services.collectors.base import RawListing
from tests import _accounts as T
from tests.kit import _env
from tests.test_product_lines import ONE_MILLION

TABLES = [
    Account.__table__, Source.__table__, CollectionRun.__table__,
    Brand.__table__, ProductLine.__table__, ProductVariant.__table__, AttributeAlias.__table__,
    Retailer.__table__, Shop.__table__, RawRecord.__table__, Listing.__table__, Award.__table__,
    Merge.__table__, Suggestion.__table__, PriceObservation.__table__,    *LEDGER_TABLES, Place.__table__, ShopPlace.__table__,
]
RIAN = 1


@pytest.fixture
def db():
    engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
    Base.metadata.create_all(engine, tables=TABLES)
    factory = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False, future=True)
    keying.invalidate()
    with factory() as session:
        session.add(Account(id=RIAN, username="rian", display_name="rian"))
        yield session
    keying.invalidate()


def seed(db):
    """The fourteen rows at their shops, keyed and lined the way the deploy leaves them."""
    rabanne, paco = Brand(slug="rabanne", name="Rabanne"), Brand(slug="paco-rabanne", name="Paco Rabanne")
    retailer = Retailer(slug="shop", name="Shop")
    db.add_all([rabanne, paco, retailer])
    db.flush()
    for n, (brand, name, size) in enumerate(ONE_MILLION, start=1):
        product = ProductVariant(id=n, name=name, brand=brand, brand_id=paco.id if brand == "Paco Rabanne" else rabanne.id,
                          vertical="beauty", quantity_ml=size, match_key="pending", attributes={})
        shop = Shop(retailer_id=retailer.id, code=f"S{n}", iata=f"A{n:02d}", name=f"Shop {n}", currency="EUR")
        db.add_all([product, shop])
        db.flush()
        listing = Listing(variant_id=product.id, shop_id=shop.id, source_sku=f"sku{n}")
        db.add(listing)
        db.flush()
        db.add(PriceObservation(listing_id=listing.id, price=100 + n, currency="EUR", price_usd=110 + n,
                                observed_at=datetime.now(UTC)))
    db.commit()
    cli.backfill_lines(db)
    cli.backfill_attributes(db)
    merges.rekey_product_variants(db, list(db.scalars(select(ProductVariant))), keying.load_maps(db))
    merges.merge_duplicates(db)
    db.commit()
    keying.invalidate()
    return rabanne, paco


def rekey_all(db):
    keying.invalidate()
    maps = keying.load_maps(db)
    merges.rekey_product_variants(db, list(db.scalars(select(ProductVariant))), maps)
    db.commit()
    keying.invalidate()


def edt_100(db) -> ProductVariant:
    return db.scalar(select(ProductVariant).where(ProductVariant.name == "Rabanne 1 Million EDT 100ml"))


class TestOverridesSurvive:
    def test_a_variation_and_a_name_override_survive_rekey_and_backfill_lines(self, db):
        seed(db)
        survivor = edt_100(db)
        assert survivor.merged_into_id is None and survivor.match_key == "rabanne|1-million|concentration=edt|100ml"
        # The typed name drops the concentration; the collected spelling keeps the key.
        overrides.decide(db, "product", str(survivor.id), "name", "1 Million", set_by=RIAN,
                         reason="typed", collected_value=survivor.name)
        survivor.name = "1 Million"
        parfum = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Parfum 10cl"))
        overrides.decide(db, "product", str(parfum.id), "attribute", "edp", set_by=RIAN, reason="a person read the box")
        db.commit()
        for _ in range(2):
            rekey_all(db)
            cli.backfill_lines(db)
            db.refresh(survivor)
            db.refresh(parfum)
            assert survivor.name == "1 Million"
            assert survivor.match_key == "rabanne|1-million|concentration=edt|100ml"
            assert survivor.product_line_id is not None
            # Identity rules v6: the decided attribute is what a page reads; the key stays what the
            # shop's words give, or the variant's own listing would key elsewhere at its next sighting.
            assert parfum.match_key == "paco-rabanne|1-million|concentration=parfum|100ml"
            assert (parfum.attributes or {}).get("attribute") == "edp"
            assert parfum.identity_rules_version == IDENTITY_RULES_VERSION

    def test_a_second_decision_keeps_what_the_rules_had_first(self, db):
        """Keep-first `collected_value` (the review's amendment to the catalogue decisions §2.2):
        L1 -> L2 -> L3 must still say the rules had L1, and the prune must keep L1 alive because a
        decision names it."""
        seed(db)
        product = edt_100(db)
        rabanne = db.scalar(select(Brand).where(Brand.slug == "rabanne"))
        l1 = product.product_line_id
        l2 = ProductLine(brand_id=rabanne.id, key="somewhere", name="Somewhere", slug="rabanne-somewhere")
        l3 = ProductLine(brand_id=rabanne.id, key="elsewhere", name="Elsewhere", slug="rabanne-elsewhere")
        db.add_all([l2, l3])
        db.flush()
        overrides.decide_product(db, product.id, "product_line_id", l2.id, set_by=RIAN)
        overrides.decide_product(db, product.id, "product_line_id", l3.id, set_by=RIAN)
        row = overrides.fields_of(db, "product", str(product.id))["product_line_id"]
        assert (row.value, row.collected_value) == (l3.id, l1)
        # Every other product moves off L1 by hand; L1 stays because the ledger names it.
        for other in db.scalars(select(ProductVariant).where(ProductVariant.product_line_id == l1, ProductVariant.id != product.id)):
            other.product_line_id = l2.id
        db.commit()
        assert "0 empty line row(s)" in cli.backfill_prune_lines(db)
        assert db.get(ProductLine, l1) is not None

    def test_a_line_override_is_never_pointed_elsewhere_by_backfill_lines(self, db):
        seed(db)
        survivor = edt_100(db)
        other = ProductLine(brand_id=survivor.brand_id, key="somewhere else", slug="rabanne-somewhere-else", name="Elsewhere")
        db.add(other)
        db.flush()
        overrides.decide(db, "product", str(survivor.id), "product_line_id", other.id, set_by=RIAN, collected_value=survivor.product_line_id)
        survivor.product_line_id = other.id
        db.commit()
        cli.backfill_lines(db)
        rekey_all(db)
        db.refresh(survivor)
        assert survivor.product_line_id == other.id
        # The decided line holds the variant; the key stays the rules' (identity rules v6), so the
        # variant's own listing still lands on it at the next sighting.
        assert survivor.match_key == "rabanne|1-million|concentration=edt|100ml"

    def test_a_quantity_override_names_the_slot(self, db):
        seed(db)
        survivor = edt_100(db)
        overrides.decide(db, "product", str(survivor.id), "quantity", {"value": 75, "unit": "g"}, set_by=RIAN)
        db.commit()
        rekey_all(db)
        db.refresh(survivor)
        # The decided quantity is what a page reads; the key keeps what the listing states (identity
        # rules v6), so the shop's next "100 ml" sighting lands here and mints nothing.
        assert survivor.match_key == "rabanne|1-million|concentration=edt|100ml"

    def test_a_merge_moves_the_losers_override_and_keeps_the_survivors(self, db):
        seed(db)
        survivor = edt_100(db)
        loser = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Eau de Toilette 200 ml"))
        overrides.decide(db, "product", str(loser.id), "attribute", "edp", set_by=RIAN)
        overrides.decide(db, "product", str(loser.id), "name", "Loser", set_by=RIAN, collected_value=loser.name)
        overrides.decide(db, "product", str(survivor.id), "name", "Winner", set_by=RIAN, collected_value=survivor.name)
        db.flush()
        merges.merge_product_variants(db, survivor, [loser], reason="confirmed", merged_by=RIAN)
        db.commit()
        rows = overrides.fields_of(db, "product", str(survivor.id))
        assert set(rows) == {"name", "attribute"} and rows["name"].value == "Winner"
        record = db.scalar(select(Merge).where(Merge.from_id == loser.id))
        assert record.detail["overrides"] == {"moved": ["attribute"], "kept": ["name"]}

    def test_enrich_never_writes_over_a_guarded_column(self, db):
        seed(db)
        survivor = edt_100(db)
        loser = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Eau de Toilette 200 ml"))
        overrides.decide(db, "product", str(survivor.id), "quantity", {"value": 75, "unit": "g"}, set_by=RIAN)
        survivor.quantity_ml = None
        loser.quantity_ml = 200
        db.flush()
        merges.merge_product_variants(db, survivor, [loser], reason="confirmed", merged_by=RIAN)
        assert survivor.quantity_ml is None, "a decided quantity is never overwritten by a merged row's size"


def _tombstone(db, *, key: str, into: ProductVariant, version: str = IDENTITY_RULES_VERSION, reason: str = "confirmed",
               name: str = "1 Million 10cl") -> ProductVariant:
    row = ProductVariant(name=name, brand="Rabanne", brand_id=into.brand_id, vertical="beauty", quantity_ml=100,
                  match_key=key, attributes={}, identity_rules_version=version, merged_into_id=into.id,
                  product_line_id=into.product_line_id)
    db.add(row)
    db.flush()
    db.add(Merge(from_id=row.id, to_id=into.id, merged_by=RIAN, reason=reason, detail={}))
    db.flush()
    return row


def bare_tile(quantity_ml: int | None = 100) -> RawListing:
    return RawListing(source_sku="bare", name="1 Million 10cl", price=80.0, currency="EUR", shop_code="S1",
                      brand="Rabanne", quantity_ml=quantity_ml, vertical="beauty")


class TestTombstoneFollow:
    def test_the_bare_10cl_resolves_to_the_confirmed_survivor_not_a_new_product(self, db):
        seed(db)
        survivor = edt_100(db)
        bare = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million 10cl", ProductVariant.brand == "Paco Rabanne"))
        # A person confirmed the bare 10cl (keyed apart on an unknown attribute) into the EDT.
        bare.brand, bare.brand_id = "Rabanne", survivor.brand_id
        merges.merge_product_variants(db, survivor, [bare], reason="confirmed", merged_by=RIAN)
        rekey_all(db)
        db.refresh(bare)
        assert bare.match_key == "rabanne|1-million||100ml" and bare.merged_into_id == survivor.id
        before = db.scalar(select(ProductVariant.id).order_by(ProductVariant.id.desc()))
        resolved = ingest._resolve_product(db, bare_tile())
        assert resolved.id == survivor.id
        assert db.scalar(select(ProductVariant.id).order_by(ProductVariant.id.desc())) == before, "no new product"

    def test_a_tombstone_at_a_stale_rules_version_is_ignored_until_rederive_moves_it(self, db):
        seed(db)
        survivor = edt_100(db)
        tomb = _tombstone(db, key="rabanne|1-million||100", into=survivor, version="3")
        db.commit()
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", None) is None
        rekey_all(db)
        db.refresh(tomb)
        assert tomb.match_key == "rabanne|1-million||100ml" and tomb.identity_rules_version == IDENTITY_RULES_VERSION
        assert tomb.merged_into_id == survivor.id, "rederive keeps the tombstone a tombstone"
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", None).id == survivor.id

    def test_a_rules_fold_is_never_followed_only_a_human_merge(self, db):
        seed(db)
        survivor = edt_100(db)
        _tombstone(db, key="rabanne|1-million||100ml", into=survivor, reason="duplicate_match_key")
        db.commit()
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", None) is None

    def test_a_tombstone_keyed_unknown_is_never_followed(self, db):
        seed(db)
        survivor = edt_100(db)
        _tombstone(db, key="rabanne|1-million||unknown", into=survivor)
        db.commit()
        assert ingest._follow_tombstone(db, "rabanne|1-million||unknown", None) is None
        silent = RawListing(source_sku="silent", name="1 Million", price=80.0, currency="EUR", shop_code="S1",
                            brand="Rabanne", vertical="beauty")
        resolved = ingest._resolve_product(db, silent)
        assert resolved.id != survivor.id and resolved.match_key == "rabanne|1-million||unknown"

    def test_two_tombstones_on_one_key_follow_the_newest_merge(self, db):
        seed(db)
        first = edt_100(db)
        second = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Parfum 10cl"))
        _tombstone(db, key="rabanne|1-million||100ml", into=first)
        _tombstone(db, key="rabanne|1-million||100ml", into=second)
        db.commit()
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", None).id == second.id

    def test_a_survivor_with_another_barcode_is_not_followed(self, db):
        seed(db)
        survivor = edt_100(db)
        survivor.gtin = "3349668508587"
        _tombstone(db, key="rabanne|1-million||100ml", into=survivor)
        db.commit()
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", "5010327703053") is None
        assert ingest._follow_tombstone(db, "rabanne|1-million||100ml", "3349668508587").id == survivor.id


class TestPinAndIgnore:
    def test_a_pinned_listing_stays_pinned_when_resolved_with_another_product(self, db):
        seed(db)
        survivor = edt_100(db)
        listing = db.scalar(select(Listing).where(Listing.source_sku == "sku4"))
        other = db.get(ProductVariant, listing.variant_id)
        shop = db.get(Shop, listing.shop_id)
        overrides.pin_listing(db, listing.id, survivor.id, set_by=RIAN, reason="same bottle")
        assert listing.variant_id == survivor.id and listing.pinned_by == RIAN
        # The one ledger (§2.2): the pin is recorded with the product the rules had it on.
        pinned = overrides.fields_of(db, "listing", str(listing.id))["pinned_variant_id"]
        assert (pinned.value, pinned.collected_value, pinned.set_by, pinned.reason) == (survivor.id, other.id, RIAN, "same bottle")
        raw = RawListing(source_sku="sku4", name=other.name, price=1.0, currency="EUR", shop_code=shop.code)
        again = ingest._resolve_listing(db, other, shop, raw)
        assert again.id == listing.id and again.variant_id == survivor.id
        overrides.unpin_listing(db, listing.id, set_by=RIAN)
        assert "pinned_variant_id" not in overrides.fields_of(db, "listing", str(listing.id))
        assert ingest._resolve_listing(db, other, shop, raw).variant_id == other.id

    def test_a_pin_follows_the_pinned_products_survivor(self, db):
        seed(db)
        survivor = edt_100(db)
        loser = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Eau de Toilette 200 ml"))
        listing = db.scalar(select(Listing).where(Listing.source_sku == "sku4"))
        overrides.pin_listing(db, listing.id, loser.id, set_by=RIAN)
        merges.merge_product_variants(db, survivor, [loser], reason="confirmed", merged_by=RIAN)
        db.commit()
        shop = db.get(Shop, listing.shop_id)
        raw = RawListing(source_sku="sku4", name="x", price=1.0, currency="EUR", shop_code=shop.code)
        assert ingest._resolve_listing(db, db.get(ProductVariant, 1), shop, raw).variant_id == survivor.id

    def test_an_ignored_listing_leaves_the_collectors_page_and_returns_with_the_filter(self, db):
        seed(db)
        listing = db.scalar(select(Listing).where(Listing.source_sku == "sku6"))
        overrides.ignore_listing(db, listing.id, set_by=RIAN, reason="a display tester, not for sale")
        assert listing.ignored_by == RIAN and listing.ignore_reason.startswith("a display")
        assert overrides.fields_of(db, "listing", str(listing.id))["ignored"].value is True
        page = collector_view.listings(db, per_page=100)
        assert listing.id not in {r["listing_id"] for r in page["rows"]}
        only = collector_view.listings(db, ignored="only", per_page=100)
        assert {r["listing_id"] for r in only["rows"]} == {listing.id}
        assert only["rows"][0]["ignore_reason"].startswith("a display")
        both = collector_view.listings(db, ignored="any", per_page=100)
        assert listing.id in {r["listing_id"] for r in both["rows"]}
        table = collector_view.product_variants(db, q="1 Million EDT 100ml")
        assert all(s["iata"] != "A06" for row in table["rows"] for s in row["shops"])
        overrides.unignore_listing(db, listing.id, set_by=RIAN)
        assert "ignored" not in overrides.fields_of(db, "listing", str(listing.id))
        assert listing.id in {r["listing_id"] for r in collector_view.listings(db, per_page=100)["rows"]}

    def test_a_hidden_brand_leaves_the_floor_and_a_check_is_a_stamp(self, db):
        """The human publish gate (the catalogue decisions §2.8): mechanical eligibility first,
        then a person's `hidden` takes the page off the site everywhere the one definition is
        read; `checked` changes nothing the site shows. The states are named by their effect,
        because the desk's confirm button once said Approve."""
        seed(db)
        db.info["blocked_shop_ids"] = []  # this world builds no verification tables
        for shop in db.scalars(select(Shop)):
            shop.visible = True  # the seed's shops are hidden, as every new shop is
        db.flush()
        rabanne = db.scalar(select(Brand).where(Brand.slug == "rabanne"))
        paco = db.scalar(select(Brand).where(Brand.slug == "paco-rabanne"))
        assert {h for h, _, _ in cq._brand_counts(db, min_product_variants=2, min_airports=2)} == {rabanne.id, paco.id}
        overrides.review(db, "brand", rabanne.id, "hidden", set_by=RIAN, reason="two brands in one row")
        db.info.pop("hidden_brand_ids", None)
        assert {h for h, _, _ in cq._brand_counts(db, min_product_variants=2, min_airports=2)} == {paco.id}
        assert rabanne.id not in cq.brand_page_slugs(db, [rabanne.id]), "a hidden brand has no page and no link from its product variants"
        row = overrides.fields_of(db, "brand", str(rabanne.id))["review"]
        assert (row.value, row.set_by, row.reason) == ("hidden", RIAN, "two brands in one row")
        # `checked` is retired (K2, plan W18): an approved sheet is the quality fact, not a stamp.
        with pytest.raises(overrides.Refused) as retired:
            overrides.review(db, "brand", rabanne.id, "checked", set_by=RIAN)
        assert retired.value.code == "VALUE_INVALID"
        overrides.review(db, "brand", rabanne.id, None, set_by=RIAN)
        db.info.pop("hidden_brand_ids", None)
        assert rabanne.id in {h for h, _, _ in cq._brand_counts(db, min_product_variants=2, min_airports=2)}
        assert overrides.hidden_ids(db, "brand") == set()
        assert "review" not in overrides.fields_of(db, "brand", str(rabanne.id))
        with pytest.raises(overrides.Refused) as refused:
            overrides.review(db, "brand", rabanne.id, "approved", set_by=RIAN)
        assert refused.value.code == "VALUE_INVALID"
        with pytest.raises(overrides.Refused) as refused:
            overrides.review(db, "brand", 999999, "approved", set_by=RIAN)
        assert refused.value.code == "ENTITY_NOT_FOUND"

    def test_live_listings_is_the_condition_and_the_site_readers_carry_it(self):
        assert "ignored_at IS NULL" in str(cq.live_listings())
        wired = ("_latest_observation_subquery", "_summary_base", "_priced_shops", "suggest_product_variants",
                 "get_product", "_filtered_summary", "_airport_shop_views", "_airport_summary", "_airport_rows",
                 "airport_shops")  # the list and every featured sort read through `_filtered_summary` (Stream AW2)
        for name in wired:
            fn = getattr(cq, name, None)
            assert fn is not None, name
            assert "live_listings()" in inspect.getsource(fn), name


# --------------------------------------------------------------------------- the routes, on the kit

CATALOGUE = [
    Source.__table__, CollectionRun.__table__,
    Retailer.__table__, Shop.__table__, ProductLine.__table__, ProductVariant.__table__, RawRecord.__table__,
    Listing.__table__, PriceObservation.__table__, Award.__table__, Merge.__table__, Suggestion.__table__,
    AttributeAlias.__table__,
]


@pytest.fixture
def world(monkeypatch):
    T.fresh(monkeypatch)
    Base.metadata.drop_all(_env.engine, tables=CATALOGUE)
    Base.metadata.create_all(_env.engine, tables=CATALOGUE)
    keying.invalidate()
    T.person("rian")
    T.person("adam", level="admin")
    with _env.TestSessionLocal() as db:
        seed(db)
    yield
    Base.metadata.drop_all(_env.engine, tables=CATALOGUE)


def _ids():
    with _env.TestSessionLocal() as db:
        survivor = edt_100(db)
        listing = db.scalar(select(Listing).where(Listing.source_sku == "sku4"))
        return survivor.id, listing.id, listing.variant_id


class TestRoutes:
    def test_refused_without_a_session_or_the_permission(self, world):
        c = T.client()
        assert c.post("/api/collectors/products/1/override", json={"field": "name", "value": "x"}).status_code == 401
        assert c.post("/api/collectors/listings/1/ignore", json={}).status_code == 401
        T.as_user(c, "adam")
        assert c.post("/api/collectors/products/1/override", json={"field": "name", "value": "x"}).status_code == 403
        assert c.post("/api/collectors/listings/1/pin", json={"variant_id": 1}).status_code == 403
        assert c.post("/api/collectors/listings/1/unpin", json={}).status_code == 403
        assert c.post("/api/collectors/listings/1/ignore", json={}).status_code == 403
        assert c.post("/api/collectors/listings/1/unignore", json={}).status_code == 403

    def test_the_owner_decides_and_the_who_columns_come_from_the_session(self, world):
        survivor_id, listing_id, other_id = _ids()
        c = T.client()
        T.as_user(c, "rian")
        r = c.post(f"/api/collectors/products/{survivor_id}/override",
                   json={"field": "name", "value": "1 Million", "reason": "the box says so"})
        assert r.status_code == 200, r.text
        assert r.json()["match_key"] == "rabanne|1-million|concentration=edt|100ml"
        r = c.post(f"/api/collectors/listings/{listing_id}/pin", json={"variant_id": survivor_id, "reason": "same"})
        assert r.status_code == 200 and r.json()["variant_id"] == survivor_id
        r = c.post(f"/api/collectors/listings/{listing_id}/ignore", json={"reason": "tester"})
        assert r.status_code == 200 and r.json()["ignored"] is True
        with _env.TestSessionLocal() as db:
            rian_id = db.scalar(select(Account.id).where(Account.username == "rian"))
            row = overrides.fields_of(db, "product", str(survivor_id))["name"]
            assert row.set_by == rian_id and row.collected_value == "Rabanne 1 Million EDT 100ml"
            listing = db.get(Listing, listing_id)
            assert listing.pinned_by == rian_id and listing.ignored_by == rian_id
        assert c.post(f"/api/collectors/listings/{listing_id}/unpin", json={}).status_code == 200
        assert c.post(f"/api/collectors/listings/{listing_id}/unignore", json={}).status_code == 200
        r = c.post(f"/api/collectors/products/{survivor_id}/override", json={"field": "quantity", "value": {"value": 1, "unit": "cups"}})
        assert r.status_code == 422 and r.json()["detail"]["error_code"] == "VALUE_INVALID"
        assert c.post("/api/collectors/products/999999/override", json={"field": "name", "value": "x"}).status_code == 404

    def test_a_typed_name_at_confirm_leaves_an_override_set_by_rian(self, world):
        from app.services import merge_session

        with _env.TestSessionLocal() as db:
            rian_id = db.scalar(select(Account.id).where(Account.username == "rian"))
            survivor = edt_100(db)
            other = db.scalar(select(ProductVariant).where(ProductVariant.name == "1 Million Eau de Toilette 200 ml"))
            candidate = Suggestion(level="product", left_id=min(survivor.id, other.id), right_id=max(survivor.id, other.id),
                                       reason="variation_unknown", score=0.9, detail={"why": "test"})
            db.add(candidate)
            db.commit()
            result = merge_session.confirm(db, candidate.id, decided_by=rian_id, preferred_name="1 Million", keep="left")
            kept = db.get(ProductVariant, result["applied"]["survivor_id"])
            row = overrides.fields_of(db, "product", str(kept.id))["name"]
            assert kept.name == "1 Million" and row.set_by == rian_id and row.reason == "chosen at merge"
            assert row.collected_value in ("Rabanne 1 Million EDT 100ml", "1 Million Eau de Toilette 200 ml")
            rekey_all(db)
            db.refresh(kept)
            assert kept.match_key.startswith("rabanne|1-million|concentration=edt|"), "the key keeps the collected spelling"
