"""Identity rules after migration #3: the brand fold, the attribute veto, stated sizes,
and the forwarding a merged product does.

No database: the rules are pure, and the two lookups that need one are fed a stub that
answers exactly what a row would.
"""

from app.models import Product
from app.models.catalog import IDENTITY_RULES_VERSION
from app.services import catalog_queries
from app.services.collectors.base import RawListing
from app.services.ingest import (
    attributes_disagree,
    brand_slug,
    raw_attributes,
    stated_size,
    survivor_of,
)


def raw(name, *, brand="Dior", size_ml=100, vertical="beauty", **kw):
    return RawListing(source_sku="1", name=name, price=10.0, currency="EUR", location_code="X",
                      brand=brand, size_ml=size_ml, vertical=vertical, **kw)


class TestBrandSlug:
    def test_the_fold_key_hyphenated(self):
        assert brand_slug("Moët & Chandon") == "moet-chandon"
        assert brand_slug("MOET ET CHANDON") == "moet-chandon"
        assert brand_slug("Don Julio Tequila") == brand_slug("Don Julio®") == "don-julio"
        assert brand_slug(None) == "" and brand_slug("  ") == ""


class TestAttributeVeto:
    """An EDP and an EDT of one line and size share a match key; they are different
    bottles. A feed that says nothing about it must never split a product."""

    def test_two_declared_concentrations_that_differ_veto(self):
        assert attributes_disagree({"concentration": "edp"}, {"concentration": "edt"})

    def test_a_missing_value_on_either_side_is_not_a_disagreement(self):
        assert not attributes_disagree({"concentration": "edp"}, {})
        assert not attributes_disagree({}, {"concentration": "edp"})
        assert not attributes_disagree(None, None)

    def test_the_same_value_agrees(self):
        assert not attributes_disagree({"concentration": "edp"}, {"concentration": "edp"})

    def test_only_beauty_declares_a_concentration(self):
        assert raw_attributes(raw("Sauvage Eau de Toilette 100ml")) == {"concentration": "edt"}
        assert raw_attributes(raw("Sauvage 100ml")) == {}
        assert raw_attributes(raw("Talisker 10 Eau de Vie 70cl", brand="Talisker", vertical="liquor")) == {}

    def test_the_vertical_is_inferred_when_the_collector_did_not_say(self):
        assert raw_attributes(raw("J'adore Eau de Parfum 5cl", vertical=None,
                                  feed_categories=["Fragrance"])) == {"concentration": "edp"}


class TestStatedSize:
    def test_the_name_states_the_size_in_its_own_unit(self):
        assert stated_size(raw("Talisker 10 70cl", size_ml=700)) == (70.0, "cl")
        assert stated_size(raw("Sauvage Eau de Toilette 100ml", size_ml=100)) == (100.0, "ml")

    def test_a_size_known_only_in_millilitres_is_stated_as_such(self):
        """Extime's payload knows the size when the name does not."""
        assert stated_size(raw("Single Malt Whisky Ten Years Old", size_ml=1000)) == (1000.0, "ml")

    def test_a_name_that_contradicts_the_size_does_not_override_it(self):
        assert stated_size(raw("Eau Sauvage Cologne 50ML", size_ml=100)) == (100.0, "ml")

    def test_no_size_anywhere_is_none(self):
        assert stated_size(raw("CHEIROSA 76", size_ml=None)) == (None, None)


class _Db:
    """Just enough of a session: rows by id, and scalar() for the forwarding lookup."""

    def __init__(self, forwards: dict[int, int | None]) -> None:
        self.rows = {pid: Product(id=pid, name=str(pid), match_key="k", merged_into_id=to)
                     for pid, to in forwards.items()}

    def get(self, model, pid):
        return self.rows.get(pid)

    def scalar(self, stmt):
        pid = stmt.whereclause.right.value
        return self.rows[pid].merged_into_id if pid in self.rows else None


class TestForwarding:
    def test_a_merged_product_answers_under_its_survivor(self):
        db = _Db({42: 1156, 1156: None})
        assert catalog_queries.resolve_product_id(db, 42) == 1156
        assert catalog_queries.resolve_product_id(db, 1156) == 1156

    def test_a_chain_is_followed_to_its_end(self):
        db = _Db({1: 2, 2: 3, 3: None})
        assert catalog_queries.resolve_product_id(db, 1) == 3
        assert survivor_of(db, db.rows[1]).id == 3

    def test_a_cycle_cannot_hang_a_request(self):
        db = _Db({1: 2, 2: 1})
        assert catalog_queries.resolve_product_id(db, 1) in (1, 2)
        assert survivor_of(db, db.rows[1]) is not None

    def test_an_unknown_id_is_itself_so_the_404_still_fires(self):
        assert catalog_queries.resolve_product_id(_Db({}), 7) == 7

    def test_without_a_session_it_is_the_identity(self):
        assert catalog_queries.resolve_product_id(None, 7) == 7


class TestRulesVersion:
    def test_the_current_rules_are_newer_than_the_server_default(self):
        """Existing rows carry "1" from the migration; ingest stamps the current one."""
        assert IDENTITY_RULES_VERSION != "1"
        assert Product.__table__.c.identity_rules_version.server_default.arg == "1"
