"""
Unit tests for message parsing functionality.

Tests cover:
- Binary message decoding
- Text message parsing
- State update message processing
- Malformed message error handling
- Message type detection
- Encoding detection and handling
"""

import json
import struct
import uuid
from unittest.mock import patch
import pytest

from loxone_mcp.loxone_client import (
    LoxoneClient,
    MessageType,
    check_and_decode_if_needed,
    detect_encoding,
)
from loxone_mcp.config import LoxoneConfig


class TestEncodingDetection:
    """Test encoding detection and message decoding utilities."""

    def test_detect_encoding_utf8(self):
        """Test detecting UTF-8 encoding."""
        utf8_bytes = "Hello, 世界!".encode("utf-8")
        encoding = detect_encoding(utf8_bytes)
        assert encoding == "utf-8"

    def test_detect_encoding_latin1(self):
        """Test detecting Latin-1 encoding."""
        latin1_bytes = "Café".encode("latin-1")
        encoding = detect_encoding(latin1_bytes)
        # Should detect as latin-1 or iso-8859-1 (they're equivalent)
        assert encoding in ["latin-1", "iso-8859-1"]

    def test_detect_encoding_ascii(self):
        """Test detecting ASCII encoding."""
        ascii_bytes = "Hello World".encode("ascii")
        encoding = detect_encoding(ascii_bytes)
        # ASCII is a subset of UTF-8, so UTF-8 will be detected first
        assert encoding == "utf-8"

    def test_detect_encoding_invalid_bytes(self):
        """Test detecting encoding for invalid byte sequences."""
        invalid_bytes = b"\xff\xfe\x00\x00\x01\x02\x03"
        encoding = detect_encoding(invalid_bytes)
        # Should find some encoding that can handle these bytes
        assert encoding is not None

    def test_detect_encoding_empty_bytes(self):
        """Test detecting encoding for empty byte string."""
        empty_bytes = b""
        encoding = detect_encoding(empty_bytes)
        assert encoding == "utf-8"  # Empty string is valid UTF-8

    def test_check_and_decode_if_needed_string(self):
        """Test that string input is returned unchanged."""
        text = "Already a string"
        result = check_and_decode_if_needed(text)
        assert result == text
        assert isinstance(result, str)

    def test_check_and_decode_if_needed_utf8_bytes(self):
        """Test decoding UTF-8 bytes."""
        text = "Hello, 世界!"
        utf8_bytes = text.encode("utf-8")
        result = check_and_decode_if_needed(utf8_bytes)
        assert result == text
        assert isinstance(result, str)

    def test_check_and_decode_if_needed_latin1_bytes(self):
        """Test decoding Latin-1 bytes when UTF-8 fails."""
        text = "Café"
        latin1_bytes = text.encode("latin-1")

        # This should fail UTF-8 decoding and fall back to encoding detection
        result = check_and_decode_if_needed(latin1_bytes)
        assert result == text
        assert isinstance(result, str)

    def test_check_and_decode_if_needed_invalid_bytes(self):
        """Test handling of completely invalid byte sequences."""
        invalid_bytes = b"\xff\xfe\x00\x00\x01\x02\x03"

        # Should not crash and return some decoded string
        result = check_and_decode_if_needed(invalid_bytes)
        assert isinstance(result, str)
        # Content may be garbled but should not crash

    def test_check_and_decode_if_needed_undecodable_bytes(self):
        """Test handling when no encoding can decode the bytes."""
        # Create bytes that can't be decoded by any common encoding
        undecodable_bytes = bytes([0xFF, 0xFE, 0xFD, 0xFC])

        with patch("loxone_mcp.loxone_client.detect_encoding", return_value=None):
            result = check_and_decode_if_needed(undecodable_bytes)
            assert isinstance(result, str)
            # Should use UTF-8 with error replacement
            assert "�" in result or len(result) > 0


class TestMessageTypeDetection:
    """Test message type detection and header parsing."""

    def test_message_type_enum_values(self):
        """Test that MessageType enum has expected values."""
        assert MessageType.TextMessage.value == 0
        assert MessageType.BinaryFile.value == 1
        assert MessageType.EventTableValueStates.value == 2
        assert MessageType.EventTableTextStates.value == 3
        assert MessageType.EventTableDaytimerStates.value == 4
        assert MessageType.OutOfService.value == 5
        assert MessageType.Keepalive.value == 6
        assert MessageType.EventTableWeatherStates.value == 7

    @pytest.mark.asyncio
    async def test_parse_loxone_message_valid_header(self):
        """Test parsing valid Loxone message header."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create a valid 8-byte header for EventTableValueStates (type 2)
        header = struct.pack("ccccI", b"\x03", b"\x02", b"\x00", b"\x00", 0)

        await client._parse_loxone_message(header)

        assert client._current_message_type == MessageType.EventTableValueStates

    @pytest.mark.asyncio
    async def test_parse_loxone_message_text_message(self):
        """Test parsing header for text message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create header for TextMessage (type 0)
        header = struct.pack("ccccI", b"\x03", b"\x00", b"\x00", b"\x00", 0)

        await client._parse_loxone_message(header)

        assert client._current_message_type == MessageType.TextMessage

    @pytest.mark.asyncio
    async def test_parse_loxone_message_keepalive(self):
        """Test parsing header for keepalive message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create header for Keepalive (type 6)
        header = struct.pack("ccccI", b"\x03", b"\x06", b"\x00", b"\x00", 0)

        await client._parse_loxone_message(header)

        assert client._current_message_type == MessageType.Keepalive

    @pytest.mark.asyncio
    async def test_parse_loxone_message_unknown_type(self):
        """Test parsing header with unknown message type."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create header with unknown message type (99)
        header = struct.pack("ccccI", b"\x03", b"\x63", b"\x00", b"\x00", 0)

        await client._parse_loxone_message(header)

        # Should default to TextMessage for unknown types
        assert client._current_message_type == MessageType.TextMessage

    @pytest.mark.asyncio
    async def test_parse_loxone_message_invalid_length(self):
        """Test parsing message with invalid header length."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create header with wrong length (should be 8 bytes)
        invalid_header = b"\x03\x02\x00\x00"  # Only 4 bytes

        await client._parse_loxone_message(invalid_header)

        # Should default to TextMessage for invalid headers
        assert client._current_message_type == MessageType.TextMessage

    @pytest.mark.asyncio
    async def test_parse_loxone_message_malformed_header(self):
        """Test parsing malformed message header."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create malformed header that will cause struct.error
        malformed_header = b"\x03\x02\x00"  # Too short for struct.unpack

        await client._parse_loxone_message(malformed_header)

        # Should default to TextMessage for malformed headers
        assert client._current_message_type == MessageType.TextMessage


class TestValueStatesParsing:
    """Test parsing of binary value state updates."""

    def test_parse_value_states_single_update(self):
        """Test parsing a single value state update."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create a test UUID and value
        test_uuid = uuid.uuid4()
        test_value = 42.5

        # Create binary message: 16 bytes UUID (little-endian) + 8 bytes double
        uuid_bytes = test_uuid.bytes_le
        value_bytes = struct.pack("d", test_value)
        message = uuid_bytes + value_bytes

        result = client._parse_value_states(message)

        # Convert UUID to expected format
        fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
        expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"

        assert len(result) == 1
        assert expected_uuid in result
        assert result[expected_uuid] == test_value

    def test_parse_value_states_multiple_updates(self):
        """Test parsing multiple value state updates."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create multiple test UUIDs and values
        test_data = [
            (uuid.uuid4(), 10.0),
            (uuid.uuid4(), 25.5),
            (uuid.uuid4(), -5.2),
        ]

        # Create binary message with multiple updates
        message = b""
        for test_uuid, test_value in test_data:
            uuid_bytes = test_uuid.bytes_le
            value_bytes = struct.pack("d", test_value)
            message += uuid_bytes + value_bytes

        result = client._parse_value_states(message)

        assert len(result) == 3

        for test_uuid, test_value in test_data:
            fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
            expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"
            assert expected_uuid in result
            assert result[expected_uuid] == test_value

    def test_parse_value_states_empty_message(self):
        """Test parsing empty value states message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        result = client._parse_value_states(b"")

        assert result == {}

    def test_parse_value_states_incomplete_message(self):
        """Test parsing incomplete value states message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create incomplete message (only 20 bytes instead of 24)
        test_uuid = uuid.uuid4()
        incomplete_message = test_uuid.bytes_le + b"\x00\x00\x00\x00"  # Missing 4 bytes

        result = client._parse_value_states(incomplete_message)

        # Should return empty dict for malformed message
        assert result == {}

    def test_parse_value_states_malformed_message(self):
        """Test parsing malformed value states message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create malformed message that will cause parsing errors
        malformed_message = b"\x00" * 16 + b"\xff\xff\xff\xff\xff\xff\xff\xff"

        # Should not crash and return empty dict
        result = client._parse_value_states(malformed_message)
        assert isinstance(result, dict)


class TestTextStatesParsing:
    """Test parsing of binary text state updates."""

    def test_parse_text_states_single_update(self):
        """Test parsing a single text state update."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create test data
        test_uuid = uuid.uuid4()
        icon_uuid = uuid.uuid4()
        test_text = "Hello World"

        # Create binary message
        message = b""
        message += test_uuid.bytes_le  # 16 bytes: UUID
        message += icon_uuid.bytes_le  # 16 bytes: Icon UUID
        message += struct.pack("<I", len(test_text))  # 4 bytes: text length
        message += test_text.encode("utf-8")  # N bytes: text content

        # Add padding to 4-byte boundary
        total_size = 16 + 16 + 4 + len(test_text)
        padding = (4 - (total_size % 4)) % 4
        message += b"\x00" * padding

        result = client._parse_text_states(message)

        # Convert UUID to expected format
        fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
        expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"

        assert len(result) == 1
        assert expected_uuid in result
        assert result[expected_uuid] == test_text

    def test_parse_text_states_multiple_updates(self):
        """Test parsing multiple text state updates."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create test data
        test_data = [
            (uuid.uuid4(), "First text"),
            (uuid.uuid4(), "Second text"),
            (uuid.uuid4(), "Third text with special chars: äöü"),
        ]

        # Create binary message with multiple updates
        message = b""
        for test_uuid, test_text in test_data:
            icon_uuid = uuid.uuid4()
            text_bytes = test_text.encode("utf-8")

            message += test_uuid.bytes_le  # UUID
            message += icon_uuid.bytes_le  # Icon UUID
            message += struct.pack("<I", len(text_bytes))  # Text length
            message += text_bytes  # Text content

            # Add padding
            total_size = 16 + 16 + 4 + len(text_bytes)
            padding = (4 - (total_size % 4)) % 4
            message += b"\x00" * padding

        result = client._parse_text_states(message)

        assert len(result) == 3

        for test_uuid, test_text in test_data:
            fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
            expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"
            assert expected_uuid in result
            assert result[expected_uuid] == test_text

    def test_parse_text_states_empty_text(self):
        """Test parsing text state update with empty text."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create test data with empty text
        test_uuid = uuid.uuid4()
        icon_uuid = uuid.uuid4()

        # Create binary message
        message = b""
        message += test_uuid.bytes_le  # UUID
        message += icon_uuid.bytes_le  # Icon UUID
        message += struct.pack("<I", 0)  # Text length = 0
        # No text content
        # No padding needed (total size is already multiple of 4)

        result = client._parse_text_states(message)

        fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
        expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"

        assert len(result) == 1
        assert expected_uuid in result
        assert result[expected_uuid] == ""

    def test_parse_text_states_empty_message(self):
        """Test parsing empty text states message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        result = client._parse_text_states(b"")

        assert result == {}

    def test_parse_text_states_malformed_message(self):
        """Test parsing malformed text states message."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create malformed message (incomplete header)
        malformed_message = b"\x00" * 20  # Not enough bytes for complete header

        # Should not crash and return empty dict
        result = client._parse_text_states(malformed_message)
        assert result == {}


class TestMessageContentParsing:
    """Test parsing of different message content types."""

    @pytest.mark.asyncio
    async def test_parse_message_content_text_message(self):
        """Test parsing text message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.TextMessage

        # Test with JSON content
        json_data = {"LL": {"control": "dev/sps/io/test", "value": "1", "Code": "200"}}
        json_message = json.dumps(json_data).encode("utf-8")

        result = await client._parse_message_content(json_message)

        assert result == json_data

    @pytest.mark.asyncio
    async def test_parse_message_content_text_message_invalid_json(self):
        """Test parsing text message with invalid JSON."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.TextMessage

        # Test with invalid JSON
        invalid_json = b"not valid json content"

        result = await client._parse_message_content(invalid_json)

        # Should return text content when JSON parsing fails
        assert result == {"text": "not valid json content"}

    @pytest.mark.asyncio
    async def test_parse_message_content_value_states(self):
        """Test parsing value states message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.EventTableValueStates

        # Create test value states message
        test_uuid = uuid.uuid4()
        test_value = 123.45
        message = test_uuid.bytes_le + struct.pack("d", test_value)

        result = await client._parse_message_content(message)

        assert isinstance(result, dict)
        assert len(result) == 1
        # Should contain the parsed UUID and value
        uuid_key = list(result.keys())[0]
        assert result[uuid_key] == test_value

    @pytest.mark.asyncio
    async def test_parse_message_content_text_states(self):
        """Test parsing text states message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.EventTableTextStates

        # Create test text states message
        test_uuid = uuid.uuid4()
        icon_uuid = uuid.uuid4()
        test_text = "Test Status"
        text_bytes = test_text.encode("utf-8")

        message = b""
        message += test_uuid.bytes_le
        message += icon_uuid.bytes_le
        message += struct.pack("<I", len(text_bytes))
        message += text_bytes

        result = await client._parse_message_content(message)

        assert isinstance(result, dict)
        assert len(result) == 1
        uuid_key = list(result.keys())[0]
        assert result[uuid_key] == test_text

    @pytest.mark.asyncio
    async def test_parse_message_content_keepalive(self):
        """Test parsing keepalive message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.Keepalive

        result = await client._parse_message_content(b"")

        assert result == {"keepalive": True}

    @pytest.mark.asyncio
    async def test_parse_message_content_binary_file(self):
        """Test parsing binary file message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.BinaryFile

        binary_data = b"\x00\x01\x02\x03\x04\x05"
        result = await client._parse_message_content(binary_data)

        assert result == {"binary_file": True, "size": 6}

    @pytest.mark.asyncio
    async def test_parse_message_content_out_of_service(self):
        """Test parsing out of service message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.OutOfService

        result = await client._parse_message_content(b"")

        assert result == {"out_of_service": True}

    @pytest.mark.asyncio
    async def test_parse_message_content_daytimer_states(self):
        """Test parsing daytimer states message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.EventTableDaytimerStates

        result = await client._parse_message_content(b"some_daytimer_data")

        # Should return dict (implementation may vary)
        assert isinstance(result, dict)

    @pytest.mark.asyncio
    async def test_parse_message_content_weather_states(self):
        """Test parsing weather states message content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.EventTableWeatherStates

        result = await client._parse_message_content(b"some_weather_data")

        # Should return dict (implementation may vary)
        assert isinstance(result, dict)

    @pytest.mark.asyncio
    async def test_parse_message_content_exception_handling(self):
        """Test that parsing exceptions are handled gracefully."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)
        client._current_message_type = MessageType.EventTableValueStates

        # Create malformed message that will cause parsing error
        malformed_message = b"\x00\x01\x02"  # Too short for value states

        result = await client._parse_message_content(malformed_message)

        # Should return None or empty dict, not crash
        assert result is None or result == {}


class TestMessageParsingIntegration:
    """Test integration of message parsing components."""

    @pytest.mark.asyncio
    async def test_full_message_parsing_flow(self):
        """Test complete message parsing flow from header to content."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create a complete message with header and content
        # Header for EventTableValueStates
        header = struct.pack("ccccI", b"\x03", b"\x02", b"\x00", b"\x00", 24)

        # Content: single value state update
        test_uuid = uuid.uuid4()
        test_value = 99.9
        content = test_uuid.bytes_le + struct.pack("d", test_value)

        # Parse header first
        await client._parse_loxone_message(header)
        assert client._current_message_type == MessageType.EventTableValueStates

        # Parse content
        result = await client._parse_message_content(content)

        # Verify result
        assert isinstance(result, dict)
        assert len(result) == 1

        fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
        expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"
        assert expected_uuid in result
        assert result[expected_uuid] == test_value

    @pytest.mark.asyncio
    async def test_error_recovery_after_malformed_message(self):
        """Test that client recovers after processing malformed messages."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Process malformed header
        malformed_header = b"\x00\x01\x02"  # Too short
        await client._parse_loxone_message(malformed_header)

        # Should default to TextMessage
        assert client._current_message_type == MessageType.TextMessage

        # Should still be able to process valid messages
        valid_header = struct.pack("ccccI", b"\x03", b"\x06", b"\x00", b"\x00", 0)
        await client._parse_loxone_message(valid_header)

        assert client._current_message_type == MessageType.Keepalive

    def test_uuid_formatting_consistency(self):
        """Test that UUID formatting is consistent across parsing functions."""
        config = LoxoneConfig(host="test.local", port=80, username="test", password="test")
        client = LoxoneClient(config)

        # Create test UUID
        test_uuid = uuid.uuid4()

        # Test value states parsing
        value_message = test_uuid.bytes_le + struct.pack("d", 42.0)
        value_result = client._parse_value_states(value_message)
        value_uuid_key = list(value_result.keys())[0]

        # Test text states parsing
        icon_uuid = uuid.uuid4()
        test_text = "test"
        text_bytes = test_text.encode("utf-8")
        text_message = (
            test_uuid.bytes_le
            + icon_uuid.bytes_le
            + struct.pack("<I", len(text_bytes))
            + text_bytes
        )
        text_result = client._parse_text_states(text_message)
        text_uuid_key = list(text_result.keys())[0]

        # UUID formatting should be consistent
        assert value_uuid_key == text_uuid_key

        # Verify expected format
        fields = test_uuid.urn.replace("urn:uuid:", "").split("-")
        expected_uuid = f"{fields[0]}-{fields[1]}-{fields[2]}-{fields[3]}{fields[4]}"
        assert value_uuid_key == expected_uuid
