"""The product line page's read (Stream K5; plan W1, W20). One line, "1 Million", with three product
variants: the EDT and the Elixir at 100 ml, each at Heathrow and Paris (two comparison units, a tie),
and the Parfum at 50 ml at Singapore only, also listed by an online catalogue. Each W20 case is
one test. What it cost before: a variant page printed "cheapest at" whichever shop it found, the
shopper's own airports or not, a catalogue shop could price the page, and a card chose its bottle
by whatever list order a query returned. SQLite kit, no network."""
from __future__ import annotations

from datetime import UTC, datetime, timedelta

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

from app.models import (Award, Base, Brand, Listing, PriceObservation, ProductLine, ProductVariant, Redirect, Retailer, Shop,
                        Source)
from app.models.places import Place, ShopPlace
from app.services import catalog_queries as cq

NOW = datetime(2026, 9, 17, 8, 0, tzinfo=UTC)
TABLES = [Brand.__table__, ProductLine.__table__, ProductVariant.__table__, Retailer.__table__, Shop.__table__, Listing.__table__,
          PriceObservation.__table__, Award.__table__, Source.__table__, Place.__table__, ShopPlace.__table__, Redirect.__table__]
EDT, ELIXIR, PARFUM = 101, 102, 103


@pytest.fixture
def db(monkeypatch):
    engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
    Base.metadata.create_all(engine, tables=TABLES)
    # Verification lives in JSONB tables the kit cannot build; nothing is blocked here.
    monkeypatch.setattr(cq, "blocked_shop_ids", lambda db: [])
    with sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)() as s:
        s.add_all([Brand(id=1, slug="rabanne", name="Rabanne"), Retailer(id=1, slug="wdf", name="World Duty Free"),
                   Retailer(id=2, slug="catalogue", name="The Catalogue")])
        s.flush()
        s.add(ProductLine(id=1, brand_id=1, key="1 million", slug="rabanne-1-million", name="1 Million"))
        s.add_all([Shop(id=1, retailer_id=1, code="LHR1", iata="LHR", name="Heathrow T5", city="London", currency="GBP", visible=True),
                   Shop(id=2, retailer_id=1, code="CDG1", iata="CDG", name="Paris CDG", city="Paris", currency="EUR", visible=True),
                   Shop(id=3, retailer_id=1, code="SIN1", iata="SIN", name="Changi T3", city="Singapore", currency="SGD", visible=True),
                   Shop(id=4, retailer_id=2, code="ONLINE", iata=None, name="The Catalogue", currency="EUR", is_catalogue_only=True, visible=True)])
        s.flush()
        for vid, name, ml, concentration in ((EDT, "1 Million Eau de Toilette 100 ml", 100, "eau de toilette"),
                                             (ELIXIR, "1 Million Elixir 100 ml", 100, "elixir"),
                                             (PARFUM, "1 Million Parfum 50 ml", 50, "parfum")):
            s.add(ProductVariant(id=vid, name=name, brand="Rabanne", brand_id=1, vertical="beauty", category="Perfume",
                                 match_key=f"k{vid}", product_line_id=1, quantity_value=ml, quantity_unit="ml",
                                 quantity_state="stated", quantity_ml=ml, form="single",
                                 attributes={"attribute": concentration, "attribute_kind": "concentration"}))
        s.flush()
        listing_id = 0
        for vid, shop_id, price, usd in ((EDT, 1, 80, 101.0), (EDT, 2, 95, 104.0), (ELIXIR, 1, 110, 139.0),
                                         (ELIXIR, 2, 120, 131.0), (PARFUM, 3, 150, 112.0), (PARFUM, 4, 70, 76.0)):
            listing_id += 1
            s.add(Listing(id=listing_id, variant_id=vid, shop_id=shop_id, source_sku=f"sku{listing_id}"))
            s.flush()
            s.add(PriceObservation(listing_id=listing_id, price=price, currency="GBP", price_usd=usd, price_type="duty_free",
                                   in_stock=True, observed_at=NOW - timedelta(days=1)))
        s.commit()
        yield s


def line(db, variant=None, airports=None):
    return cq.get_product_line(db, "rabanne-1-million", variant, airports)


def test_the_line_holds_every_variant_with_its_attributes_and_units(db):
    d = line(db)
    assert [v.id for v in d.variants] == [EDT, ELIXIR, PARFUM]
    assert {v.id: v.units for v in d.variants} == {EDT: 2, ELIXIR: 2, PARFUM: 1}, "a catalogue is never a comparison unit"
    assert {v.id: v.airports for v in d.variants} == {EDT: ["CDG", "LHR"], ELIXIR: ["CDG", "LHR"], PARFUM: ["SIN"]}
    kinds = {s.kind: [o.text for o in s.options] for s in d.selectors}
    assert kinds == {"quantity": ["100 ml", "50 ml"], "concentration": ["eau de toilette", "elixir", "parfum"]}
    assert d.path == "/products/rabanne-1-million" and d.brand == "Rabanne" and d.indexed is False


def test_case_2_several_variants_and_none_chosen_asks_for_a_variant(db):
    block = line(db).price_block
    assert block.case == "ask_variant" and block.prices == []


def test_case_3_no_airports_and_a_variant_at_several_places_asks_for_an_airport(db):
    block = line(db, ELIXIR).price_block
    assert block.case == "ask_airport" and block.prices == []
    assert [a.iata for a in block.elsewhere] == ["CDG", "LHR"]


def test_case_1_no_airports_and_a_variant_at_one_place_prints_that_price_and_shop_never_the_catalogues(db):
    block = line(db, PARFUM).price_block
    assert block.case == "single_place"
    assert [(p.shop_iata, p.price) for p in block.prices] == [("SIN", 150.0)], "the cheaper online catalogue never prices a page"
    assert block.prices[0].observed_at is not None


def test_case_4_chosen_airports_that_stock_it_print_those_prices_and_name_the_ones_that_do_not(db):
    block = line(db, EDT, ["LHR", "SIN"]).price_block
    assert block.case == "chosen_airports"
    assert [p.shop_iata for p in block.prices] == ["LHR"] and block.missing_airports == ["SIN"]


def test_case_5_chosen_airports_that_stock_none_say_so_and_offer_where_it_is_even_at_one_place(db):
    block = line(db, PARFUM, ["LHR", "CDG"]).price_block
    assert block.case == "none_at_chosen" and block.prices == []
    assert block.missing_airports == ["LHR", "CDG"] and [a.iata for a in block.elsewhere] == ["SIN"]
    assert block.elsewhere[0].path == "/airports/changi-sin-singapore"


def test_airports_with_no_variant_chosen_name_the_variants_stocked_there(db):
    block = line(db, None, ["SIN"]).price_block
    assert block.case == "ask_variant" and block.variants_at_airports == [PARFUM]


def test_the_representative_is_the_most_units_then_the_lowest_id_on_a_tie(db):
    """The Rabanne tie: the EDT and the Elixir at 100 ml each at two places; the card stands for
    the lower id, whichever one a collection touched last."""
    assert line(db).representative_variant_id == EDT
    db.get(PriceObservation, 3).observed_at = NOW
    db.commit()
    assert line(db).representative_variant_id == EDT


def test_a_merged_away_variant_follows_to_its_survivor_and_a_foreign_one_is_not_chosen(db):
    db.add(ProductVariant(id=999, name="1 Million EDT 100ml", brand="Rabanne", brand_id=1, match_key="old", merged_into_id=EDT,
                          product_line_id=1))
    db.commit()
    assert line(db, 999).chosen_variant_id == EDT
    assert line(db, 424242).chosen_variant_id is None


def test_an_alias_line_or_a_retired_slug_forwards_and_a_hidden_line_answers_nothing(db):
    db.add(ProductLine(id=2, brand_id=1, key="one million", slug="rabanne-one-million", name="One Million", alias_of_id=1))
    db.add(Redirect(from_slug="paco-rabanne-1-million", kind="product_line", to_slug="rabanne-1-million"))
    db.commit()
    assert cq.line_by_slug(db, "rabanne-one-million") == (None, "rabanne-1-million")
    assert cq.line_by_slug(db, "paco-rabanne-1-million") == (None, "rabanne-1-million")
    assert cq.get_product_line(db, "rabanne-one-million") is None
    db.get(ProductLine, 1).hidden = True
    db.commit()
    assert cq.line_by_slug(db, "rabanne-1-million") == (None, None) and line(db) is None


def test_a_card_deep_links_to_its_line_variant_and_the_airports_its_comparison_shows(db):
    """Plan W1's card contract: the card opens exactly the comparison it promised. Its priced rows
    are `(label, iata, usd, in_stock)`, cheapest first; a catalogue row has no code; four are shown."""
    priced = [("LHR", "LHR", 101.0, True), ("Online", None, 76.0, True), ("CDG", "CDG", 104.0, True),
              ("SIN", "SIN", 112.0, True), ("DXB", "DXB", 120.0, True)]
    assert cq.card_path("rabanne-1-million", EDT, priced) == "/products/rabanne-1-million?variant=101&airports=LHR,CDG,SIN"
    assert cq.card_path(None, EDT, priced) is None
    assert cq.line_slugs_of(db, [EDT, PARFUM, 555]) == {EDT: "rabanne-1-million", PARFUM: "rabanne-1-million"}
    block = line(db, EDT, ["LHR", "CDG", "SIN"]).price_block
    assert block.case == "chosen_airports" and [p.shop_iata for p in block.prices] == ["LHR", "CDG"] and block.missing_airports == ["SIN"]


def test_the_address_helpers_read_leniently_and_the_spa_mirrors_them():
    """urls.py line_path and read_airports are mirrored by lib/urls.ts linePath and readAirports: a
    server link and a client link to one view must be the same string, or a card's comparison would
    open at a different address than the server rendered."""
    import pathlib

    from app.services import urls

    assert urls.read_airports("lhr, CDG,xx,LHR,sin2,,JFK") == ["LHR", "CDG", "JFK"]
    assert urls.read_airports(None) == [] and len(urls.read_airports(",".join(["AAA", "BBB", "CCC"] * 5))) == 3
    assert urls.line_path("rabanne-1-million") == "/products/rabanne-1-million"
    assert urls.line_path("rabanne-1-million", 101, ["lhr", "CDG"]) == "/products/rabanne-1-million?variant=101&airports=LHR,CDG"
    ts = (pathlib.Path(__file__).resolve().parents[1] / "web" / "src" / "lib" / "urls.ts").read_text()
    assert "export function linePath(" in ts and "export function readAirports(" in ts
    assert "`variant=${variantId}`" in ts and "`airports=${codes.join(\",\")}`" in ts and "/^[A-Z]{3}$/" in ts
