"""
Tests for the IPC protocol implementation.
"""

import pytest
import base64
import json
from esppal.protocol import (
    SubscribeCommand, BuildCommand, UploadCommand, StatusCommand,
    ShutdownCommand, UnsubscribeCommand,
    DataMessage, MarkerMessage, StatusMessage, ErrorMessage,
    BuildOutputMessage, BuildCompleteMessage, SubscribedMessage, ShutdownNotice,
    serialize, parse_command, parse_response, parse_json_line,
    MessageFramer, ProtocolError, SubscribeMode
)


class TestCommandSerialization:
    """Tests for command serialisation."""

    def test_subscribe_serialization(self):
        """Round-trip for subscribe command."""
        cmd = SubscribeCommand(mode="live")
        data = cmd.to_dict()

        assert data["cmd"] == "subscribe"
        assert data["mode"] == "live"

        # Parse back
        parsed = parse_command(data)
        assert isinstance(parsed, SubscribeCommand)
        assert parsed.mode == "live"

    def test_subscribe_with_marker(self):
        """Subscribe with marker parameter."""
        cmd = SubscribeCommand(mode="since", marker="upload_complete")
        data = cmd.to_dict()

        assert data["marker"] == "upload_complete"

        parsed = parse_command(data)
        assert parsed.marker == "upload_complete"

    def test_subscribe_with_lines(self):
        """Subscribe with lines parameter."""
        cmd = SubscribeCommand(mode="tail", lines=100)
        data = cmd.to_dict()

        assert data["lines"] == 100

        parsed = parse_command(data)
        assert parsed.lines == 100

    def test_build_command(self):
        """Build command serialisation."""
        cmd = BuildCommand(args=["--verbose"], environment="esp32dev")
        data = cmd.to_dict()

        assert data["cmd"] == "build"
        assert data["args"] == ["--verbose"]
        assert data["environment"] == "esp32dev"

        parsed = parse_command(data)
        assert isinstance(parsed, BuildCommand)
        assert parsed.args == ["--verbose"]

    def test_upload_command(self):
        """Upload command serialisation."""
        cmd = UploadCommand(args=["--port", "/dev/ttyUSB0"])
        data = cmd.to_dict()

        assert data["cmd"] == "upload"
        assert data["args"] == ["--port", "/dev/ttyUSB0"]

        parsed = parse_command(data)
        assert isinstance(parsed, UploadCommand)

    def test_status_command(self):
        """Status command serialisation."""
        cmd = StatusCommand()
        data = cmd.to_dict()

        assert data["cmd"] == "status"

        parsed = parse_command(data)
        assert isinstance(parsed, StatusCommand)

    def test_shutdown_command(self):
        """Shutdown command serialisation."""
        cmd = ShutdownCommand()
        data = cmd.to_dict()

        assert data["cmd"] == "shutdown"

        parsed = parse_command(data)
        assert isinstance(parsed, ShutdownCommand)

    def test_unsubscribe_command(self):
        """Unsubscribe command serialisation."""
        cmd = UnsubscribeCommand()
        data = cmd.to_dict()

        assert data["cmd"] == "unsubscribe"

        parsed = parse_command(data)
        assert isinstance(parsed, UnsubscribeCommand)


class TestResponseSerialization:
    """Tests for response serialisation."""

    def test_data_message_base64(self):
        """Binary data encoding as base64."""
        # Test with binary data including null bytes
        binary_data = b"\x00\x01\x02\xff\xfe"
        msg = DataMessage(source="serial", timestamp=1234567890.123, data=binary_data)
        data = msg.to_dict()

        assert data["type"] == "data"
        assert data["source"] == "serial"
        assert data["ts"] == 1234567890.123

        # Verify base64 encoding
        decoded = base64.b64decode(data["data"])
        assert decoded == binary_data

        # Parse back
        parsed = DataMessage.from_dict(data)
        assert parsed.data == binary_data

    def test_data_message_text(self):
        """Text data encoding."""
        text_data = "Hello, ESP32!\n".encode("utf-8")
        msg = DataMessage(source="stdout", timestamp=1234567890.0, data=text_data)
        data = msg.to_dict()

        parsed = DataMessage.from_dict(data)
        assert parsed.data == text_data
        assert parsed.source == "stdout"

    def test_marker_message(self):
        """Marker message serialisation."""
        msg = MarkerMessage(name="upload_complete", timestamp=1234567890.456)
        data = msg.to_dict()

        assert data["type"] == "marker"
        assert data["name"] == "upload_complete"
        assert data["ts"] == 1234567890.456

        parsed = MarkerMessage.from_dict(data)
        assert parsed.name == "upload_complete"
        assert parsed.timestamp == 1234567890.456

    def test_status_message(self):
        """Status message with all fields."""
        msg = StatusMessage(
            port="/dev/ttyUSB0",
            port_state="connected",
            clients=3,
            buffer_bytes=1048576,
            buffer_entries=500,
            uptime=3600.5,
            idle_timeout=1800.0,
            vid_pid="10c4:ea60"
        )
        data = msg.to_dict()

        assert data["type"] == "status"
        assert data["port"] == "/dev/ttyUSB0"
        assert data["clients"] == 3
        assert data["buffer_bytes"] == 1048576

        parsed = StatusMessage.from_dict(data)
        assert parsed.port == "/dev/ttyUSB0"
        assert parsed.clients == 3
        assert parsed.vid_pid == "10c4:ea60"

    def test_error_message(self):
        """Error message formatting."""
        msg = ErrorMessage(code="DEVICE_NOT_FOUND", message="No ESP32 device detected")
        data = msg.to_dict()

        assert data["type"] == "error"
        assert data["code"] == "DEVICE_NOT_FOUND"
        assert data["message"] == "No ESP32 device detected"

        parsed = ErrorMessage.from_dict(data)
        assert parsed.code == "DEVICE_NOT_FOUND"
        assert parsed.message == "No ESP32 device detected"

    def test_build_output_message(self):
        """Build output line serialisation."""
        msg = BuildOutputMessage(stream="stdout", line="Compiling main.cpp...")
        data = msg.to_dict()

        assert data["type"] == "build_output"
        assert data["stream"] == "stdout"
        assert data["line"] == "Compiling main.cpp..."

        parsed = BuildOutputMessage.from_dict(data)
        assert parsed.stream == "stdout"
        assert parsed.line == "Compiling main.cpp..."

    def test_build_complete_message(self):
        """Build completion message."""
        msg = BuildCompleteMessage(exit_code=0, success=True)
        data = msg.to_dict()

        assert data["type"] == "build_complete"
        assert data["exit_code"] == 0
        assert data["success"] is True

        parsed = BuildCompleteMessage.from_dict(data)
        assert parsed.exit_code == 0
        assert parsed.success is True

    def test_subscribed_message(self):
        """Subscription confirmation."""
        msg = SubscribedMessage(mode="live", position=1234)
        data = msg.to_dict()

        assert data["type"] == "subscribed"
        assert data["mode"] == "live"
        assert data["position"] == 1234

        parsed = SubscribedMessage.from_dict(data)
        assert parsed.mode == "live"
        assert parsed.position == 1234

    def test_shutdown_notice(self):
        """Shutdown notification."""
        msg = ShutdownNotice(reason="idle_timeout")
        data = msg.to_dict()

        assert data["type"] == "shutdown"
        assert data["reason"] == "idle_timeout"

        parsed = ShutdownNotice.from_dict(data)
        assert parsed.reason == "idle_timeout"


class TestParseErrors:
    """Tests for error handling in parsing."""

    def test_invalid_json(self):
        """Graceful parse failure for invalid JSON."""
        with pytest.raises(ProtocolError) as exc_info:
            parse_json_line(b"not valid json")

        assert "Invalid JSON" in str(exc_info.value)

    def test_missing_required_field(self):
        """Validation errors for missing fields."""
        # Missing 'cmd' field
        with pytest.raises(ProtocolError) as exc_info:
            parse_command({"mode": "live"})

        assert "Missing 'cmd' field" in str(exc_info.value)

        # Missing 'type' field
        with pytest.raises(ProtocolError) as exc_info:
            parse_response({"data": "test"})

        assert "Missing 'type' field" in str(exc_info.value)

    def test_unknown_command(self):
        """Unknown command handling."""
        with pytest.raises(ProtocolError) as exc_info:
            parse_command({"cmd": "unknown_command"})

        assert "Unknown command" in str(exc_info.value)

    def test_unknown_response_type(self):
        """Unknown response type handling."""
        with pytest.raises(ProtocolError) as exc_info:
            parse_response({"type": "unknown_type"})

        assert "Unknown message type" in str(exc_info.value)

    def test_empty_message(self):
        """Empty message handling."""
        with pytest.raises(ProtocolError) as exc_info:
            parse_json_line(b"")

        assert "Empty message" in str(exc_info.value)

    def test_invalid_utf8(self):
        """Invalid UTF-8 handling."""
        with pytest.raises(ProtocolError) as exc_info:
            parse_json_line(b"\xff\xfe")

        assert "Invalid UTF-8" in str(exc_info.value)


class TestSerialize:
    """Tests for the serialize function."""

    def test_serialize_command(self):
        """Serialize command to bytes."""
        cmd = SubscribeCommand(mode="live")
        data = serialize(cmd)

        assert isinstance(data, bytes)
        assert data.endswith(b"\n")

        # Parse back
        parsed = json.loads(data.decode("utf-8"))
        assert parsed["cmd"] == "subscribe"

    def test_serialize_response(self):
        """Serialize response to bytes."""
        msg = ErrorMessage(code="TEST", message="Test error")
        data = serialize(msg)

        assert isinstance(data, bytes)
        assert data.endswith(b"\n")

        parsed = json.loads(data.decode("utf-8"))
        assert parsed["type"] == "error"


class TestMessageFramer:
    """Tests for the message framing logic."""

    def test_single_message(self):
        """Single complete message."""
        framer = MessageFramer()

        messages = framer.feed(b'{"cmd": "status"}\n')
        assert len(messages) == 1
        assert messages[0] == b'{"cmd": "status"}'

    def test_multiple_messages(self):
        """Multiple messages in one chunk."""
        framer = MessageFramer()

        messages = framer.feed(b'{"cmd": "status"}\n{"cmd": "build"}\n')
        assert len(messages) == 2
        assert messages[0] == b'{"cmd": "status"}'
        assert messages[1] == b'{"cmd": "build"}'

    def test_partial_message(self):
        """Message split across chunks."""
        framer = MessageFramer()

        # First chunk - incomplete
        messages = framer.feed(b'{"cmd": "stat')
        assert len(messages) == 0

        # Second chunk - completes the message
        messages = framer.feed(b'us"}\n')
        assert len(messages) == 1
        assert messages[0] == b'{"cmd": "status"}'

    def test_mixed_partial_complete(self):
        """Mix of partial and complete messages."""
        framer = MessageFramer()

        # Complete message + start of another
        messages = framer.feed(b'{"cmd": "a"}\n{"cmd": "b')
        assert len(messages) == 1
        assert messages[0] == b'{"cmd": "a"}'

        # Complete the second message + another complete
        messages = framer.feed(b'"}\n{"cmd": "c"}\n')
        assert len(messages) == 2
        assert messages[0] == b'{"cmd": "b"}'
        assert messages[1] == b'{"cmd": "c"}'

    def test_empty_lines_skipped(self):
        """Empty lines are skipped."""
        framer = MessageFramer()

        messages = framer.feed(b'\n\n{"cmd": "test"}\n\n')
        assert len(messages) == 1
        assert messages[0] == b'{"cmd": "test"}'

    def test_clear(self):
        """Clear resets the buffer."""
        framer = MessageFramer()

        framer.feed(b'{"incomplete": ')
        framer.clear()

        # New message should work
        messages = framer.feed(b'{"cmd": "test"}\n')
        assert len(messages) == 1
        assert messages[0] == b'{"cmd": "test"}'


class TestSubscribeMode:
    """Tests for SubscribeMode enum."""

    def test_mode_values(self):
        """Mode enum values."""
        assert SubscribeMode.LIVE.value == "live"
        assert SubscribeMode.SINCE.value == "since"
        assert SubscribeMode.TAIL.value == "tail"
        assert SubscribeMode.REPLAY.value == "replay"
