"""Raw records and verticals: what every collector must carry back.

Decided 2026-09-04 after confirming nothing of what a collector saw was kept
(`\\d price_observations` had no payload): every rule change meant a re-fetch,
at crawl delays of 30 to 60 seconds a page. Two rules live here. A raw record
must be facts only, because the legal posture (COLLECTORS.md L2) rests on never
ingesting marketing copy or imagery. And a listing names the family it was
found in, so a beauty product is never filed as a drink by default.
"""

import json
import re
from pathlib import Path

from app.services.collectors.ari import listing_from_page
from app.services.collectors.avolta import STORES, AvoltaCollector, parse_product_variants
from app.services.collectors.base import ListingRef, facts_only, listing_ref
from app.services.collectors.extime import Extime
from app.services.collectors.shopify import SHOPS, ShopifyCollector
from app.services.ingest import resolve_vertical
from app.services.taxonomy import vertical_from_hints, vertical_of

FIXTURES = Path(__file__).parent / "fixtures"
EXTIME_PAGE = (FIXTURES / "extime_product.html").read_text()
EXTIME_URL = "https://www.extime.com/en/paris/product/single-malt-whisky-ten-years-old-107222991"


class TestFactsOnly:
    def test_expression_keys_are_dropped_at_any_depth(self):
        """A stored description or image URL hands a plaintiff the copyrighted
        work the price data itself never is."""
        item = {
            "displayName": "Some Whisky",
            "longDescription": "<p>Velvety notes of ...</p>",
            "primaryFullImageURL": "/img/x.jpg",
            "price": {"value": 1, "image": "x"},
            "variants": [{"sku": "1", "featured_image": {"src": "y"}, "body_html": "z"}],
            "seo_title": "buy now",
        }
        kept = facts_only(item)
        assert kept == {"displayName": "Some Whisky", "price": {"value": 1}, "variants": [{"sku": "1"}]}

    def test_shopify_body_html_and_images_never_reach_storage(self):
        product = {"id": 1, "title": "Gin", "vendor": "X", "body_html": "<p>copy</p>",
                   "images": [{"src": "a"}], "image": {"src": "a"}, "product_type": "Gin",
                   "tags": ["gin"], "handle": "gin", "variants": []}
        variant = {"id": 9, "title": "70cl", "price": "20.00", "available": True,
                   "featured_image": {"src": "b"}, "barcode": ""}
        shop, slug, name, op = SHOPS[0]
        row = ShopifyCollector(shop, slug, name, op).listing(product, variant)
        blob = json.dumps(row.raw)
        assert "body_html" not in blob and "images" not in blob and "featured_image" not in blob
        assert row.raw["product"]["title"] == "Gin" and row.raw["variant"]["price"] == "20.00"
        assert "variants" not in row.raw["product"]


class TestVerticals:
    def test_every_category_maps_to_one_family(self):
        assert vertical_of("Whisky") == "liquor"
        assert vertical_of("Perfume") == "beauty"
        assert vertical_of("Confectionery") == "confectionery"
        assert vertical_of(None) is None

    def test_a_retailers_own_tree_name_is_read(self):
        assert vertical_from_hints("https://x/en/1/liquor/whisky") == "liquor"
        assert vertical_from_hints("Beverage") == "liquor"
        assert vertical_from_hints("/beauty/fragrance/") == "beauty"
        assert vertical_from_hints("Gifts") is None

    def test_our_category_beats_the_collectors_claim_beats_the_default(self):
        """A skincare product found on a drinks walk is still beauty."""
        assert resolve_vertical("liquor", "Skincare", None) == "beauty"
        assert resolve_vertical("beauty", None, None) == "beauty"
        assert resolve_vertical(None, None, "tobacco") == "tobacco"
        # A product nothing placed is `unknown`, never "liquor" by default (identity rules v6: it
        # also takes the `n/a` quantity slot when it states none, like the ring from Panama).
        assert resolve_vertical(None, None, None) == "unknown"


class TestCollectorsCarryTheFragment:
    def test_avolta_keeps_the_tile_and_names_the_tree(self):
        html = '''<div class="product-item-info">
          <a class="product-item-link" href="https://x/en/1/p">Some Whisky 1L</a>
          <span data-price-type="finalPrice" data-price-amount="38.23">£38.23</span>
          <span data-product-sku="5000267023625"></span></div>'''
        item = parse_product_variants(html)[0]
        row = AvoltaCollector(STORES[0])._listing(item, "GBP", "https://x/en/1/liquor/whisky", 38.23, None)
        assert row.vertical == "liquor"
        assert row.raw["tile"]["sku"] == "5000267023625" and "html" not in row.raw["tile"]
        assert "tile_html" not in row.raw, "the tile's markup is not a fact; see test_no_fragment_carries_markup"
        assert row.raw["variant"] is None

    def test_extime_keeps_offer_and_variation_and_reads_the_family(self):
        row = Extime()._listings(EXTIME_URL, EXTIME_PAGE)[0]
        assert row.vertical == "liquor"  # categories_name[0] is "Beverage"
        assert row.raw["jsonld"]["@type"] == "Product"
        assert "description" not in row.raw["jsonld"] and "image" not in row.raw["jsonld"]
        assert row.raw["main_offer"]["gtin"] == "5010494195125"
        assert row.raw["variation"]["capacity"] == 100

    def test_ari_keeps_the_jsonld_and_reads_the_path(self):
        page = '''<html><script type="application/ld+json">{"@type": "Product",
          "name": "Redbreast 12 Year Old 70cl", "sku": "12345", "description": "copy",
          "image": "x.jpg", "brand": {"name": "Redbreast"}, "category": "Irish Whiskey",
          "offers": [{"price": "55.00", "priceCurrency": "EUR", "availability": "InStock"},
                     {"price": "72.00", "priceCurrency": "EUR"}]}</script></html>'''
        row = listing_from_page("https://www.dublinandcorkdutyfree.ie/alcohol/irish-whiskey/12345.html", page)
        assert row.price == 55.0 and row.vertical == "liquor"
        assert "description" not in row.raw["jsonld"] and "image" not in row.raw["jsonld"]


class TestListingRef:
    def test_a_database_listing_is_reduced_to_what_a_collector_needs(self):
        class Loc:
            code = "LHR"
        class DbListing:
            source_sku = "123"
            url = "https://x/p"
            shop = Loc()
        assert listing_ref(DbListing()) == ListingRef("123", "https://x/p", "LHR")
        assert listing_ref(ListingRef("1", None, "X")) == ListingRef("1", None, "X")


class TestAFragmentHoldsFactsAndNotExpression:
    """A kept fragment carries what a shop stated, never how it dressed it (agents.md, "Collect
    facts, not expression"; `facts_only`).

    Cost: Avolta pulled the tile's rendered markup out of the item and stored it under its own
    key, which walked straight past the filter. 5,463 rows averaged 24 KB and one reached 1.4 MB
    -- 124 MB, 91% of every fragment kept, with an `<img>` and its alt text in 98% of them. Nothing
    read it: two readers dropped it as the one bulky key and the third fetched it and discarded it.
    The facts it held were already parsed beside it. It is the shape of the thing every plaintiff
    in the comparable litigation actually won on.
    """

    #: A value holding a tag and an attribute is markup, not a stated fact.
    MARKUP = re.compile(r"<\s*[a-z]+[^>]*\s+[a-z-]+\s*=", re.IGNORECASE)

    def _values(self, node, path="raw"):
        if isinstance(node, dict):
            for k, v in node.items():
                yield from self._values(v, f"{path}.{k}")
        elif isinstance(node, list | tuple):
            for i, v in enumerate(node):
                yield from self._values(v, f"{path}[{i}]")
        elif isinstance(node, str):
            yield path, node

    def _check(self, raw: dict, who: str):
        bad = [f"{who} {path} ({len(text)} chars)" for path, text in self._values(raw)
               if self.MARKUP.search(text)]
        assert not bad, ("a fragment is storing markup; parse the facts out of it and keep those:\n"
                         + "\n".join(bad))

    def test_no_fragment_carries_markup(self):
        html = '''<div class="product-item-info">
          <a class="product-item-link" href="https://x/en/1/p">Some Whisky 1L</a>
          <img src="https://x/img/whisky.jpg" alt="Some Whisky 1L bottle shot" />
          <span data-price-type="finalPrice" data-price-amount="38.23">£38.23</span>
          <span data-product-sku="5000267023625"></span></div>'''
        item = parse_product_variants(html)[0]
        avolta = AvoltaCollector(STORES[0])._listing(item, "GBP", "https://x/en/1/liquor/whisky", 38.23, None)
        self._check(avolta.raw, "avolta")
        self._check(Extime()._listings(EXTIME_URL, EXTIME_PAGE)[0].raw, "extime")

    def test_the_filter_drops_an_expression_key_wherever_it_sits(self):
        kept = facts_only({"sku": "1", "description": "lyrical copy", "a": {"image": "x.jpg", "gtin": "2"},
                           "b": [{"thumbnail": "t.jpg", "price": 3}]})
        assert kept == {"sku": "1", "a": {"gtin": "2"}, "b": [{"price": 3}]}
