"""Wave 4: optional catalog_item relation on quote, invoice, and order lines.

catalog_composition is a related catalog_item on the line row, not an engine.
Header amount stays explicit. No new Cloud jobs.
"""
from __future__ import annotations

import json
from pathlib import Path

import pytest
import yaml
from src.domain.app_blocks.generated_product_conformance_12h import (
    ENTITY_BY_BLOCK,
    KernelDeniedError,
    PlatformKernel,
)
from src.domain.feature_packs.catalog_v2 import overlay_for
from src.integrations.platform_ports import QUEUE_REQUEST_TYPES

from tests.test_phase11_pack_migration_materialization import (
    _load_migration,
)

ROOT = Path(__file__).resolve().parents[1]
REGISTRY = ROOT / "src/domain/app_blocks/platform_coverage_registry.v1.yaml"
WAVE2 = "alembic/versions/158_quote_invoice_catalog_child_records.py"
WAVE3 = "alembic/versions/159_order_line_child_records.py"
WAVE4 = "alembic/versions/160_line_catalog_item_relation.py"
LINE_KEYS = ("quote_line", "invoice_line", "order_line")


def _entity(composition: dict, key: str) -> dict:
    return next(item for item in composition["entities"] if item["entity_key"] == key)


def _keys(entity: dict) -> set[str]:
    return {field["key"] for field in entity["schema"]["fields"]}


def _catalog_field(entity: dict) -> dict:
    return next(
        field
        for field in entity["schema"]["fields"]
        if field["key"] == "catalog_item_record_id"
    )


def _schema_dir(framework: str) -> Path:
    return {
        "html": Path("js/entity-schemas"),
        "nuxt_app": Path("lib/entity-schemas"),
        "react_app": Path("src/lib/entity-schemas"),
    }[framework]


def test_historical_line_migrations_omit_catalog_item_relation():
    quote = _load_migration(WAVE2).quote_lifecycle_v1_1()
    invoice = _load_migration(WAVE2).billing_management_v1_1()
    order = _load_migration(WAVE3).order_lifecycle_v1_1()
    for entity in (
        _entity(quote, "quote_line"),
        _entity(invoice, "invoice_line"),
        _entity(order, "order_line"),
    ):
        assert "catalog_item_record_id" not in _keys(entity)


def test_live_wave4_adds_optional_catalog_item_relation_without_jobs():
    module = _load_migration(WAVE4)
    quote = module.quote_lifecycle_v1_2()
    invoice = module.billing_management_v1_2()
    order = module.order_lifecycle_v1_2()
    for composition, key, parent_key, parent_entity in (
        (quote, "quote_line", "quote_record_id", "quote"),
        (invoice, "invoice_line", "invoice_record_id", "invoice_record"),
        (order, "order_line", "order_record_id", "order_record"),
    ):
        assert composition["function_slugs"] == []
        assert composition["connector_actions"] == []
        line = _entity(composition, key)
        assert parent_key in _keys(line)
        rel = _catalog_field(line)
        assert rel["type"] == "relation"
        assert rel["relation_entity_key"] == "catalog_item"
        assert rel["indexed"] is True
        assert rel.get("required") is not True
        parent = next(
            field for field in line["schema"]["fields"] if field["key"] == parent_key
        )
        assert parent["relation_entity_key"] == parent_entity
    assert "amount" in _keys(_entity(quote, "quote"))
    assert "amount" in _keys(_entity(invoice, "invoice_record"))
    assert "amount" in _keys(_entity(order, "order_record"))


def test_overlays_do_not_copy_catalog_item_onto_line_packs():
    quote_keys = {entity.entity_key for entity in overlay_for("quote-lifecycle")["entities"]}
    invoice_keys = {entity.entity_key for entity in overlay_for("billing-management")["entities"]}
    order_keys = {entity.entity_key for entity in overlay_for("order-management")["entities"]}
    catalog_keys = {entity.entity_key for entity in overlay_for("product-catalog")["entities"]}
    assert quote_keys == {"quote", "quote_line"}
    assert invoice_keys == {"invoice_record", "invoice_line"}
    assert order_keys == {"order_record", "order_line"}
    assert "catalog_item" in catalog_keys
    assert "catalog_item" not in quote_keys | invoice_keys | order_keys


def test_entity_by_block_stays_on_parent_records():
    assert ENTITY_BY_BLOCK["crm.quote"] == "quote"
    assert ENTITY_BY_BLOCK["billing.invoice"] == "invoice_record"
    assert ENTITY_BY_BLOCK["orders.order"] == "order_record"
    assert ENTITY_BY_BLOCK["catalog.product"] == "catalog_item"


def test_registry_drops_catalog_composition_keeps_billing_engines():
    registry = yaml.safe_load(REGISTRY.read_text(encoding="utf-8"))
    by_id = {row["id"]: row for row in registry["capabilities"]}
    assert "catalog_composition" not in by_id["billing.invoice"]["gaps"]
    assert by_id["billing.invoice"]["gaps"] == [
        "payments",
        "tax_engine",
        "subscriptions",
    ]
    assert "checkout_app_block" in by_id["orders.order"]["gaps"]
    assert "payments" in by_id["billing"]["gaps"]


def test_no_new_cloud_jobs_for_catalog_composition():
    from src.domain.cloud.service import _REQUEST_TYPE_POLICY

    forbidden = {
        "catalog.compose",
        "quote.line.catalog",
        "invoice.line.catalog",
        "order.line.catalog",
    }
    assert not (forbidden & set(QUEUE_REQUEST_TYPES))
    assert not (forbidden & set(_REQUEST_TYPE_POLICY))


def test_header_amount_stays_explicit_with_optional_catalog_item():
    kernel = PlatformKernel(namespace="wave4-line-catalog")
    kernel.as_anonymous()
    with pytest.raises(KernelDeniedError):
        kernel.create_record("quote_line", {"description": "Labor"})
    kernel.as_staff()
    item = kernel.create_record(
        "catalog_item",
        {"name": "Widget", "sku": "W1", "status": "active", "price_amount": 12},
    )
    quote = kernel.create_record(
        "quote",
        {"reference": "Q-1", "status": "draft", "amount": 100, "currency": "USD"},
    )
    kernel.create_record(
        "quote_line",
        {
            "quote_record_id": quote["id"],
            "description": "Labor",
            "quantity": 2,
            "unit_price": 40,
        },
    )
    kernel.create_record(
        "quote_line",
        {
            "quote_record_id": quote["id"],
            "catalog_item_record_id": item["id"],
            "description": "Widget",
            "quantity": 1,
            "unit_price": 12,
        },
    )
    assert kernel.get_record("quote", quote["id"])["amount"] == 100
    invoice = kernel.create_record(
        "invoice_record",
        {"reference": "INV-1", "status": "draft", "amount": 250, "currency": "USD"},
    )
    kernel.create_record(
        "invoice_line",
        {
            "invoice_record_id": invoice["id"],
            "catalog_item_record_id": item["id"],
            "description": "Widget",
            "quantity": 1,
            "unit_price": 12,
        },
    )
    assert kernel.get_record("invoice_record", invoice["id"])["amount"] == 250
    order = kernel.create_record(
        "order_record",
        {"reference": "WO-1", "status": "open", "amount": 120, "currency": "USD"},
    )
    kernel.create_record(
        "order_line",
        {
            "order_record_id": order["id"],
            "description": "Labor",
            "quantity": 1,
            "unit_price": 120,
        },
    )
    assert kernel.get_record("order_record", order["id"])["amount"] == 120


def test_materialized_line_schemas_include_optional_catalog_item():
    for framework in ("html", "nuxt_app", "react_app"):
        root = ROOT / "boilerplates" / framework
        for key in LINE_KEYS:
            schema = json.loads(
                (root / _schema_dir(framework) / f"{key}.json").read_text(encoding="utf-8")
            )
            rel = next(
                field
                for field in schema["fields"]
                if field["key"] == "catalog_item_record_id"
            )
            assert rel["type"] == "relation"
            assert rel["relation_entity_key"] == "catalog_item"
            assert rel.get("required") is not True
            source = (root / _schema_dir(framework) / f"{key}.json").read_text(
                encoding="utf-8"
            )
            assert "/api/jobs/" not in source
            assert "cloudgate" not in source
