"""Tests for the data provider framework.

Tests the documented requirements:
    1. DataProviderResult structure and contract
    2. SyncState watermark tracking
    3. Commerce schema validation
    4. Shopify transformer — maps GraphQL responses to schema
    5. Pipeline orchestration — sync flow, state management, upsert
"""

import asyncio
import json
import pytest
import tempfile
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

from platoon.data.provider import DataProvider, DataProviderResult
from platoon.data.pipeline import Pipeline, SyncState, SyncSummary
from platoon.data.schema import (
    Customer, InventoryItem, InventoryLevel, LineItem,
    Order, Product, Variant,
)


# ---------------------------------------------------------------------------
# DataProviderResult
# ---------------------------------------------------------------------------


def test_provider_result_empty():
    """Empty result has zero count and no errors."""
    result = DataProviderResult()
    assert result.count == 0
    assert result.errors == []
    assert result.watermark is None


def test_provider_result_count():
    """Count reflects number of records."""
    records = [Product(title="A"), Product(title="B")]
    result = DataProviderResult(records=records, entity_type="products", raw_count=2)
    assert result.count == 2
    assert result.raw_count == 2


# ---------------------------------------------------------------------------
# SyncState
# ---------------------------------------------------------------------------


def test_sync_state_initial():
    """New sync state starts with no watermark (triggers full sync)."""
    state = SyncState(
        tenant_id="test",
        integration_name="shopify",
        entity_type="orders",
    )
    assert state.last_watermark is None
    assert state.status == "pending"
    assert state.last_count == 0
    assert state.total_synced == 0


def test_sync_state_after_successful_run():
    """After a successful run, watermark and count are updated."""
    state = SyncState(
        tenant_id="test",
        integration_name="shopify",
        entity_type="orders",
        last_watermark=datetime(2026, 2, 25),
        last_count=142,
        total_synced=500,
        status="success",
    )
    assert state.last_watermark == datetime(2026, 2, 25)
    assert state.last_count == 142
    assert state.status == "success"


def test_sync_state_serialization():
    """SyncState round-trips through JSON."""
    state = SyncState(
        tenant_id="test",
        integration_name="shopify",
        entity_type="orders",
        last_watermark=datetime(2026, 2, 25, tzinfo=timezone.utc),
        status="success",
    )
    data = state.model_dump(mode="json")
    restored = SyncState.model_validate(data)
    assert restored.tenant_id == "test"
    assert restored.status == "success"


# ---------------------------------------------------------------------------
# Commerce Schema
# ---------------------------------------------------------------------------


def test_product_schema():
    """Product model validates and has required fields."""
    product = Product(title="Test Widget")
    assert product.title == "Test Widget"
    assert product.status == "active"
    assert product.variants == []


def test_order_schema():
    """Order model validates with line items."""
    order = Order(total_price=Decimal("99.99"))
    assert order.financial_status == "pending"
    assert order.line_items == []


def test_inventory_level_schema():
    """InventoryLevel tracks multiple quantity states."""
    level = InventoryLevel(available=50, on_hand=60, committed=10)
    assert level.available == 50
    assert level.on_hand == 60


# ---------------------------------------------------------------------------
# Shopify Transform
# ---------------------------------------------------------------------------


class TestShopifyTransform:
    """Tests that the Shopify transformer correctly maps GraphQL responses."""

    def setup_method(self):
        from platoon.data.providers.shopify.transform import ShopifyTransformer
        self.transformer = ShopifyTransformer()

    def test_extract_nodes_edges(self):
        """Extracts nodes from Shopify edges/node GraphQL pattern."""
        from platoon.data.providers.shopify.transform import _extract_nodes
        data = {"variants": {"edges": [{"node": {"id": "1"}}, {"node": {"id": "2"}}]}}
        nodes = _extract_nodes(data, "variants")
        assert len(nodes) == 2
        assert nodes[0]["id"] == "1"

    def test_extract_nodes_flat_list(self):
        """Handles flat list format from bulk operations."""
        from platoon.data.providers.shopify.transform import _extract_nodes
        data = {"variants": [{"id": "1"}, {"id": "2"}]}
        nodes = _extract_nodes(data, "variants")
        assert len(nodes) == 2

    def test_parse_dt(self):
        """Parses Shopify ISO datetime strings."""
        from platoon.data.providers.shopify.transform import _parse_dt
        assert _parse_dt(None) is None
        dt = _parse_dt("2026-02-25T10:00:00Z")
        assert dt.year == 2026
        assert dt.month == 2

    def test_transform_products(self):
        """Transforms product nodes into Product schema instances."""
        nodes = [{
            "id": "gid://shopify/Product/123",
            "title": "Cool Widget",
            "descriptionHtml": "<p>A widget</p>",
            "vendor": "Acme",
            "productType": "Gadgets",
            "tags": ["sale", "new"],
            "status": "ACTIVE",
            "createdAt": "2026-01-01T00:00:00Z",
            "updatedAt": "2026-02-01T00:00:00Z",
            "variants": {"edges": [{
                "node": {
                    "id": "gid://shopify/ProductVariant/456",
                    "sku": "WIDGET-001",
                    "title": "Default",
                    "price": "29.99",
                    "compareAtPrice": "39.99",
                    "inventoryQuantity": 100,
                }
            }]},
        }]

        products = self.transformer.products(nodes)
        assert len(products) == 1

        p = products[0]
        assert p.title == "Cool Widget"
        assert p.vendor == "Acme"
        assert p.product_type == "Gadgets"
        assert p.tags == ["sale", "new"]
        assert p.status == "active"
        assert p.source_id == "gid://shopify/Product/123"
        assert len(p.variants) == 1

        v = p.variants[0]
        assert v.sku == "WIDGET-001"
        assert v.price == Decimal("29.99")
        assert v.compare_at_price == Decimal("39.99")
        assert v.inventory_quantity == 100
        assert v.product_id == p.id

    def test_transform_orders(self):
        """Transforms order nodes into Order schema with line items."""
        nodes = [{
            "id": "gid://shopify/Order/789",
            "name": "#1001",
            "email": "customer@example.com",
            "displayFinancialStatus": "PAID",
            "displayFulfillmentStatus": "FULFILLED",
            "currencyCode": "USD",
            "createdAt": "2026-02-20T12:00:00Z",
            "updatedAt": "2026-02-20T12:00:00Z",
            "totalPriceSet": {"shopMoney": {"amount": "59.98", "currencyCode": "USD"}},
            "subtotalPriceSet": {"shopMoney": {"amount": "49.98", "currencyCode": "USD"}},
            "totalTaxSet": {"shopMoney": {"amount": "5.00", "currencyCode": "USD"}},
            "totalDiscountsSet": {"shopMoney": {"amount": "5.00", "currencyCode": "USD"}},
            "customer": {"id": "gid://shopify/Customer/111"},
            "lineItems": {"edges": [{
                "node": {
                    "id": "gid://shopify/LineItem/999",
                    "title": "Cool Widget",
                    "sku": "WIDGET-001",
                    "quantity": 2,
                    "originalUnitPriceSet": {"shopMoney": {"amount": "29.99", "currencyCode": "USD"}},
                    "totalDiscountSet": {"shopMoney": {"amount": "5.00", "currencyCode": "USD"}},
                    "product": {"id": "gid://shopify/Product/123"},
                    "variant": {"id": "gid://shopify/ProductVariant/456"},
                }
            }]},
        }]

        orders = self.transformer.orders(nodes)
        assert len(orders) == 1

        o = orders[0]
        assert o.order_number == "#1001"
        assert o.email == "customer@example.com"
        assert o.total_price == Decimal("59.98")
        assert o.total_tax == Decimal("5.00")
        assert o.currency == "USD"
        assert len(o.line_items) == 1

        li = o.line_items[0]
        assert li.title == "Cool Widget"
        assert li.quantity == 2
        assert li.price == Decimal("29.99")

    def test_transform_customers(self):
        """Transforms customer nodes into Customer schema.

        Uses numberOfOrders (String) matching actual Shopify API 2025-01+.
        """
        nodes = [{
            "id": "gid://shopify/Customer/111",
            "email": "alice@example.com",
            "firstName": "Alice",
            "lastName": "Smith",
            "numberOfOrders": "5",  # Shopify returns UnsignedInt64 as String
            "amountSpent": {"amount": "499.95", "currencyCode": "USD"},
            "tags": ["vip"],
            "createdAt": "2025-01-01T00:00:00Z",
            "updatedAt": "2026-02-01T00:00:00Z",
        }]

        customers = self.transformer.customers(nodes)
        assert len(customers) == 1

        c = customers[0]
        assert c.email == "alice@example.com"
        assert c.first_name == "Alice"
        assert c.orders_count == 5  # Converted from String "5" to int
        assert c.total_spent == Decimal("499.95")
        assert c.tags == ["vip"]

    def test_transform_inventory(self):
        """Transforms inventory item nodes with levels per location."""
        nodes = [{
            "id": "gid://shopify/InventoryItem/500",
            "sku": "WIDGET-001",
            "unitCost": {"amount": "12.50", "currencyCode": "USD"},
            "tracked": True,
            "createdAt": "2025-01-01T00:00:00Z",
            "updatedAt": "2026-02-01T00:00:00Z",
            "variant": {"id": "gid://shopify/ProductVariant/456"},
            "inventoryLevels": {"edges": [{
                "node": {
                    "id": "gid://shopify/InventoryLevel/600",
                    "location": {"id": "gid://shopify/Location/1", "name": "Main Warehouse"},
                    "quantities": [
                        {"name": "available", "quantity": 80},
                        {"name": "on_hand", "quantity": 100},
                        {"name": "committed", "quantity": 20},
                        {"name": "incoming", "quantity": 50},
                    ],
                }
            }]},
        }]

        items = self.transformer.inventory(nodes)
        assert len(items) == 1

        item = items[0]
        assert item.sku == "WIDGET-001"
        assert item.cost == Decimal("12.50")
        assert item.tracked is True
        assert len(item.levels) == 1

        level = item.levels[0]
        assert level.location_name == "Main Warehouse"
        assert level.available == 80
        assert level.on_hand == 100
        assert level.committed == 20
        assert level.incoming == 50

    def test_deterministic_ids(self):
        """Same source_id produces same UUID across runs."""
        nodes = [{"id": "gid://shopify/Product/123", "title": "Widget",
                  "createdAt": None, "updatedAt": None, "variants": {"edges": []}}]
        products1 = self.transformer.products(nodes)
        products2 = self.transformer.products(nodes)
        assert products1[0].id == products2[0].id


# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------


class MockProvider(DataProvider):
    """Test provider that returns canned data."""

    name = "mock"
    entity_types = ["products", "orders"]

    def __init__(self, data: Optional[Dict[str, List]] = None):
        self._data = data or {}
        self._fetch_count = 0

    async def fetch(
        self,
        entity_type: str,
        since: Optional[datetime] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> DataProviderResult:
        self._fetch_count += 1
        records = self._data.get(entity_type, [])
        watermark = datetime(2026, 2, 26, tzinfo=timezone.utc)
        return DataProviderResult(
            records=records,
            watermark=watermark,
            entity_type=entity_type,
            raw_count=len(records),
        )

    async def check_connection(self) -> bool:
        return True


class TestPipeline:
    """Tests pipeline sync orchestration."""

    def test_register_and_get_provider(self):
        """Providers can be registered and retrieved."""
        pipeline = Pipeline()
        mock = MockProvider()
        pipeline.register("mock", mock)
        assert pipeline.get_provider("mock") is mock

    def test_get_unknown_provider_raises(self):
        """Requesting an unregistered provider raises ValueError."""
        pipeline = Pipeline()
        with pytest.raises(ValueError, match="No provider registered"):
            pipeline.get_provider("nonexistent")

    def test_sync_state_creation(self):
        """First access creates a fresh sync state."""
        pipeline = Pipeline()
        state = pipeline.get_sync_state("tenant-1", "shopify", "products")
        assert state.tenant_id == "tenant-1"
        assert state.integration_name == "shopify"
        assert state.entity_type == "products"
        assert state.last_watermark is None
        assert state.status == "pending"

    def test_sync_state_reuse(self):
        """Same key returns the same sync state instance."""
        pipeline = Pipeline()
        s1 = pipeline.get_sync_state("t", "s", "products")
        s2 = pipeline.get_sync_state("t", "s", "products")
        assert s1 is s2

    def test_run_entity(self):
        """run_entity fetches, sinks, and updates watermark."""
        products = [Product(title="A"), Product(title="B")]
        mock = MockProvider(data={"products": products})
        pipeline = Pipeline()

        count = asyncio.get_event_loop().run_until_complete(
            pipeline.run_entity("tenant-1", "mock", "products", provider=mock)
        )

        assert count == 2
        assert mock._fetch_count == 1

        state = pipeline.get_sync_state("tenant-1", "mock", "products")
        assert state.status == "success"
        assert state.last_count == 2
        assert state.total_synced == 2
        assert state.last_watermark is not None

        # Records are stored
        stored = pipeline.get_records("tenant-1", "products")
        assert len(stored) == 2

    def test_run_entity_incremental(self):
        """Second run passes watermark from first run as since."""
        mock = MockProvider(data={"products": [Product(title="X")]})
        pipeline = Pipeline()

        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            pipeline.run_entity("t", "mock", "products", provider=mock)
        )
        loop.run_until_complete(
            pipeline.run_entity("t", "mock", "products", provider=mock)
        )

        assert mock._fetch_count == 2
        state = pipeline.get_sync_state("t", "mock", "products")
        assert state.total_synced == 2

    def test_run_all(self):
        """run_all iterates all providers and entity types."""
        products = [Product(title="A")]
        orders = [Order(total_price=Decimal("10"))]
        mock = MockProvider(data={"products": products, "orders": orders})

        pipeline = Pipeline()
        pipeline.register("mock", mock)

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

        assert isinstance(summary, SyncSummary)
        assert summary.tenant_id == "tenant-1"
        assert summary.results["mock"]["products"] == 1
        assert summary.results["mock"]["orders"] == 1
        assert summary.errors == []

    def test_sink_upserts_by_id(self):
        """Records with the same id are replaced, not duplicated."""
        from uuid import uuid4
        fixed_id = uuid4()

        pipeline = Pipeline()
        p1 = Product(id=fixed_id, title="V1")
        p2 = Product(id=fixed_id, title="V2")

        pipeline._sink("t", "products", [p1])
        pipeline._sink("t", "products", [p2])

        stored = pipeline.get_records("t", "products")
        assert len(stored) == 1
        assert stored[0].title == "V2"

    def test_list_states(self):
        """list_states returns all tracked sync states."""
        pipeline = Pipeline()
        pipeline.get_sync_state("t1", "shopify", "products")
        pipeline.get_sync_state("t1", "shopify", "orders")
        pipeline.get_sync_state("t2", "stripe", "charges")

        all_states = pipeline.list_states()
        assert len(all_states) == 3

        t1_states = pipeline.list_states(tenant_id="t1")
        assert len(t1_states) == 2

    def test_state_file_persistence(self):
        """State persists to and loads from a JSON file."""
        with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
            state_path = f.name

        try:
            # Create pipeline, run, check state is persisted
            mock = MockProvider(data={"products": [Product(title="A")]})
            p1 = Pipeline(state_file=state_path)

            asyncio.get_event_loop().run_until_complete(
                p1.run_entity("t", "mock", "products", provider=mock)
            )

            # New pipeline loads from file
            p2 = Pipeline(state_file=state_path)
            states = p2.list_states()
            assert len(states) == 1
            assert states[0].status == "success"
            assert states[0].last_count == 1
        finally:
            Path(state_path).unlink(missing_ok=True)

    def test_run_entity_error_handling(self):
        """Errors are caught and state is set to 'error'."""
        class FailingProvider(DataProvider):
            name = "fail"
            entity_types = ["products"]

            async def fetch(self, entity_type, since=None, config=None):
                raise ConnectionError("API down")

            async def check_connection(self):
                return False

        pipeline = Pipeline()
        with pytest.raises(ConnectionError):
            asyncio.get_event_loop().run_until_complete(
                pipeline.run_entity("t", "fail", "products", provider=FailingProvider())
            )

        state = pipeline.get_sync_state("t", "fail", "products")
        assert state.status == "error"
        assert "API down" in state.error_message


# ---------------------------------------------------------------------------
# Shopify Client (unit tests with mocked HTTP)
# ---------------------------------------------------------------------------


class TestShopifyClient:
    """Tests for ShopifyGraphQLClient with mocked HTTP."""

    def test_client_url_construction(self):
        """Client builds the correct API URL."""
        from platoon.data.providers.shopify.client import ShopifyGraphQLClient
        client = ShopifyGraphQLClient(shop="test.myshopify.com", token="shpat_test")
        assert "test.myshopify.com" in client.url
        assert "2026-01" in client.url

    def test_rate_limit_parsing(self):
        """Client parses throttleStatus from extensions."""
        from platoon.data.providers.shopify.client import ShopifyGraphQLClient
        client = ShopifyGraphQLClient(shop="test.myshopify.com", token="shpat_test")

        extensions = {
            "cost": {
                "requestedQueryCost": 10,
                "actualQueryCost": 8,
                "throttleStatus": {
                    "maximumAvailable": 1000,
                    "currentlyAvailable": 750,
                    "restoreRate": 50,
                },
            }
        }
        client._update_rate_limit(extensions)
        assert client._available_points == 750.0

    def test_bulk_jsonl_reassembly(self):
        """Flat JSONL with __parentId is reassembled into nested structures."""
        from platoon.data.providers.shopify.client import _reassemble_bulk_jsonl

        flat = [
            {"id": "gid://shopify/Product/1", "title": "Widget"},
            {"id": "gid://shopify/ProductVariant/10", "__parentId": "gid://shopify/Product/1",
             "sku": "W-A", "price": "9.99"},
            {"id": "gid://shopify/ProductVariant/11", "__parentId": "gid://shopify/Product/1",
             "sku": "W-B", "price": "14.99"},
            {"id": "gid://shopify/Product/2", "title": "Gadget"},
            {"id": "gid://shopify/ProductVariant/20", "__parentId": "gid://shopify/Product/2",
             "sku": "G-A", "price": "19.99"},
        ]

        result = _reassemble_bulk_jsonl(flat)

        assert len(result) == 2
        assert result[0]["title"] == "Widget"
        assert len(result[0]["variants"]) == 2
        assert result[0]["variants"][0]["sku"] == "W-A"
        assert result[0]["variants"][1]["sku"] == "W-B"
        assert "__parentId" not in result[0]["variants"][0]
        assert result[1]["title"] == "Gadget"
        assert len(result[1]["variants"]) == 1
