"""Applying an alias makes the catalogue follow (Stream M, task M4).

The fourteen 1 Million rows again, on the in-memory SQLite, with a listing each: before
rian confirms "Paco Rabanne is Rabanne" they are two houses with two lines and the 100 ml
Eau de Toilette is two products; applying the alias points the row, moves the lines and
the products, re-keys them under the house, folds the two Eau de Toilette rows and the two
200 ml Elixirs, and records who decided. A line alias and a variation alias do the same
at their level. A second application changes nothing.
"""

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 (
    RawRecord,
    Account, Award, Base, Brand, Listing, Location, MergeCandidate, Product, ProductLine, ProductMerge,
    Retailer, VariationAlias,
)
from app.services import merges
from tests.test_lines import ONE_MILLION

TABLES = [
    Account.__table__, Brand.__table__, ProductLine.__table__, Product.__table__, VariationAlias.__table__,
    Retailer.__table__, Location.__table__, Listing.__table__, Award.__table__, ProductMerge.__table__,
    MergeCandidate.__table__, RawRecord.__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)
    with factory() as session:
        session.add(Account(id=RIAN, username="rian", display_name="rian"))
        yield session


def seed(db):
    """The fourteen rows, each at its own shop, 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 = Product(id=n, name=name, brand=brand, brand_id=paco.id if brand == "Paco Rabanne" else rabanne.id,
                          vertical="beauty", size_ml=size, match_key="pending", attributes={})
        location = Location(retailer_id=retailer.id, code=f"S{n}", iata=f"A{n:02d}", name=f"Shop {n}", currency="EUR")
        db.add_all([product, location])
        db.flush()
        db.add(Listing(product_id=product.id, location_id=location.id, source_sku=f"sku{n}"))
    db.commit()
    cli.backfill_lines(db)
    cli.backfill_variations(db)
    from app.services import keying
    maps = keying.load_maps(db)
    merges.rekey_products(db, list(db.scalars(select(Product))), maps)
    merges.merge_duplicates(db)
    db.commit()
    return rabanne, paco


def alive(db):
    return {p.id: p for p in db.scalars(select(Product).where(Product.merged_into_id.is_(None)))}


class TestBrandAlias:
    def test_before_the_alias_two_houses_keep_the_same_bottle_apart(self, db):
        seed(db)
        rows = alive(db)
        # Inside the Rabanne house the rules already fold the two 100 ml Eau de Toilette
        # spellings (the brief's 14747 and 17217) and the two 200 ml Elixirs; nothing of
        # Paco Rabanne's agrees with anything across the two houses.
        assert len(rows) == 12
        assert db.get(Product, 10).merged_into_id == 6 and db.get(Product, 13).merged_into_id == 9
        assert rows[6].match_key == "rabanne|1-million|edt|100ml" and rows[4].match_key == "paco-rabanne|1-million||100ml"

    def test_the_alias_moves_lines_rekeys_products_and_folds_what_then_agrees(self, db):
        rabanne, paco = seed(db)
        counts = merges.apply_brand_alias(db, paco, rabanne, decided_by=RIAN, preferred_name="Rabanne")
        db.commit()
        assert (paco.canonical_id, paco.decided_by) == (rabanne.id, RIAN) and paco.decided_at is not None
        assert counts["lines_pointed"] == 1 and counts["products_rekeyed"] == 5
        assert counts["groups_merged"] == 0, "the alias's own five rows agreed with nothing yet: their variation is unknown or Parfum at other sizes"
        rows = alive(db)
        assert all(p.match_key.startswith("rabanne|") for p in rows.values())
        assert {p.line_id for p in rows.values()} == {db.scalar(select(ProductLine.id).where(ProductLine.brand_id == rabanne.id, ProductLine.key == "1 million"))}
        alias_line = db.scalar(select(ProductLine).where(ProductLine.brand_id == paco.id))
        assert alias_line.canonical_id is not None and alias_line.decided_by == RIAN
        assert rows[4].match_key == "rabanne|1-million||100ml", "the bare 10cl keys apart on an unknown variation and waits for a person"
        again = merges.apply_brand_alias(db, paco, rabanne, decided_by=RIAN)
        assert again["lines_pointed"] == 0 and again["groups_merged"] == 0

    def test_the_same_bottle_at_two_shops_folds_once_it_agrees(self, db):
        """14747 and 17217 in the brief: one EDT 100 ml, two spellings, two shops."""
        rabanne, paco = seed(db)
        db.add(Product(id=99, name="1 Million EDT 100ml", brand="Paco Rabanne", brand_id=paco.id, vertical="beauty",
                       size_ml=100, match_key="paco-rabanne|1-million|edt|100ml", attributes={"concentration": "edt", "variation": "edt"},
                       line_id=db.scalar(select(ProductLine.id).where(ProductLine.brand_id == paco.id))))
        db.flush()
        location = Location(retailer_id=1, code="S99", iata="A99", name="Shop 99", currency="EUR")
        db.add(location)
        db.flush()
        db.add(Listing(product_id=99, location_id=location.id, source_sku="sku99"))
        db.commit()
        counts = merges.apply_brand_alias(db, paco, rabanne, decided_by=RIAN)
        db.commit()
        assert counts["groups_merged"] == 1 and counts["rows_merged"] == 1
        survivor = db.get(Product, 6) if db.get(Product, 99).merged_into_id else db.get(Product, 99)
        assert survivor.merged_into_id is None
        record = db.scalar(select(ProductMerge).where((ProductMerge.from_id == 99) | (ProductMerge.to_id == 99)))
        assert record.merged_by == RIAN, "a merge an alias caused is recorded against the person who confirmed the alias"
        assert len(db.scalars(select(Listing).where(Listing.product_id == survivor.id)).all()) == 3


class TestLineAlias:
    def test_a_line_alias_moves_its_products_and_rekeys_them(self, db):
        rabanne, _ = seed(db)
        one = db.scalar(select(ProductLine).where(ProductLine.brand_id == rabanne.id, ProductLine.key == "1 million"))
        other = ProductLine(brand_id=rabanne.id, key="one million", name="One Million", slug="rabanne-one-million")
        db.add(other)
        db.flush()
        db.add(Product(id=50, name="One Million Eau de Toilette 200 ml", brand="Rabanne", brand_id=rabanne.id, vertical="beauty",
                       size_ml=200, match_key="rabanne|one-million|edt|200ml", attributes={"variation": "edt"}, line_id=other.id))
        db.commit()
        counts = merges.apply_line_alias(db, other, one, decided_by=RIAN, preferred_name="1 Million")
        db.commit()
        assert counts["products_moved"] == 1 and counts["groups_merged"] == 1
        assert (other.canonical_id, other.decided_by) == (one.id, RIAN) and one.name == "1 Million"
        moved = db.get(Product, 50)
        assert moved.line_id == one.id and moved.match_key == "rabanne|1-million|edt|200ml"
        assert moved.merged_into_id == 11 or db.get(Product, 11).merged_into_id == 50

    def test_two_houses_lines_never_fold_before_the_brand_alias(self, db):
        rabanne, paco = seed(db)
        mine = db.scalar(select(ProductLine).where(ProductLine.brand_id == paco.id))
        theirs = db.scalar(select(ProductLine).where(ProductLine.brand_id == rabanne.id))
        with pytest.raises(ValueError):
            merges.apply_line_alias(db, mine, theirs, decided_by=RIAN)


class TestVariationAlias:
    def test_a_wording_mapped_elsewhere_rekeys_every_product_that_carries_it(self, db):
        seed(db)
        counts = merges.apply_variation_alias(db, "beauty", "elixir parfum intense", "parfum intense", decided_by=RIAN)
        db.commit()
        assert counts["products_rekeyed"] == 3
        row = db.scalar(select(VariationAlias).where(VariationAlias.raw == "elixir parfum intense"))
        assert (row.canonical, row.display, row.decided_by) == ("parfum intense", "Parfum Intense", RIAN)
        keys = {p.id: p.match_key for p in db.scalars(select(Product)) if p.merged_into_id is None}
        assert keys[7] == "rabanne|1-million|parfum-intense|50ml" and keys[9] == "rabanne|1-million|parfum-intense|200ml"
        # 13 ("Elixir Eau de Parfum Intense", the other wording) was folded into 9 while both
        # read Elixir; a merge is forward and recorded, so a later alias never undoes it: a
        # person does, by hand, if the new reading says they were two bottles.
        assert db.get(Product, 13).merged_into_id == 9
