import json
import pathlib

import httpx
import pytest

from app.services.catalog.shopify import (
    PRODUCTS_QUERY, fetch_products, to_source_product,
)
from app.services.catalog.normalise import normalise

FIXTURES = pathlib.Path("tests/fixtures/shopify")
SHOP = "galaxiq-braexaal.myshopify.com"

CONTEXT = {
    "shop_name": "Galaxiq",
    "primary_domain": "https://galaxiq-braexaal.myshopify.com",
    "url_template": None,
    "currency": "INR",
    "reference_currency": "INR",
    "known_brands": [],
}


def load(name):
    return json.loads((FIXTURES / f"{name}.json").read_text())


def test_query_requests_every_field_normalisation_needs():
    for field in ["category", "productType", "vendor", "tags", "handle",
                  "status", "featuredImage", "collections", "tracksInventory",
                  "variants", "compareAtPrice", "availableForSale",
                  "selectedOptions", "metafields"]:
        assert field in PRODUCTS_QUERY


def test_maps_a_real_node():
    node = load("no-category")
    sp = to_source_product(node, SHOP)
    assert sp["source_kind"] == "shopify"
    assert sp["source_ref"] == SHOP
    assert sp["external_id"] == node["id"]
    assert sp["title"] == node["title"]
    assert sp["handle"] == node["handle"]
    assert sp["raw_category"] is None
    assert sp["variants"][0]["price"] == node["variants"]["nodes"][0]["price"]


def test_native_category_is_flattened():
    sp = to_source_product(load("native-category"), SHOP)
    assert sp["raw_category"] == "Gift Cards"


def test_collections_flattened_to_titles():
    sp = to_source_product(load("has-metafields"), SHOP)
    assert all(isinstance(c, str) for c in sp["collections"])


def test_variant_options_flattened_to_a_dict():
    sp = to_source_product(load("title-placeholder-option"), SHOP)
    assert sp["variants"][0]["options"]["Title"] == "Default Title"


def test_missing_image_maps_to_none():
    sp = to_source_product(load("no-image"), SHOP)
    assert sp["image_url"] is None


def test_every_fixture_normalises_without_raising():
    for node in load("all-products"):
        normalise(to_source_product(node, SHOP), CONTEXT)


def test_the_whole_live_catalog_survives_validation():
    nodes = load("all-products")
    out = [normalise(to_source_product(n, SHOP), CONTEXT) for n in nodes]
    kept = [p for p in out if p is not None]
    # Every live product has a handle, so the URL fallback resolves for all.
    assert len(kept) == len(nodes), "some real products were rejected"


def test_shop_name_vendor_nulled_across_the_real_catalog():
    nodes = load("all-products")
    products = [normalise(to_source_product(n, SHOP), CONTEXT) for n in nodes]
    galaxiq_vendors = [n for n in nodes if n["vendor"] == "Galaxiq"]
    assert len(galaxiq_vendors) == 8, "fixture drifted from the profiled store"
    assert all(p["brand"] is None for p, n in zip(products, nodes)
               if n["vendor"] == "Galaxiq")


def test_no_product_gains_a_title_attribute():
    for node in load("all-products"):
        p = normalise(to_source_product(node, SHOP), CONTEXT)
        assert all(a["key"] != "title" for a in p["attributes"])


def test_archived_and_draft_are_preserved_not_dropped():
    # Status filtering belongs at recommendation time, not ingest: an archived
    # product may still be the anchor a visitor is looking at.
    for name, expected in [("archived", "ARCHIVED"), ("draft", "DRAFT")]:
        p = normalise(to_source_product(load(name), SHOP), CONTEXT)
        assert p is not None
        assert p["status"] == expected


def test_out_of_stock_detected():
    p = normalise(to_source_product(load("out-of-stock"), SHOP), CONTEXT)
    assert p["in_stock"] is False


def test_multi_variant_spread_produces_a_range():
    p = normalise(to_source_product(load("multi-variant-price-spread"), SHOP),
                  CONTEXT)
    assert p["price_cents"] < p["price_max_cents"]


def test_url_built_from_handle_since_no_product_has_online_store_url():
    for node in load("all-products"):
        assert node["onlineStoreUrl"] is None
        p = normalise(to_source_product(node, SHOP), CONTEXT)
        assert p["product_url"].startswith(
            "https://galaxiq-braexaal.myshopify.com/products/")


@pytest.mark.asyncio
async def test_fetch_products_follows_pagination():
    pages = [
        {"data": {"products": {
            "nodes": [{"id": "gid://1"}],
            "pageInfo": {"hasNextPage": True, "endCursor": "CUR"}}}},
        {"data": {"products": {
            "nodes": [{"id": "gid://2"}],
            "pageInfo": {"hasNextPage": False, "endCursor": None}}}},
    ]
    seen = []

    def handler(request):
        body = json.loads(request.content)
        seen.append(body["variables"]["cursor"])
        return httpx.Response(200, json=pages[len(seen) - 1])

    async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as c:
        out = await fetch_products(SHOP, "shpat_x", client=c)

    assert [n["id"] for n in out] == ["gid://1", "gid://2"]
    assert seen == [None, "CUR"]


@pytest.mark.asyncio
async def test_fetch_products_raises_on_graphql_errors():
    def handler(request):
        return httpx.Response(200, json={"errors": [{"message": "Access denied"}]})

    from app.services.integrations.shopify_oauth import ShopQueryError
    async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as c:
        with pytest.raises(ShopQueryError):
            await fetch_products(SHOP, "shpat_x", client=c)


def test_complementary_metafield_becomes_tenant_relations():
    node = dict(load("no-category"))
    node["complementary"] = {"value": '["gid://shopify/Product/1","gid://shopify/Product/2"]'}
    sp = to_source_product(node, SHOP)
    assert sp["tenant_relations"]["related"] == [
        "gid://shopify/Product/1", "gid://shopify/Product/2"]


def test_absent_complementary_metafield_yields_an_empty_object():
    # Never None: downstream reads .get("related") without a guard.
    sp = to_source_product(load("no-category"), SHOP)
    assert sp["tenant_relations"] == {}


def test_malformed_complementary_value_is_ignored():
    # A merchant or app can write anything into a metafield; a sync must not die.
    node = dict(load("no-category"))
    node["complementary"] = {"value": "not json at all"}
    assert to_source_product(node, SHOP)["tenant_relations"] == {}


def test_query_requests_the_complementary_metafield():
    assert "shopify--discovery--product_recommendation" in PRODUCTS_QUERY
    assert "complementary_products" in PRODUCTS_QUERY


def test_shopify_has_no_rating_or_review_count():
    # The Admin API exposes neither; the prominence term drops out, which the
    # matching specification permits as its option 4.
    sp = to_source_product(load("no-category"), SHOP)
    assert sp["rating"] is None
    assert sp["review_count"] is None
