"""Integration tests for oversized payloads and headers that should cause disconnection."""

from __future__ import annotations

import asyncio

import pytest

from .types import APIClientConnectedWithDisconnectFactory, RunCompiledFunction


@pytest.mark.asyncio
async def test_oversized_payload_plaintext(
    yaml_config: str,
    run_compiled: RunCompiledFunction,
    api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
    """Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing."""
    process_exited = False
    helper_log_event = asyncio.Event()

    def check_logs(line: str) -> None:
        nonlocal process_exited
        # Check for signs that the process exited/crashed
        if "Segmentation fault" in line or "core dumped" in line:
            process_exited = True
        # Check for HELPER_LOG message about message size exceeding maximum
        if (
            "[VV]" in line
            and "Bad packet: message size" in line
            and "exceeds maximum" in line
        ):
            helper_log_event.set()

    async with run_compiled(yaml_config, line_callback=check_logs):
        async with api_client_connected_with_disconnect() as (client, disconnect_event):
            # Verify basic connection works first
            device_info = await client.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-plaintext"

            # Create an oversized payload (>32768 bytes which is our new limit)
            oversized_data = b"X" * 40000  # ~40KiB, exceeds the 32768 byte limit

            # Access the internal connection to send raw data
            frame_helper = client._connection._frame_helper
            # Create a message with oversized payload
            # Using message type 1 (DeviceInfoRequest) as an example
            message_type = 1
            frame_helper.write_packets([(message_type, oversized_data)], True)

            # Wait for the connection to be closed by ESPHome
            await asyncio.wait_for(disconnect_event.wait(), timeout=5.0)

        # After disconnection, verify process didn't crash
        assert not process_exited, "ESPHome process should not crash"
        # Wait for the expected log message (may arrive after disconnect event)
        try:
            await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
        except TimeoutError:
            pytest.fail(
                "Expected to see HELPER_LOG about message size exceeding maximum"
            )

        # Try to reconnect to verify the process is still running
        async with api_client_connected_with_disconnect() as (client2, _):
            device_info = await client2.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-plaintext"


@pytest.mark.asyncio
async def test_oversized_protobuf_message_id_plaintext(
    yaml_config: str,
    run_compiled: RunCompiledFunction,
    api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
    """Test that protobuf messages with ID > UINT16_MAX cause disconnection without crashing.

    This tests the message type limit - message IDs must fit in a uint16_t (0-65535).
    """
    process_exited = False
    helper_log_event = asyncio.Event()

    def check_logs(line: str) -> None:
        nonlocal process_exited
        # Check for signs that the process exited/crashed
        if "Segmentation fault" in line or "core dumped" in line:
            process_exited = True
        # Check for HELPER_LOG message about message type exceeding maximum
        if (
            "[VV]" in line
            and "Bad packet: message type" in line
            and "exceeds maximum" in line
        ):
            helper_log_event.set()

    async with run_compiled(yaml_config, line_callback=check_logs):
        async with api_client_connected_with_disconnect() as (client, disconnect_event):
            # Verify basic connection works first
            device_info = await client.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-protobuf-plaintext"

            # Access the internal connection to send raw message with large ID
            frame_helper = client._connection._frame_helper
            # Message ID that exceeds uint16_t limit (> 65535)
            large_message_id = 65536  # 2^16, exceeds UINT16_MAX
            # Small payload for the test
            payload = b"test"

            # This should cause disconnection due to oversized varint
            frame_helper.write_packets([(large_message_id, payload)], True)

            # Wait for the connection to be closed by ESPHome
            await asyncio.wait_for(disconnect_event.wait(), timeout=5.0)

        # After disconnection, verify process didn't crash
        assert not process_exited, "ESPHome process should not crash"
        # Wait for the expected log message (may arrive after disconnect event)
        try:
            await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
        except TimeoutError:
            pytest.fail(
                "Expected to see HELPER_LOG about message type exceeding maximum"
            )

        # Try to reconnect to verify the process is still running
        async with api_client_connected_with_disconnect() as (client2, _):
            device_info = await client2.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-protobuf-plaintext"


@pytest.mark.asyncio
async def test_oversized_payload_noise(
    yaml_config: str,
    run_compiled: RunCompiledFunction,
    api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
    """Test that oversized payloads from client cause disconnection without crashing with noise encryption."""
    noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
    process_exited = False
    helper_log_event = asyncio.Event()

    def check_logs(line: str) -> None:
        nonlocal process_exited
        # Check for signs that the process exited/crashed
        if "Segmentation fault" in line or "core dumped" in line:
            process_exited = True
        # Check for HELPER_LOG message about message size exceeding maximum
        # With our new protection, oversized messages are rejected at frame level
        if (
            "[VV]" in line
            and "Bad packet: message size" in line
            and "exceeds maximum" in line
        ):
            helper_log_event.set()

    async with run_compiled(yaml_config, line_callback=check_logs):
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client,
            disconnect_event,
        ):
            # Verify basic connection works first
            device_info = await client.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"

            # Create an oversized payload (>32768 bytes which is our new limit)
            oversized_data = b"Y" * 40000  # ~40KiB, exceeds the 32768 byte limit

            # Access the internal connection to send raw data
            frame_helper = client._connection._frame_helper
            # For noise connections, we still send through write_packets
            # but the frame helper will handle encryption
            # Using message type 1 (DeviceInfoRequest) as an example
            message_type = 1
            frame_helper.write_packets([(message_type, oversized_data)], True)

            # Wait for the connection to be closed by ESPHome
            await asyncio.wait_for(disconnect_event.wait(), timeout=5.0)

        # After disconnection, verify process didn't crash
        assert not process_exited, "ESPHome process should not crash"
        # Wait for the expected log message (may arrive after disconnect event)
        try:
            await asyncio.wait_for(helper_log_event.wait(), timeout=2.0)
        except TimeoutError:
            pytest.fail(
                "Expected to see HELPER_LOG about message size exceeding maximum"
            )

        # Try to reconnect to verify the process is still running
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client2,
            _,
        ):
            device_info = await client2.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"


@pytest.mark.asyncio
async def test_oversized_protobuf_message_id_noise(
    yaml_config: str,
    run_compiled: RunCompiledFunction,
    api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
    """Test that the noise protocol handles unknown message types correctly.

    With noise encryption, message types are stored as uint16_t (2 bytes) after decryption.
    Unknown message types should be ignored without disconnecting, as ESPHome needs to
    read the full message to maintain encryption stream continuity.
    """
    noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
    process_exited = False

    def check_logs(line: str) -> None:
        nonlocal process_exited
        # Check for signs that the process exited/crashed
        if "Segmentation fault" in line or "core dumped" in line:
            process_exited = True

    async with run_compiled(yaml_config, line_callback=check_logs):
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client,
            disconnect_event,
        ):
            # Verify basic connection works first
            device_info = await client.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"

            # With noise, message types are uint16_t, so we test with an unknown but valid value
            frame_helper = client._connection._frame_helper

            # Test with an unknown message type (65535 is not used by ESPHome)
            unknown_message_id = 65535  # Valid uint16_t but unknown to ESPHome
            payload = b"test"

            # Send the unknown message type - ESPHome should read and ignore it
            frame_helper.write_packets([(unknown_message_id, payload)], True)

            # Give ESPHome a moment to process (but expect no disconnection)
            # The connection should stay alive as ESPHome ignores unknown message types
            with pytest.raises(asyncio.TimeoutError):
                await asyncio.wait_for(disconnect_event.wait(), timeout=0.5)

            # Connection should still be alive - unknown types are ignored, not fatal
            assert client._connection.is_connected, (
                "Connection should remain open for unknown message types"
            )

            # Verify we can still communicate by sending a valid request
            device_info2 = await client.device_info()
            assert device_info2 is not None
            assert device_info2.name == "oversized-noise"

        # After test, verify process didn't crash
        assert not process_exited, "ESPHome process should not crash"

        # Verify we can still reconnect
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client2,
            _,
        ):
            device_info = await client2.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"


@pytest.mark.asyncio
async def test_noise_corrupt_encrypted_frame(
    yaml_config: str,
    run_compiled: RunCompiledFunction,
    api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
    """Test that noise protocol properly handles corrupt encrypted frames.

    Send a frame with valid size but corrupt encrypted content (garbage bytes).
    This should fail decryption and cause disconnection.
    """
    noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
    process_exited = False
    cipherstate_event = asyncio.Event()

    def check_logs(line: str) -> None:
        nonlocal process_exited
        # Check for signs that the process exited/crashed
        if "Segmentation fault" in line or "core dumped" in line:
            process_exited = True
        # Check for the expected log about decryption failure
        # This can appear as either a VV-level log from noise or a W-level log from connection
        if (
            "[VV][api.noise" in line
            and "noise_cipherstate_decrypt failed: MAC_FAILURE" in line
        ) or (
            "[W][api.connection" in line
            and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
        ):
            cipherstate_event.set()

    async with run_compiled(yaml_config, line_callback=check_logs):
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client,
            disconnect_event,
        ):
            # Verify basic connection works first
            device_info = await client.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"

            # Get the socket to send raw corrupt data
            socket = client._connection._socket

            # Send a corrupt noise frame directly to the socket
            # Format: [indicator=0x01][size_high][size_low][garbage_encrypted_data]
            # Size of 32 bytes (reasonable size for a noise frame with MAC)
            corrupt_frame = bytes(
                [
                    0x01,  # Noise indicator
                    0x00,  # Size high byte
                    0x20,  # Size low byte (32 bytes)
                ]
            ) + bytes(32)  # 32 bytes of zeros (invalid encrypted data)

            # Send the corrupt frame
            socket.sendall(corrupt_frame)

            # Wait for ESPHome to disconnect due to decryption failure
            await asyncio.wait_for(disconnect_event.wait(), timeout=5.0)

        # After disconnection, verify process didn't crash
        assert not process_exited, (
            "ESPHome process should not crash on corrupt encrypted frames"
        )
        # Wait for the expected log message (may arrive after disconnect event)
        try:
            await asyncio.wait_for(cipherstate_event.wait(), timeout=2.0)
        except TimeoutError:
            pytest.fail(
                "Expected to see log about noise_cipherstate_decrypt failure"
                " or CIPHERSTATE_DECRYPT_FAILED"
            )

        # Verify we can still reconnect after handling the corrupt frame
        async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
            client2,
            _,
        ):
            device_info = await client2.device_info()
            assert device_info is not None
            assert device_info.name == "oversized-noise"
