"""Featured at this airport, by family (Stream G, G4): the picker over a seeded set of rows.

The rows are the airport page's own (`catalog_queries._airport_rows`: id, category,
airport_count, here_usd, cheapest_usd, dearest_usd, thumb_url, and since Stream AW2 the
featured columns, read with a default when a hand-seeded row lacks them). What it pins: only
comparisons this airport wins are candidates; a family with no comparable product yields no
row rather than a guessed one; a hidden or unknown category belongs to no family; the v3
order's own rules hold per family (an award on a real saving and a picture win a close call,
the champions stay, the tier comes through `tiers`); the rows come in family order and never
exceed the row length; and the summaries fetch happens once for every card. No database.
"""

from types import SimpleNamespace

from app.models.hubs import FeaturedFamily
from app.models.schemas import ProductSummary
from app.services import airport_featured as af


def row(pid, category, here, cheapest=None, dearest=None, airports=2, thumb="x"):
    return SimpleNamespace(
        id=pid, category=category, airport_count=airports, here_usd=here,
        cheapest_usd=here if cheapest is None else cheapest, dearest_usd=dearest if dearest is not None else here,
        thumb_url=thumb,
    )


class TestCandidates:
    def test_only_a_comparison_this_airport_wins_is_a_candidate(self):
        rows = [
            row(1, "Whisky", 40.0, dearest=55.0),                # cheapest here, real gap
            row(2, "Whisky", 45.0, cheapest=40.0, dearest=55.0),  # cheaper elsewhere
            row(3, "Whisky", 40.0, dearest=40.4),                 # a gap that is rounding
            row(4, "Whisky", 40.0, dearest=55.0, airports=1),     # sold here only
            row(5, "Gin", None, dearest=30.0),                    # no price here
        ]
        assert [c.variant_id for c in af.candidates(rows)] == [1]
        c = af.candidates(rows)[0]
        assert c.saving_usd == 15.0 and round(c.saving_pct, 3) == 0.273 and c.has_image and c.shop_count == 2


class TestPick:
    def test_a_family_with_nothing_comparable_has_no_row(self):
        rows = [
            row(1, "Whisky", 40.0, dearest=55.0),
            row(2, "Perfume", 80.0, cheapest=70.0, dearest=95.0),  # perfume: cheaper elsewhere, so no row
            row(3, "Wine", 12.0, dearest=12.0),                     # wine: no gap
        ]
        assert af.pick_by_family(rows, set()) == [("spirits", "Spirits", [1])]

    def test_families_come_in_order_capped_to_the_row_and_hidden_categories_belong_nowhere(self):
        rows = [row(i, "Gin", 20.0, dearest=30.0 + i) for i in range(1, 8)]  # seven gins
        rows += [row(20, "Perfume", 50.0, dearest=80.0), row(21, "Champagne & Sparkling", 30.0, dearest=60.0)]
        rows += [row(30, "Makeup", 10.0, dearest=15.0), row(31, None, 10.0, dearest=40.0), row(32, "Twinpack", 1.0, dearest=9.0)]
        picked = af.pick_by_family(rows, set())
        assert [(key, len(ids)) for key, _, ids in picked] == [("spirits", 4), ("wine", 1), ("perfume", 1), ("skincare", 1)]
        # The biggest spirits saving leads its row; the picker keeps the champions.
        assert picked[0][2][0] == 7
        assert af.family_of(None) is None and af.family_of("Twinpack") is None

    def test_an_extreme_spread_at_two_shops_is_held_back_like_the_home_page_holds_it(self):
        """The picker's guard, unchanged: a 75% gap seen at only two shops is as likely an entry
        error as a bargain, so the family draws no row rather than headline it."""
        assert af.pick_by_family([row(30, "Makeup", 10.0, dearest=40.0)], set()) == []
        assert af.pick_by_family([row(30, "Makeup", 10.0, dearest=40.0, airports=3)], set()) == [("skincare", "Skincare and makeup", [30])]

    def test_an_award_on_a_real_saving_and_a_picture_win_a_close_call_within_a_family(self):
        """v3 (Stream AW2): twelve rums, the $20 champion first whatever else; the medal on a
        $12.20 saving (standing 0.82, over the award floor) beats the $12.50 card one rung
        above it without one (0.91 against 0.82 + 0.10 since version 2: a rung of twelve is
        0.09); a supplied bottle shot (tier 6, through `tiers`, 0.36) lifts the $11.90 card
        past both of them. Six rums made a rung 0.2, which no award is meant to buy."""
        rows = [row(1, "Rum", 30.0, dearest=50.0, thumb=None), row(2, "Rum", 30.0, dearest=42.5, thumb=None),
                row(3, "Rum", 30.0, dearest=42.2, thumb=None), row(4, "Rum", 30.0, dearest=42.1, thumb=None),
                row(5, "Rum", 30.0, dearest=41.9, thumb=None), row(6, "Rum", 30.0, dearest=39.0, thumb=None)]
        rows += [row(10 + i, "Rum", 30.0, dearest=38.5 - i, thumb=None) for i in range(6)]
        assert af.pick_by_family(rows, awarded_ids={3}, per_family=4, tiers={5: 6})[0][2] == [1, 5, 3, 2]
        assert af.pick_by_family(rows, awarded_ids=set(), per_family=4)[0][2] == [1, 2, 3, 4]

    def test_a_set_thumb_reads_as_a_public_bottle_when_no_tiers_are_given(self):
        rows = [row(1, "Rum", 30.0, dearest=50.0, thumb=None), row(2, "Rum", 30.0, dearest=42.5, thumb=None),
                row(3, "Rum", 30.0, dearest=42.5, thumb="x")]  # the same saving: the photo decides
        assert af.candidates(rows)[2].has_image and af.pick_by_family(rows, set(), per_family=3)[0][2] == [1, 3, 2]


class TestBlock:
    def test_one_summaries_fetch_for_every_card_and_no_row_without_items(self):
        rows = [row(1, "Whisky", 40.0, dearest=55.0), row(2, "Perfume", 50.0, dearest=80.0)]
        calls = []

        def fetch(ids):
            calls.append(list(ids))
            # The perfume is gone from the catalogue between the rows and the fetch: its row disappears.
            return [ProductSummary(id=1, name="A whisky", shop_count=2)]

        class Db:
            def scalars(self, stmt):
                return iter([])

        block = af.featured_at(Db(), rows, fetch)
        assert calls == [[1, 2]]
        assert block == [FeaturedFamily(key="spirits", label="Spirits", items=[ProductSummary(id=1, name="A whisky", shop_count=2)])]
        assert af.featured_at(Db(), [], fetch) == [] and calls == [[1, 2]]  # nothing comparable: no fetch, no rows
