import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import pytest

from bolt_pet.state import InvalidTransition, PetState, PetStateMachine


def test_starts_idle():
    sm = PetStateMachine()
    assert sm.state == PetState.IDLE


def test_happy_path_transitions():
    sm = PetStateMachine()
    sm.transition(PetState.LISTENING)
    sm.transition(PetState.THINKING)
    sm.transition(PetState.TALKING)
    sm.transition(PetState.IDLE)
    assert sm.state == PetState.IDLE


def test_idle_to_talking_is_allowed_for_proactive_announcements():
    # The heartbeat poll can make the pet speak unprompted (a reminder
    # firing, a nudge from the server) with no preceding listen/think leg.
    sm = PetStateMachine()
    sm.transition(PetState.TALKING)
    assert sm.state == PetState.TALKING


def test_invalid_transition_raises():
    sm = PetStateMachine()
    with pytest.raises(InvalidTransition):
        sm.transition(PetState.THINKING)  # can't skip straight to thinking with no utterance


def test_same_state_transition_is_a_noop():
    calls = []
    sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
    sm.transition(PetState.IDLE)  # already idle
    assert calls == []


def test_on_change_callback_fires_with_old_and_new():
    calls = []
    sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
    sm.transition(PetState.LISTENING)
    assert calls == [(PetState.IDLE, PetState.LISTENING)]


def test_every_state_can_reach_error_and_recover():
    for path in (
        [PetState.LISTENING],
        [PetState.LISTENING, PetState.THINKING],
        [PetState.LISTENING, PetState.THINKING, PetState.TALKING],
    ):
        sm = PetStateMachine()
        for step in path:
            sm.transition(step)
        sm.transition(PetState.ERROR)
        sm.transition(PetState.IDLE)
        assert sm.state == PetState.IDLE


def test_force_recovers_from_talking_directly_to_idle_without_validation():
    sm = PetStateMachine()
    sm.transition(PetState.LISTENING)
    sm.transition(PetState.THINKING)
    sm.transition(PetState.TALKING)
    sm.force(PetState.LISTENING)  # not in TALKING's allowed set, but force skips the check
    assert sm.state == PetState.LISTENING
