"""The family landing and the site-wide category page (B6): addresses, bodies, indexing.

The pages the site had no answer for: a category was a filter on `/products` that named
`/products` as its canonical, so the broadest search terms had no page of their own to rank,
which is the whole point of the review's `/alcohol/whisky`.

What it pins: the two declared word tables and their TypeScript mirrors; that a category page
exists from the moment the category has a published product, never gated by a floor, so a
category link can never point at a 404; that a category addressed under the wrong family is
one 301; that both renderers say the same words; that nothing is in the index or the sitemap
until a person approves the address; and that the routes cannot swallow another path.

No database: the detail objects are built by hand.
"""

import json
import pathlib
import re

import pytest

from app.models.hubs import (
    BrandAirport, CategoryBrand, CategoryDetail, CategoryGuide, CategorySummary, FamilyDetail,
)
from app.services import access, catalog_queries, publish, seo, taxonomy, urls
from test_airport_category import CHECKED, SHELL
from test_seo_airport import summary

MAIN = pathlib.Path(__file__).resolve().parents[1]
URLS_TS = (MAIN / "web" / "src" / "lib" / "urls.ts").read_text()


def sibling(category="Gin", n=156):
    return CategorySummary(
        category=category, slug=urls.category_slug(category),
        path=urls.category_page_path("liquor", category), family="liquor",
        family_label="Drinks", family_path="/alcohol", product_variants=n, airports=14,
    )


def whisky(**over) -> CategoryDetail:
    base = dict(
        category="Whisky", slug="whisky", path="/alcohol/whisky", family="liquor",
        family_label="Drinks", family_path="/alcohol", product_variants=888, airports=15,
        comparable=249, exclusives=127, last_collected_at=CHECKED,
        airport_rows=[BrandAirport(iata="LHR", path="/airports/heathrow-lhr-london",
                                   name="London Heathrow", product_variants=78,
                                   cheapest_for=24, last_collected_at=CHECKED)],
        brands=[CategoryBrand(slug="johnnie-walker", path="/brands/johnnie-walker",
                              name="Johnnie Walker", product_variants=58)],
        siblings=[sibling()],
        total=888, limit=24, offset=0, items=[summary()],
    )
    base.update(over)
    return CategoryDetail(**base)


def drinks(**over) -> FamilyDetail:
    base = dict(
        family="liquor", slug="alcohol", path="/alcohol", label="Drinks",
        product_variants=3984, comparable=862, exclusives=158, last_collected_at=CHECKED,
        categories=[sibling("Whisky", 888), sibling()], savings=[summary()],
        airport_rows=[BrandAirport(iata="CDG", path="/airports/paris-cdg", name="Paris Charles de Gaulle",
                                   product_variants=326, cheapest_for=24, last_collected_at=CHECKED)],
    )
    base.update(over)
    return FamilyDetail(**base)


class TestAddresses:
    def test_a_category_sits_under_its_familys_word(self):
        assert urls.family_path("liquor") == "/alcohol" and urls.family_path("beauty") == "/beauty"
        assert urls.category_page_path("liquor", "Whisky") == "/alcohol/whisky"
        assert urls.category_page_path("beauty", "Perfume") == "/beauty/perfume"
        assert urls.category_page_path("liquor", "Cognac & Brandy") == "/alcohol/cognac-brandy"

    def test_the_word_is_never_the_column_name(self):
        """The structure review's rule: the address carries what people search, so the
        family the database calls `liquor` is `alcohol` in a URL."""
        assert urls.FAMILY_SLUGS["liquor"] == "alcohol"
        assert "liquor" not in set(urls.FAMILY_SLUGS.values())

    def test_a_family_or_category_with_no_word_has_no_page(self):
        for family in ("confectionery", "tobacco", "", None):
            assert urls.family_slug(family) is None
        with pytest.raises(ValueError):
            urls.family_path("tobacco")
        with pytest.raises(ValueError):
            urls.category_page_path("liquor", "Twinpack")

    def test_slugs_round_trip_and_nothing_else_is_forgiven(self):
        assert urls.family_from_slug("ALCOHOL") == "liquor"
        assert urls.family_from_slug("alcohols") is None and urls.family_from_slug(None) is None
        assert urls.category_from_slug("Whisky") == "Whisky"

    def test_the_ts_mirror_holds_the_same_tables(self):
        """`lib/urls.ts` builds these addresses in the browser; a drifted table would have
        the SPA linking somewhere the server does not answer."""
        block = re.search(r"FAMILY_SLUGS: Record<string, string> = \{(.*?)\};", URLS_TS, re.S).group(1)
        mirrored = dict(re.findall(r'(\w+):\s*"([^"]+)"', block))
        assert mirrored == urls.FAMILY_SLUGS
        block = re.search(r"FAMILY_OF_CATEGORY: Record<string, string> = \{(.*?)\};", URLS_TS, re.S).group(1)
        mirrored = dict(re.findall(r'"([^"]+)":\s*"([^"]+)"', block))
        assert mirrored == taxonomy.VERTICAL_OF_CATEGORY
        hidden = re.search(r"HIDDEN_CATEGORIES = new Set\(\[(.*?)\]\)", URLS_TS, re.S).group(1)
        assert set(re.findall(r'"([^"]+)"', hidden)) == set(taxonomy.HIDDEN_CATEGORIES)

    def test_a_category_name_links_to_its_page_and_never_to_a_404(self):
        """Every shown category in a family with a landing links to its own page; the rest
        keep the filter they always had. The K6 lesson: a floor that decides existence is
        what once pointed a quarter of the product pages at pages that were not there."""
        for category, family in taxonomy.VERTICAL_OF_CATEGORY.items():
            path = urls.category_path(category)
            if taxonomy.is_shown(category) and urls.family_slug(family):
                assert path == urls.category_page_path(family, category), category
            else:
                assert path.startswith("/products?category="), category
        assert urls.category_path("Skincare").startswith("/products?category=")
        assert urls.category_path("Tobacco").startswith("/products?category=")

    def test_a_department_leads_to_its_landing(self):
        for key, to, _label in seo._DEPARTMENTS:
            if key in urls.FAMILY_SLUGS:
                assert to == urls.family_path(key)


class TestTheCategoryBody:
    def test_the_page_reads_without_javascript(self):
        body = seo.category_body(whisky())
        assert '<h1 class="airports-hero__title">Duty free whisky prices</h1>' in body
        assert ("888 whisky products priced at 15 airports we track, checked 25 Aug 2026. "
                "249 of them are sold at more than one, so you can see where each is the better buy.") in body
        assert '<a href="/alcohol">Drinks</a>' in body
        assert '<h2 class="section-heading">Where to buy whisky</h2>' in body
        assert '<a class="brand-airport__name" href="/airports/heathrow-lhr-london">London Heathrow (LHR)</a>' in body
        assert '<a class="filter-chip" href="/brands/johnnie-walker">Johnnie Walker' in body
        assert '<h2 class="section-heading airport-categories__title">More drinks</h2>' in body
        assert '<a class="filter-chip" href="/alcohol/gin">Gin ' in body
        assert "—" not in body

    def test_the_list_links_the_method_exactly_when_the_sort_is_featured(self):
        """The category list sorted featured runs `featured.order()` (AW2), so its head says how;
        sorted by price it does not, and the anchor would be a lie there."""
        featured_body = seo.category_body(whisky())
        assert ('product we price, at every airport we track.</p>'
                '<a class="how-we-choose" href="/how-we-choose">How we choose</a></div>') in featured_body
        assert "how-we-choose" not in seo.category_body(whisky(sort="price"))

    def test_the_primer_rides_on_it(self):
        guide = CategoryGuide(summary="What it is.", takeaway="What to look for.")
        body = seo.category_body(whisky(guide=guide))
        assert '<section class="category-primer"' in body and "What it is." in body
        assert body.index("category-primer") < body.index('id="products"')
        assert "category-primer" not in seo.category_body(whisky())

    def test_the_pager_never_writes_the_page_it_is_already_on(self):
        """`_list_section` puts `?category=` into every pager link, which here would name
        this page inside its own address."""
        body = seo.category_body(whisky(total=888, offset=24, sort="price"))
        # On page two: Next is page three, and Previous drops the parameter for page one.
        assert 'href="/alcohol/whisky?sort=price&amp;page=3#products"' in body
        assert 'href="/alcohol/whisky?sort=price#products"' in body
        assert "?category=" not in body

    def test_the_feature_chips_keep_the_sort_and_toggle_one_flag(self):
        body = seo.category_body(whisky(awarded_only=True, sort="price"))
        assert 'class="filter-chip filter-chip--gold filter-chip--active" href="/alcohol/whisky?sort=price#products"' in body
        assert 'href="/alcohol/whisky?multi_only=true&amp;awarded_only=true&amp;sort=price#products"' in body

    def test_the_airports_past_the_first_few_are_disclosed_but_present(self):
        rows = [
            BrandAirport(iata=f"A{n:02d}", path=f"/airports/a{n}", name=f"Airport {n}",
                         product_variants=n, cheapest_for=0, last_collected_at=CHECKED)
            for n in range(12)
        ]
        body = seo.category_body(whisky(airport_rows=rows))
        assert '<details class="airports-more">' in body
        assert f"The other {12 - seo.AIRPORTS_SHOWN} airports" in body
        for row in rows:
            assert row.name in body, "a disclosed airport must still be in the page"

    def test_the_head_carries_the_markup_a_hub_page_owes(self):
        head = seo.head_for_category(whisky())
        assert head.title == "Duty free whisky prices | Duty Free Professor"
        assert head.canonical_path == "/alcohol/whisky"
        assert head.seed_key == "__DFP_CATEGORY__" and head.seed["slug"] == "whisky"
        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"] == "https://s.example/alcohol/whisky#page"
        assert collection["mainEntity"]["numberOfItems"] == 888
        crumbs = next(b for b in blocks if b["@type"] == "BreadcrumbList")["itemListElement"]
        assert [c["name"] for c in crumbs] == ["Products", "Drinks", "Whisky"]
        assert crumbs[1]["item"] == "https://s.example/alcohol" and "item" not in crumbs[2]


class TestTheFamilyBody:
    def test_the_landing_reads_without_javascript(self):
        body = seo.family_body(drinks())
        assert '<h1 class="airports-hero__title">Duty free drinks</h1>' in body
        assert "3,984 products across 2 drinks categories, priced at every airport we track" in body
        assert '<h2 class="section-heading">Every drinks category we price</h2>' in body
        assert '<a class="family-category__name" href="/alcohol/whisky">Whisky</a>' in body
        assert '<h2 class="section-heading">Biggest drinks savings right now</h2>' in body
        # The savings rail is a featured rail, and every featured rail links the method (AW2, D8).
        assert ('<span class="muted">The widest gaps between the airports we price</span>'
                '<a class="how-we-choose" href="/how-we-choose">How we choose</a></div>') in body
        assert '<h2 class="section-heading">Airports strongest for drinks</h2>' in body
        assert "—" not in body

    def test_the_head_lists_its_categories(self):
        head = seo.head_for_family(drinks())
        assert head.title == "Duty free drinks: prices compared | Duty Free Professor"
        assert head.canonical_path == "/alcohol" and head.seed_key == "__DFP_FAMILY__"
        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 [i["name"] for i in collection["mainEntity"]["itemListElement"]] == ["Whisky", "Gin"]
        # `apply` absolutises every url in the markup against the site's base.
        assert collection["mainEntity"]["itemListElement"][0]["url"] == "https://s.example/alcohol/whisky"


class TestIndexing:
    def test_nothing_is_indexed_until_a_person_approves_the_address(self):
        assert publish.INDEXED_PAGES == frozenset()
        assert not publish.is_indexed_path("/alcohol") and not publish.is_indexed_path("/alcohol/whisky")

    def test_an_approved_address_is_the_only_thing_that_changes_it(self, monkeypatch):
        monkeypatch.setattr(publish, "INDEXED_PAGES", frozenset({"/alcohol/whisky"}))
        assert publish.is_indexed_path("/alcohol/whisky")
        assert not publish.is_indexed_path("/alcohol") and not publish.is_indexed_path("/beauty/perfume")


class TestTheRoutesCannotSwallowAnotherPath:
    def test_the_policy_names_a_concrete_route_per_family_word(self):
        """A parameterised `/{slug}` would match every other single-segment path, and the
        access policy classifies by route key before any handler runs, so /docs and every
        member-only SPA path would have been reclassified as a public storefront page."""
        for word in urls.FAMILY_SLUGS.values():
            assert f"GET /{word}" in access.PUBLIC_WHEN_OPEN
            assert f"GET /{word}/{{category_slug}}" in access.PUBLIC_WHEN_OPEN
        assert "GET /{family_slug}" not in access.PUBLIC_WHEN_OPEN
        assert not any(key.startswith("GET /{") and "family" in key for key in access.PUBLIC_WHEN_OPEN)

    def test_the_reviewed_fixture_still_matches(self):
        fixture = json.loads((MAIN / "tests" / "fixtures" / "open_routes.json").read_text())
        assert sorted(access.PUBLIC_WHEN_OPEN) == fixture["routes"]

    def test_the_server_knows_every_address_the_spa_routes(self):
        for word in urls.FAMILY_SLUGS.values():
            assert seo.is_known_route(f"/{word}")
            assert seo.is_known_route(f"/{word}/whisky")
        assert not seo.is_known_route("/alcohol/whisky/extra")
        assert not seo.is_known_route("/alcoholism")


class TestTheQueryLayer:
    def test_a_summary_needs_a_shown_category_in_a_family_with_a_landing(self):
        rows = [(1, "Whisky", False, 9, "LHR", "London", "Heathrow", 10.0, CHECKED)]
        assert catalog_queries._category_summary("Whisky", rows).path == "/alcohol/whisky"
        assert catalog_queries._category_summary("Skincare", rows) is None      # hidden
        assert catalog_queries._category_summary("Tobacco", rows) is None       # no family landing
        assert catalog_queries._category_summary("Twinpack", rows) is None      # not a category

    def test_the_standings_count_comparables_and_who_is_cheapest(self):
        rows = [
            (1, "Whisky", False, 9, "LHR", "London", "Heathrow", 10.0, CHECKED),
            (1, "Whisky", False, 7, "CDG", "Paris", "CDG", 8.0, CHECKED),
            (2, "Whisky", True, 9, "LHR", "London", "Heathrow", 20.0, CHECKED),
        ]
        airports, comparable, cheapest, exclusives, last = catalog_queries._airport_standings(rows)
        assert comparable == 1 and cheapest == 1 and exclusives == 1 and last == CHECKED
        assert {a.iata for a in airports} == {"LHR", "CDG"}
        assert next(a for a in airports if a.iata == "CDG").cheapest_for == 1
        assert next(a for a in airports if a.iata == "LHR").cheapest_for == 0
