"""Tests for infrastructure.presence — presence state serializer."""

from unittest.mock import patch

import pytest

from infrastructure.presence import (
    DEFAULT_PIP_STATE,
    _get_familiar_state,
    produce_agent_state,
    produce_bark,
    produce_system_status,
    produce_thought,
    serialize_presence,
)


class TestSerializePresence:
    """Round-trip and edge-case tests for serialize_presence()."""

    @pytest.fixture()
    def full_presence(self):
        """A complete ADR-023 presence dict."""
        return {
            "version": 1,
            "liveness": "2026-03-21T12:00:00Z",
            "current_focus": "writing tests",
            "mood": "focused",
            "energy": 0.9,
            "confidence": 0.85,
            "active_threads": [
                {"type": "thinking", "ref": "refactor presence", "status": "active"}
            ],
            "recent_events": ["committed code"],
            "concerns": ["test coverage"],
            "familiar": {"name": "Pip", "state": "alert"},
        }

    def test_full_round_trip(self, full_presence):
        """All ADR-023 fields map to the expected camelCase keys."""
        result = serialize_presence(full_presence)

        assert result["timmyState"]["mood"] == "focused"
        assert result["timmyState"]["activity"] == "writing tests"
        assert result["timmyState"]["energy"] == 0.9
        assert result["timmyState"]["confidence"] == 0.85
        assert result["familiar"] == {"name": "Pip", "state": "alert"}
        assert result["activeThreads"] == full_presence["active_threads"]
        assert result["recentEvents"] == ["committed code"]
        assert result["concerns"] == ["test coverage"]
        assert result["visitorPresent"] is False
        assert result["updatedAt"] == "2026-03-21T12:00:00Z"
        assert result["version"] == 1

    def test_defaults_on_empty_dict(self):
        """Missing fields fall back to safe defaults."""
        result = serialize_presence({})

        assert result["timmyState"]["mood"] == "calm"
        assert result["timmyState"]["activity"] == "idle"
        assert result["timmyState"]["energy"] == 0.5
        assert result["timmyState"]["confidence"] == 0.7
        assert result["familiar"] is None
        assert result["activeThreads"] == []
        assert result["recentEvents"] == []
        assert result["concerns"] == []
        assert result["visitorPresent"] is False
        assert result["version"] == 1
        # updatedAt should be an ISO timestamp string
        assert "T" in result["updatedAt"]

    def test_partial_presence(self):
        """Only some fields provided — others get defaults."""
        result = serialize_presence({"mood": "excited", "energy": 0.3})

        assert result["timmyState"]["mood"] == "excited"
        assert result["timmyState"]["energy"] == 0.3
        assert result["timmyState"]["confidence"] == 0.7  # default
        assert result["activeThreads"] == []  # default

    def test_return_type_is_dict(self, full_presence):
        """serialize_presence always returns a plain dict."""
        result = serialize_presence(full_presence)
        assert isinstance(result, dict)
        assert isinstance(result["timmyState"], dict)

    def test_visitor_present_always_false(self, full_presence):
        """visitorPresent is always False — set by the WS layer, not here."""
        assert serialize_presence(full_presence)["visitorPresent"] is False
        assert serialize_presence({})["visitorPresent"] is False


class TestProduceAgentState:
    """Tests for produce_agent_state() — Matrix agent_state message producer."""

    @pytest.fixture()
    def full_presence(self):
        """A presence dict with all agent_state-relevant fields."""
        return {
            "display_name": "Timmy",
            "role": "companion",
            "current_focus": "thinking about tests",
            "mood": "focused",
            "energy": 0.9,
            "bark": "Running test suite...",
        }

    @patch("infrastructure.presence.time")
    def test_full_message_structure(self, mock_time, full_presence):
        """Returns dict with type, agent_id, data, and ts keys."""
        mock_time.time.return_value = 1742529600
        result = produce_agent_state("timmy", full_presence)

        assert result["type"] == "agent_state"
        assert result["agent_id"] == "timmy"
        assert result["ts"] == 1742529600
        assert isinstance(result["data"], dict)

    def test_data_fields(self, full_presence):
        """data dict contains all required presence fields."""
        data = produce_agent_state("timmy", full_presence)["data"]

        assert data["display_name"] == "Timmy"
        assert data["role"] == "companion"
        assert data["status"] == "thinking"
        assert data["mood"] == "focused"
        assert data["energy"] == 0.9
        assert data["bark"] == "Running test suite..."

    def test_defaults_on_empty_presence(self):
        """Missing fields get sensible defaults."""
        result = produce_agent_state("timmy", {})
        data = result["data"]

        assert data["display_name"] == "Timmy"  # agent_id.title()
        assert data["role"] == "assistant"
        assert data["status"] == "idle"
        assert data["mood"] == "calm"
        assert data["energy"] == 0.5
        assert data["bark"] == ""

    def test_ts_is_unix_timestamp(self):
        """ts should be an integer Unix timestamp."""
        result = produce_agent_state("timmy", {})
        assert isinstance(result["ts"], int)
        assert result["ts"] > 0

    @pytest.mark.parametrize(
        ("focus", "expected_status"),
        [
            ("thinking about code", "thinking"),
            ("speaking to user", "speaking"),
            ("talking with agent", "speaking"),
            ("idle", "idle"),
            ("", "idle"),
            ("writing tests", "online"),
            ("reviewing PR", "online"),
        ],
    )
    def test_status_derivation(self, focus, expected_status):
        """current_focus maps to the correct Matrix status."""
        data = produce_agent_state("t", {"current_focus": focus})["data"]
        assert data["status"] == expected_status

    def test_agent_id_passed_through(self):
        """agent_id appears in the top-level message."""
        result = produce_agent_state("spark", {})
        assert result["agent_id"] == "spark"

    def test_display_name_from_agent_id(self):
        """When display_name is missing, it's derived from agent_id.title()."""
        data = produce_agent_state("spark", {})["data"]
        assert data["display_name"] == "Spark"

    def test_familiar_in_data(self):
        """agent_state.data includes familiar field with required keys."""
        data = produce_agent_state("timmy", {})["data"]

        assert "familiar" in data
        familiar = data["familiar"]
        assert familiar["name"] == "Pip"
        assert "mood" in familiar
        assert "energy" in familiar
        assert familiar["color"] == "0x00b450"
        assert familiar["trail_color"] == "0xdaa520"

    def test_familiar_has_all_required_fields(self):
        """familiar dict contains all required fields per acceptance criteria."""
        data = produce_agent_state("timmy", {})["data"]
        familiar = data["familiar"]

        required_fields = {"name", "mood", "energy", "color", "trail_color"}
        assert set(familiar.keys()) >= required_fields


class TestFamiliarState:
    """Tests for _get_familiar_state() — Pip familiar state retrieval."""

    def test_get_familiar_state_returns_dict(self):
        """_get_familiar_state returns a dict."""
        result = _get_familiar_state()
        assert isinstance(result, dict)

    def test_get_familiar_state_has_required_fields(self):
        """Result contains name, mood, energy, color, trail_color."""
        result = _get_familiar_state()

        assert result["name"] == "Pip"
        assert "mood" in result
        assert isinstance(result["energy"], (int, float))
        assert result["color"] == "0x00b450"
        assert result["trail_color"] == "0xdaa520"

    def test_default_pip_state_constant(self):
        """DEFAULT_PIP_STATE has expected values."""
        assert DEFAULT_PIP_STATE["name"] == "Pip"
        assert DEFAULT_PIP_STATE["mood"] == "sleepy"
        assert DEFAULT_PIP_STATE["energy"] == 0.5
        assert DEFAULT_PIP_STATE["color"] == "0x00b450"
        assert DEFAULT_PIP_STATE["trail_color"] == "0xdaa520"

    @patch("infrastructure.presence.logger")
    def test_get_familiar_state_fallback_on_exception(self, mock_logger):
        """When familiar module raises, falls back to default and logs warning."""
        # Patch inside the function where pip_familiar is imported
        with patch("timmy.familiar.pip_familiar.snapshot") as mock_snapshot:
            mock_snapshot.side_effect = RuntimeError("Pip is napping")
            result = _get_familiar_state()

        assert result["name"] == "Pip"
        assert result["mood"] == "sleepy"
        mock_logger.warning.assert_called_once()
        assert "Pip is napping" in str(mock_logger.warning.call_args)


class TestProduceBark:
    """Tests for produce_bark() — Matrix bark message producer."""

    @patch("infrastructure.presence.time")
    def test_full_message_structure(self, mock_time):
        """Returns dict with type, agent_id, data, and ts keys."""
        mock_time.time.return_value = 1742529600
        result = produce_bark("timmy", "Hello world!")

        assert result["type"] == "bark"
        assert result["agent_id"] == "timmy"
        assert result["ts"] == 1742529600
        assert isinstance(result["data"], dict)

    def test_data_fields(self):
        """data dict contains text, reply_to, and style."""
        result = produce_bark("timmy", "Hello world!", reply_to="msg-123", style="shout")
        data = result["data"]

        assert data["text"] == "Hello world!"
        assert data["reply_to"] == "msg-123"
        assert data["style"] == "shout"

    def test_default_style_is_speech(self):
        """When style is not provided, defaults to 'speech'."""
        result = produce_bark("timmy", "Hello!")
        assert result["data"]["style"] == "speech"

    def test_default_reply_to_is_none(self):
        """When reply_to is not provided, defaults to None."""
        result = produce_bark("timmy", "Hello!")
        assert result["data"]["reply_to"] is None

    def test_text_truncated_to_280_chars(self):
        """Text longer than 280 chars is truncated."""
        long_text = "A" * 500
        result = produce_bark("timmy", long_text)
        assert len(result["data"]["text"]) == 280
        assert result["data"]["text"] == "A" * 280

    def test_text_exactly_280_chars_not_truncated(self):
        """Text exactly 280 chars is not truncated."""
        text = "B" * 280
        result = produce_bark("timmy", text)
        assert result["data"]["text"] == text

    def test_text_shorter_than_280_not_padded(self):
        """Text shorter than 280 chars is not padded."""
        result = produce_bark("timmy", "Short")
        assert result["data"]["text"] == "Short"

    @pytest.mark.parametrize(
        ("style", "expected_style"),
        [
            ("speech", "speech"),
            ("thought", "thought"),
            ("whisper", "whisper"),
            ("shout", "shout"),
        ],
    )
    def test_valid_styles_preserved(self, style, expected_style):
        """Valid style values are preserved."""
        result = produce_bark("timmy", "Hello!", style=style)
        assert result["data"]["style"] == expected_style

    @pytest.mark.parametrize(
        "invalid_style",
        ["yell", "scream", "", "SPEECH", "Speech", None, 123],
    )
    def test_invalid_style_defaults_to_speech(self, invalid_style):
        """Invalid style values fall back to 'speech'."""
        result = produce_bark("timmy", "Hello!", style=invalid_style)
        assert result["data"]["style"] == "speech"

    def test_empty_text_handled(self):
        """Empty text is handled gracefully."""
        result = produce_bark("timmy", "")
        assert result["data"]["text"] == ""

    def test_ts_is_unix_timestamp(self):
        """ts should be an integer Unix timestamp."""
        result = produce_bark("timmy", "Hello!")
        assert isinstance(result["ts"], int)
        assert result["ts"] > 0

    def test_agent_id_passed_through(self):
        """agent_id appears in the top-level message."""
        result = produce_bark("spark", "Hello!")
        assert result["agent_id"] == "spark"

    def test_with_all_parameters(self):
        """Full parameter set produces expected output."""
        result = produce_bark(
            agent_id="timmy",
            text="Running test suite...",
            reply_to="parent-msg-456",
            style="thought",
        )

        assert result["type"] == "bark"
        assert result["agent_id"] == "timmy"
        assert result["data"]["text"] == "Running test suite..."
        assert result["data"]["reply_to"] == "parent-msg-456"
        assert result["data"]["style"] == "thought"


class TestProduceThought:
    """Tests for produce_thought() — Matrix thought message producer."""

    @patch("infrastructure.presence.time")
    def test_full_message_structure(self, mock_time):
        """Returns dict with type, agent_id, data, and ts keys."""
        mock_time.time.return_value = 1742529600
        result = produce_thought("timmy", "Considering the options...", 42)

        assert result["type"] == "thought"
        assert result["agent_id"] == "timmy"
        assert result["ts"] == 1742529600
        assert isinstance(result["data"], dict)

    def test_data_fields(self):
        """data dict contains text, thought_id, and chain_id."""
        result = produce_thought("timmy", "Considering...", 42, chain_id="chain-123")
        data = result["data"]

        assert data["text"] == "Considering..."
        assert data["thought_id"] == 42
        assert data["chain_id"] == "chain-123"

    def test_default_chain_id_is_none(self):
        """When chain_id is not provided, defaults to None."""
        result = produce_thought("timmy", "Thinking...", 1)
        assert result["data"]["chain_id"] is None

    def test_text_truncated_to_500_chars(self):
        """Text longer than 500 chars is truncated."""
        long_text = "A" * 600
        result = produce_thought("timmy", long_text, 1)
        assert len(result["data"]["text"]) == 500
        assert result["data"]["text"] == "A" * 500

    def test_text_exactly_500_chars_not_truncated(self):
        """Text exactly 500 chars is not truncated."""
        text = "B" * 500
        result = produce_thought("timmy", text, 1)
        assert result["data"]["text"] == text

    def test_text_shorter_than_500_not_padded(self):
        """Text shorter than 500 chars is not padded."""
        result = produce_thought("timmy", "Short thought", 1)
        assert result["data"]["text"] == "Short thought"

    def test_empty_text_handled(self):
        """Empty text is handled gracefully."""
        result = produce_thought("timmy", "", 1)
        assert result["data"]["text"] == ""

    def test_ts_is_unix_timestamp(self):
        """ts should be an integer Unix timestamp."""
        result = produce_thought("timmy", "Hello!", 1)
        assert isinstance(result["ts"], int)
        assert result["ts"] > 0

    def test_agent_id_passed_through(self):
        """agent_id appears in the top-level message."""
        result = produce_thought("spark", "Hello!", 1)
        assert result["agent_id"] == "spark"

    def test_thought_id_passed_through(self):
        """thought_id appears in the data."""
        result = produce_thought("timmy", "Hello!", 999)
        assert result["data"]["thought_id"] == 999

    def test_with_all_parameters(self):
        """Full parameter set produces expected output."""
        result = produce_thought(
            agent_id="timmy",
            thought_text="Analyzing the situation...",
            thought_id=42,
            chain_id="chain-abc",
        )

        assert result["type"] == "thought"
        assert result["agent_id"] == "timmy"
        assert result["data"]["text"] == "Analyzing the situation..."
        assert result["data"]["thought_id"] == 42
        assert result["data"]["chain_id"] == "chain-abc"


class TestProduceSystemStatus:
    """Tests for produce_system_status() — Matrix system_status message producer."""

    @patch("infrastructure.presence.time")
    def test_full_message_structure(self, mock_time):
        """Returns dict with type, data, and ts keys."""
        mock_time.time.return_value = 1742529600
        result = produce_system_status()

        assert result["type"] == "system_status"
        assert result["ts"] == 1742529600
        assert isinstance(result["data"], dict)

    def test_data_has_required_fields(self):
        """data dict contains all required system status fields."""
        result = produce_system_status()
        data = result["data"]

        assert "agents_online" in data
        assert "visitors" in data
        assert "uptime_seconds" in data
        assert "thinking_active" in data
        assert "memory_count" in data

    def test_data_field_types(self):
        """All data fields have correct types."""
        result = produce_system_status()
        data = result["data"]

        assert isinstance(data["agents_online"], int)
        assert isinstance(data["visitors"], int)
        assert isinstance(data["uptime_seconds"], int)
        assert isinstance(data["thinking_active"], bool)
        assert isinstance(data["memory_count"], int)

    def test_agents_online_is_non_negative(self):
        """agents_online is never negative."""
        result = produce_system_status()
        assert result["data"]["agents_online"] >= 0

    def test_visitors_is_non_negative(self):
        """visitors is never negative."""
        result = produce_system_status()
        assert result["data"]["visitors"] >= 0

    def test_uptime_seconds_is_non_negative(self):
        """uptime_seconds is never negative."""
        result = produce_system_status()
        assert result["data"]["uptime_seconds"] >= 0

    def test_memory_count_is_non_negative(self):
        """memory_count is never negative."""
        result = produce_system_status()
        assert result["data"]["memory_count"] >= 0

    @patch("infrastructure.presence.time")
    def test_ts_is_unix_timestamp(self, mock_time):
        """ts should be an integer Unix timestamp."""
        mock_time.time.return_value = 1742529600
        result = produce_system_status()
        assert isinstance(result["ts"], int)
        assert result["ts"] == 1742529600

    @patch("infrastructure.presence.logger")
    def test_graceful_degradation_on_import_errors(self, mock_logger):
        """Function returns valid dict even when imports fail."""
        # This test verifies the function handles failures gracefully
        # by checking it always returns the expected structure
        result = produce_system_status()

        assert result["type"] == "system_status"
        assert isinstance(result["data"], dict)
        assert isinstance(result["ts"], int)

    def test_returns_dict(self):
        """produce_system_status always returns a plain dict."""
        result = produce_system_status()
        assert isinstance(result, dict)
