"""Tests for cognitive state tracking in src/timmy/cognitive_state.py."""

import asyncio
from unittest.mock import patch

from timmy.cognitive_state import (
    ENGAGEMENT_LEVELS,
    MOOD_VALUES,
    CognitiveState,
    CognitiveTracker,
    _extract_commitments,
    _extract_topic,
    _infer_engagement,
    _infer_mood,
)


class TestCognitiveState:
    """Test the CognitiveState dataclass."""

    def test_defaults(self):
        state = CognitiveState()
        assert state.focus_topic is None
        assert state.engagement == "idle"
        assert state.mood == "settled"
        assert state.conversation_depth == 0
        assert state.last_initiative is None
        assert state.active_commitments == []

    def test_to_dict_excludes_private_fields(self):
        state = CognitiveState(focus_topic="testing")
        d = state.to_dict()
        assert "focus_topic" in d
        assert "_confidence_sum" not in d
        assert "_confidence_count" not in d

    def test_to_dict_includes_public_fields(self):
        state = CognitiveState(
            focus_topic="loop architecture",
            engagement="deep",
            mood="curious",
            conversation_depth=42,
            last_initiative="proposed refactor",
            active_commitments=["draft ticket", "review PR"],
        )
        d = state.to_dict()
        assert d["focus_topic"] == "loop architecture"
        assert d["engagement"] == "deep"
        assert d["mood"] == "curious"
        assert d["conversation_depth"] == 42


class TestInferEngagement:
    """Test engagement level inference."""

    def test_deep_keywords(self):
        assert _infer_engagement("help me debug this", "looking at the stack trace") == "deep"

    def test_architecture_is_deep(self):
        assert (
            _infer_engagement("explain the architecture", "the system has three layers") == "deep"
        )

    def test_short_response_is_surface(self):
        assert _infer_engagement("hi", "hello there") == "surface"

    def test_normal_conversation_is_surface(self):
        result = _infer_engagement("what time is it", "It is 3pm right now.")
        assert result == "surface"


class TestInferMood:
    """Test mood inference."""

    def test_low_confidence_is_hesitant(self):
        assert _infer_mood("I'm not really sure about this", 0.3) == "hesitant"

    def test_exclamation_with_positive_words_is_energized(self):
        assert _infer_mood("That's a great idea!", 0.8) == "energized"

    def test_question_words_are_curious(self):
        assert _infer_mood("I wonder if that would work", 0.6) == "curious"

    def test_neutral_is_settled(self):
        assert _infer_mood("The answer is 42.", 0.7) == "settled"

    def test_valid_mood_values(self):
        for mood in MOOD_VALUES:
            assert isinstance(mood, str)


class TestExtractTopic:
    """Test topic extraction from messages."""

    def test_simple_message(self):
        assert _extract_topic("Python decorators") == "Python decorators"

    def test_strips_question_prefix(self):
        topic = _extract_topic("what is a monad")
        assert topic == "a monad"

    def test_truncates_long_messages(self):
        long_msg = "a" * 100
        topic = _extract_topic(long_msg)
        assert len(topic) <= 60

    def test_empty_returns_none(self):
        assert _extract_topic("") is None
        assert _extract_topic("   ") is None


class TestExtractCommitments:
    """Test commitment extraction from responses."""

    def test_i_will_commitment(self):
        result = _extract_commitments("I will draft the skeleton ticket for you.")
        assert len(result) >= 1
        assert "I will draft the skeleton ticket for you" in result[0]

    def test_let_me_commitment(self):
        result = _extract_commitments("Let me look into that for you.")
        assert len(result) >= 1

    def test_no_commitments(self):
        result = _extract_commitments("The answer is 42.")
        assert result == []

    def test_caps_at_three(self):
        text = "I will do A. I'll do B. Let me do C. I'm going to do D."
        result = _extract_commitments(text)
        assert len(result) <= 3


class TestCognitiveTracker:
    """Test the CognitiveTracker behaviour."""

    def test_update_increments_depth(self):
        tracker = CognitiveTracker()
        tracker.update("hello", "Hi there, how can I help?")
        assert tracker.get_state().conversation_depth == 1
        tracker.update("thanks", "You're welcome!")
        assert tracker.get_state().conversation_depth == 2

    def test_update_sets_focus_topic(self):
        tracker = CognitiveTracker()
        tracker.update(
            "Python decorators", "Decorators are syntactic sugar for wrapping functions."
        )
        assert tracker.get_state().focus_topic == "Python decorators"

    def test_reset_clears_state(self):
        tracker = CognitiveTracker()
        tracker.update("hello", "world")
        tracker.reset()
        state = tracker.get_state()
        assert state.conversation_depth == 0
        assert state.focus_topic is None

    def test_to_json(self):
        import json

        tracker = CognitiveTracker()
        tracker.update("test", "response")
        data = json.loads(tracker.to_json())
        assert "focus_topic" in data
        assert "engagement" in data
        assert "mood" in data

    def test_engagement_values_are_valid(self):
        for level in ENGAGEMENT_LEVELS:
            assert isinstance(level, str)

    async def test_update_emits_cognitive_state_changed(self):
        """CognitiveTracker.update() emits a sensory event."""
        from timmy.event_bus import SensoryBus

        mock_bus = SensoryBus()
        received = []
        mock_bus.subscribe("cognitive_state_changed", lambda e: received.append(e))

        with patch("timmy.event_bus.get_sensory_bus", return_value=mock_bus):
            tracker = CognitiveTracker()
            tracker.update("debug the memory leak", "Looking at the stack trace now.")
            # Give the fire-and-forget task a chance to run
            await asyncio.sleep(0.05)

        assert len(received) == 1
        event = received[0]
        assert event.source == "cognitive"
        assert event.event_type == "cognitive_state_changed"
        assert "mood" in event.data
        assert "engagement" in event.data
        assert "depth" in event.data
        assert event.data["depth"] == 1

    async def test_update_tracks_mood_change(self):
        """Event data includes whether mood/engagement changed."""
        from timmy.event_bus import SensoryBus

        mock_bus = SensoryBus()
        received = []
        mock_bus.subscribe("cognitive_state_changed", lambda e: received.append(e))

        with patch("timmy.event_bus.get_sensory_bus", return_value=mock_bus):
            tracker = CognitiveTracker()
            # First message — "!" + "great" with high confidence → "energized"
            tracker.update("wow", "That's a great discovery!")
            await asyncio.sleep(0.05)

        assert len(received) == 1
        # Default mood is "settled", energized response → mood changes
        assert received[0].data["mood"] == "energized"
        assert received[0].data["mood_changed"] is True

    def test_emit_skipped_without_event_loop(self):
        """Event emission gracefully skips when no async loop is running."""
        tracker = CognitiveTracker()
        # Should not raise — just silently skips
        tracker.update("hello", "Hi there!")
