"""
Tests für _poll_loop's Umgang mit einer toten MQTT-Session (Issue #105).
publish()/query_info() swallow send failures internally and return None
instead of raising - the poll loop must treat that (combined with
is_connected() == False) as "connection lost" and switch to the offline
branch, instead of silently retrying forever every poll_interval.
"""
import argparse
import tempfile
import threading
import time
from unittest.mock import MagicMock

import pytest

from kobrax_moonraker_bridge import KobraXBridge


def _bridge():
    c = MagicMock()
    c.callbacks = {}
    c.connected = False
    args = argparse.Namespace(
        printer_ip="192.168.1.100", mqtt_port=9883, username="", password="",
        mode_id="20030", device_id="", host="127.0.0.1", port=7125,
        data_dir=tempfile.mkdtemp(prefix="kxpoll-"), poll_interval=0.05,
    )
    return KobraXBridge(c, args=args)


def test_poll_loop_switches_to_offline_when_query_returns_none_and_disconnected():
    b = _bridge()
    b._state["print_state"] = "standby"
    b._state["kobra_state"] = "free"
    b.client.query_info.return_value = None
    b.client.is_connected.return_value = False
    b._printer_reachable = MagicMock(return_value=True)  # TCP still fine

    stop_event = threading.Event()
    t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True)
    t.start()
    time.sleep(0.2)
    stop_event.set()
    t.join(timeout=2)

    assert b._state["kobra_state"] == "offline"
    b.client.disconnect.assert_called()


def test_poll_loop_stays_online_when_query_returns_none_but_still_connected():
    """A single missed poll tick (info momentarily falsy) must not flip the
    bridge offline if the MQTT session itself is still alive."""
    b = _bridge()
    b._state["print_state"] = "standby"
    b._state["kobra_state"] = "free"
    b.client.query_info.return_value = None
    b.client.is_connected.return_value = True  # session still up
    b.client.query_multicolor_box.return_value = None
    b._printer_reachable = MagicMock(return_value=True)

    stop_event = threading.Event()
    t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True)
    t.start()
    time.sleep(0.2)
    stop_event.set()
    t.join(timeout=2)

    assert b._state["kobra_state"] != "offline"


def test_poll_loop_recovers_via_offline_branch_once_reachable_again():
    """Once flipped offline, the existing offline branch should re-connect
    as soon as the printer becomes reachable again (pre-existing behavior,
    unaffected by this fix)."""
    b = _bridge()
    b._state["print_state"] = "standby"
    b._state["kobra_state"] = "free"
    b.client.query_info.return_value = None
    b.client.is_connected.return_value = False
    b._printer_reachable = MagicMock(return_value=True)

    stop_event = threading.Event()
    t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True)
    t.start()
    time.sleep(0.15)  # let it flip offline
    assert b._state["kobra_state"] == "offline"

    # Printer "comes back": client.connect() succeeds, subsequent query_info
    # starts returning real data again.
    b.client.connect.side_effect = None
    b.client.query_info.return_value = {"data": {"state": "free"}}
    time.sleep(0.2)
    stop_event.set()
    t.join(timeout=2)

    b.client.connect.assert_called()
