"""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), seeded here by hand. 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 picker's own rules hold per family (an award and a photo win a close call, the champions
stay); 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_and_a_photo_win_a_close_call_within_a_family(self):
        rows = [row(1, "Rum", 30.0, dearest=40.0, thumb=None), row(2, "Rum", 30.0, dearest=39.5), row(3, "Rum", 30.0, dearest=39.0, thumb=None)]
        assert af.pick_by_family(rows, awarded_ids={3}, per_family=2)[0][2][0] == 3  # the medal leads
        ids = af.pick_by_family(rows, awarded_ids=set(), per_family=2)[0][2]
        assert ids[0] == 2 and 1 in ids  # the photo wins the close call; the champion stays on the row


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
