"""The crawl connector: schema.org markup in, source products out."""
from app.services.catalog.crawl import to_source_product
from app.services.catalog.normalise import normalise

REF = "shop.example.com"

CONTEXT = {"shop_name": None, "primary_domain": None, "url_template": None,
           "currency": None, "known_brands": [], "reference_currency": "USD"}


def _crawled(**overrides):
    """One product as the page extractor hands it over."""
    block = {
        "@type": "Product",
        "name": "Oxford Shirt",
        "brand": {"name": "Brookfield"},
        "offers": {"price": "59.00", "priceCurrency": "USD",
                   "availability": "https://schema.org/InStock"},
    }
    block.update(overrides.pop("jsonld", {}))
    product = {
        "name": "Oxford Shirt",
        "description": "A cotton Oxford shirt.",
        "product_url": "https://shop.example.com/products/oxford-shirt",
        "image_url": "https://cdn.example.com/oxford.jpg",
        "category": "shirts",
        "raw": {"jsonld": block},
    }
    product.update(overrides)
    return product


def test_price_and_currency_come_from_the_markup():
    sp = to_source_product(_crawled(), REF)
    assert sp["variants"][0]["price"] == 59.00
    assert sp["currency"] == "USD"


def test_brand_reads_through_a_nested_node():
    # schema.org lets brand be a string or a {"name": ...} node.
    assert to_source_product(_crawled(), REF)["brand"] == "Brookfield"
    plain = _crawled(jsonld={"brand": "Ashford"})
    assert to_source_product(plain, REF)["brand"] == "Ashford"


def test_an_explicit_out_of_stock_is_honoured():
    sold_out = _crawled(jsonld={"offers": {
        "price": "59.00", "availability": "https://schema.org/OutOfStock"}})
    assert to_source_product(sold_out, REF)["variants"][0]["available"] is False


def test_saying_nothing_about_stock_means_available():
    # Most small storefronts omit availability entirely. Hiding their whole
    # catalogue over a missing annotation would be worse than the alternative.
    quiet = _crawled(jsonld={"offers": {"price": "59.00"}})
    assert to_source_product(quiet, REF)["variants"][0]["available"] is True


def test_a_page_with_no_markup_still_yields_a_product():
    bare = _crawled(raw={})
    sp = to_source_product(bare, REF)
    assert sp is not None
    assert sp["variants"][0]["price"] is None


def test_a_product_with_no_price_is_kept_and_flagged():
    # Dropping it would make a site without markup look like a site with no
    # products. It is flagged instead, exactly as a missing URL is.
    sp = to_source_product(_crawled(raw={}), REF)
    row = normalise(sp, CONTEXT)
    assert row is not None
    assert row["price_cents"] is None
    assert "price" in row["missing_fields"]


def test_a_priced_product_is_not_flagged():
    row = normalise(to_source_product(_crawled(), REF), CONTEXT)
    assert row["missing_fields"] == []
    assert row["price_cents"] == 5900


def test_the_page_url_is_the_id_so_a_rename_updates_the_row():
    renamed = _crawled(name="Oxford Button-Down Shirt")
    assert (to_source_product(renamed, REF)["external_id"]
            == to_source_product(_crawled(), REF)["external_id"])


def test_a_sku_wins_over_the_url_as_the_id():
    with_sku = _crawled(raw={"jsonld": {}, "sku": "SHIRT-OXF"})
    assert to_source_product(with_sku, REF)["external_id"] == "SHIRT-OXF"


def test_a_product_with_no_name_is_skipped():
    assert to_source_product(_crawled(name=""), REF) is None


def test_the_source_currency_falls_back_to_the_merchants_declaration():
    # A site does not reliably state its currency in markup; the merchant does.
    no_currency = _crawled(jsonld={"offers": {"price": "59.00"}})
    assert to_source_product(no_currency, REF, "GBP")["currency"] == "GBP"


def test_markup_currency_wins_over_the_declaration():
    assert to_source_product(_crawled(), REF, "GBP")["currency"] == "USD"


def test_a_crawled_product_normalises_into_a_full_catalogue_row():
    row = normalise(to_source_product(_crawled(), REF), CONTEXT)
    assert row["product_key"] == (
        "crawl:shop.example.com:https://shop.example.com/products/oxford-shirt")
    assert row["source_kind"] == "crawl"
    assert row["source_ref"] == REF
    assert row["name"] == "Oxford Shirt"
    assert row["brand"] == "Brookfield"
    assert row["currency"] == "USD"
    assert row["in_stock"] is True
    # The fields the old crawl path never wrote, which is what made a crawled
    # product second-class in the pairing graph.
    assert row["price_cents"] == 5900
    assert row["taxonomy_path"]
    assert row["price_reference_cents"] == 5900


def test_llm_brand_and_price_are_used_only_when_markup_has_neither():
    # extract_with_llm only ever reaches a page when JSON-LD/meta both came
    # back empty, so this mirrors what it hands to_source_product then.
    llm_sourced = _crawled(raw={"jsonld": {}}, brand="Cordori", price=129.0)
    sp = to_source_product(llm_sourced, REF)

    assert sp["brand"] == "Cordori"
    assert sp["variants"][0]["price"] == 129.0


def test_markup_brand_and_price_win_over_the_llm_fallback():
    # A product that DID come from JSON-LD carries the real block; the
    # model-shaped brand/price keys should never be consulted then.
    with_markup = _crawled(brand="Wrong Brand", price=1.0)
    sp = to_source_product(with_markup, REF)

    assert sp["brand"] == "Brookfield"
    assert sp["variants"][0]["price"] == 59.00
