"""Article pages (Stream D's text, served by B's routes).

`/articles` is a static head over the SPA and `/articles/<slug>` a page of its own:
head, Article + BreadcrumbList markup, the ArticlePage markup class for class, the
seed the SPA reads instead of fetching, the feed and the sitemap. The fixtures
stand in for the database; test_site_routes.py mounts the real routes over them.
"""

import json
import re
from datetime import UTC, datetime

from app.models.editorial import ArticleOut
from app.services import catalog_queries, editorial, feeds, seo
from test_seo_airport import SHELL, summary

PUBLISHED = datetime(2026, 9, 1, 9, 30, tzinfo=UTC)
EDITED = datetime(2026, 9, 3, 16, 0, tzinfo=UTC)


def article(**over) -> ArticleOut:
    base = dict(
        slug="how-to-read-a-duty-free-price",
        path="/articles/how-to-read-a-duty-free-price",
        title="How to read a duty-free price",
        standfirst="A price is an observation with a date, never a quote.",
        excerpt="A price is an observation with a date, never a quote.",
        kind="article",
        category="Duty free basics",
        airport_code=None,
        hero_image=None,
        published_at=PUBLISHED,
        updated_at=EDITED,
        body_html="<p>Airport prices vary by <em>destination</em> and tier.</p>",
        description="A price is an observation with a date, never a quote.",
    )
    base.update(over)
    return ArticleOut(**base)


def _jsonld(page: str) -> list[dict]:
    return [json.loads(m) for m in re.findall(r'<script type="application/ld\+json">(.*?)</script>', page)]


class TestUpdatedRule:
    def test_an_edit_on_publication_day_is_not_an_update(self):
        same_day = datetime(2026, 9, 1, 23, 59, tzinfo=UTC)
        assert seo.updated_after_published(PUBLISHED, same_day) is False
        assert seo.article_meta(article(updated_at=same_day)) == "1 Sep 2026 · Duty Free Professor · 1 min read"

    def test_a_later_day_is(self):
        assert seo.updated_after_published(PUBLISHED, EDITED) is True
        assert seo.article_meta(article()) == "1 Sep 2026 · Duty Free Professor · 1 min read · Updated 3 Sep 2026"

    def test_days_are_counted_in_utc_like_the_spa(self):
        """23:30 UTC on the 1st and 00:30 UTC on the 2nd are different days both sides."""
        assert seo.updated_after_published(
            datetime(2026, 9, 1, 23, 30, tzinfo=UTC), datetime(2026, 9, 2, 0, 30, tzinfo=UTC)
        ) is True


class TestArticleHead:
    def test_title_description_canonical_and_seed(self):
        head = seo.head_for_article(article())
        assert head.title == "How to read a duty-free price | Duty Free Professor"
        assert head.description == "A price is an observation with a date, never a quote."
        assert head.canonical_path == "/articles/how-to-read-a-duty-free-price"
        assert head.seed_key == "__DFP_ARTICLE__" and head.seed["slug"] == "how-to-read-a-duty-free-price"
        assert head.last_modified == EDITED

    def test_article_markup_with_absolute_urls_and_both_dates(self):
        page = seo.head_for_article(article()).apply(SHELL, "https://s.example")
        article_ld, crumbs = _jsonld(page)
        assert article_ld["@type"] == "Article"
        assert article_ld["@id"] == "https://s.example/articles/how-to-read-a-duty-free-price#article"
        assert article_ld["mainEntityOfPage"]["@id"] == "https://s.example/articles/how-to-read-a-duty-free-price"
        assert article_ld["headline"] == "How to read a duty-free price"
        assert article_ld["datePublished"] == "2026-09-01T09:30:00+00:00"
        assert article_ld["dateModified"] == "2026-09-03T16:00:00+00:00"
        assert article_ld["publisher"] == {"@id": "https://s.example/#organization"}
        assert article_ld["articleSection"] == "Duty free basics"
        assert "image" not in article_ld
        assert crumbs["itemListElement"][0]["item"] == "https://s.example/articles"
        assert crumbs["itemListElement"][1]["name"] == "How to read a duty-free price"
        assert '<meta property="og:type" content="article" />' in page

    def test_a_hero_image_is_the_page_image_too(self):
        page = seo.head_for_article(article(hero_image="/articles/reading-a-price.jpg")).apply(SHELL, "https://s.example")
        assert _jsonld(page)[0]["image"] == "https://s.example/articles/reading-a-price.jpg"
        assert '<meta property="og:image" content="/articles/reading-a-price.jpg" />' in page
        assert '<figure class="article-page__hero"><img src="/articles/reading-a-price.jpg" alt="" /></figure>' in page

    def test_body_is_article_page_class_for_class(self):
        body = seo.article_body(article(), {"myAirports": True, "teasers": True})
        assert body.startswith('<div class="announce">')
        assert (
            '<main><article class="article-page"><header class="article-page__head">'
            '<a class="eyebrow eyebrow--ruled article-page__kicker" href="/articles">Duty free basics</a>'
            '<h1 class="article-page__title">How to read a duty-free price</h1>'
            '<p class="article-page__standfirst">A price is an observation with a date, never a quote.</p>'
            '<p class="article-page__meta">1 Sep 2026 · Duty Free Professor · 1 min read · Updated 3 Sep 2026</p></header>'
            '<hr class="article-page__rule" />'
            '<div class="article-page__layout"><div class="article-page__main">'
            '<div class="prose prose--article"><p>Airport prices vary by <em>destination</em> and tier.</p></div>'
            '<aside class="article-page__about"><span class="eyebrow">About Duty Free Professor</span>'
            "<p>We compare the public shelf prices of airport duty-free shops, bottle by bottle and dated, "
            "and set beside them the medals from the Professor's own wine and spirits competitions.</p></aside>"
            "</div></div>"
            '<div class="article-page__sponsors"></div>'
            '<footer class="article-page__foot"><a href="/articles" class="btn btn--ghost">All articles</a></footer>'
            "</article></main>"
        ) in body

    def test_the_byline_names_the_author_account_and_the_reading_time(self):
        """The Professor sites print "date · author" on every piece; an article
        with an author account names it, one without takes the brand byline."""
        body = seo.article_body(article(author="Adam", reading_minutes=6))
        assert '<p class="article-page__meta">1 Sep 2026 · Adam · 6 min read · Updated 3 Sep 2026</p>' in body

    def test_no_category_no_standfirst(self):
        body = seo.article_body(article(category=None, standfirst=None))
        assert 'href="/articles">Article</a>' in body
        assert "article-page__standfirst" not in body

    def test_untrusted_title_is_escaped_in_html_and_json(self):
        head = seo.head_for_article(article(title='Best <script>alert(1)</script> "deals"'))
        page = head.apply(SHELL, "https://s.example")
        assert "<script>alert(1)</script>" not in page
        assert "&lt;script&gt;" in page
        assert _jsonld(page)[0]["headline"] == 'Best <script>alert(1)</script> "deals"'


class TestEditorialBlock:
    def test_airport_write_up_markup_matches_the_component(self):
        note = article(kind="airport_writeup", airport_code="LHR", title="Heathrow: where to start")
        assert seo.editorial_block_html(note) == (
            '<section class="editorial-block"><span class="eyebrow">From the Professor</span>'
            '<h2 class="editorial-block__title">Heathrow: where to start</h2>'
            '<div class="prose"><p>Airport prices vary by <em>destination</em> and tier.</p></div></section>'
        )

    def test_airport_body_shows_the_write_up_once_and_only_when_present(self):
        from test_seo_airport import airport

        plain = seo.airport_body(airport())
        assert "editorial-block" not in plain
        with_note = seo.airport_body(airport(writeup=article(kind="airport_writeup", airport_code="LHR", title="Heathrow: where to start")))
        assert with_note.count('<section class="editorial-block">') == 1
        # After the facts and the terminals, before the shelves, as in AirportPage.tsx.
        assert with_note.index("airport-facts") < with_note.index("editorial-block") < with_note.index("shelf-tabs")


class TestFeedAndSitemap:
    def test_articles_join_the_channel_newest_first(self, monkeypatch):
        added = datetime(2026, 9, 2, 12, 0, tzinfo=UTC)
        monkeypatch.setattr(catalog_queries, "newest_product_variants", lambda db, limit: [(summary(), added)])
        monkeypatch.setattr(editorial, "feed_items", lambda db, limit=20: [article(), article(slug="older", path="/articles/older", title="Older", published_at=datetime(2026, 8, 20, tzinfo=UTC))])
        items = feeds.feed_items(None)
        assert [i.path for i in items] == [
            "/products/santa-teresa-1796-solera-rum-1l-1156", "/articles/how-to-read-a-duty-free-price", "/articles/older",
        ]
        piece = items[1]
        assert piece.title == "How to read a duty-free price" and piece.guid == piece.path
        assert piece.published == PUBLISHED
        assert piece.description == "A price is an observation with a date, never a quote."
        xml = feeds.rss_xml(items, "https://s.example")
        assert "<link>https://s.example/articles/how-to-read-a-duty-free-price</link>" in xml
        assert "<em>" not in xml  # the description, never the body

    def test_sitemap_lists_the_index_and_every_published_article(self, monkeypatch):
        # Nothing generated is approved for indexing (Stream K6): an article a person wrote
        # still indexes on publish, with no decision of its own.
        monkeypatch.setattr(seo, "indexed_airport_codes", lambda db: set())
        monkeypatch.setattr(seo, "indexed_brand_slugs", lambda db: set())
        monkeypatch.setattr(seo, "indexed_line_rows", lambda db, since=None: [])
        monkeypatch.setattr(editorial, "sitemap_rows", lambda db: [("/articles/how-to-read-a-duty-free-price", EDITED)])
        xml, newest = seo.sitemap_entries(None, "https://s.example")
        assert "<url><loc>https://s.example/articles</loc></url>" in xml
        assert "<url><loc>https://s.example/articles/how-to-read-a-duty-free-price</loc><lastmod>2026-09-03</lastmod></url>" in xml
        assert newest == EDITED
