"""Tests for leave protocol."""
from unittest.mock import MagicMock, patch

import pytest

from greymatter_core.cluster.leave import LeaveProtocol, LeaveResult
from greymatter_core.cluster.state import NodeState, NodeStateMachine
from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.migrations import apply_all


@pytest.fixture
def db():
    d = SQLiteDatabase(":memory:")
    apply_all(d)
    return d


@pytest.fixture
def state_machine():
    sm = NodeStateMachine()
    sm.transition(NodeState.JOINING)
    sm.transition(NodeState.SPOKE)
    sm.transition(NodeState.LEAVING)
    return sm


def _mock_response(status_code=200, json_data=None, text=""):
    resp = MagicMock()
    resp.status_code = status_code
    resp.text = text
    resp.json.return_value = json_data or {}
    return resp


class _MockClient:
    """Mock httpx.AsyncClient."""

    def __init__(self, handler):
        self._handler = handler

    async def get(self, url, **kwargs):
        return await self._handler("GET", url, **kwargs)

    async def post(self, url, **kwargs):
        return await self._handler("POST", url, **kwargs)

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        pass


def _hub_side_effects():
    """Simulate hub responses for leave protocol."""
    async def _request(method, url, **kwargs):
        if "/cluster/deregister" in url:
            return _mock_response(200, {"deregistered": True})
        if "/cluster/revoke-cert" in url:
            return _mock_response(200, {"revoked": True, "serial_number": "abc123"})
        return _mock_response(404, text="Not found")
    return _request


class TestLeaveResult:
    def test_success_when_no_error(self):
        r = LeaveResult()
        assert r.success

    def test_failure_when_error_set(self):
        r = LeaveResult(error="something broke")
        assert not r.success


class TestLeaveProtocol:
    @pytest.mark.asyncio
    async def test_leave_succeeds_with_hub(self, db, state_machine, tmp_path):
        """Full leave protocol with hub URL, no hub_db."""
        handler = _hub_side_effects()
        mock_client = _MockClient(handler)

        # Create a fake node cert so revoke step has something to read
        cert_dir = tmp_path / "certs"
        cert_dir.mkdir()
        (cert_dir / "node.crt").write_bytes(b"-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----")
        (cert_dir / "ca.crt").write_bytes(b"-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----")

        with patch("greymatter_core.cluster.leave.httpx.AsyncClient", return_value=mock_client):
            protocol = LeaveProtocol(
                db,
                hub_url="http://hub:8000",
                node_id="node-1",
                state_machine=state_machine,
                cert_dir=cert_dir,
            )
            result = await protocol.execute()

        assert result.success
        assert state_machine.state == NodeState.SOLO
        assert "deregister" in result.steps_completed
        assert "revoke_cert" in result.steps_completed
        assert "transition_solo" in result.steps_completed

    @pytest.mark.asyncio
    async def test_leave_without_hub_skips_network_steps(self, db, state_machine):
        """Without hub_url, deregister and revoke skip gracefully."""
        protocol = LeaveProtocol(
            db,
            state_machine=state_machine,
        )
        result = await protocol.execute()

        assert result.success
        assert "stop_sync" in result.steps_completed
        assert "final_sync" in result.steps_completed
        assert "snapshot" in result.steps_completed
        assert "deregister" in result.steps_completed
        assert "revoke_cert" in result.steps_completed
        assert "transition_solo" in result.steps_completed

    @pytest.mark.asyncio
    async def test_leave_without_state_machine(self, db):
        protocol = LeaveProtocol(db)
        result = await protocol.execute()
        assert result.success

    @pytest.mark.asyncio
    async def test_leave_stops_scheduler(self, db, state_machine):
        class MockScheduler:
            def __init__(self):
                self.is_running = True
                self.stopped = False
            async def stop(self):
                self.stopped = True
                self.is_running = False

        scheduler = MockScheduler()
        protocol = LeaveProtocol(db, state_machine=state_machine, scheduler=scheduler)
        result = await protocol.execute()
        assert scheduler.stopped
        assert result.success

    @pytest.mark.asyncio
    async def test_leave_with_hub_db_runs_sync(self, db, state_machine):
        """When hub_db is provided, final_sync and snapshot run delta/full sync."""
        hub_db = SQLiteDatabase(":memory:")
        apply_all(hub_db)

        protocol = LeaveProtocol(
            db,
            state_machine=state_machine,
            hub_db=hub_db,
        )
        result = await protocol.execute()
        assert result.success
        assert "final_sync" in result.steps_completed
        assert "snapshot" in result.steps_completed

    @pytest.mark.asyncio
    async def test_deregister_failure_still_transitions_solo(self, db, state_machine, tmp_path):
        """If deregister fails, protocol still attempts transition_solo."""
        async def _fail_deregister(method, url, **kwargs):
            if "/cluster/deregister" in url:
                return _mock_response(500, text="Internal Server Error")
            return _mock_response(200)

        mock_client = _MockClient(_fail_deregister)

        with patch("greymatter_core.cluster.leave.httpx.AsyncClient", return_value=mock_client):
            protocol = LeaveProtocol(
                db,
                hub_url="http://hub:8000",
                node_id="node-1",
                state_machine=state_machine,
            )
            result = await protocol.execute()

        # Should have errored at deregister
        assert not result.success
        assert "deregister" in result.error
        # But should still have transitioned to Solo (safety net)
        assert "transition_solo" in result.steps_completed
        assert state_machine.state == NodeState.SOLO

    @pytest.mark.asyncio
    async def test_leave_rows_synced_with_hub_db_and_triggers(self, db, state_machine):
        """Verify rows_synced accumulates when sync triggers are installed."""
        from greymatter_core.sync.tracking import install_sqlite_triggers

        hub_db = SQLiteDatabase(":memory:")
        apply_all(hub_db)

        # Install change tracking triggers so delta_sync can detect changes
        install_sqlite_triggers(db)

        # Insert a row in local that hub doesn't have (triggers will log it)
        db.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content, confidence) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            ("obs-leave-1", "sess-1", "test", "insight", "Test", "test observation", 0.8),
        )

        protocol = LeaveProtocol(
            db,
            state_machine=state_machine,
            hub_db=hub_db,
        )
        result = await protocol.execute()
        assert result.success
        # At least 1 row should have been synced (the observation we inserted)
        assert result.rows_synced >= 1
