"""
Acceptance tests for Plytix → Magento incremental sync.
Run: pytest tests/test_incremental_sync.py -v
"""

from __future__ import annotations

from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from magento.sync_hashes import (
    _parse_children_from_row,
    compute_data_hash,
    compute_images_hash,
    compute_relations_hash,
)
from magento.sync_planner import (
    plan_incremental_sync,
    _is_configurable_parent,
    _is_child_row,
    ACTION_UPSERT_MEDIA,
    ACTION_UPSERT_PRODUCT,
    ACTION_SET_RELATIONS,
)
from magento.payload_mapper import snapshot_to_magento_product
from magento.sync_overrides import OverrideSet


def test_planner_new_sku_with_images_schedules_actions():
    """New SKU with 2 images → planner schedules UPSERT_PRODUCT + UPSERT_MEDIA."""
    row = {
        "sku": "NEW-SKU-001",
        "name": "Test Product",
        "product_type": "simple",
        "price": "10.00",
        "base_image": "https://example.com/img1.jpg",
        "additional_images": "https://example.com/img2.jpg",
    }
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku)
    actions = [a.action for a in plan.actions]
    assert ACTION_UPSERT_PRODUCT in actions
    assert ACTION_UPSERT_MEDIA in actions
    assert len(plan.actions) >= 2


def test_planner_new_configurable_schedules_three_actions():
    """New configurable with 2 images → planner schedules UPSERT_PRODUCT, UPSERT_MEDIA, SET_RELATIONS."""
    row = {
        "sku": "CONFIG-NEW",
        "name": "Config Product",
        "product_type": "configurable",
        "base_image": "https://example.com/img1.jpg",
        "additional_images": "https://example.com/img2.jpg",
        "variant_list": "C1,C2",
        "configurable_attributes": "color",
        "configurable_variations": "sku=C1,color=Red|sku=C2,color=Blue",
    }
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku)
    actions = [a.action for a in plan.actions]
    assert ACTION_UPSERT_PRODUCT in actions
    assert ACTION_UPSERT_MEDIA in actions
    assert ACTION_SET_RELATIONS in actions
    assert len(plan.actions) == 3


def test_planner_configurable_new_sku_schedules_relations():
    """Configurable with children schedules SET_RELATIONS."""
    row = {
        "sku": "CONFIG-PARENT",
        "name": "Configurable Product",
        "product_type": "configurable",
        "variant_list": "CHILD-1,CHILD-2",
        "configurable_attributes": "color,size",
        "configurable_variations": "sku=CHILD-1,color=Red,size=M|sku=CHILD-2,color=Blue,size=L",
    }
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku)
    actions = [a.action for a in plan.actions]
    assert ACTION_UPSERT_PRODUCT in actions
    assert ACTION_SET_RELATIONS in actions


def test_planner_no_changes_schedules_none():
    """Same data → no actions."""
    row = {
        "sku": "EXISTING-SKU",
        "name": "Existing Product",
        "product_type": "simple",
        "price": "10.00",
    }
    payload = snapshot_to_magento_product(row, is_new=False)
    data_hash = compute_data_hash(row, payload)
    images_hash = compute_images_hash(row)
    relations_hash = compute_relations_hash(row)
    state_by_sku = {
        "EXISTING-SKU": {
            "data_hash": data_hash,
            "images_hash": images_hash,
            "relations_hash": relations_hash,
        },
    }
    plan = plan_incremental_sync(1, [row], state_by_sku)
    assert len(plan.actions) == 0


def test_planner_magento_has_images_never_pushed_skips_media():
    """When Magento has images (bootstrap) but we never pushed Plytix: do not overwrite."""
    row = {
        "sku": "BOOTSTRAP-SKU",
        "name": "Product",
        "product_type": "simple",
        "base_image": "https://plytix.com/img.jpg",  # Plytix has images too
    }
    state_by_sku = {}  # No sync state - we never pushed
    catalog_media = {"BOOTSTRAP-SKU": "abc123"}  # Magento has images from bootstrap
    plan = plan_incremental_sync(
        1, [row], state_by_sku, catalog_state_media_by_sku=catalog_media
    )
    assert ACTION_UPSERT_MEDIA not in [a.action for a in plan.actions]


def test_planner_magento_has_images_we_pushed_schedules_when_plytix_differs():
    """When Magento has images and we previously pushed: schedule UPSERT_MEDIA only if Plytix differs."""
    row = {
        "sku": "SKU",
        "name": "Product",
        "product_type": "simple",
        "base_image": "https://example.com/new.jpg",
    }
    images_hash = compute_images_hash(row)
    state_by_sku = {"SKU": {"images_hash": "old_hash", "data_hash": "x", "relations_hash": None}}
    catalog_media = {"SKU": "magento_media_hash"}  # Magento has images, we also pushed before
    plan = plan_incremental_sync(
        1, [row], state_by_sku, catalog_state_media_by_sku=catalog_media
    )
    assert ACTION_UPSERT_MEDIA in [a.action for a in plan.actions]


def test_planner_only_image_changed_schedules_media():
    """Change only image URL → only UPSERT_MEDIA (product exists in Magento)."""
    row1 = {
        "sku": "SKU-IMG",
        "name": "Product",
        "product_type": "simple",
        "base_image": "https://example.com/old.jpg",
    }
    payload = snapshot_to_magento_product(row1, is_new=False)
    data_hash = compute_data_hash(row1, payload)
    images_hash1 = compute_images_hash(row1)
    relations_hash = compute_relations_hash(row1)
    state = {
        "data_hash": data_hash,
        "images_hash": images_hash1,
        "relations_hash": relations_hash,
    }
    row2 = {**row1, "base_image": "https://example.com/new.jpg"}
    state_by_sku = {"SKU-IMG": state}
    # Product exists in Magento → MEDIA_REQUIRE_PRODUCT_EXISTENCE does not add UPSERT_PRODUCT
    catalog_state_by_sku = {"SKU-IMG": {"magento_product_id": 123}}
    plan = plan_incremental_sync(
        1, [row2], state_by_sku,
        catalog_state_by_sku=catalog_state_by_sku,
    )
    actions = [a.action for a in plan.actions]
    assert actions == [ACTION_UPSERT_MEDIA]


def test_planner_only_relation_changed_schedules_relations():
    """Change only parent/child relation → only SET_RELATIONS."""
    row1 = {
        "sku": "PARENT",
        "name": "Parent",
        "product_type": "configurable",
        "variant_list": "C1",
        "configurable_attributes": "color",
        "configurable_variations": "sku=C1,color=Red",
    }
    payload = snapshot_to_magento_product(row1, is_new=False)
    data_hash = compute_data_hash(row1, payload)
    images_hash = compute_images_hash(row1)
    relations_hash1 = compute_relations_hash(row1)
    state = {
        "data_hash": data_hash,
        "images_hash": images_hash,
        "relations_hash": relations_hash1,
    }
    row2 = {
        **row1,
        "variant_list": "C1,C2",
        "configurable_variations": "sku=C1,color=Red|sku=C2,color=Blue",
    }
    state_by_sku = {"PARENT": state}
    plan = plan_incremental_sync(1, [row2], state_by_sku)
    actions = [a.action for a in plan.actions]
    assert actions == [ACTION_SET_RELATIONS]


def test_url_key_never_included():
    """url_key never sent; Magento auto-generates on create, keeps on update."""
    row = {"sku": "TEST", "name": "Test Product", "product_type": "simple"}
    payload_new = snapshot_to_magento_product(row, is_new=True)
    payload_existing = snapshot_to_magento_product(row, is_new=False)
    assert "url_key" not in payload_new
    assert "url_key" not in payload_existing


def test_url_key_retry_variants():
    """Title variants for URL key retry: Magento auto-generates from name, so we modify name."""
    from magento.sync_service import (
        _is_option_label_exists_error,
        _is_url_key_exists_error,
        _option_map_from_rows,
        _parse_magento_option_rows,
        _title_variants_for_url_key_retry,
    )

    assert _is_url_key_exists_error(400, {"message": "URL key for specified store already exists"})
    assert _is_url_key_exists_error(400, "URL key already exists")
    assert not _is_url_key_exists_error(400, "Other error")
    assert not _is_url_key_exists_error(200, {"message": "URL key exists"})

    assert _is_option_label_exists_error(
        400,
        {"message": 'Admin store attribute option label "%1" already exists.', "parameters": ["90"]},
    )
    assert not _is_option_label_exists_error(400, "Other error")
    assert not _is_option_label_exists_error(200, {"message": "already exists"})
    assert _is_option_label_exists_error(
        400,
        {"error": '{"message":"Admin store attribute option label \\"%1\\" already exists.","parameters":["12"]}'},
    )

    rows = _parse_magento_option_rows(
        [{"label": " 90 ", "value": "583"}, {"label": "", "value": "1"}, {"label": "48", "value_index": 584}]
    )
    assert rows == [{"label": "90", "value_index": 583}, {"label": "48", "value_index": 584}]
    assert _option_map_from_rows(rows) == {"90": 583, "48": 584}

    variants = _title_variants_for_url_key_retry("SKU-123", "Test Product")
    assert len(variants) >= 3
    assert "Test Product SKU-123" in variants
    assert "Test Product - SKU-123" in variants


def test_hash_determinism():
    """Same input → same hash."""
    row = {"sku": "X", "name": "Y", "product_type": "simple", "base_image": "https://a.com/1.jpg"}
    payload = snapshot_to_magento_product(row, is_new=False)
    h1 = compute_data_hash(row, payload)
    h2 = compute_data_hash(row, payload)
    assert h1 == h2
    hi1 = compute_images_hash(row)
    hi2 = compute_images_hash(row)
    assert hi1 == hi2


def test_child_rows_do_not_schedule_set_relations():
    """Child rows (variant_of) must NOT schedule SET_RELATIONS."""
    row = {
        "sku": "CHILD-1",
        "name": "Child Product",
        "product_type": "simple",
        "variant_of": "PARENT-SKU",
    }
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku)
    set_relations_actions = [a for a in plan.actions if a.action == ACTION_SET_RELATIONS]
    assert len(set_relations_actions) == 0
    assert _is_child_row(row)
    assert not _is_configurable_parent(row)


def test_parent_rows_schedule_set_relations():
    """Configurable parent rows schedule SET_RELATIONS."""
    row = {
        "sku": "PARENT",
        "name": "Parent",
        "product_type": "configurable",
        "variant_list": "C1",
        "configurable_variations": "sku=C1,color=Red",
    }
    assert _is_configurable_parent(row)
    assert not _is_child_row(row)
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku)
    set_relations_actions = [a for a in plan.actions if a.action == ACTION_SET_RELATIONS]
    assert len(set_relations_actions) == 1


def test_parse_children_only_sku_tokens():
    """configurable_variations: only sku= indicates child; color=Red etc are not SKUs."""
    row = {
        "sku": "PARENT",
        "configurable_variations": "sku=C1,color=Red|sku=C2,color=Blue",
        "variant_list": "",
    }
    children, mappings, child_to_mapping = _parse_children_from_row(row)
    assert children == ["C1", "C2"]
    assert "color" in mappings[0] or "color" in mappings[1]
    assert "sku" not in mappings[0] and "sku" not in mappings[1]

    row2 = {"configurable_variations": "color=Red,size=M"}
    children2, _, _ = _parse_children_from_row(row2)
    assert children2 == []


def test_parse_children_child_to_mapping_order_independent():
    """child_to_mapping correctly maps each SKU when variant_list order differs from configurable_variations."""
    row = {
        "sku": "PARENT",
        "variant_list": "C2,C1",  # C2 first
        "configurable_variations": "sku=C1,height=42,width=24|sku=C2,height=36,width=24",  # C1 first
    }
    children, mappings, child_to_mapping = _parse_children_from_row(row)
    assert set(children) == {"C1", "C2"}
    assert child_to_mapping["C1"] == {"height": "42", "width": "24"}
    assert child_to_mapping["C2"] == {"height": "36", "width": "24"}


def test_planner_actions_change_only_when_hashes_change():
    """Planner schedules actions only when relevant hashes differ."""
    row = {"sku": "S1", "name": "X", "product_type": "simple", "base_image": "https://a.com/1.jpg"}
    payload = snapshot_to_magento_product(row, is_new=False)
    state = {
        "data_hash": compute_data_hash(row, payload),
        "images_hash": compute_images_hash(row),
        "relations_hash": compute_relations_hash(row),
    }
    catalog_state = {"S1": {"magento_product_id": 1}}  # Product exists
    plan = plan_incremental_sync(1, [row], {"S1": state}, catalog_state_by_sku=catalog_state)
    assert len(plan.actions) == 0

    row_name_change = {**row, "name": "Y"}
    plan2 = plan_incremental_sync(1, [row_name_change], {"S1": state}, catalog_state_by_sku=catalog_state)
    assert any(a.action == ACTION_UPSERT_PRODUCT for a in plan2.actions)

    row_img_change = {**row, "base_image": "https://a.com/2.jpg"}
    plan3 = plan_incremental_sync(1, [row_img_change], {"S1": state}, catalog_state_by_sku=catalog_state)
    assert [a.action for a in plan3.actions] == [ACTION_UPSERT_MEDIA]


def test_build_configurable_option_payload_structure():
    """Configurable option payload matches Magento expected structure."""
    from magento.sync_service import MagentoSyncService
    from magento.magento_api import MagentoRestClient
    from magento.oauth_client import MagentoOAuthClient

    mock_api = MagicMock()
    mock_api.get_attribute_detail.return_value = (200, {"attribute_id": 259, "frontend_label": "Width (inches)"})
    mock_api.get_attribute_options.return_value = (
        200,
        [{"label": "36", "value": "583"}, {"label": "38", "value": "589"}],
    )
    mock_api.get_attribute_id_by_code.return_value = 259

    svc = MagentoSyncService(mock_api, {"id": 1})
    payload, err = svc._build_configurable_option_payload(
        "PARENT", "width", [{"width": "36"}, {"width": "38"}], 0
    )
    assert err is None
    assert payload is not None
    assert payload["attribute_id"] == "259"
    assert payload["label"] == "Width (inches)"
    assert payload["position"] == 0
    assert payload["is_use_default"] is True
    assert {"value_index": 583} in payload["values"]
    assert {"value_index": 589} in payload["values"]


def test_attribute_option_label_mapping_missing_fails():
    """Feed value not in Magento options returns clear error."""
    from magento.sync_service import MagentoSyncService
    from unittest.mock import MagicMock

    mock_api = MagicMock()
    mock_api.get_attribute_detail.return_value = (200, {"attribute_id": 259, "frontend_label": "Width"})
    mock_api.get_attribute_options.return_value = (
        200,
        [{"label": "36", "value": "583"}, {"label": "38", "value": "589"}],
    )

    svc = MagentoSyncService(mock_api, {"id": 1})
    payload, err = svc._build_configurable_option_payload(
        "PARENT", "width", [{"width": "XXL"}], 0
    )
    assert err is not None
    assert "XXL" in err
    assert "width" in err
    assert "PARENT" in err


def test_resolve_media_entry_id_by_position_and_file():
    """_resolve_media_entry_id matches by position and file suffix."""
    from magento.sync_service import MagentoSyncService
    from unittest.mock import MagicMock

    mock_api = MagicMock()
    mock_api.get_product_media.return_value = (
        200,
        [
            {"id": 287, "position": 0, "file": "/6/1/abc123.jpg", "label": "Main"},
            {"id": 288, "position": 1, "file": "/7/2/def456.jpg"},
        ],
        None,
    )

    svc = MagentoSyncService(mock_api, {"id": 1})
    entry = {"label": "Main", "content": {"name": "image_0.jpg"}}
    eid = svc._resolve_media_entry_id("SKU1", 0, entry)
    assert eid == 287

    entry2 = {"content": {"name": "image_1.jpg"}}
    eid2 = svc._resolve_media_entry_id("SKU1", 1, entry2)
    assert eid2 == 288


def test_verify_configurable_links_via_children():
    """_verify_configurable_links returns True when children endpoint matches."""
    from magento.sync_service import MagentoSyncService
    from unittest.mock import MagicMock

    mock_api = MagicMock()
    mock_api.get_configurable_children.return_value = (200, ["C1", "C2"], None)

    svc = MagentoSyncService(mock_api, {"id": 1})
    assert svc._verify_configurable_links("PARENT", ["C1", "C2"]) is True
    assert svc._verify_configurable_links("PARENT", ["C1", "C2", "C3"]) is False


def test_payload_never_includes_stock():
    """snapshot_to_magento_product with include_stock=False never sets stock_item."""
    row = {"sku": "SKU1", "name": "Product", "product_type": "simple", "price": 10}
    payload = snapshot_to_magento_product(row, is_new=False, include_stock=False)
    assert "extension_attributes" not in payload or "stock_item" not in payload.get(
        "extension_attributes", {}
    )


def test_apply_overrides_force():
    """Force overrides set name and meta_title in payload."""
    from magento.sync_overrides import apply_overrides_to_payload

    payload = {
        "sku": "SKU1",
        "name": "Original Name",
        "custom_attributes": [
            {"attribute_code": "meta_title", "value": "Original Meta"},
        ],
    }
    out = apply_overrides_to_payload(
        payload, overrides_force={"name": "Overridden Name", "meta_title": "SEO Title"}, locked_fields=set()
    )
    assert out["name"] == "Overridden Name"
    assert any(a.get("attribute_code") == "meta_title" and a.get("value") == "SEO Title" for a in out.get("custom_attributes", []))


def test_apply_overrides_lock_excludes_field():
    """Locked fields are excluded from payload."""
    from magento.sync_overrides import apply_overrides_to_payload

    payload = {"sku": "SKU1", "name": "Name", "url_key": "some-key"}
    out = apply_overrides_to_payload(
        payload, overrides_force={}, locked_fields={"url_key", "name"}
    )
    assert "url_key" not in out
    assert "name" not in out
    assert out["sku"] == "SKU1"


def test_planner_with_overrides_uses_override_in_hash():
    """When override exists, payload uses it and planner does not repeatedly schedule."""
    from magento.sync_overrides import OverrideSet

    row = {"sku": "OVR-SKU", "name": "Feed Name", "product_type": "simple", "price": 5}
    overrides = OverrideSet(force={"OVR-SKU": {"name": "Override Name"}}, lock={})
    state_by_sku = {}
    plan = plan_incremental_sync(1, [row], state_by_sku, overrides=overrides)
    upsert = next(a for a in plan.actions if a.action == ACTION_UPSERT_PRODUCT)
    assert upsert.payload["name"] == "Override Name"


def test_data_hash_includes_custom_attributes():
    """Changing width (axis attr) must change data_hash and trigger UPSERT_PRODUCT."""
    row1 = {
        "sku": "CHILD-1",
        "name": "Child",
        "product_type": "simple",
        "variant_of": "PARENT",
    }
    parent_row = {
        "sku": "PARENT",
        "product_type": "configurable",
        "variant_list": "CHILD-1,CHILD-2",
        "configurable_variations": "sku=CHILD-1,width=30|sku=CHILD-2,width=36",
    }
    rows_by_sku = {"CHILD-1": row1, "PARENT": parent_row}
    payload1 = snapshot_to_magento_product(row1, is_new=False)
    h1 = compute_data_hash(row1, payload1, rows_by_sku=rows_by_sku)

    # Simulate width change 30 -> 36 in parent mapping
    parent_row2 = {**parent_row, "configurable_variations": "sku=CHILD-1,width=36|sku=CHILD-2,width=36"}
    rows_by_sku2 = {"CHILD-1": row1, "PARENT": parent_row2}
    h2 = compute_data_hash(row1, payload1, rows_by_sku=rows_by_sku2)
    assert h1 != h2, "data_hash must change when axis value changes"


def test_get_axis_attribute_codes():
    """get_axis_attribute_codes from configurable_attributes or configurable_variations."""
    from magento.payload_mapper import get_axis_attribute_codes

    row1 = {"configurable_attributes": "width=Width (inches),color"}
    assert "width" in get_axis_attribute_codes(row1)
    assert "color" in get_axis_attribute_codes(row1)

    row2 = {"configurable_variations": "sku=C1,width=30,color=Red|sku=C2,width=36,color=Blue"}
    codes = get_axis_attribute_codes(row2)
    assert "width" in codes
    assert "color" in codes


def test_parent_excludes_axis_from_custom_attrs():
    """Configurable parent payload must not include axis attrs in custom_attributes."""
    from magento.payload_mapper import snapshot_to_magento_product, get_axis_attribute_codes

    row = {
        "sku": "PARENT",
        "name": "Parent Product",
        "product_type": "configurable",
        "variant_list": "C1,C2",
        "configurable_attributes": "width,color",
        "configurable_variations": "sku=C1,width=30,color=Red|sku=C2,width=36,color=Blue",
        "width": "30",
        "color": "Red",
    }
    payload = snapshot_to_magento_product(row, is_new=False, axis_exclude=get_axis_attribute_codes(row))
    custom = payload.get("custom_attributes") or []
    codes = {a.get("attribute_code") for a in custom if a.get("attribute_code")}
    assert "width" not in codes
    assert "color" not in codes


def test_build_media_and_relations_payload():
    """Canonical payloads for versioning."""
    from magento.sync_hashes import build_media_payload_from_row, build_relations_payload_from_row

    row = {
        "sku": "SKU1",
        "name": "Product",
        "base_image": "https://a.com/1.jpg",
        "additional_images": "https://a.com/1.jpg,https://a.com/2.jpg,https://a.com/3.jpg,https://a.com/2.jpg",
    }
    media = build_media_payload_from_row(row)
    assert len(media) == 3
    assert media[0]["url"] == "https://a.com/1.jpg"
    assert media[0]["roles"] == ["image", "small_image", "thumbnail"]
    assert media[1]["position"] == 1

    row2 = {
        "sku": "PARENT",
        "product_type": "configurable",
        "variant_list": "C1,C2",
        "configurable_attributes": "color,size",
        "configurable_variations": "sku=C1,color=Red,size=M|sku=C2,color=Blue,size=L",
    }
    rel = build_relations_payload_from_row(row2)
    assert rel["children"] == ["C1", "C2"]
    assert "color" in rel["attr_codes"]
    assert "C1" in rel["child_option_map"]
    assert rel["child_option_map"]["C1"]["color"] == "Red"


def test_partial_attribute_sync_bypasses_connection_scoped_pending_table(monkeypatch):
    from magento.sync_service import MagentoSyncService

    class FakeStateRepo:
        def get_states_for_skus(self, connection_id, skus):
            return {}

        def upsert_state(self, connection_id, sku, **kwargs):
            return None

    class FakeOverrideRepo:
        def get_overrides_for_skus(self, connection_id, skus):
            return OverrideSet(force={}, lock={})

    class FakeJobRepo:
        def __init__(self):
            self._items = []

        def get_pending_items(self, job_id):
            return list(self._items)

        def add_sync_items(self, items):
            self._items = [
                {
                    "id": idx,
                    "sku": item.sku,
                    "action": item.action,
                    "payload": item.payload,
                    "idempotency_key": item.idempotency_key,
                }
                for idx, item in enumerate(items, start=1)
            ]

        def update_sync_job(self, job_id, **kwargs):
            return None

        def update_sync_item(self, item_id, **kwargs):
            return None

        def get_job_items(self, job_id):
            return list(self._items)

        def get_sync_job(self, job_id):
            return {"id": job_id}

    class FakePendingRepo:
        def upsert_pending(self, *args, **kwargs):
            raise AssertionError("partial attribute sync should not use connection-scoped pending rows")

        def prune_pending_not_in_plan(self, *args, **kwargs):
            raise AssertionError("partial attribute sync should not prune connection-scoped pending rows")

        def get_pending_items(self, *args, **kwargs):
            raise AssertionError("partial attribute sync should not read connection-scoped pending rows")

        def update_pending_item(self, *args, **kwargs):
            raise AssertionError("partial attribute sync should not update connection-scoped pending rows")

    monkeypatch.setattr(
        "magento.sync_service.plan_incremental_sync",
        lambda *args, **kwargs: SimpleNamespace(
            actions=[
                SimpleNamespace(
                    sku="SKU1",
                    action=ACTION_UPSERT_PRODUCT,
                    payload={"sku": "SKU1", "custom_attributes": [{"attribute_code": "cabinet_type", "value": "wall cabinets"}]},
                    pass_number=1,
                    idempotency_key="k1",
                )
            ]
        ),
    )

    svc = MagentoSyncService(
        MagicMock(),
        {"id": 1},
        sync_pending_repo=FakePendingRepo(),
    )
    monkeypatch.setattr(
        svc,
        "execute_upsert_product",
        lambda sku, payload, row, **kwargs: {"status": "success", "duration_ms": 0, "http_status": 200, "payload_sent": payload},
    )

    result = svc.sync_incremental(
        1,
        [{"sku": "SKU1", "product_type": "simple"}],
        job_id=1,
        state_repo=FakeStateRepo(),
        job_repo=FakeJobRepo(),
        override_repo=FakeOverrideRepo(),
        selected_attribute_codes={"cabinet_type"},
    )

    assert result["status"] == "success"
    assert result["success_count"] == 1


def test_sync_incremental_uses_bulk_product_writer_when_enabled(monkeypatch):
    """Opt-in bulk UPSERT_PRODUCT batches instead of per-SKU execute_upsert_product."""
    from magento.sync_service import MagentoSyncService
    from settings import MagentoBulkProductConfig

    state_updates = []
    bulk_calls = []

    class FakeStateRepo:
        def get_states_for_skus(self, connection_id, skus):
            return {}

        def upsert_state(self, connection_id, sku, **kwargs):
            state_updates.append((sku, kwargs))

    class FakeOverrideRepo:
        def get_overrides_for_skus(self, connection_id, skus):
            return OverrideSet(force={}, lock={})

    class FakeJobRepo:
        def __init__(self):
            self._items = []
            self.updated = []

        def get_pending_items(self, job_id):
            return list(self._items)

        def add_sync_items(self, items):
            self._items = [
                {
                    "id": idx,
                    "sku": item.sku,
                    "action": item.action,
                    "payload": item.payload,
                    "idempotency_key": item.idempotency_key,
                }
                for idx, item in enumerate(items, start=1)
            ]

        def update_sync_job(self, job_id, **kwargs):
            return None

        def update_sync_item(self, item_id, **kwargs):
            self.updated.append((item_id, kwargs))

        def get_job_items(self, job_id):
            return list(self._items)

        def get_sync_job(self, job_id):
            return {"id": job_id}

    monkeypatch.setattr(
        "settings.load_magento_bulk_product_config",
        lambda: MagentoBulkProductConfig(
            enabled=True, batch_size=50, poll_interval_ms=100, poll_timeout_s=30
        ),
    )
    monkeypatch.setattr(
        "magento.sync_service.plan_incremental_sync",
        lambda *args, **kwargs: SimpleNamespace(
            actions=[
                SimpleNamespace(
                    sku="SKU1",
                    action=ACTION_UPSERT_PRODUCT,
                    payload={"sku": "SKU1", "name": "One", "status": 1},
                    pass_number=2,
                    idempotency_key="k1",
                ),
                SimpleNamespace(
                    sku="SKU2",
                    action=ACTION_UPSERT_PRODUCT,
                    payload={"sku": "SKU2", "name": "Two", "status": 1},
                    pass_number=2,
                    idempotency_key="k2",
                ),
            ]
        ),
    )

    svc = MagentoSyncService(MagicMock(), {"id": 1})

    def _fake_bulk(items, **kwargs):
        bulk_calls.append(list(items))
        return [
            {
                "status": "success",
                "duration_ms": 10,
                "http_status": 200,
                "payload_sent": it["payload"],
                "bulk_uuid": "bulk-1",
                "sku": it["sku"],
            }
            for it in items
        ]

    monkeypatch.setattr(svc, "execute_upsert_products_bulk", _fake_bulk)
    monkeypatch.setattr(
        svc,
        "execute_upsert_product",
        lambda *a, **k: (_ for _ in ()).throw(AssertionError("single upsert should not run")),
    )
    # Avoid category/attr side effects
    monkeypatch.setattr(svc, "_inject_category_ids", lambda payload, *a, **k: payload)

    job_repo = FakeJobRepo()
    result = svc.sync_incremental(
        1,
        [
            {"sku": "SKU1", "product_type": "simple", "name": "One"},
            {"sku": "SKU2", "product_type": "simple", "name": "Two"},
        ],
        job_id=1,
        state_repo=FakeStateRepo(),
        job_repo=job_repo,
        override_repo=FakeOverrideRepo(),
        use_connection_pending_table=False,
    )

    assert result["status"] == "success"
    assert result["success_count"] == 2
    assert result["error_count"] == 0
    assert len(bulk_calls) == 1
    assert [i["sku"] for i in bulk_calls[0]] == ["SKU1", "SKU2"]
    assert {sku for sku, _ in state_updates} == {"SKU1", "SKU2"}
    assert all(u.get("status") == "succeeded" for _, u in job_repo.updated)
