"""TCP keepalive + TCP_USER_TIMEOUT on the MQTT socket (Issue #103 follow-up).

Without these, a printer that disappears without a clean TCP close
(unplugged, not gracefully shut down) leaves the socket looking alive to
is_connected() for as long as the OS's default dead-connection timeout -
often 15+ minutes on Linux - since sendall() on a half-open connection is
buffered by the kernel and doesn't fail immediately. This left the
dashboard's printer-state indicator stuck showing the last known state
(e.g. green "ready") long after the printer was actually unreachable,
reported when testing the smart-plug power-switch feature by physically
unplugging the printer.

Verified live (real printer, physically unplugged) that SO_KEEPALIVE alone
is not sufficient: keepalive probes only fire on an idle connection, but if
the printer disappears while a send is still unacknowledged - the normal
case, since the poll loop is sending every few seconds - the kernel's
regular TCP retransmission timer takes over instead (tcp_retries2, 13-30+
minutes on Linux), which keepalive settings don't affect. TCP_USER_TIMEOUT
closes that gap by capping how long ANY unacknowledged data may sit in the
send queue, regardless of which retry mechanism would otherwise still be
running.
"""
import socket
from unittest.mock import MagicMock

import pytest

from kobrax_client import _enable_tcp_keepalive


def test_enable_tcp_keepalive_sets_so_keepalive():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        _enable_tcp_keepalive(s)
        assert s.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) == 1
    finally:
        s.close()


@pytest.mark.skipif(not hasattr(socket, "TCP_KEEPIDLE"), reason="Linux-specific option")
def test_enable_tcp_keepalive_sets_short_idle_and_interval():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        _enable_tcp_keepalive(s)
        idle = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE)
        intvl = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL)
        cnt = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT)
        # Short enough that a dead connection is detected within a couple of
        # poll cycles (default poll_interval is 3s), not the OS default of
        # minutes.
        assert idle <= 10
        assert intvl <= 5
        assert cnt <= 5
    finally:
        s.close()


@pytest.mark.skipif(not hasattr(socket, "TCP_USER_TIMEOUT"), reason="Linux-specific option")
def test_enable_tcp_keepalive_sets_user_timeout():
    """The critical fix, verified live against a real printer: without this,
    a dead connection with unacknowledged data in flight is only detected
    after the OS's normal TCP retransmission timeout (13-30+ minutes on
    Linux), not the keepalive interval."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        _enable_tcp_keepalive(s)
        user_timeout_ms = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT)
        # Short enough that a dead connection with in-flight data is detected
        # within a couple of poll cycles, not tens of minutes.
        assert 0 < user_timeout_ms <= 20000
    finally:
        s.close()


def test_enable_tcp_keepalive_does_not_raise_on_unsupported_platform():
    """A platform without TCP_KEEPIDLE/INTVL/CNT (e.g. some Windows builds)
    must not crash the connection attempt - keepalive is best-effort."""
    s = MagicMock()
    s.setsockopt.side_effect = OSError("unsupported")
    _enable_tcp_keepalive(s)  # must not raise
