"""Tests for sync ↔ state machine integration (lifecycle.py)."""
import asyncio

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.migrations import apply_all
from greymatter_core.sync.tracking import install_sqlite_triggers
from greymatter_core.sync.scheduler import SyncScheduler
from greymatter_core.sync.models import SyncResult
from greymatter_core.cluster.health import HealthChecker
from greymatter_core.cluster.state import NodeState, NodeStateMachine
from greymatter_core.cluster.lifecycle import bind_sync_to_state_machine


def _make_db(node_id: str = "test"):
    d = SQLiteDatabase(":memory:")
    apply_all(d)
    install_sqlite_triggers(d)
    d.execute(
        "UPDATE _sync_state SET value = ? WHERE key = 'node_id'",
        (node_id,),
    )
    return d


class TestSyncLifecycleBinding:
    """Test that state transitions start/stop the scheduler."""

    def test_entering_spoke_starts_scheduler(self):
        db = _make_db("hub")
        sm = NodeStateMachine()
        scheduler = SyncScheduler(db, db)

        bind_sync_to_state_machine(sm, scheduler)
        assert not scheduler.is_running

        sm.transition(NodeState.JOINING)
        assert not scheduler.is_running

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            sm.transition(NodeState.SPOKE)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert scheduler.is_running
        finally:
            loop.run_until_complete(scheduler.stop())
            loop.close()

    def test_leaving_spoke_stops_scheduler(self):
        db = _make_db("hub")
        sm = NodeStateMachine()
        scheduler = SyncScheduler(db, db)

        bind_sync_to_state_machine(sm, scheduler)

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            sm.transition(NodeState.JOINING)
            sm.transition(NodeState.SPOKE)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert scheduler.is_running

            sm.transition(NodeState.LEAVING)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert not scheduler.is_running
        finally:
            loop.close()

    def test_spoke_to_error_stops_scheduler(self):
        db = _make_db("hub")
        sm = NodeStateMachine()
        scheduler = SyncScheduler(db, db)

        bind_sync_to_state_machine(sm, scheduler)

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            sm.transition(NodeState.JOINING)
            sm.transition(NodeState.SPOKE)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert scheduler.is_running

            sm.transition(NodeState.ERROR, error_info="test failure")
            loop.run_until_complete(asyncio.sleep(0.05))
            assert not scheduler.is_running
        finally:
            loop.close()

    def test_bind_preserves_original_callback(self):
        db = _make_db()
        sm = NodeStateMachine()
        results = []

        def my_callback(result):
            results.append(result)

        scheduler = SyncScheduler(db, db, on_sync_complete=my_callback)
        checker = HealthChecker(db)

        bind_sync_to_state_machine(sm, scheduler, checker, health_on_sync=True)

        # The wrapper should still call the original
        fake_result = SyncResult(rows_pushed=0, tables_synced=0)
        scheduler.on_sync_complete(fake_result)

        assert len(results) == 1

    def test_no_health_checker_no_crash(self):
        db = _make_db()
        sm = NodeStateMachine()
        scheduler = SyncScheduler(db, db)

        bind_sync_to_state_machine(sm, scheduler, health_checker=None)
        # Should not raise

    def test_full_lifecycle_solo_join_spoke_leave_solo(self):
        """Full lifecycle: Solo → Joining → Spoke → Leaving → Solo."""
        db = _make_db("node-1")
        sm = NodeStateMachine()
        scheduler = SyncScheduler(db, db, active_interval=60)

        bind_sync_to_state_machine(sm, scheduler)

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        try:
            assert sm.state == NodeState.SOLO

            sm.transition(NodeState.JOINING)
            assert not scheduler.is_running

            sm.transition(NodeState.SPOKE)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert scheduler.is_running

            sm.transition(NodeState.LEAVING)
            loop.run_until_complete(asyncio.sleep(0.05))
            assert not scheduler.is_running

            sm.transition(NodeState.SOLO)
            assert sm.state == NodeState.SOLO
            assert len(sm.history) == 4
        finally:
            loop.close()
