from __future__ import annotations

import tempfile
from pathlib import Path

import pytest
import pytest_asyncio

from sync_state_machine.common.binding import BindingManager
from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.persistence import PersistenceBackend
from sync_state_machine.common.sqlite_backend import SQLitePersistenceBackend
from sync_state_machine.common.types import BindingStatus, SyncAction, SyncStatus
from sync_state_machine.sync_system.resolve_ids import IDResolver
from sync_state_machine.sync_system.config import OrphanAction, UpdateDirection
from sync_state_machine.sync_system.strategy_ops.bind_ops import (
    phase_auto_binding,
    phase_core_state,
    phase_dependency_check,
)
from tests.fixtures.mock_domain import (
    TestContractNode,
    TestProjectNode,
    TestContractStrategy,
    TestProjectStrategy,
    build_contract_node,
    build_project_node,
    register_test_domain,
)


@pytest_asyncio.fixture
async def setup_env():
    register_test_domain()
    db = Path(tempfile.mkdtemp(prefix="sync_state_machine_biz_") ) / "state.db"
    persistence = SQLitePersistenceBackend(str(db))
    await persistence.initialize()

    local = DataCollection("local", persistence=persistence, auto_persist=False)
    remote = DataCollection("remote", persistence=persistence, auto_persist=False)
    binding_manager = BindingManager(persistence, auto_persist=False)

    yield local, remote, binding_manager, persistence
    await persistence.close()


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "name,has_binding,local_data,remote_data,expected_local,expected_remote",
    [
        ("bind_yes_yes_yes", True, True, True, BindingStatus.NORMAL, BindingStatus.NORMAL),
        ("bind_yes_yes_no", True, True, False, BindingStatus.WARNING, BindingStatus.ABNORMAL),
        ("bind_yes_no_yes", True, False, True, BindingStatus.ABNORMAL, BindingStatus.WARNING),
        ("bind_yes_no_no", True, False, False, BindingStatus.ABNORMAL, BindingStatus.ABNORMAL),
        ("bind_no_local_only", False, True, None, BindingStatus.MISSING, None),
        ("bind_no_remote_only", False, None, True, None, BindingStatus.MISSING),
    ],
)
async def test_core_binding_matrix_migrated(
    setup_env,
    name: str,
    has_binding: bool,
    local_data: bool | None,
    remote_data: bool | None,
    expected_local: BindingStatus | None,
    expected_remote: BindingStatus | None,
):
    local, remote, bm, _ = setup_env
    strategy = TestProjectStrategy("test_project", local, remote, bm)

    if local_data is not None:
        await local.add(
            build_project_node(
                f"{name}_l",
                f"L-{name}",
                name=f"Local {name}",
                code=f"LC-{name}",
                data_present=local_data,
            )
        )
    if remote_data is not None:
        await remote.add(
            build_project_node(
                f"{name}_r",
                f"R-{name}",
                name=f"Remote {name}",
                code=f"RC-{name}",
                data_present=remote_data,
            )
        )

    if has_binding:
        await bm.bind("test_project", f"{name}_l", f"{name}_r")

    await phase_core_state(
        strategy,
        local.filter(node_type="test_project"),
        remote.filter(node_type="test_project"),
    )

    if expected_local is not None:
        assert local.get(f"{name}_l").binding_status == expected_local
    if expected_remote is not None:
        assert remote.get(f"{name}_r").binding_status == expected_remote


@pytest.mark.asyncio
async def test_dependency_resolution_migrated(setup_env):
    local, remote, bm, _ = setup_env

    project_strategy = TestProjectStrategy("test_project", local, remote, bm)
    project_strategy.update_config(depend_fields={})

    await local.add(build_project_node("proj_l", "P-1", name="Project A", code="P-A"))
    await remote.add(build_project_node("proj_r", "P-1-R", name="Project A", code="P-A"))
    await bm.bind("test_project", "proj_l", "proj_r")

    await phase_core_state(
        project_strategy,
        local.filter(node_type="test_project"),
        remote.filter(node_type="test_project"),
    )
    assert local.get("proj_l").binding_status == BindingStatus.NORMAL

    contract_strategy = TestContractStrategy("test_contract", local, remote, bm)
    contract_strategy.update_config(
        depend_fields={"project_ref": "test_project"},
        local_orphan_action=OrphanAction.CREATE_REMOTE,
        remote_orphan_action=OrphanAction.NONE,
        update_direction=UpdateDirection.PUSH,
    )

    await local.add(
        build_contract_node(
            "contract_ok",
            "C-OK",
            name="Contract OK",
            code="C-OK",
            project_ref="P-1",
        )
    )
    await local.add(
        build_contract_node(
            "contract_bad",
            "C-BAD",
            name="Contract Bad",
            code="C-BAD",
            project_ref="P-NOPE",
        )
    )

    local_contracts = local.filter(node_type="test_contract")
    remote_contracts = remote.filter(node_type="test_contract")
    await phase_core_state(contract_strategy, local_contracts, remote_contracts)
    await phase_dependency_check(contract_strategy, local_contracts, remote_contracts)

    assert local.get("contract_ok").binding_status == BindingStatus.MISSING
    assert local.get("contract_ok").depend_ids == ["proj_l"]
    assert local.get("contract_bad").binding_status == BindingStatus.DEPENDENCY_ERROR


@pytest.mark.asyncio
async def test_auto_binding_migrated(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(auto_bind=True, auto_bind_fields=["code"], depend_fields={})

    await local.add(build_project_node("l1", "P1", name="A", code="AUTO-1"))
    await remote.add(build_project_node("r1", "RP1", name="A", code="AUTO-1"))

    await strategy.bind()

    assert await bm.get_remote_id("test_project", "l1") == "r1"
    assert await bm.get_local_id("test_project", "r1") == "l1"


@pytest.mark.asyncio
async def test_auto_binding_skip_on_ambiguous_migrated(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(auto_bind=True, auto_bind_fields=["code"], depend_fields={})

    await local.add(build_project_node("l1", "P1", name="A", code="AMB-1"))
    await local.add(build_project_node("l2", "P2", name="A", code="AMB-1"))
    await remote.add(build_project_node("r1", "RP1", name="A", code="AMB-1"))

    await strategy.bind()

    assert await bm.get_remote_id("test_project", "l1") is None
    assert await bm.get_remote_id("test_project", "l2") is None
    assert await bm.get_local_id("test_project", "r1") is None


@pytest.mark.asyncio
async def test_pre_bind_data_id_works_when_auto_bind_disabled(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(
        auto_bind=False,
        auto_bind_fields=[],
        pre_bind_data_id=[{"local_data_id": "PRE-DID-1", "remote_data_id": "PRE-DID-R-1"}],
        depend_fields={},
    )

    await local.add(build_project_node("pre_l1", "PRE-DID-1", name="Local A", code="LOCAL-ONLY"))
    await remote.add(build_project_node("pre_r1", "PRE-DID-R-1", name="Remote A", code="REMOTE-ONLY"))

    await strategy.bind()

    assert await bm.get_remote_id("test_project", "pre_l1") == "pre_r1"
    assert await bm.get_local_id("test_project", "pre_r1") == "pre_l1"
    assert local.get("pre_l1").binding_status == BindingStatus.NORMAL
    assert remote.get("pre_r1").binding_status == BindingStatus.NORMAL


@pytest.mark.asyncio
async def test_dependency_check_data_missing_goes_abnormal(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestContractStrategy("test_contract", local, remote, bm)
    strategy.update_config(depend_fields={"project_ref": "test_project"})

    await local.add(
        build_contract_node(
            "contract_no_data",
            "C-NODATA",
            name="Contract No Data",
            code="C-NODATA",
            project_ref="P-1",
            data_present=False,
        )
    )

    local_contracts = local.filter(node_type="test_contract")
    await phase_core_state(strategy, local_contracts, [])
    await phase_dependency_check(strategy, local_contracts, [])

    assert local.get("contract_no_data").binding_status == BindingStatus.ABNORMAL


@pytest.mark.asyncio
async def test_auto_binding_key_missing_migrated(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(auto_bind=True, auto_bind_fields=["code", "never_exists"], depend_fields={})

    await local.add(build_project_node("km_l", "KM-L", name="KM", code="KM-1"))

    await strategy.bind()

    node = local.get("km_l")
    assert node.binding_status == BindingStatus.WARNING
    assert "缺少业务键字段" in (node.error or "")


@pytest.mark.asyncio
async def test_auto_binding_key_id_resolve_fail_migrated(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(
        auto_bind=True,
        auto_bind_fields=["id", "code"],
        depend_fields={"id": "test_project"},
    )

    await local.add(build_project_node("kid_l", "KID-L", name="KID", code="KID-1"))

    local_nodes = local.filter(node_type="test_project")
    await phase_core_state(strategy, local_nodes, [])
    await phase_auto_binding(strategy, local_nodes, [])

    node = local.get("kid_l")
    assert node.binding_status == BindingStatus.DEPENDENCY_ERROR
    assert "业务键中的ID字段解析失败" in (node.error or "")


@pytest.mark.asyncio
async def test_create_prepare_e21_on_id_resolve_fail(setup_env, monkeypatch: pytest.MonkeyPatch):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(
        auto_bind=True,
        auto_bind_fields=["code"],
        depend_fields={"dummy_id": "test_project"},
        local_orphan_action=OrphanAction.CREATE_REMOTE,
        remote_orphan_action=OrphanAction.NONE,
        update_direction=UpdateDirection.NONE,
    )

    async def _always_fail_resolve_and_validate(self, data, node_type, depend_fields, to_remote):
        return None, "forced resolve fail"

    monkeypatch.setattr(IDResolver, "resolve_and_validate", _always_fail_resolve_and_validate)

    node = TestProjectNode(
        node_id="create_fail_src",
        data_id="C-CREATE-FAIL",
        data={"id": "C-CREATE-FAIL", "name": "Create Fail", "code": "CF-1"},
        binding_status=BindingStatus.UNCHECKED,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
    )
    await local.add(node)
    await strategy.bind()

    created_nodes = await strategy.create()

    assert created_nodes == []
    failed_node = local.get("create_fail_src")
    assert failed_node is not None
    assert failed_node.binding_status == BindingStatus.PEER_CREATING
    assert "spawn_failed" in (failed_node.error or "")


@pytest.mark.asyncio
async def test_update_prepare_target_missing_data_id_goes_precondition_failed(setup_env):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(update_direction=UpdateDirection.PUSH, depend_fields={})

    local_src = TestProjectNode(
        node_id="upd_src",
        data_id="UPD-SRC",
        data={"id": "UPD-SRC", "name": "Project New", "code": "UPD-1"},
        binding_status=BindingStatus.NORMAL,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
    )
    remote_target = TestProjectNode(
        node_id="upd_tgt",
        data_id="",
        data={"id": "UPD-TGT", "name": "Project Old", "code": "UPD-1"},
        binding_status=BindingStatus.NORMAL,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
    )
    await local.add(local_src)
    await remote.add(remote_target)
    await bm.bind("test_project", "upd_src", "upd_tgt")

    updated_nodes = await strategy.update()

    assert updated_nodes == []
    target = remote.get("upd_tgt")
    assert target.status == SyncStatus.PRECONDITION_FAILED
    assert target.action == SyncAction.UPDATE


@pytest.mark.asyncio
async def test_update_prepare_e30b_on_id_resolve_fail(setup_env, monkeypatch: pytest.MonkeyPatch):
    local, remote, bm, _ = setup_env

    strategy = TestProjectStrategy("test_project", local, remote, bm)
    strategy.update_config(
        depend_fields={"dummy_id": "test_project"},
        update_direction=UpdateDirection.PUSH,
    )

    async def _always_fail_resolve_and_validate(self, data, node_type, depend_fields, to_remote):
        return None, "forced resolve fail"

    monkeypatch.setattr(IDResolver, "resolve_and_validate", _always_fail_resolve_and_validate)

    local_src = TestProjectNode(
        node_id="upd_b_src",
        data_id="UPDB-SRC",
        data={"id": "UPDB-SRC", "name": "Project New", "code": "UPDB-1"},
        binding_status=BindingStatus.NORMAL,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
    )
    remote_target = TestProjectNode(
        node_id="upd_b_tgt",
        data_id="UPDB-TGT",
        data={"id": "UPDB-TGT", "name": "Project Old", "code": "UPDB-1"},
        binding_status=BindingStatus.NORMAL,
        action=SyncAction.NONE,
        status=SyncStatus.PENDING,
    )
    await local.add(local_src)
    await remote.add(remote_target)
    await bm.bind("test_project", "upd_b_src", "upd_b_tgt")

    updated_nodes = await strategy.update()

    assert updated_nodes == []
    target = remote.get("upd_b_tgt")
    assert target.status == SyncStatus.PRECONDITION_FAILED
    assert target.action == SyncAction.UPDATE
