"""The word lists propose; none of them applies (Stream K3.3, K3.7; plan W14 option D).

Each case here is a list that once ACTED on a key and was found by a person after the fact:
"triple" on the format list keyed Macallan Triple Cask as Cask; a category noun joins Montblanc
Legend with Legend Spirit (rehearsed 17 Sep); a shade in the name made 59 product lines of one
lipstick. The list's knowledge is kept as a reading with its reason; the certain key keeps the
words. Pure readings first, then the generators over the SQLite kit; no network.
"""

from __future__ import annotations

import re
from pathlib import Path

from sqlalchemy import select

from app.models import Brand, ProductLine, ProductVariant, Proposal, ProposalPass, Suggestion
from app.services import product_lines, proposal_rules
from app.services.normalize import match_key
from tests.test_decided import db  # noqa: F401  (the SQLite kit)
from tests.test_product_lines import ONE_MILLION

ALL = frozenset(proposal_rules.GROUPING_RULES)


class TestTheReadings:
    def test_triple_cask_proposes_that_triple_is_the_expression(self):
        name = "The Macallan Triple Cask 12 Years Old 70cl"
        assert proposal_rules.pack_word_reading(name) == ("expression", "form: single")
        reading = proposal_rules.read_line(name, listed_brand="The Macallan", brand="Macallan", vertical="liquor", rules=ALL)
        assert reading.key == "triple cask 12" and "pack_words" not in reading.removed
        assert "triple" in product_lines.product_line_key(name, listed_brand="The Macallan", brand="Macallan", vertical="liquor")

    def test_a_triple_pack_proposes_the_pack_word_as_packaging_and_the_key_keeps_it(self):
        name = "Chivas Regal 12 Years Old Triple Pack 3x1L"
        assert proposal_rules.pack_word_reading(name) == ("packaging", "form: pack")
        reading = proposal_rules.read_line(name, listed_brand="Chivas Regal", vertical="liquor", rules={"pack_words"})
        assert reading.removed == {"pack_words": ["triple"]} and reading.key == "12yo pack"
        assert product_lines.product_line_key(name, listed_brand="Chivas Regal", vertical="liquor") == "12yo triple pack"

    def test_a_category_noun_is_proposed_with_its_reason_never_deleted(self):
        name = "Chivas Regal 12 Year Old Blended Scotch Whisky 1L"
        reading = proposal_rules.read_line(name, listed_brand="Chivas Regal", vertical="liquor", rules={"drink_words"})
        assert reading.key == "12yo" and reading.removed == {"drink_words": ["blended", "scotch", "whisky"]}
        assert reading.reason() == "removed 'blended', 'scotch', 'whisky' as drink category or describing words"
        assert product_lines.product_line_key(name, listed_brand="Chivas Regal", vertical="liquor") == "12yo blended scotch whisky"

    def test_the_noun_that_names_the_scent_is_why_the_list_may_not_act(self):
        """Boundary (b) dropped "spirit" and the automatic merge folded Legend with Legend Spirit."""
        legend = match_key("Montblanc", "Legend Eau de Toilette 100ml", 100, vertical="beauty")
        spirit = match_key("Montblanc", "Legend Spirit Eau de Toilette 100ml", 100, vertical="beauty")
        assert legend != spirit and spirit == "montblanc|legend-spirit|concentration=edt|100ml"

    def test_the_shade_shapes_propose_a_shade_and_read_nothing_off_a_number_that_names_the_thing(self):
        assert proposal_rules.read_shade("Rouge Dior Lipstick - 447 Mellow Shade", "beauty", "Makeup") == ("447 mellow shade", "- 447 Mellow Shade")
        assert proposal_rules.read_shade("Joli Rouge Lipstick 3.5g 01", "beauty", "Makeup") == ("01", "01")
        assert proposal_rules.read_shade("Chanel N°5", "beauty", "Makeup") is None
        assert proposal_rules.read_shade("Rouge Dior Lipstick - 447 Mellow Shade", "beauty", "Fragrance") is None
        assert "447" in product_lines.product_line_key("Rouge Dior Lipstick - 447 Mellow Shade", listed_brand="Dior", vertical="beauty", category="Makeup")

    def test_a_cask_is_proposed_as_an_attribute_and_stays_in_the_name(self):
        assert proposal_rules.read_cask("Balvenie 14 Caribbean Rum Cask 70cl", "liquor") == ("caribbean rum cask", "caribbean rum cask")
        assert proposal_rules.read_cask("Macallan Triple Cask 12", "liquor") is None, "triple cask is the expression, not a cask"
        assert product_lines.product_line_key("Balvenie 14 Caribbean Rum Cask 70cl", listed_brand="Balvenie", vertical="liquor") == "14 caribbean rum cask"

    def test_a_brand_trailer_is_reported_not_silently_folded_away_from_sight(self):
        assert proposal_rules.trailer_words("Appleton Estate") == ["estate"] and proposal_rules.trailer_words("Gin Mare") == []

    def test_the_process_version_is_the_documents(self):
        doc = (Path(__file__).resolve().parents[1] / "docs" / "REVIEW-PROCESS.md").read_text()
        assert re.search(r"\*\*Version (\d+),", doc).group(1) == proposal_rules.PROCESS_VERSION


class TestTheFourteenOneMillionRows:
    def test_the_synonyms_fold_the_set_stays_apart_and_nothing_else_joins(self):
        keys: dict[str, list[str]] = {}
        for brand, name, size in ONE_MILLION:
            keys.setdefault(match_key(brand, name, size, vertical="beauty", brand="rabanne"), []).append(name)
        joined = {k: v for k, v in keys.items() if len(v) > 1}
        for key, names in joined.items():
            slots = key.split("|")
            assert slots[1] == "1-million" and slots[2].startswith("concentration="), (key, names)
            assert not any("Set" in n for n in names), "the set never shares a key with a bottle"
        edt_100 = [k for k in keys if k == "rabanne|1-million|concentration=edt|100ml"]
        assert edt_100 and len(keys[edt_100[0]]) >= 2, "EDT and Eau de Toilette at one size are one variant"
        assert any(k.endswith("|set") and "set-duo" in k for k in keys)


def seed(db):  # noqa: F811
    brand = Brand(slug="chivas-regal", name="Chivas Regal")
    db.add(brand)
    db.flush()
    out = []
    for key, name in (("12yo", "Chivas Regal 12 Years Old 1L"), ("12yo blended scotch whisky", "Chivas Regal 12 Year Old Blended Scotch Whisky 1L"),
                      ("12", "Chivas Regal 12 1L"), ("18yo", "Chivas Regal 18 Years Old 1L")):
        line = ProductLine(brand_id=brand.id, key=key, slug=f"chivas-regal-{key.replace(' ', '-')}", name=key)
        db.add(line)
        db.flush()
        v = ProductVariant(name=name, brand="Chivas Regal", brand_id=brand.id, vertical="liquor", product_line_id=line.id,
                           match_key=match_key("Chivas Regal", name, 1000, vertical="liquor"), attributes={}, quantity_ml=1000)
        db.add(v)
        out.append((line, v))
    db.commit()
    return out


class TestTheGenerators:
    def test_each_list_writes_its_own_pass_and_nothing_is_applied(self, db):  # noqa: F811
        rows = seed(db)
        before = [(v.match_key, v.product_line_id, line.alias_of_id) for line, v in rows]
        counts = proposal_rules.generate(db)
        db.commit()
        assert counts["drink_words"] == 1 and counts["age_words"] == 1 and counts["lists_together"] == 0
        assert [(v.match_key, v.product_line_id, line.alias_of_id) for line, v in rows] == before, "nothing applied"
        passes = {p.name: p for p in db.scalars(select(ProposalPass))}
        assert set(passes) == {"rule:drink_words:6", "rule:age_words:6"} and all(p.kind == "rule" for p in passes.values())
        proposal = db.scalar(select(Proposal).where(Proposal.pass_id == passes["rule:drink_words:6"].id))
        assert (proposal.entity_type, proposal.field, proposal.value, proposal.status) == ("suggestion", "decision", {"decision": "same"}, "open")
        assert proposal.reason.startswith("rule:drink_words: removed 'blended', 'scotch', 'whisky' as drink category")
        assert proposal.evidence[0]["matched"] == "blended, scotch, whisky" and proposal.natural_key.startswith("pair:line:line:")
        assert "span" not in proposal.evidence[0], "a rule has no character span; the words go in `matched`"
        pair = db.get(Suggestion, proposal.entity_id)
        assert pair.level == "line" and pair.reason == "rule:drink_words" and pair.decision is None
        assert {rows[0][0].id, rows[1][0].id} == {pair.left_id, pair.right_id}, "the 12 and the 18 are never offered as one"

    def test_a_second_run_inserts_nothing_and_a_decided_pair_is_never_proposed_again(self, db):  # noqa: F811
        seed(db)
        proposal_rules.generate(db)
        db.commit()
        n = len(list(db.scalars(select(Proposal))))
        proposal_rules.generate(db)
        db.commit()
        assert len(list(db.scalars(select(Proposal)))) == n
        for pair in db.scalars(select(Suggestion)):
            pair.decision = "separate"
        db.commit()
        assert sum(proposal_rules.generate(db, check=True).values()) == 0


class TestARuleRowReadsOnTheSheet:
    """The sheet answered 500 on every brand whose rows came from a rule (17 Sep, staging):
    `ResponseValidationError: span 'input': '01'` against `list[int]`, because the generators put
    the words they matched where the spec's character offsets go. Rule evidence also cites no
    listing, and the reader called that "the listing is gone", so every rule row read as stale.
    Both are pinned here: the API model must validate, and the row must read as current.
    """

    def test_evidence_without_a_listing_validates_and_is_not_stale(self, db):  # noqa: F811
        from app.models.schemas import ReviewEvidenceOut
        from app.services import proposals

        seed(db)
        proposal_rules.generate(db)
        db.commit()
        row = db.scalar(select(Proposal))
        cache = proposals.ListingCache(db)
        ok, why, spans = proposals.evidence_state(cache, row.evidence)
        assert ok and why is None, f"a rule's own evidence is not stale: {why}"
        for state in spans:
            ReviewEvidenceOut(**state)  # the shape the sheet returns; a string span raised here
            assert state["span"] is None and "matched" in state, "no character span; the words ride in `matched`"
