"""Tests for the FakeShopifyProvider — realistic mock data pipeline.

Tests the documented requirements:
    1. Generated nodes match exact Shopify Admin GraphQL API shapes
    2. Nodes transform cleanly through ShopifyTransformer
    3. Full pipeline (fetch → transform → parquet) works end-to-end
    4. Bulk JSONL output reassembles correctly via __parentId
    5. Data is reproducible (seeded random)
"""

import asyncio
import tempfile
from decimal import Decimal
from pathlib import Path

import polars as pl
import pytest

from platoon.data.pipeline import Pipeline
from platoon.data.providers.shopify.fake import FakeShopifyProvider
from platoon.data.providers.shopify.transform import ShopifyTransformer
from platoon.data.schema import Customer, InventoryItem, Order, Product


@pytest.fixture
def provider():
    return FakeShopifyProvider(seed=42, num_products=20, num_customers=50, num_orders=200)


@pytest.fixture
def tmp_dir():
    with tempfile.TemporaryDirectory() as d:
        yield Path(d)


# ---------------------------------------------------------------------------
# Node shape validation
# ---------------------------------------------------------------------------


class TestNodeShapes:
    """Verify generated nodes match actual Shopify GraphQL response structure."""

    def test_product_node_shape(self, provider):
        """Product nodes have all required fields in correct Shopify format."""
        nodes = provider.get_product_nodes()
        assert len(nodes) == 20

        node = nodes[0]
        assert node["id"].startswith("gid://shopify/Product/")
        assert isinstance(node["title"], str)
        assert isinstance(node["descriptionHtml"], str)
        assert isinstance(node["vendor"], str)
        assert isinstance(node["productType"], str)
        assert isinstance(node["tags"], list)
        assert node["status"] in ("ACTIVE", "DRAFT", "ARCHIVED")
        assert "T" in node["createdAt"] and "Z" in node["createdAt"]

        # Variants use edges/node pattern
        variants = node["variants"]
        assert "edges" in variants
        assert len(variants["edges"]) >= 1

        v = variants["edges"][0]["node"]
        assert v["id"].startswith("gid://shopify/ProductVariant/")
        assert isinstance(v["price"], str)  # String scalar, not float
        assert isinstance(v["sku"], str)

    def test_customer_node_shape(self, provider):
        """Customer nodes use numberOfOrders (String) and amountSpent (MoneyV2)."""
        nodes = provider.get_customer_nodes()
        assert len(nodes) == 50

        node = nodes[0]
        assert node["id"].startswith("gid://shopify/Customer/")
        assert isinstance(node["numberOfOrders"], str)  # UnsignedInt64 → String
        assert int(node["numberOfOrders"]) >= 0
        assert "amount" in node["amountSpent"]
        assert "currencyCode" in node["amountSpent"]
        assert isinstance(node["amountSpent"]["amount"], str)

    def test_order_node_shape(self, provider):
        """Order nodes use MoneyBag (shopMoney) and proper connections."""
        nodes = provider.get_order_nodes()
        assert len(nodes) == 200

        node = nodes[0]
        assert node["id"].startswith("gid://shopify/Order/")
        assert node["name"].startswith("#")
        assert "shopMoney" in node["totalPriceSet"]
        assert "amount" in node["totalPriceSet"]["shopMoney"]
        assert node["displayFinancialStatus"] in (
            "PAID", "PARTIALLY_REFUNDED", "REFUNDED", "PENDING",
            "AUTHORIZED", "VOIDED",
        )

        # Line items use edges/node
        li_edges = node["lineItems"]["edges"]
        assert len(li_edges) >= 1
        li = li_edges[0]["node"]
        assert li["id"].startswith("gid://shopify/LineItem/")
        assert "shopMoney" in li["originalUnitPriceSet"]
        assert li["product"]["id"].startswith("gid://shopify/Product/")

    def test_inventory_node_shape(self, provider):
        """Inventory nodes include quantities breakdown per location."""
        nodes = provider.get_inventory_nodes()
        assert len(nodes) > 0

        node = nodes[0]
        assert node["id"].startswith("gid://shopify/InventoryItem/")
        assert "amount" in node["unitCost"]
        assert node["tracked"] is True
        assert node["variant"]["id"].startswith("gid://shopify/ProductVariant/")

        levels = node["inventoryLevels"]["edges"]
        assert len(levels) >= 1
        level = levels[0]["node"]
        assert "location" in level
        assert "quantities" in level
        qty_names = {q["name"] for q in level["quantities"]}
        assert qty_names >= {"available", "on_hand", "committed"}


# ---------------------------------------------------------------------------
# Transform validation
# ---------------------------------------------------------------------------


class TestTransformCompat:
    """Verify fake nodes transform cleanly into Pydantic schema."""

    def test_products_transform(self, provider):
        """All fake product nodes transform without errors."""
        transformer = ShopifyTransformer()
        products = transformer.products(provider.get_product_nodes())
        assert len(products) == 20
        assert all(isinstance(p, Product) for p in products)
        assert all(p.source_id.startswith("gid://shopify/Product/") for p in products)
        assert all(len(p.variants) >= 1 for p in products)

    def test_customers_transform(self, provider):
        """numberOfOrders (String) converts to int correctly."""
        transformer = ShopifyTransformer()
        customers = transformer.customers(provider.get_customer_nodes())
        assert len(customers) == 50
        assert all(isinstance(c, Customer) for c in customers)
        assert all(isinstance(c.orders_count, int) for c in customers)
        assert all(c.total_spent >= 0 for c in customers)

    def test_orders_transform(self, provider):
        """Order nodes with MoneyBag transform to Decimal prices."""
        transformer = ShopifyTransformer()
        orders = transformer.orders(provider.get_order_nodes())
        assert len(orders) == 200
        assert all(isinstance(o, Order) for o in orders)
        assert all(o.total_price > 0 for o in orders)
        assert all(len(o.line_items) >= 1 for o in orders)

    def test_inventory_transform(self, provider):
        """Inventory levels preserve quantity breakdown."""
        transformer = ShopifyTransformer()
        items = transformer.inventory(provider.get_inventory_nodes())
        assert len(items) > 0
        assert all(isinstance(i, InventoryItem) for i in items)
        for item in items[:5]:
            assert len(item.levels) >= 1
            assert item.levels[0].on_hand >= 0


# ---------------------------------------------------------------------------
# Pipeline integration
# ---------------------------------------------------------------------------


class TestPipelineIntegration:
    """Test FakeShopifyProvider through the full Pipeline."""

    def test_fetch_all_entity_types(self, provider):
        """Pipeline fetches and stores all 4 entity types."""
        pipeline = Pipeline()
        pipeline.register("fake_shopify", provider)

        summary = asyncio.get_event_loop().run_until_complete(
            pipeline.run_all("test-tenant")
        )

        assert summary.errors == []
        assert summary.results["fake_shopify"]["products"] == 20
        assert summary.results["fake_shopify"]["customers"] == 50
        assert summary.results["fake_shopify"]["orders"] == 200
        assert summary.results["fake_shopify"]["inventory"] > 0

        # Verify records are stored
        products = pipeline.get_records("test-tenant", "products")
        assert len(products) == 20
        assert all(isinstance(p, Product) for p in products)

    def test_parquet_export(self, provider, tmp_dir):
        """Full flow: fake provider → pipeline → parquet."""
        from tests.test_parquet_roundtrip import models_to_parquet

        pipeline = Pipeline()
        pipeline.register("fake_shopify", provider)

        asyncio.get_event_loop().run_until_complete(
            pipeline.run_all("test-tenant")
        )

        # Export each entity type
        products = pipeline.get_records("test-tenant", "products")
        models_to_parquet(
            products,
            tmp_dir / "products.parquet",
            flatten_fields={"variants": tmp_dir / "variants.parquet"},
        )

        orders = pipeline.get_records("test-tenant", "orders")
        models_to_parquet(
            orders,
            tmp_dir / "orders.parquet",
            flatten_fields={"line_items": tmp_dir / "line_items.parquet"},
        )

        customers = pipeline.get_records("test-tenant", "customers")
        models_to_parquet(customers, tmp_dir / "customers.parquet")

        inventory = pipeline.get_records("test-tenant", "inventory")
        models_to_parquet(
            inventory,
            tmp_dir / "inventory.parquet",
            flatten_fields={"levels": tmp_dir / "levels.parquet"},
        )

        # Verify parquet files
        assert pl.read_parquet(tmp_dir / "products.parquet").height == 20
        assert pl.read_parquet(tmp_dir / "variants.parquet").height > 20
        assert pl.read_parquet(tmp_dir / "orders.parquet").height == 200
        assert pl.read_parquet(tmp_dir / "line_items.parquet").height >= 200
        assert pl.read_parquet(tmp_dir / "customers.parquet").height == 50
        assert pl.read_parquet(tmp_dir / "inventory.parquet").height > 0

        # Print summary
        print("\n  Fake Shopify → Parquet export:")
        for f in sorted(tmp_dir.glob("*.parquet")):
            df = pl.read_parquet(f)
            print(f"    {f.name:<25} {df.height:>5} rows  {f.stat().st_size:>8,} B")


# ---------------------------------------------------------------------------
# Bulk JSONL format
# ---------------------------------------------------------------------------


class TestBulkJSONL:
    """Test bulk operation JSONL output with __parentId."""

    def test_bulk_jsonl_format(self, provider):
        """Bulk JSONL has flat lines with __parentId for children."""
        lines = provider.get_bulk_jsonl("products")
        assert len(lines) > 20  # products + their variants

        # First line should be a parent (no __parentId)
        assert "__parentId" not in lines[0]
        assert lines[0]["id"].startswith("gid://shopify/Product/")

        # Find a child line
        children = [l for l in lines if "__parentId" in l]
        assert len(children) > 0
        assert children[0]["id"].startswith("gid://shopify/ProductVariant/")
        assert children[0]["__parentId"].startswith("gid://shopify/Product/")

    def test_bulk_jsonl_reassembles(self, provider):
        """Bulk JSONL reassembles back into nested nodes via _reassemble_bulk_jsonl."""
        from platoon.data.providers.shopify.client import _reassemble_bulk_jsonl

        flat = provider.get_bulk_jsonl("products")
        reassembled = _reassemble_bulk_jsonl(flat)

        assert len(reassembled) == 20  # Back to 20 products
        assert all("variants" in p for p in reassembled)
        assert all(len(p["variants"]) >= 1 for p in reassembled)
        assert all("__parentId" not in v for p in reassembled for v in p["variants"])

    def test_bulk_reassembled_transforms(self, provider):
        """Reassembled bulk data transforms correctly through ShopifyTransformer."""
        from platoon.data.providers.shopify.client import _reassemble_bulk_jsonl

        flat = provider.get_bulk_jsonl("products")
        reassembled = _reassemble_bulk_jsonl(flat)

        transformer = ShopifyTransformer()
        products = transformer.products(reassembled)

        assert len(products) == 20
        assert all(isinstance(p, Product) for p in products)
        assert all(len(p.variants) >= 1 for p in products)


# ---------------------------------------------------------------------------
# Reproducibility
# ---------------------------------------------------------------------------


class TestReproducibility:
    """Verify seeded data is deterministic."""

    def test_same_seed_same_data(self):
        """Same seed produces identical data."""
        p1 = FakeShopifyProvider(seed=123, num_products=5, num_customers=10, num_orders=20)
        p2 = FakeShopifyProvider(seed=123, num_products=5, num_customers=10, num_orders=20)

        nodes1 = p1.get_product_nodes()
        nodes2 = p2.get_product_nodes()

        assert len(nodes1) == len(nodes2)
        for n1, n2 in zip(nodes1, nodes2):
            assert n1["id"] == n2["id"]
            assert n1["title"] == n2["title"]

    def test_different_seed_different_data(self):
        """Different seeds produce different data."""
        p1 = FakeShopifyProvider(seed=1, num_products=5, num_customers=10, num_orders=20)
        p2 = FakeShopifyProvider(seed=2, num_products=5, num_customers=10, num_orders=20)

        tags1 = [n.get("tags") for n in p1.get_product_nodes()]
        tags2 = [n.get("tags") for n in p2.get_product_nodes()]
        # At least some tags should differ (extremely unlikely to be identical)
        assert tags1 != tags2
