from __future__ import annotations

from pathlib import Path

import pytest

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.datasource.datasource import BaseDataSource
from sync_state_machine.pipeline.full_sync_pipeline import FullSyncPipeline
from sync_state_machine.sync_system.strategy import BaseSyncStrategy


class _DS(BaseDataSource):
    pass


class _FailStrategy(BaseSyncStrategy):
    schema = object  # type: ignore

    async def bind(self, **kwargs):
        raise RuntimeError("bind failed")

    async def create(self):
        return []

    async def update(self):
        return []


class _OkStrategy(BaseSyncStrategy):
    schema = object  # type: ignore

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bound = False
        self.create_calls = 0
        self.update_calls = 0

    async def bind(self, **kwargs):
        self.bound = True

    async def create(self):
        self.create_calls += 1
        return []

    async def update(self):
        self.update_calls += 1
        return []


@pytest.mark.asyncio
async def test_phase_sync_by_node_type_continues_after_strategy_error(tmp_path: Path) -> None:
    db_path = tmp_path / "state.db"
    persistence = SQLitePersistenceBackend(str(db_path))
    await persistence.initialize()

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

    local_ds = _DS()
    remote_ds = _DS()

    fail_strategy = _FailStrategy("fail", local_collection, remote_collection, binding_manager)
    fail_strategy.skip_sync = True

    ok_strategy = _OkStrategy("ok", local_collection, remote_collection, binding_manager)
    ok_strategy.skip_sync = True

    pipeline = FullSyncPipeline(
        local_datasource=local_ds,
        remote_datasource=remote_ds,
        strategies=[fail_strategy, ok_strategy],
        persistence=persistence,
        local_collection=local_collection,
        remote_collection=remote_collection,
        binding_manager=binding_manager,
        node_types=[],
        load_order=[],
        sync_order=["fail", "ok"],
        enable_persistence=False,
    )

    async def _noop_load_node_type(node_type: str, reason: str = "") -> None:
        return None

    pipeline._load_node_type = _noop_load_node_type  # type: ignore[method-assign]

    await pipeline.phase_sync_by_node_type()

    assert ok_strategy.bound is True
    assert ok_strategy.create_calls == 0
    assert ok_strategy.update_calls == 0
    assert pipeline.stats["failed"] >= 1

    await pipeline.close()


@pytest.mark.asyncio
async def test_phase_sync_by_node_type_skip_sync_keeps_bind_but_skips_writes(tmp_path: Path) -> None:
    db_path = tmp_path / "state.db"
    persistence = SQLitePersistenceBackend(str(db_path))
    await persistence.initialize()

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

    strategy = _OkStrategy("ok", local_collection, remote_collection, binding_manager)
    strategy.skip_sync = True

    pipeline = FullSyncPipeline(
        local_datasource=_DS(),
        remote_datasource=_DS(),
        strategies=[strategy],
        persistence=persistence,
        local_collection=local_collection,
        remote_collection=remote_collection,
        binding_manager=binding_manager,
        node_types=["ok"],
        load_order=["ok"],
        sync_order=["ok"],
        enable_persistence=False,
    )

    load_calls: list[tuple[str, str]] = []

    async def _record_load(node_type: str, reason: str = "") -> None:
        load_calls.append((node_type, reason))

    pipeline._load_node_type = _record_load  # type: ignore[method-assign]

    await pipeline.phase_sync_by_node_type()

    assert load_calls == [("ok", "pre-strategy refresh")]
    assert strategy.bound is True
    assert strategy.create_calls == 0
    assert strategy.update_calls == 0

    await pipeline.close()
