"""Tests for node state machine."""
import pytest

from greymatter_core.cluster.state import (
    InvalidTransitionError,
    NodeState,
    NodeStateMachine,
)


class TestNodeState:
    def test_initial_state_is_solo(self):
        sm = NodeStateMachine()
        assert sm.state == NodeState.SOLO

    def test_custom_initial_state(self):
        sm = NodeStateMachine(initial_state=NodeState.ERROR)
        assert sm.state == NodeState.ERROR

    def test_not_clustered_when_solo(self):
        sm = NodeStateMachine()
        assert not sm.is_clustered


class TestTransitions:
    def test_solo_to_joining(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        assert sm.state == NodeState.JOINING

    def test_joining_to_spoke(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        assert sm.state == NodeState.SPOKE
        assert sm.is_clustered

    def test_spoke_to_leaving(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        sm.transition(NodeState.LEAVING)
        assert sm.state == NodeState.LEAVING

    def test_leaving_to_solo(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        sm.transition(NodeState.LEAVING)
        sm.transition(NodeState.SOLO)
        assert sm.state == NodeState.SOLO
        assert not sm.is_clustered

    def test_joining_to_error(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.ERROR, error_info="Connection refused")
        assert sm.state == NodeState.ERROR
        assert sm.error_info == "Connection refused"

    def test_error_to_solo(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.ERROR)
        sm.transition(NodeState.SOLO)
        assert sm.state == NodeState.SOLO
        assert sm.error_info == ""

    def test_error_to_joining_retry(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.ERROR)
        sm.transition(NodeState.JOINING)
        assert sm.state == NodeState.JOINING

    def test_invalid_transition_raises(self):
        sm = NodeStateMachine()
        with pytest.raises(InvalidTransitionError):
            sm.transition(NodeState.SPOKE)  # Can't go directly from Solo to Spoke

    def test_cannot_go_backwards(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        with pytest.raises(InvalidTransitionError):
            sm.transition(NodeState.JOINING)  # Can't go back to Joining from Spoke


class TestCallbacks:
    def test_on_enter_fires(self):
        events = []
        sm = NodeStateMachine()
        sm.on_enter(NodeState.JOINING, lambda old, new: events.append(("enter", new)))
        sm.transition(NodeState.JOINING)
        assert len(events) == 1
        assert events[0] == ("enter", NodeState.JOINING)

    def test_on_exit_fires(self):
        events = []
        sm = NodeStateMachine()
        sm.on_exit(NodeState.SOLO, lambda old, new: events.append(("exit", old)))
        sm.transition(NodeState.JOINING)
        assert len(events) == 1
        assert events[0] == ("exit", NodeState.SOLO)

    def test_on_any_transition_fires(self):
        events = []
        sm = NodeStateMachine()
        sm.on_any_transition(lambda old, new: events.append((old, new)))
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        assert len(events) == 2

    def test_callback_exception_doesnt_block_transition(self):
        sm = NodeStateMachine()
        sm.on_enter(NodeState.JOINING, lambda old, new: 1 / 0)  # Will raise
        sm.transition(NodeState.JOINING)  # Should not raise
        assert sm.state == NodeState.JOINING


class TestHistory:
    def test_tracks_transitions(self):
        sm = NodeStateMachine()
        sm.transition(NodeState.JOINING)
        sm.transition(NodeState.SPOKE)
        assert sm.history == [
            (NodeState.SOLO, NodeState.JOINING),
            (NodeState.JOINING, NodeState.SPOKE),
        ]


class TestSerialization:
    def test_to_dict(self):
        sm = NodeStateMachine()
        d = sm.to_dict()
        assert d["state"] == "solo"
        assert d["is_clustered"] is False
