"""Category at airport (Stream G, G7): the address, the bar, the body and the head.

The pairing page (`/airports/<airport>/<category>`) is built only where the pair holds enough
published products (`coverage.CATEGORY_AT_AIRPORT_MIN_PRODUCTS`, read at request time). What it
pins: the category slug table and its TypeScript mirror; that every shown category has an
address; the rail of category pages on the airport page; the renderer's H1, standing line,
crumbs, canonical, structured data, siblings, exclusives row, feature chips and pager; and that
the query layer answers None under the bar, for an unknown category word, or a hidden airport.
No database: the counts and the shops are stubbed.
"""

import json
import pathlib
import re

import pytest

from app.models.hubs import AirportCategoryDetail, AirportShop, CategoryPageLink
from app.config import settings
from app.services import catalog_queries, coverage, seo, taxonomy, urls
from test_seo_airport import CHECKED, SHELL, airport, summary

MAIN = pathlib.Path(__file__).resolve().parents[1]
LHR = "/airports/heathrow-lhr-london"


def link(category, count):
    return CategoryPageLink(category=category, slug=urls.category_slug(category), path=f"{LHR}/{urls.category_slug(category)}", count=count)


def pair(**over) -> AirportCategoryDetail:
    base = dict(
        iata="LHR", path=f"{LHR}/whisky", name="London Heathrow", city="London", country="United Kingdom",
        currency="GBP", product_variants=67, last_collected_at=CHECKED,
        shops=[AirportShop(code="LHR", retailer_name="Avolta", product_variants=266, last_collected_at=CHECKED)],
        category="Whisky", category_slug="whisky", airport_path=LHR,
        comparable=30, cheapest_here=12, exclusives=3,
        exclusive_items=[summary(id=7, name="Talisker Dark Storm 1L", is_exclusive=True, award_count=0, top_award=None,
                                 shop_count=1, top_prices=[], thumb_url=None, cheapest_usd=60.0, dearest_usd=60.0)],
        siblings=[link("Gin", 24), link("Cognac & Brandy", 18)],
        total=67, limit=24, offset=0,
        items=[summary(), summary(id=8, name="Bell's Blended Scotch 1l", award_count=0, top_award=None)],
    )
    base.update(over)
    return AirportCategoryDetail(**base)


class TestAddress:
    def test_every_shown_category_has_a_word_and_the_pair_path_is_the_airports_plus_it(self):
        for category in taxonomy.VERTICAL_OF_CATEGORY:
            assert urls.category_slug(category), category
            assert re.fullmatch(r"[a-z]+(-[a-z]+)*", urls.category_slug(category))
        assert urls.airport_category_path(LHR, "Cognac & Brandy") == f"{LHR}/cognac-brandy"
        assert urls.category_from_slug("Cognac-Brandy") == "Cognac & Brandy"
        assert urls.category_from_slug("categories") is None and urls.category_from_slug(None) is None
        with pytest.raises(ValueError):
            urls.airport_category_path(LHR, "Twinpack")
        assert len(set(urls.CATEGORY_SLUGS.values())) == len(urls.CATEGORY_SLUGS)

    def test_the_ts_mirror_holds_the_same_table(self):
        src = (MAIN / "web" / "src" / "lib" / "urls.ts").read_text()
        start = src.index("export const CATEGORY_SLUGS")
        block = src[start:src.index("};", start)]
        entries = dict(re.findall(r'^\s+"([^"]+)": "([a-z-]+)",$', block, re.M))
        assert entries == urls.CATEGORY_SLUGS


class TestBar:
    @pytest.fixture(autouse=True)
    def _pages_switched_on(self, monkeypatch):
        """The pages are built in full and held behind a flag that ships off (rian, 13 Sep:
        saved for a later quote). These tests are about the bar, so they run with it on."""
        monkeypatch.setattr(settings, "feature_category_at_airport", True)

    def test_off_by_default_no_pair_qualifies_and_the_page_is_none(self, monkeypatch):
        monkeypatch.setattr(settings, "feature_category_at_airport", False)
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [("LHR", "Whisky", 67)])
        assert coverage.qualifying_pairs(None) == []
        assert coverage.qualifying_pairs(None, "LHR") == []

    def test_the_threshold_is_the_proposals_real_list_and_the_pairs_over_it_qualify(self, monkeypatch):
        assert coverage.CATEGORY_AT_AIRPORT_MIN_PRODUCTS == 15
        rows = [("LHR", "Whisky", 67), ("LHR", "Gin", 15), ("LHR", "Rum", 14), ("ZRH", "Whisky", 40)]
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [r for r in rows if iata is None or r[0] == iata])
        assert coverage.qualifying_pairs(None) == [("LHR", "Whisky", 67), ("LHR", "Gin", 15), ("ZRH", "Whisky", 40)]
        assert coverage.qualifying_pairs(None, "LHR") == [("LHR", "Whisky", 67), ("LHR", "Gin", 15)]

    def test_the_airport_pages_rail_lists_only_the_pairs_over_the_bar(self, monkeypatch):
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [("LHR", "Whisky", 67), ("LHR", "Rum", 14)])
        assert catalog_queries.category_page_links(None, "LHR", LHR) == [link("Whisky", 67)]

    def test_under_the_bar_an_unknown_word_or_a_hidden_airport_is_none(self, monkeypatch):
        monkeypatch.setattr(catalog_queries, "airport_shops", lambda db, iata=None: ["a shop"] if iata == "LHR" else [])
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [("LHR", "Whisky", 67), ("LHR", "Rum", 14)])
        assert catalog_queries.airport_category_detail(None, "LHR", "Rum") is None
        assert catalog_queries.airport_category_detail(None, "LHR", "Twinpack") is None
        assert catalog_queries.airport_category_detail(None, "JFK", "Whisky") is None

    def test_the_detail_carries_the_pairs_own_address_and_the_categorys_count(self, monkeypatch):
        """The first rehearsal on a copy of the dump answered with the airport's path as the
        pair's, so the canonical of /airports/heathrow-lhr-london/whisky named the airport page."""
        from types import SimpleNamespace

        from app.models.hubs import AirportSummary

        lhr = airport()
        summary_ = AirportSummary(**{k: getattr(lhr, k) for k in AirportSummary.model_fields})
        monkeypatch.setattr(catalog_queries, "airport_shops", lambda db, iata=None: [SimpleNamespace(id=13)] if iata == "LHR" else [])
        monkeypatch.setattr(catalog_queries, "_airport_shop_views", lambda db, shops: lhr.shops)
        monkeypatch.setattr(catalog_queries, "_airport_summary", lambda db, shops, shop_views: summary_)
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [("LHR", "Whisky", 67), ("LHR", "Gin", 24)])
        rows = [SimpleNamespace(id=1, category="Whisky", airport_count=3, here_usd=38.0, cheapest_usd=38.0, dearest_usd=55.5, is_exclusive=False),
                SimpleNamespace(id=2, category="Whisky", airport_count=1, here_usd=60.0, cheapest_usd=60.0, dearest_usd=60.0, is_exclusive=True),
                SimpleNamespace(id=3, category="Gin", airport_count=2, here_usd=20.0, cheapest_usd=20.0, dearest_usd=30.0, is_exclusive=False)]
        monkeypatch.setattr(catalog_queries, "_airport_rows", lambda db, ids: rows)
        monkeypatch.setattr(catalog_queries, "list_product_variants", lambda db, **kw: (67, [summary()]) if not kw.get("exclusives_only") else (1, []))
        detail = catalog_queries.airport_category_detail(None, "LHR", "Whisky", sort="price")
        assert detail.path == f"{LHR}/whisky" and detail.airport_path == LHR and detail.category_slug == "whisky"
        assert detail.product_variants == 67 and detail.total == 67 and detail.comparable == 1 and detail.cheapest_here == 1 and detail.exclusives == 1
        assert [s.category for s in detail.siblings] == ["Gin"] and detail.sort == "price"
        assert detail.name == "London Heathrow" and detail.iata == "LHR"

    def test_the_sitemap_rows_are_the_qualifying_pairs_dated_like_their_airport(self, monkeypatch):
        monkeypatch.setattr(catalog_queries, "list_airports", lambda db: [airport()])
        monkeypatch.setattr(catalog_queries, "category_pair_counts", lambda db, iata=None: [("LHR", "Whisky", 67), ("LHR", "Rum", 14), ("ZRH", "Gin", 30)])
        assert catalog_queries.category_page_rows(None) == [(f"{LHR}/whisky", CHECKED)]


class TestBody:
    def test_the_page_reads_without_javascript(self):
        body = seo.airport_category_body(pair())
        assert '<h1 class="airports-hero__title">Whisky at London Heathrow</h1>' in body
        assert ("67 whisky products priced by Avolta at London Heathrow (LHR), checked 25 Aug 2026. "
                "30 of them are sold at other airports we track, and LHR is the cheapest of our airports for 12.") in body
        assert f'<a href="{LHR}">London Heathrow</a><span class="crumbs__sep" aria-hidden="true">›</span><span>Whisky</span></nav>' in body
        assert f'<a href="{LHR}" class="btn btn--light">All duty free at London Heathrow</a>' in body
        # The airport's other category pages, biggest first, as links to pages.
        assert '<h2 class="section-heading airport-categories__title">Also at London Heathrow</h2>' in body
        assert f'<a class="filter-chip" href="{LHR}/gin">Gin <span class="chip-count">24</span></a>' in body
        assert f'href="{LHR}/cognac-brandy">Cognac &amp; Brandy <span class="chip-count">18</span></a>' in body
        # The exclusives in the category here, the list, the chips, the sort.
        assert "<h2 class=\"section-heading\">Whisky exclusives at London Heathrow</h2>" in body
        assert "Talisker Dark Storm 1L" in body
        assert "Every whisky product we price at London Heathrow, cheapest first where we can compare." in body
        assert f'<a class="filter-chip" href="{LHR}/whisky?multi_only=true#products" aria-pressed="false">Comparable</a>' in body
        assert "collection-bar__sort" in body and '<span class="collection-bar__count">67 products</span>' in body
        assert f'href="{LHR}/whisky?page=2#products">Next</a>' in body
        assert "—" not in body and "erified" not in body

    def test_the_list_links_the_method_exactly_when_the_sort_is_featured(self):
        """The category-at-airport list sorted featured runs `featured.order()` (AW2, D8)."""
        body = seo.airport_category_body(pair())
        assert ('cheapest first where we can compare.</p>'
                '<a class="how-we-choose" href="/how-we-choose">How we choose</a></div>') in body
        assert "how-we-choose" not in seo.airport_category_body(pair(sort="price"))

    def test_a_filtered_view_keeps_the_flags_on_every_link_and_the_clean_canonical(self):
        detail = pair(awarded_only=True, sort="price", offset=24)
        body = seo.airport_category_body(detail)
        assert f'class="filter-chip filter-chip--gold filter-chip--active" href="{LHR}/whisky?sort=price#products" aria-pressed="true">Award winners' in body
        assert f'href="{LHR}/whisky?awarded_only=true&amp;sort=price&amp;page=3#products">Next</a>' in body
        head = seo.head_for_airport_category(detail)
        assert head.canonical_path == f"{LHR}/whisky" and not head.noindex

    def test_no_siblings_no_rail_and_no_exclusives_no_row(self):
        body = seo.airport_category_body(pair(siblings=[], exclusive_items=[], comparable=0))
        assert "airport-categories" not in body and "exclusives at" not in body
        assert "sold at other airports" not in body

    def test_head_title_description_structured_data_and_seed(self):
        head = seo.head_for_airport_category(pair())
        assert head.title == "Whisky at London Heathrow (LHR) duty-free prices | Duty Free Professor"
        assert head.description.startswith("67 whisky products priced by Avolta at London Heathrow (LHR), checked 25 Aug 2026.")
        assert head.seed_key == "__DFP_AIRPORT_CATEGORY__" and head.seed["category_slug"] == "whisky"
        assert head.last_modified == CHECKED
        page = head.apply(SHELL, "https://s.example")
        blocks = [json.loads(m) for m in re.findall(r'<script type="application/ld\+json">(.*?)</script>', page)]
        collection = next(b for b in blocks if b["@type"] == "CollectionPage")
        assert collection["@id"] == f"https://s.example{LHR}/whisky#page" and collection["name"] == "Whisky at London Heathrow"
        assert collection["about"]["@id"] == f"https://s.example{LHR}#airport" and collection["about"]["iataCode"] == "LHR"
        assert collection["mainEntity"]["numberOfItems"] == 67 and collection["mainEntity"]["itemListElement"][0]["position"] == 1
        crumbs = next(b for b in blocks if b["@type"] == "BreadcrumbList")["itemListElement"]
        assert [c["name"] for c in crumbs] == ["Airports we price", "London Heathrow", "Whisky"]
        assert crumbs[1]["item"] == f"https://s.example{LHR}" and "item" not in crumbs[2]


class TestAirportPageRail:
    def test_the_airport_page_links_the_category_pages_it_has(self):
        body = seo.airport_body(airport(category_pages=[link("Whisky", 67), link("Gin", 24)]))
        assert '<h2 class="section-heading airport-categories__title">Categories at London Heathrow</h2>' in body
        assert f'<a class="filter-chip" href="{LHR}/whisky">Whisky <span class="chip-count">67</span></a>' in body
        assert body.index('class="airport-categories"') < body.index('id="products"')
        assert "airport-categories" not in seo.airport_body(airport())
