"""The line and attribute backfills on the fourteen 1 Million rows (Stream M, task M2).

Run against an in-memory SQLite holding the catalogue tables, like the account suites: no
server, no network. What is pinned is the brief's acceptance line, "one listed_brand brand, one
line, three attribute_values, and the set kept apart", and that a second run changes nothing.
Before the Paco Rabanne alias is confirmed the same rows are two brands and two lines,
which is the honest state; the alias is rian's decision, never a rule's.
"""

from __future__ import annotations

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, Base, Brand, Suggestion, ProductVariant, ProductLine, AttributeAlias
from app.services.normalize import match_key
from tests.test_product_lines import ONE_MILLION

TABLES = [Account.__table__, Brand.__table__, ProductLine.__table__, ProductVariant.__table__, AttributeAlias.__table__,
          Suggestion.__table__]


@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)
    with factory() as session:
        yield session


def seed(db, *, alias: bool):
    rabanne = Brand(slug="rabanne", name="Rabanne")
    paco = Brand(slug="paco-rabanne", name="Paco Rabanne")
    db.add_all([rabanne, paco])
    db.flush()
    if alias:
        paco.alias_of_id = rabanne.id
    for n, (brand, name, size) in enumerate(ONE_MILLION, start=1):
        db.add(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=match_key(brand, name, size), attributes={},
        ))
    db.commit()
    return rabanne, paco


class TestBeforeTheAlias:
    def test_two_houses_are_two_lines_and_a_second_run_changes_nothing(self, db):
        seed(db, alias=False)
        # Three since identity rules v6: the coffret ("Set: Duo 1 Million ...") is its own product
        # line, as REVIEW-PROCESS.md section 2 says of a set; v5's format list deleted "set duo".
        assert cli.backfill_lines(db) == "lines: 3 line(s) created, 14 product(s) pointed at their line"
        assert cli.backfill_lines(db) == "lines: 0 line(s) created, 0 product(s) pointed at their line"
        lines = db.scalars(select(ProductLine).order_by(ProductLine.slug)).all()
        assert [(line.slug, line.key) for line in lines] == [
            ("paco-rabanne-1-million", "1 million"), ("rabanne-1-million", "1 million"),
            ("rabanne-set-duo-1-million", "set duo 1 million"),
        ]
        assert [line.name for line in lines][:2] == ["1 Million", "1 Million"]


class TestWithTheAlias:
    def test_one_house_one_line_three_variations_and_the_set_apart(self, db):
        rabanne, _ = seed(db, alias=True)
        assert cli.backfill_lines(db) == "lines: 2 line(s) created, 14 product(s) pointed at their line"  # the bottle's line and the set's own
        line = db.scalar(select(ProductLine).where(ProductLine.key == "1 million"))
        assert (line.brand_id, line.key, line.name, line.slug) == (rabanne.id, "1 million", "1 Million", "rabanne-1-million")
        assert cli.backfill_attributes(db) == "attribute_values: 4 wording(s) seeded, 11 product(s) stamped with their attribute"
        assert cli.backfill_attributes(db) == "attribute_values: 0 wording(s) seeded, 0 product(s) stamped with their attribute"
        product_variants = db.scalars(select(ProductVariant).order_by(ProductVariant.id)).all()
        from app.services.product_lines import form_of
        assert {p.product_line_id for p in product_variants if not form_of(p.name)} == {line.id}
        assert {p.product_line_id for p in product_variants if form_of(p.name)} != {line.id}, "the set has its own product line"
        attribute_values = {(p.attributes or {}).get("attribute") for p in product_variants}
        assert attribute_values == {"parfum", "edt", "elixir", None}
        assert sorted(a.raw for a in db.scalars(select(AttributeAlias))) == [
            "edt", "elixir edp intense", "elixir parfum intense", "parfum"]
        from app.services.product_lines import form_of
        assert [p.name for p in product_variants if form_of(p.name)] == ["Rabanne Set: Duo 1 Million Eau de Toilette 50 ml 100 ml"]

    def test_a_name_a_person_set_is_never_overwritten(self, db):
        seed(db, alias=True)
        cli.backfill_lines(db)
        line = db.scalar(select(ProductLine))
        line.name = "One Million"
        db.commit()
        cli.backfill_lines(db)
        assert db.scalar(select(ProductLine)).name == "One Million"

    def test_a_variation_a_person_decided_wins_over_the_rule(self, db):
        seed(db, alias=True)
        db.add(AttributeAlias(vertical="beauty", raw="elixir parfum intense", canonical="parfum intense",
                              display="Parfum Intense", decided_by=None))
        db.commit()
        cli.backfill_attributes(db)
        rows = db.scalars(select(ProductVariant).where(ProductVariant.name.like("%Elixir Parfum Intense%"))).all()
        assert {p.attributes["attribute"] for p in rows} == {"parfum intense"}


class TestDecisionsFollowProducts:
    """A person's decision on a line follows its product variants when a rule change empties the row
    (the catalogue decisions §2.2): the decision is about the line, not the row id the rules
    minted; a row whose product variants scattered keeps its decision and is reported."""

    def test_a_review_and_a_name_move_to_the_row_the_products_went_to(self, db):
        from app.models import LEDGER_TABLES
        from app.models.places import Place, ShopPlace
        from app.services import overrides
        for table in [Place.__table__, ShopPlace.__table__, *LEDGER_TABLES]:
            table.create(db.get_bind(), checkfirst=True)
        overrides._present.clear()
        rabanne, _ = seed(db, alias=True)
        cli.backfill_lines(db)
        live = db.scalar(select(ProductLine))
        overrides.decide(db, "line", str(live.id), "review", "hidden", set_by=1)
        overrides.decide(db, "line", str(live.id), "name", "One Million", set_by=1, collected_value="1 Million")
        db.commit()
        # A rule change: every product of the old row keys elsewhere. Simulated by renaming the
        # row's key so the backfill mints a fresh row for the same names.
        live.key = "old rule key"
        db.commit()
        out = cli.backfill_lines(db)
        assert "2 line decision(s) moved with their product variants" in out
        new = db.scalar(select(ProductLine).where(ProductLine.key == "1 million"))
        assert set(overrides.fields_of(db, "line", str(new.id))) == {"review", "name"}
        # The ledger is append-only (K2): the emptied row keeps its rows as history, carried copies
        # sit on the new row with their provenance, and a row any decision names is never pruned.
        assert set(overrides.fields_of(db, "line", str(live.id))) == {"review", "name"}
        assert cli.backfill_prune_lines(db) == "prune_lines: 0 empty line row(s) nothing referenced deleted"
        overrides._present.clear()


class TestPrune:
    """Identity rules v5 left the rows the v4 Makeup line rule made with nothing on them (the
    catalogue decisions §2.3): a line row is a pure function of the name, so an empty one that
    nothing references is a cache and is pruned; anything referenced, aliased, suggested or
    decided is kept. A tombstone's line is a reference."""

    def test_an_empty_line_nothing_references_is_pruned_and_every_referenced_one_kept(self, db):
        rabanne, _ = seed(db, alias=True)
        cli.backfill_lines(db)
        live = db.scalar(select(ProductLine))
        empty = ProductLine(brand_id=rabanne.id, key="old shade key", name="Old", slug="rabanne-old")
        aliased = ProductLine(brand_id=rabanne.id, key="spelling", name="Spelling", slug="rabanne-spelling")
        decided = ProductLine(brand_id=rabanne.id, key="chosen", name="Chosen", slug="rabanne-chosen", decided_by=1)
        tombstoned = ProductLine(brand_id=rabanne.id, key="gone", name="Gone", slug="rabanne-gone")
        suggested = ProductLine(brand_id=rabanne.id, key="offered", name="Offered", slug="rabanne-offered")
        db.add_all([empty, aliased, decided, tombstoned, suggested])
        db.flush()
        aliased.alias_of_id = live.id
        db.add(ProductVariant(id=99, name="1 Million Old 50ml", brand="Rabanne", brand_id=rabanne.id, vertical="beauty",
                       match_key="x", attributes={}, product_line_id=tombstoned.id, merged_into_id=1))
        db.add(Suggestion(level="line", left_id=min(live.id, suggested.id), right_id=max(live.id, suggested.id),
                              reason="words_shared", score=0.5))
        db.commit()
        assert cli.backfill_prune_lines(db) == "prune_lines: 1 empty line row(s) nothing referenced deleted"
        kept = sorted(row.key for row in db.scalars(select(ProductLine)))
        assert kept == ["1 million", "chosen", "gone", "offered", "set duo 1 million", "spelling"]
        assert cli.backfill_prune_lines(db) == "prune_lines: 0 empty line row(s) nothing referenced deleted"

