"""Integration tests for the MCP server tools.

Tests tool implementations with a MockSerialTransport to verify
correct behavior without real hardware. Each tool function is
called directly with a mock context that provides a pre-populated
GNSSState, bypassing the MCP transport layer (which FastMCP tests
upstream).
"""

from unittest.mock import MagicMock

import pytest

from lc29h.mcp_server import (
    GNSSState,
    configure_constellations,
    dead_reckoning_config,
    easy_enable,
    easy_status,
    elevation_mask,
    get_module_info,
    get_position,
    get_satellites,
    gnss_suspend_resume,
    mcp,
    ntrip_disconnect,
    ntrip_status,
    odometer,
    periodic_mode,
    restart_module,
    save_config,
    set_fix_rate,
    speed_clamping,
    survey_in_start,
    survey_in_status,
)

from .conftest import MockSerialTransport


class MockContext:
    """Mock FastMCP Context for direct tool function testing.

    Provides the same attribute path that real FastMCP context uses:
    ctx.request_context.lifespan_context -> GNSSState
    """

    def __init__(self, state: GNSSState):
        self.request_context = MagicMock()
        self.request_context.lifespan_context = state


@pytest.fixture
def ctx(populated_gnss_state):
    """Create a mock context backed by populated GNSS state."""
    return MockContext(populated_gnss_state)


# ---------------------------------------------------------------------------
# Tool registration / discovery
# ---------------------------------------------------------------------------


class TestToolRegistration:
    """Verify all expected tools and resources are registered on the MCP server."""

    async def test_has_position_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "get_position" in tool_names

    async def test_has_satellites_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "get_satellites" in tool_names

    async def test_has_module_info_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "get_module_info" in tool_names

    async def test_has_configuration_tools(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "configure_constellations" in tool_names
        assert "set_fix_rate" in tool_names
        assert "save_config" in tool_names
        assert "restart_module" in tool_names

    async def test_has_ntrip_tools(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "ntrip_connect" in tool_names
        assert "ntrip_status" in tool_names
        assert "ntrip_disconnect" in tool_names

    async def test_has_survey_tools(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "survey_in_start" in tool_names
        assert "survey_in_status" in tool_names

    async def test_has_dr_tools(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "dead_reckoning_config" in tool_names

    async def test_has_easy_tools(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "easy_status" in tool_names
        assert "easy_enable" in tool_names

    async def test_has_odometer_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "odometer" in tool_names

    async def test_has_elevation_mask_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "elevation_mask" in tool_names

    async def test_has_speed_clamping_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "speed_clamping" in tool_names

    async def test_has_gnss_suspend_resume_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "gnss_suspend_resume" in tool_names

    async def test_has_periodic_mode_tool(self):
        tool_names = [t.name for t in await mcp.list_tools()]
        assert "periodic_mode" in tool_names

    async def test_tool_count(self):
        tools = await mcp.list_tools()
        assert len(tools) >= 18


# ---------------------------------------------------------------------------
# get_position
# ---------------------------------------------------------------------------


class TestGetPosition:
    async def test_returns_valid_position(self, ctx):
        result = await get_position(ctx)
        assert isinstance(result, dict)
        assert "latitude" in result
        assert "longitude" in result
        assert "altitude" in result
        assert "fix_type" in result

    async def test_latitude_range(self, ctx):
        result = await get_position(ctx)
        # From test data: 4334.1234567,N -> ~43.57 degrees
        assert 43.0 < result["latitude"] < 44.0

    async def test_longitude_range(self, ctx):
        result = await get_position(ctx)
        # From test data: 11629.1234567,W -> ~-116.49 degrees
        assert -117.0 < result["longitude"] < -116.0

    async def test_altitude_value(self, ctx):
        result = await get_position(ctx)
        assert result["altitude"] == 830.0

    async def test_fix_type_has_fix(self, ctx):
        result = await get_position(ctx)
        assert result["fix_type"] == "GPS_FIX"

    async def test_satellite_count(self, ctx):
        result = await get_position(ctx)
        assert result["satellites"] == 10

    async def test_hdop_value(self, ctx):
        result = await get_position(ctx)
        assert result["hdop"] == 1.49

    async def test_speed_from_rmc(self, ctx):
        result = await get_position(ctx)
        assert result["speed_knots"] == 0.05

    async def test_course_from_rmc(self, ctx):
        result = await get_position(ctx)
        assert result["course"] == 220.0

    async def test_timestamp_present(self, ctx):
        result = await get_position(ctx)
        assert result["timestamp"] is not None

    async def test_no_fix_state(self):
        """Fresh GNSSState with no data should report NO_FIX."""
        state = GNSSState()
        state.transport = MockSerialTransport()
        ctx = MockContext(state)
        result = await get_position(ctx)
        assert result["fix_type"] == "NO_FIX"
        assert result["latitude"] == 0.0
        assert result["longitude"] == 0.0


# ---------------------------------------------------------------------------
# get_satellites
# ---------------------------------------------------------------------------


class TestGetSatellites:
    async def test_returns_satellite_list(self, ctx):
        result = await get_satellites(ctx)
        assert isinstance(result, list)
        assert len(result) > 0

    async def test_satellite_count(self, ctx):
        result = await get_satellites(ctx)
        # 8 GPS + 6 GLONASS + 2 BeiDou = 16 total
        assert len(result) == 16

    async def test_satellite_fields(self, ctx):
        result = await get_satellites(ctx)
        required_fields = {"prn", "constellation", "elevation", "azimuth", "snr", "used_in_fix"}
        for sat in result:
            assert required_fields.issubset(sat.keys())

    async def test_multi_constellation(self, ctx):
        result = await get_satellites(ctx)
        constellations = {s["constellation"] for s in result}
        assert "GPS" in constellations
        assert "GLONASS" in constellations
        assert "BEIDOU" in constellations

    async def test_gps_satellite_values(self, ctx):
        result = await get_satellites(ctx)
        # PRN 6 GPS: el=53, az=55, snr=22
        gps_prn6 = [s for s in result if s["prn"] == 6 and s["constellation"] == "GPS"]
        assert len(gps_prn6) == 1
        assert gps_prn6[0]["elevation"] == 53
        assert gps_prn6[0]["azimuth"] == 55
        assert gps_prn6[0]["snr"] == 22

    async def test_beidou_satellite_values(self, ctx):
        result = await get_satellites(ctx)
        # PRN 27 BeiDou: el=50, az=53, snr=33
        bd_prn27 = [s for s in result if s["prn"] == 27 and s["constellation"] == "BEIDOU"]
        assert len(bd_prn27) == 1
        assert bd_prn27[0]["snr"] == 33

    async def test_null_snr_for_weak_signals(self, ctx):
        result = await get_satellites(ctx)
        # PRN 31 GPS has no SNR (weak/not tracked)
        gps_prn31 = [s for s in result if s["prn"] == 31 and s["constellation"] == "GPS"]
        assert len(gps_prn31) == 1
        assert gps_prn31[0]["snr"] is None

    async def test_empty_when_no_data(self):
        """No satellites until GSV data is processed and GGA swaps the buffer."""
        state = GNSSState()
        state.transport = MockSerialTransport()
        ctx = MockContext(state)
        result = await get_satellites(ctx)
        assert result == []


# ---------------------------------------------------------------------------
# get_module_info
# ---------------------------------------------------------------------------


class TestGetModuleInfo:
    async def test_returns_dict(self, ctx):
        result = await get_module_info(ctx)
        assert isinstance(result, dict)

    async def test_model_from_pqtmverno(self, ctx):
        result = await get_module_info(ctx)
        # MockSerialTransport returns LC29HDANR11A03S_RSA for PQTMVERNO
        assert "LC29HDA" in result["model"]

    async def test_firmware_version(self, ctx):
        result = await get_module_info(ctx)
        assert "LC29HDANR11A03S_RSA" in result["firmware_version"]

    async def test_chip_model_from_pair021(self, ctx):
        result = await get_module_info(ctx)
        # MockSerialTransport returns AG3335A_V2.6.0 for PAIR021
        assert "AG3335A" in result["chip_model"]

    async def test_build_date(self, ctx):
        result = await get_module_info(ctx)
        assert result["build_date"] == "2024/03/19"

    async def test_not_connected_returns_error(self):
        """With no transport, get_module_info should return an error dict."""
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await get_module_info(ctx)
        assert "error" in result


# ---------------------------------------------------------------------------
# configure_constellations
# ---------------------------------------------------------------------------


class TestConfigureConstellations:
    async def test_configure_all_enabled(self, ctx):
        result = await configure_constellations(
            ctx, gps=True, glonass=True, galileo=True, beidou=True, qzss=True
        )
        assert result["success"] is True
        assert "GPS" in result["constellations"]
        assert "GLONASS" in result["constellations"]
        assert "Galileo" in result["constellations"]
        assert "BeiDou" in result["constellations"]
        assert "QZSS" in result["constellations"]

    async def test_configure_gps_only(self, ctx):
        result = await configure_constellations(
            ctx, gps=True, glonass=False, galileo=False, beidou=False, qzss=False
        )
        assert result["success"] is True
        assert result["constellations"] == ["GPS"]

    async def test_configure_partial(self, ctx):
        result = await configure_constellations(
            ctx, gps=True, glonass=True, galileo=False, beidou=True, qzss=False
        )
        assert result["success"] is True
        assert "Galileo" not in result["constellations"]
        assert "QZSS" not in result["constellations"]

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await configure_constellations(ctx, gps=True)
        assert "error" in result

    async def test_sends_command_to_transport(self, ctx):
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await configure_constellations(ctx, gps=True, glonass=False)
        assert len(transport._written) > initial_writes


# ---------------------------------------------------------------------------
# set_fix_rate
# ---------------------------------------------------------------------------


class TestSetFixRate:
    async def test_set_1hz(self, ctx):
        result = await set_fix_rate(ctx, ms=1000)
        assert result["success"] is True
        assert result["fix_rate_ms"] == 1000
        assert result["hz"] == 1.0

    async def test_set_10hz(self, ctx):
        result = await set_fix_rate(ctx, ms=100)
        assert result["success"] is True
        assert result["fix_rate_ms"] == 100
        assert result["hz"] == 10.0

    async def test_set_2hz(self, ctx):
        result = await set_fix_rate(ctx, ms=500)
        assert result["success"] is True
        assert result["hz"] == 2.0

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await set_fix_rate(ctx, ms=1000)
        assert "error" in result


# ---------------------------------------------------------------------------
# save_config
# ---------------------------------------------------------------------------


class TestSaveConfig:
    async def test_save_success(self, ctx):
        result = await save_config(ctx)
        assert result["success"] is True

    async def test_sends_both_commands(self, ctx):
        """save_config should send PAIR513 and PQTMSAVEPAR."""
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await save_config(ctx)
        # Two write() calls: pair_save_config + pqtm_save_params
        assert len(transport._written) == initial_writes + 2

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await save_config(ctx)
        assert "error" in result


# ---------------------------------------------------------------------------
# restart_module
# ---------------------------------------------------------------------------


class TestRestartModule:
    async def test_hot_restart(self, ctx):
        result = await restart_module(ctx, mode="hot")
        assert result["success"] is True
        assert result["mode"] == "hot"

    async def test_warm_restart(self, ctx):
        result = await restart_module(ctx, mode="warm")
        assert result["success"] is True
        assert result["mode"] == "warm"

    async def test_cold_restart(self, ctx):
        result = await restart_module(ctx, mode="cold")
        assert result["success"] is True
        assert result["mode"] == "cold"

    async def test_invalid_mode(self, ctx):
        result = await restart_module(ctx, mode="factory")
        assert "error" in result

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await restart_module(ctx, mode="hot")
        assert "error" in result


# ---------------------------------------------------------------------------
# NTRIP tools
# ---------------------------------------------------------------------------


class TestNtripStatus:
    async def test_disconnected_by_default(self, ctx):
        result = await ntrip_status(ctx)
        assert result["connected"] is False

    async def test_no_client_means_disconnected(self):
        state = GNSSState()
        state.transport = MockSerialTransport()
        ctx = MockContext(state)
        result = await ntrip_status(ctx)
        assert result["connected"] is False


class TestNtripDisconnect:
    async def test_disconnect_when_not_connected(self, ctx):
        result = await ntrip_disconnect(ctx)
        assert result["success"] is False
        assert "error" in result


# ---------------------------------------------------------------------------
# Survey-in tools
# ---------------------------------------------------------------------------


class TestSurveyInStart:
    async def test_start_survey(self, ctx):
        result = await survey_in_start(ctx, duration_s=3600, accuracy_m=1.0)
        assert result["success"] is True
        assert result["duration_s"] == 3600
        assert result["accuracy_m"] == 1.0

    async def test_custom_params(self, ctx):
        result = await survey_in_start(ctx, duration_s=7200, accuracy_m=0.5)
        assert result["duration_s"] == 7200
        assert result["accuracy_m"] == 0.5

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await survey_in_start(ctx, duration_s=3600, accuracy_m=1.0)
        assert "error" in result


class TestSurveyInStatus:
    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await survey_in_status(ctx)
        assert "error" in result


# ---------------------------------------------------------------------------
# dead_reckoning_config
# ---------------------------------------------------------------------------


class TestDeadReckoningConfig:
    async def test_read_dr_mode(self, ctx):
        result = await dead_reckoning_config(ctx, mode=-1)
        assert "response" in result
        assert "PQTMCFGDRMODE" in result["response"]

    async def test_set_gnss_only(self, ctx):
        result = await dead_reckoning_config(ctx, mode=0)
        assert result["success"] is True
        assert result["mode"] == 0
        assert result["description"] == "GNSS only"

    async def test_set_dr_only(self, ctx):
        result = await dead_reckoning_config(ctx, mode=1)
        assert result["success"] is True
        assert result["mode"] == 1
        assert result["description"] == "DR only"

    async def test_set_auto(self, ctx):
        result = await dead_reckoning_config(ctx, mode=2)
        assert result["success"] is True
        assert result["mode"] == 2
        assert result["description"] == "GNSS+DR auto"

    async def test_invalid_mode(self, ctx):
        result = await dead_reckoning_config(ctx, mode=5)
        assert "error" in result

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await dead_reckoning_config(ctx, mode=0)
        assert "error" in result

    async def test_not_connected_read_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await dead_reckoning_config(ctx, mode=-1)
        assert "error" in result


# ---------------------------------------------------------------------------
# easy_status / easy_enable
# ---------------------------------------------------------------------------


class TestEasyStatus:
    async def test_query_easy_status(self, ctx):
        result = await easy_status(ctx)
        assert "response" in result
        assert "PAIR491" in result["response"]

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await easy_status(ctx)
        assert "error" in result


class TestEasyEnable:
    async def test_enable_easy(self, ctx):
        result = await easy_enable(ctx, enabled=True)
        assert result["success"] is True
        assert result["enabled"] is True

    async def test_disable_easy(self, ctx):
        result = await easy_enable(ctx, enabled=False)
        assert result["success"] is True
        assert result["enabled"] is False

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await easy_enable(ctx, enabled=True)
        assert "error" in result


# ---------------------------------------------------------------------------
# odometer
# ---------------------------------------------------------------------------


class TestOdometer:
    async def test_enable_odometer(self, ctx):
        result = await odometer(ctx, action="enable")
        assert result["success"] is True
        assert result["action"] == "enable"
        assert result["enabled"] is True

    async def test_disable_odometer(self, ctx):
        result = await odometer(ctx, action="disable")
        assert result["success"] is True
        assert result["action"] == "disable"
        assert result["enabled"] is False

    async def test_reset_odometer(self, ctx):
        result = await odometer(ctx, action="reset")
        assert result["success"] is True
        assert result["action"] == "reset"

    async def test_invalid_action(self, ctx):
        result = await odometer(ctx, action="start")
        assert "error" in result

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await odometer(ctx, action="enable")
        assert "error" in result

    async def test_sends_command_to_transport(self, ctx):
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await odometer(ctx, action="enable")
        assert len(transport._written) > initial_writes


# ---------------------------------------------------------------------------
# elevation_mask
# ---------------------------------------------------------------------------


class TestElevationMask:
    async def test_read_elevation_mask(self, ctx):
        result = await elevation_mask(ctx, degrees=-1)
        assert "response" in result
        assert "PQTMCFGEAMASK" in result["response"]

    async def test_set_elevation_mask(self, ctx):
        result = await elevation_mask(ctx, degrees=10)
        assert result["success"] is True
        assert result["elevation_mask_degrees"] == 10

    async def test_set_zero_mask(self, ctx):
        result = await elevation_mask(ctx, degrees=0)
        assert result["success"] is True
        assert result["elevation_mask_degrees"] == 0

    async def test_set_max_mask(self, ctx):
        result = await elevation_mask(ctx, degrees=90)
        assert result["success"] is True
        assert result["elevation_mask_degrees"] == 90

    async def test_invalid_mask_too_high(self, ctx):
        result = await elevation_mask(ctx, degrees=91)
        assert "error" in result

    async def test_invalid_mask_negative(self, ctx):
        # -1 means "read", so test with -2
        result = await elevation_mask(ctx, degrees=-2)
        assert "error" in result

    async def test_not_connected_read_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await elevation_mask(ctx, degrees=-1)
        assert "error" in result

    async def test_not_connected_set_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await elevation_mask(ctx, degrees=10)
        assert "error" in result


# ---------------------------------------------------------------------------
# speed_clamping
# ---------------------------------------------------------------------------


class TestSpeedClamping:
    async def test_enable_clamping(self, ctx):
        result = await speed_clamping(ctx, enabled=True)
        assert result["success"] is True
        assert result["enabled"] is True

    async def test_disable_clamping(self, ctx):
        result = await speed_clamping(ctx, enabled=False)
        assert result["success"] is True
        assert result["enabled"] is False

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await speed_clamping(ctx, enabled=True)
        assert "error" in result

    async def test_sends_command_to_transport(self, ctx):
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await speed_clamping(ctx, enabled=True)
        assert len(transport._written) > initial_writes


# ---------------------------------------------------------------------------
# gnss_suspend_resume
# ---------------------------------------------------------------------------


class TestGnssSuspendResume:
    async def test_suspend(self, ctx):
        result = await gnss_suspend_resume(ctx, action="suspend")
        assert result["success"] is True
        assert result["action"] == "suspend"

    async def test_resume(self, ctx):
        result = await gnss_suspend_resume(ctx, action="resume")
        assert result["success"] is True
        assert result["action"] == "resume"

    async def test_invalid_action(self, ctx):
        result = await gnss_suspend_resume(ctx, action="pause")
        assert "error" in result

    async def test_not_connected_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await gnss_suspend_resume(ctx, action="suspend")
        assert "error" in result

    async def test_suspend_sends_write(self, ctx):
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await gnss_suspend_resume(ctx, action="suspend")
        assert len(transport._written) == initial_writes + 1

    async def test_resume_sends_write(self, ctx):
        state = ctx.request_context.lifespan_context
        transport = state.transport
        initial_writes = len(transport._written)
        await gnss_suspend_resume(ctx, action="resume")
        assert len(transport._written) == initial_writes + 1


# ---------------------------------------------------------------------------
# periodic_mode
# ---------------------------------------------------------------------------


class TestPeriodicMode:
    async def test_read_periodic_mode(self, ctx):
        result = await periodic_mode(ctx, mode=-1)
        assert "response" in result
        assert "PAIR691" in result["response"]

    async def test_disable_periodic(self, ctx):
        result = await periodic_mode(ctx, mode=0)
        assert result["success"] is True
        assert result["mode"] == 0
        assert result["description"] == "disabled"

    async def test_smart_mode(self, ctx):
        result = await periodic_mode(ctx, mode=1, run_ms=2000, sleep_ms=5000)
        assert result["success"] is True
        assert result["mode"] == 1
        assert result["description"] == "smart"
        assert result["run_ms"] == 2000
        assert result["sleep_ms"] == 5000

    async def test_strict_mode(self, ctx):
        result = await periodic_mode(ctx, mode=2, run_ms=500, sleep_ms=3000)
        assert result["success"] is True
        assert result["mode"] == 2
        assert result["description"] == "strict"
        assert result["run_ms"] == 500
        assert result["sleep_ms"] == 3000

    async def test_invalid_mode(self, ctx):
        result = await periodic_mode(ctx, mode=3)
        assert "error" in result

    async def test_not_connected_read_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await periodic_mode(ctx, mode=-1)
        assert "error" in result

    async def test_not_connected_set_returns_error(self):
        state = GNSSState()
        state.transport = None
        ctx = MockContext(state)
        result = await periodic_mode(ctx, mode=1)
        assert "error" in result


# ---------------------------------------------------------------------------
# Edge cases and error handling
# ---------------------------------------------------------------------------


class TestEdgeCases:
    async def test_position_all_fields_present(self, ctx):
        """Every expected field should be in the response dict."""
        result = await get_position(ctx)
        expected_keys = {
            "latitude",
            "longitude",
            "altitude",
            "fix_type",
            "satellites",
            "hdop",
            "speed_knots",
            "course",
            "timestamp",
        }
        assert expected_keys == set(result.keys())

    async def test_satellite_constellation_names_are_strings(self, ctx):
        """Constellation should be a string name, not an int enum value."""
        result = await get_satellites(ctx)
        for sat in result:
            assert isinstance(sat["constellation"], str)
            assert sat["constellation"] in {"GPS", "GLONASS", "GALILEO", "BEIDOU", "QZSS"}

    async def test_module_info_all_fields_present(self, ctx):
        """Module info should include all expected fields."""
        result = await get_module_info(ctx)
        expected_keys = {
            "model",
            "firmware_version",
            "build_date",
            "build_time",
            "chip_model",
            "chip_version",
        }
        assert expected_keys == set(result.keys())

    async def test_fix_rate_hz_calculation(self, ctx):
        """Verify hz is computed as 1000/ms."""
        result = await set_fix_rate(ctx, ms=200)
        assert result["hz"] == 5.0

    async def test_mock_transport_records_writes(self, ctx):
        """Verify the mock transport captures written commands."""
        state = ctx.request_context.lifespan_context
        transport = state.transport
        await restart_module(ctx, mode="hot")
        assert len(transport._written) > 0
        # Written data should be bytes (framed NMEA command)
        assert isinstance(transport._written[-1], bytes)
