"""KXGauge display integration (https://gitea.it-drui.de/viewit/kxgauge).

KXGauge is a small ESP32 round-face display with a GET-only HTTP API and no
push/websocket - the bridge has to actively poke it on printer state/temp
changes. Covers: the KXGaugeClient dedupe logic, settings GET/POST roundtrip
for the new fields, the mapping config loader, and the /api/kxgauge/test
connectivity check endpoint.
"""
import argparse
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio
from aiohttp.test_utils import TestClient, TestServer

from kobrax_moonraker_bridge import KobraXBridge, build_app
from kxgauge_client import KXGaugeClient


# ── KXGaugeClient (sync HTTP client, dedupe logic) ──────────────────────────

def test_set_emotion_sends_get_and_dedupes():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(raise_for_status=lambda: None)
        kg.set_emotion("happy")
        kg.set_emotion("happy")  # same emotion again - must not re-send
    assert mock_get.call_count == 1
    assert mock_get.call_args[0][0] == "http://192.168.1.99/emotion/happy"


def test_set_emotion_sends_again_after_change():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(raise_for_status=lambda: None)
        kg.set_emotion("happy")
        kg.set_emotion("scared")
    assert mock_get.call_count == 2
    assert mock_get.call_args[0][0] == "http://192.168.1.99/emotion/scared"


def test_set_heat_celsius_sends_peak_once_then_dedupes_within_tolerance():
    kg = KXGaugeClient("http://192.168.1.99", heat_peak=230)
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(raise_for_status=lambda: None)
        kg.set_heat_celsius(200.0)
        kg.set_heat_celsius(200.5)  # within tolerance - no new /heat/celsius call
    urls = [c.args[0] for c in mock_get.call_args_list]
    assert "http://192.168.1.99/heat/peak/celsius/230" in urls
    assert urls.count("http://192.168.1.99/heat/celsius/200.0") == 1
    assert not any("/heat/celsius/200.5" in u for u in urls)


def test_set_heat_celsius_sends_again_beyond_tolerance():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(raise_for_status=lambda: None)
        kg.set_heat_celsius(200.0)
        kg.set_heat_celsius(210.0)
    heat_calls = [c.args[0] for c in mock_get.call_args_list if "/heat/celsius/" in c.args[0]]
    assert len(heat_calls) == 2


def test_client_swallows_connection_errors():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get", side_effect=OSError("unreachable")):
        kg.set_emotion("happy")  # must not raise
    assert kg._last_emotion is None  # failed send - state not updated


def test_health_check_true_on_200():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(raise_for_status=lambda: None)
        assert kg.health_check() is True


def test_health_check_false_on_error():
    kg = KXGaugeClient("http://192.168.1.99")
    with patch("requests.get", side_effect=OSError("unreachable")):
        assert kg.health_check() is False


# ── Bridge integration: settings GET/POST, test endpoint ───────────────────

def _make_bridge(pid="1", **arg_overrides):
    c = MagicMock()
    c.callbacks = {}
    c.connected = False
    args = argparse.Namespace(
        printer_ip="192.168.1.50", mqtt_port=9883, username="", password="",
        mode_id="20030", device_id="", host="127.0.0.1", port=7125,
        data_dir=tempfile.mkdtemp(prefix="kxgauge-"),
        kxgauge_url="", kxgauge_enabled=0, kxgauge_heat_peak=250,
    )
    for k, v in arg_overrides.items():
        setattr(args, k, v)
    all_bridges = {}
    bridge = KobraXBridge(c, args=args, printer_id=pid, all_bridges=all_bridges)
    all_bridges[pid] = bridge
    return bridge


@pytest_asyncio.fixture
async def kxgauge_client():
    bridge = _make_bridge(kxgauge_url="http://192.168.1.77", kxgauge_enabled=1, kxgauge_heat_peak=230)
    # _find_config_path() otherwise resolves to the real repo config/config.ini
    # (env_loader.find_config_path() walks from the script location, ignoring
    # args.data_dir) - a settings POST in a test must never touch that file.
    import pathlib
    sandbox_cfg = pathlib.Path(tempfile.mkdtemp(prefix="kxgauge-cfg-")) / "config.ini"
    bridge._find_config_path = lambda: sandbox_cfg
    bridge._restart_bridge = lambda: None  # a settings POST schedules a restart - don't kill the test process
    app = build_app(bridge)
    async with TestClient(TestServer(app)) as c:
        yield c, bridge


def _fake_get_response(status=200, json_body=None):
    resp = MagicMock()
    resp.status = status
    resp.json = AsyncMock(return_value=json_body or {})
    ctx = MagicMock()
    ctx.__aenter__ = AsyncMock(return_value=resp)
    ctx.__aexit__ = AsyncMock(return_value=False)
    return ctx


@pytest.mark.asyncio
async def test_bridge_creates_kxgauge_client_when_enabled():
    bridge = _make_bridge(kxgauge_url="http://192.168.1.77", kxgauge_enabled=1)
    assert bridge._kxgauge is not None
    assert bridge._kxgauge.base_url == "http://192.168.1.77"


@pytest.mark.asyncio
async def test_bridge_no_kxgauge_client_when_disabled():
    bridge = _make_bridge(kxgauge_url="http://192.168.1.77", kxgauge_enabled=0)
    assert bridge._kxgauge is None


@pytest.mark.asyncio
async def test_bridge_no_kxgauge_client_when_no_url():
    bridge = _make_bridge(kxgauge_enabled=1, kxgauge_url="")
    assert bridge._kxgauge is None


@pytest.mark.asyncio
async def test_settings_roundtrip_persists_kxgauge_fields(kxgauge_client):
    c, bridge = kxgauge_client
    resp = await c.get("/api/settings")
    data = await resp.json()
    assert data["kxgauge_url"] == "http://192.168.1.77"
    assert data["kxgauge_enabled"] == 1
    assert data["kxgauge_heat_peak"] == 230
    assert isinstance(data["kxgauge_mapping"], dict)
    assert data["kxgauge_mapping"]["printing"] == "happy"


@pytest.mark.asyncio
async def test_kxgauge_mapping_default_when_no_config_section():
    bridge = _make_bridge()
    assert bridge._kxgauge_mapping["free"] == "neutral"
    assert bridge._kxgauge_mapping["error"] == "scared"


@pytest.mark.asyncio
async def test_kxgauge_test_endpoint_success(kxgauge_client):
    c, bridge = kxgauge_client
    with patch("aiohttp.ClientSession.get", return_value=_fake_get_response(200, {"emotion": "HAPPY"})) as mock_get:
        resp = await c.post("/api/kxgauge/test", json={})
        data = await resp.json()
    assert resp.status == 200
    assert data["result"] == "ok"
    assert data["emotion"] == "HAPPY"
    assert mock_get.call_args[0][0] == "http://192.168.1.77/status"


@pytest.mark.asyncio
async def test_kxgauge_test_endpoint_uses_url_from_body_over_configured():
    bridge = _make_bridge()  # no configured kxgauge_url
    app = build_app(bridge)
    async with TestClient(TestServer(app)) as c:
        with patch("aiohttp.ClientSession.get", return_value=_fake_get_response(200, {"emotion": "NEUTRAL"})) as mock_get:
            resp = await c.post("/api/kxgauge/test", json={"url": "http://192.168.1.88"})
            data = await resp.json()
        assert resp.status == 200
        assert mock_get.call_args[0][0] == "http://192.168.1.88/status"


@pytest.mark.asyncio
async def test_kxgauge_test_endpoint_no_url_configured_error():
    bridge = _make_bridge()
    app = build_app(bridge)
    async with TestClient(TestServer(app)) as c:
        resp = await c.post("/api/kxgauge/test", json={})
        assert resp.status == 400


@pytest.mark.asyncio
async def test_kxgauge_test_endpoint_unreachable_returns_502(kxgauge_client):
    c, bridge = kxgauge_client
    with patch("aiohttp.ClientSession.get", side_effect=OSError("connection refused")):
        resp = await c.post("/api/kxgauge/test", json={})
    assert resp.status == 502


@pytest.mark.asyncio
async def test_settings_post_saves_custom_mapping(kxgauge_client):
    c, bridge = kxgauge_client
    resp = await c.post("/api/settings", json={
        "printer_ip": "192.168.1.50", "mqtt_port": 9883,
        "kxgauge_url": "http://192.168.1.77", "kxgauge_enabled": 1,
        "kxgauge_heat_peak": 240,
        "kxgauge_mapping": {"printing": "love", "error": "furious"},
    })
    assert resp.status == 200
    assert bridge._kxgauge_mapping["printing"] == "love"
    assert bridge._kxgauge_mapping["error"] == "furious"


@pytest.mark.asyncio
async def test_settings_post_rejects_invalid_emotion(kxgauge_client):
    c, bridge = kxgauge_client
    before = dict(bridge._kxgauge_mapping)
    resp = await c.post("/api/settings", json={
        "printer_ip": "192.168.1.50", "mqtt_port": 9883,
        "kxgauge_mapping": {"printing": "not_a_real_emotion"},
    })
    assert resp.status == 200
    # invalid value must be rejected, mapping keeps its previous value
    assert bridge._kxgauge_mapping["printing"] == before["printing"]


# ── MQTT callback hooks fire the mapped emotion / heat value ───────────────

@pytest.mark.asyncio
async def test_on_temp_pushes_heat_to_kxgauge(kxgauge_client):
    c, bridge = kxgauge_client
    bridge._kxgauge.set_heat_celsius = MagicMock()
    bridge._on_temp({"data": {"curr_nozzle_temp": 205, "target_nozzle_temp": 210,
                               "curr_hotbed_temp": 60, "target_hotbed_temp": 60}})
    bridge._kxgauge.set_heat_celsius.assert_called_once_with(205.0)


@pytest.mark.asyncio
async def test_on_print_pushes_mapped_emotion_to_kxgauge(kxgauge_client):
    c, bridge = kxgauge_client
    bridge._kxgauge.set_emotion = MagicMock()
    bridge._on_print({"state": "printing", "data": {}})
    bridge._kxgauge.set_emotion.assert_called_once_with("happy")


@pytest.mark.asyncio
async def test_on_print_unmapped_state_does_not_call_kxgauge(kxgauge_client):
    c, bridge = kxgauge_client
    bridge._kxgauge_mapping = {}  # no entries at all
    bridge._kxgauge.set_emotion = MagicMock()
    bridge._on_print({"state": "printing", "data": {}})
    bridge._kxgauge.set_emotion.assert_not_called()


@pytest.mark.asyncio
async def test_no_kxgauge_configured_callbacks_are_noop():
    bridge = _make_bridge()  # kxgauge disabled
    assert bridge._kxgauge is None
    # must not raise even though _kxgauge is None
    bridge._on_temp({"data": {"curr_nozzle_temp": 200, "target_nozzle_temp": 200,
                               "curr_hotbed_temp": 60, "target_hotbed_temp": 60}})
    bridge._on_print({"state": "printing", "data": {}})
