from __future__ import annotations

from typing import Any, Dict, List, Tuple

from sqlalchemy import select

from app.jobs.audit_shopping_taxonomy_options import audit_connection
from app.jobs.cleanup_manual_taxonomy_assignments import _assignment_metadata
from db.channel_exports import (
    _apply_canonical_shopping_field_overrides,
    _apply_manual_taxonomy_projection_overrides,
)
from db.channel_remote_attributes import _seed_magento_options
from db.models import (
    CollectionLandingPage,
    ManualTaxonomyAssignment,
    ManualTaxonomyAssignmentCollection,
    MasterTaxonomyNode,
)
from db.shopping_taxonomy_vocabulary import (
    ShoppingTaxonomyVocabulary,
    load_shopping_taxonomy_vocabulary,
)
from db.taxonomy_invent_policy import taxonomy_remote_provision_enabled


def _add_manual_assignment(
    session,
    *,
    source_sku: str = "B15",
    master_sku: str = "ACH-B15",
    canonical_path: str = "kitchen-cabinets/mouldings",
    shopping_l1: str | None = "mouldings",
    shopping_l2: str | None = None,
) -> None:
    assignment = ManualTaxonomyAssignment(
        source_sku=source_sku,
        canonical_taxonomy_path_slug=canonical_path,
        canonical_taxonomy_path_label="Kitchen Cabinets / Mouldings",
        shopping_l1=shopping_l1,
        shopping_l2=shopping_l2,
        assignment_status="active",
    )
    session.add(assignment)
    session.flush()
    session.add(
        ManualTaxonomyAssignmentCollection(
            assignment_id=assignment.id,
            collection_code="ACH",
            sku_prefix="ACH",
            collection_name="Anna Caramel Harvest",
            collection_path_slug="kitchen-cabinets/anna-caramel-harvest",
            master_sku=master_sku,
            is_active=True,
        )
    )
    session.flush()


def _payload(sku: str, **fields: Any) -> Dict[str, Any]:
    return {
        "sku": sku,
        "price": 100.0,
        "association_fields": {},
        "canonical_fields": dict(fields),
        "fields": dict(fields),
        "taxonomy_targets": {},
        "payload_hash": "old-hash",
    }


def test_manual_projection_does_not_invent_shopping_l2_from_hub_level_path(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(session)
    payload = _payload("ACH-B15", title="Crown Moulding", shopping_l2="kitchen-cabinets/mouldings")

    manual_locked = _apply_manual_taxonomy_projection_overrides(session, "magento", [payload])

    assert manual_locked["ACH-B15"] == {"shopping_collection", "shopping_l1", "shopping_l2"}
    assert payload["canonical_fields"]["shopping_l1"] == "mouldings"
    assert payload["canonical_fields"]["shopping_collection"] == "anna-caramel-harvest"
    assert "shopping_l2" not in payload["canonical_fields"]
    assert "shopping_l2" not in payload["fields"]


def test_manual_projection_keeps_curated_shopping_l2(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )
    payload = _payload("ACH-B15", title="B15")

    _apply_manual_taxonomy_projection_overrides(session, "magento", [payload])

    assert payload["canonical_fields"]["shopping_l2"] == "base-cabinets/single-door"


def test_path_inference_cannot_overwrite_manually_curated_shopping_values():
    payload = _payload("ACH-B15", shopping_l1="mouldings")
    payload["taxonomy_targets"] = {
        "plp_path_slugs": [
            "kitchen-cabinets/wall-cabinets",
            "kitchen-cabinets/wall-cabinets/basic-wall",
        ]
    }

    _apply_canonical_shopping_field_overrides(
        [payload],
        manual_locked={"ACH-B15": {"shopping_collection", "shopping_l1", "shopping_l2"}},
    )

    assert payload["canonical_fields"]["shopping_l1"] == "mouldings"
    assert "shopping_l2" not in payload["canonical_fields"]


def test_path_inference_drops_values_outside_curated_vocabulary():
    payload = _payload("ASG-W1230")
    payload["taxonomy_targets"] = {
        "plp_path_slugs": [
            "kitchen-cabinets/wall-cabinets",
            "kitchen-cabinets/wall-cabinets/basic-wall",
        ]
    }
    vocabulary = ShoppingTaxonomyVocabulary(
        values_by_code={
            "shopping_l1": frozenset({"wall-cabinets"}),
            "shopping_l2": frozenset({"wall-cabinets/single-door"}),
        }
    )

    _apply_canonical_shopping_field_overrides([payload], vocabulary=vocabulary)

    assert payload["canonical_fields"]["shopping_l1"] == "wall-cabinets"
    assert "shopping_l2" not in payload["canonical_fields"]


def test_vocabulary_reads_manual_taxonomy_and_start_shopping_config(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )
    session.add(
        CollectionLandingPage(
            path_slug="kitchen-cabinets/anna-caramel-harvest",
            title="Anna Caramel Harvest",
            is_active=True,
            start_shopping_config={
                "hub_path_slug": "kitchen-cabinets",
                "items": [
                    {
                        "l1_path_slug": "kitchen-cabinets/wall-cabinets",
                        "l2_path_slugs": ["kitchen-cabinets/wall-cabinets/basic-wall"],
                    }
                ],
            },
        )
    )
    session.flush()

    vocabulary = load_shopping_taxonomy_vocabulary(session)

    assert not vocabulary.is_empty
    assert vocabulary.allows("shopping_l2", "base-cabinets/single-door")
    assert vocabulary.allows("shopping_l2", "wall-cabinets/basic-wall")
    assert vocabulary.allows("shopping_collection", "anna-caramel-harvest")
    assert not vocabulary.allows("shopping_l2", "kitchen-cabinets/mouldings")
    # Human labels normalize to the curated slug form.
    assert vocabulary.allows("shopping_l1", "Base Cabinets")
    assert not vocabulary.allows("shopping_l1", "Panels and Fillers")


def _add_active_node(session, path_slug: str, name: str) -> MasterTaxonomyNode:
    parts = [part for part in path_slug.split("/") if part]
    parent_id = None
    node = None
    for depth in range(1, len(parts) + 1):
        slug = "/".join(parts[:depth])
        node = session.scalar(select(MasterTaxonomyNode).where(MasterTaxonomyNode.path_slug == slug))
        if node is None:
            node = MasterTaxonomyNode(
                path_slug=slug,
                code=parts[depth - 1],
                name=name if depth == len(parts) else parts[depth - 1].replace("-", " ").title(),
                parent_id=parent_id,
                is_active=True,
            )
            session.add(node)
            session.flush()
        parent_id = node.id
    return node


def test_vocabulary_rejects_curated_values_with_no_active_taxonomy_node(catalog_intent_session):
    session = catalog_intent_session
    _add_active_node(session, "kitchen-cabinets/moldings/crown-molding", "Crown Molding")
    # A manual assignment pointing at the dead British-spelling path.
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/mouldings/crown-molding",
        shopping_l1="mouldings",
        shopping_l2="mouldings/crown-molding",
    )
    _add_manual_assignment(
        session,
        source_sku="CM8",
        master_sku="ACH-CM8",
        canonical_path="kitchen-cabinets/moldings/crown-molding",
        shopping_l1="moldings",
        shopping_l2="moldings/crown-molding",
    )

    vocabulary = load_shopping_taxonomy_vocabulary(session)

    assert vocabulary.allows("shopping_l2", "moldings/crown-molding")
    assert not vocabulary.allows("shopping_l2", "mouldings/crown-molding")
    assert vocabulary.unbacked_values("shopping_l2") == ["mouldings/crown-molding"]
    assert vocabulary.unbacked_values("shopping_l1") == ["mouldings"]


def test_vocabulary_keeps_curated_values_when_taxonomy_has_nothing_to_say(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )

    vocabulary = load_shopping_taxonomy_vocabulary(session)

    assert vocabulary.allows("shopping_l2", "base-cabinets/single-door")
    assert vocabulary.unbacked_values("shopping_l2") == []


def test_seed_magento_options_skips_uncurated_shopping_labels_under_freeze(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )

    result = _seed_magento_options(
        session,
        connection_id=1,
        attribute_code="shopping_l2",
        labels=["base-cabinets/single-door", "kitchen-cabinets/mouldings", "Panels and Fillers"],
        dry_run=False,
    )

    assert result["created_options"] == []
    assert result["would_create_options"] == ["base-cabinets/single-door"]
    assert result["taxonomy_create_blocked"] is True
    assert result["uncurated_options_skipped"] == ["kitchen-cabinets/mouldings", "Panels and Fillers"]


def test_seed_magento_options_still_filters_uncurated_when_provision_unfrozen(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )

    with taxonomy_remote_provision_enabled(True):
        result = _seed_magento_options(
            session,
            connection_id=1,
            attribute_code="shopping_l2",
            labels=["base-cabinets/single-door", "kitchen-cabinets/mouldings"],
            dry_run=True,
        )

    assert "taxonomy_create_blocked" not in result
    assert result["would_create_options"] == ["base-cabinets/single-door"]
    assert result["uncurated_options_skipped"] == ["kitchen-cabinets/mouldings"]


def test_seed_magento_options_leaves_non_taxonomy_attributes_alone(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(session)

    result = _seed_magento_options(
        session,
        connection_id=1,
        attribute_code="cabinet_door_style",
        labels=["Shaker", "Slab"],
        dry_run=True,
    )

    assert result["would_create_options"] == ["Shaker", "Slab"]
    assert "uncurated_options_skipped" not in result


class _FakeMagentoOptionApi:
    def __init__(self, options: List[Dict[str, str]], skus_by_value: Dict[str, List[str]]) -> None:
        self._options = options
        self._skus_by_value = skus_by_value
        self.updates: List[Tuple[str, Dict[str, Any]]] = []
        self.deleted: List[Tuple[str, str]] = []

    def get_attribute_options(self, attribute_code: str):
        return 200, self._options

    def search_products_by_attribute(self, attribute_code, value, *, page_size=100, current_page=1):
        if current_page > 1:
            return 200, [], 0, None
        items = [{"sku": sku} for sku in self._skus_by_value.get(str(value), [])]
        return 200, items, len(items), None

    def update_product(self, sku: str, payload: Dict[str, Any]):
        self.updates.append((sku, payload))
        return {}

    def delete_attribute_option(self, attribute_code: str, option_id: str):
        self.deleted.append((attribute_code, option_id))
        return 204, None, None


def test_audit_reports_uncurated_options_without_touching_magento(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )
    api = _FakeMagentoOptionApi(
        options=[
            {"label": "base-cabinets/single-door", "value": "10"},
            {"label": "kitchen-cabinets/mouldings", "value": "11"},
        ],
        skus_by_value={"11": ["ACH-CM8"]},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        delete=False,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert [item["label"] for item in entry["uncurated_options"]] == ["kitchen-cabinets/mouldings"]
    assert entry["uncurated_options"][0]["affected_skus"] == ["ACH-CM8"]
    assert entry["deleted_options"] == []
    assert api.updates == []
    assert api.deleted == []


def test_audit_clears_products_with_int_safe_value_then_deletes_option(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )
    api = _FakeMagentoOptionApi(
        options=[{"label": "kitchen-cabinets/mouldings", "value": "11"}],
        skus_by_value={"11": ["ACH-CM8"]},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        delete=True,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert entry["cleared_products"] == ["ACH-CM8"]
    assert entry["deleted_options"] == ["kitchen-cabinets/mouldings"]
    # Select attributes have an int backend; "" is rejected, option id 0 clears.
    assert api.updates == [
        ("ACH-CM8", {"sku": "ACH-CM8", "custom_attributes": [{"attribute_code": "shopping_l2", "value": "0"}]})
    ]
    assert api.deleted == [("shopping_l2", "11")]


def test_audit_reassigns_using_option_id_not_slug(catalog_intent_session, monkeypatch):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/wall-cabinets/blind-corner",
        shopping_l1="wall-cabinets",
        shopping_l2="wall-cabinets/blind-corner",
    )
    api = _FakeMagentoOptionApi(
        options=[
            {"label": "wall-cabinets/blind-corner", "value": "50"},
            {"label": "wall-cabinets/blind-corner-wall-cabinet", "value": "1161"},
        ],
        skus_by_value={"1161": ["FMM-WBC2430"]},
    )
    monkeypatch.setattr(
        "app.jobs.audit_shopping_taxonomy_options._curated_replacements",
        lambda *args, **kwargs: {"FMM-WBC2430": "wall-cabinets/blind-corner"},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        delete=True,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert entry["reassigned_products"] == [
        {"sku": "FMM-WBC2430", "value": "wall-cabinets/blind-corner", "option_id": "50"}
    ]
    assert api.updates == [
        (
            "FMM-WBC2430",
            {"sku": "FMM-WBC2430", "custom_attributes": [{"attribute_code": "shopping_l2", "value": "50"}]},
        )
    ]
    assert api.deleted == [("shopping_l2", "1161")]


def test_audit_dry_run_plans_writes_without_touching_magento(catalog_intent_session, monkeypatch):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/wall-cabinets/blind-corner",
        shopping_l1="wall-cabinets",
        shopping_l2="wall-cabinets/blind-corner",
    )
    api = _FakeMagentoOptionApi(
        options=[
            {"label": "wall-cabinets/blind-corner", "value": "50"},
            {"label": "wall-cabinets/blind-corner-wall-cabinet", "value": "1161"},
        ],
        skus_by_value={"1161": ["FMM-WBC2430", "FMM-WBC2436"]},
    )
    monkeypatch.setattr(
        "app.jobs.audit_shopping_taxonomy_options._curated_replacements",
        lambda *args, **kwargs: {"FMM-WBC2430": "wall-cabinets/blind-corner"},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        dry_run=True,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert entry["planned_reassignments"] == [
        {"sku": "FMM-WBC2430", "value": "wall-cabinets/blind-corner", "option_id": "50"}
    ]
    assert entry["planned_clears"] == ["FMM-WBC2436"]
    assert entry["deleted_options"] == []
    assert api.updates == []
    assert api.deleted == []


def test_audit_keeps_option_when_target_option_missing_in_magento(catalog_intent_session, monkeypatch):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/moldings/toe-kick",
        shopping_l1="moldings",
        shopping_l2="moldings/toe-kick",
    )
    api = _FakeMagentoOptionApi(
        options=[{"label": "accessories/toe-kick", "value": "1140"}],
        skus_by_value={"1140": ["ACH-TK8"]},
    )
    monkeypatch.setattr(
        "app.jobs.audit_shopping_taxonomy_options._curated_replacements",
        lambda *args, **kwargs: {"ACH-TK8": "moldings/toe-kick"},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        delete=True,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert entry["skipped_options"] == [
        {"label": "accessories/toe-kick", "reason": "target_option_missing", "missing_targets": ["moldings/toe-kick"]}
    ]
    assert entry["deleted_options"] == []
    assert api.updates == []
    assert api.deleted == []


def test_audit_keeps_option_when_product_update_fails(catalog_intent_session):
    session = catalog_intent_session
    _add_manual_assignment(
        session,
        canonical_path="kitchen-cabinets/base-cabinets/single-door",
        shopping_l1="base-cabinets",
        shopping_l2="base-cabinets/single-door",
    )

    class _FailingApi(_FakeMagentoOptionApi):
        def update_product(self, sku, payload):
            raise RuntimeError("Attribute has invalid value")

    api = _FailingApi(
        options=[{"label": "kitchen-cabinets/mouldings", "value": "11"}],
        skus_by_value={"11": ["ACH-CM8"]},
    )

    result = audit_connection(
        session,
        connection_id=1,
        api=api,
        delete=True,
        attribute_codes=["shopping_l2"],
    )

    entry = result["attributes"][0]
    assert entry["skipped_options"] == [
        {"label": "kitchen-cabinets/mouldings", "reason": "product_update_failed", "failed_skus": ["ACH-CM8"]}
    ]
    assert entry["deleted_options"] == []
    assert api.deleted == []
    assert result["errors"]


def test_audit_refuses_to_classify_when_nothing_is_curated(catalog_intent_session):
    api = _FakeMagentoOptionApi(options=[{"label": "anything", "value": "1"}], skus_by_value={})

    result = audit_connection(catalog_intent_session, connection_id=1, api=api, delete=True)

    assert result["vocabulary_empty"] is True
    assert result["attributes"] == []
    assert api.deleted == []


def test_cleanup_metadata_omits_shopping_l2_for_hub_level_node(catalog_intent_session):
    session = catalog_intent_session
    hub = MasterTaxonomyNode(
        path_slug="kitchen-cabinets", code="kitchen-cabinets", name="Kitchen Cabinets", is_active=True
    )
    session.add(hub)
    session.flush()
    l1 = MasterTaxonomyNode(
        path_slug="kitchen-cabinets/mouldings",
        code="mouldings",
        name="Mouldings",
        parent_id=hub.id,
        is_active=True,
    )
    session.add(l1)
    session.flush()
    l2 = MasterTaxonomyNode(
        path_slug="kitchen-cabinets/mouldings/crown-moulding",
        code="crown-moulding",
        name="Crown Moulding",
        parent_id=l1.id,
        is_active=True,
    )
    session.add(l2)
    session.flush()

    assert _assignment_metadata(session, l1)["shopping_l2"] is None
    assert _assignment_metadata(session, l1)["shopping_l1"] == "mouldings"
    assert _assignment_metadata(session, l2)["shopping_l2"] == "mouldings/crown-moulding"
