"""End-to-end integration test for MQTT packet publishing.

Asserts the full wire path: AirtimeManager computes airtime_ms, the value is
stored on packet_record, PacketRecord.from_packet_record serializes it as the
``duration`` field, and mqtt_handler.publish_packet hands a JSON payload to
the paho-mqtt client whose topic and content match the documented contract.

This complements ``tests/test_packet_duration.py`` (which unit-tests the
serializer and Semtech formula in isolation) by locking the wire format end
to end. paho-mqtt's network layer is mocked so no real broker is required.
"""

import json
from unittest.mock import MagicMock

from repeater.airtime import AirtimeManager
from repeater.data_acquisition.mqtt_handler import MeshCoreToMqttPusher
from repeater.data_acquisition.storage_utils import PacketRecord


# --------------------------------------------------------------------
# Test scaffolding
# --------------------------------------------------------------------
class _FakeIdentity:
    """Minimal LocalIdentity stand-in for constructor wiring."""

    def __init__(self, public_key_hex: str):
        self._pk = bytes.fromhex(public_key_hex)

    def get_public_key(self) -> bytes:
        return self._pk


def _make_config(format_value: str = "letsmesh", iata_code: str = "LAX") -> dict:
    """Minimal pyMC_Repeater config sufficient to construct MeshCoreToMqttPusher."""
    return {
        "repeater": {"node_name": "test-node"},
        "radio": {
            "spreading_factor": 8,
            "bandwidth": 62500,
            "coding_rate": 8,
            "preamble_length": 17,
            "frequency": 869618000,
            "tx_power": 14,
        },
        "duty_cycle": {"max_airtime_per_minute": 3600},
        "mqtt_brokers": {
            "iata_code": iata_code,
            "status_interval": 0,
            "owner": "",
            "email": "",
            "brokers": [
                {
                    "name": "test-broker",
                    "enabled": True,
                    "host": "broker.example",
                    "port": 1883,
                    "transport": "tcp",
                    "format": format_value,
                    "use_jwt_auth": False,
                    "tls": {"enabled": False, "insecure": False},
                }
            ],
        },
    }


def _attach_capturing_client(conn) -> list:
    """Replace ``conn.client`` with a Mock and return the capture list.

    The list is appended to on every paho-mqtt ``client.publish`` call. We also
    flip ``conn._running = True`` so the publish path doesn't short-circuit on
    the "not connected" guard.
    """
    captured: list = []

    def _fake_publish(topic, payload, retain=False, qos=0):
        captured.append({"topic": topic, "payload": payload, "retain": retain, "qos": qos})
        return None

    conn._running = True
    conn.client = MagicMock()
    conn.client.publish = _fake_publish
    return captured


# --------------------------------------------------------------------
# End-to-end integration test
# --------------------------------------------------------------------
def test_mqtt_published_packet_carries_semtech_duration_end_to_end():
    """A packet flowing through the publisher must produce JSON whose
    ``duration`` field equals the Semtech-derived airtime in ms.

    Steps mirror production:
      1. AirtimeManager (engine.py would call this in _build_packet_record).
      2. Store airtime_ms on the packet_record dict.
      3. PacketRecord.from_packet_record serializes it as 'duration'.
      4. mqtt_handler.publish_packet flows JSON to the paho client.
      5. Topic and JSON content are asserted byte-by-byte.
    """
    config = _make_config(format_value="letsmesh", iata_code="LAX")
    public_key_hex = "AB" * 32  # 64 hex chars = 32-byte Ed25519 pubkey
    identity = _FakeIdentity(public_key_hex)

    # Construct the real publisher; this builds one _BrokerConnection that
    # holds a paho-mqtt Client object. We then swap that object for a Mock.
    pusher = MeshCoreToMqttPusher(local_identity=identity, config=config)
    assert len(pusher.connections) == 1
    captured = _attach_capturing_client(pusher.connections[0])

    # Step 1: compute airtime exactly the way engine.py does.
    raw_bytes = bytes(range(40))  # 40-byte packet (typical MeshCore size)
    airtime_mgr = AirtimeManager(config)
    expected_airtime_ms = airtime_mgr.calculate_airtime(len(raw_bytes))
    expected_duration = str(int(round(expected_airtime_ms)))

    # Step 2: build a packet_record with airtime_ms (matches _build_packet_record).
    packet_record = {
        "timestamp": 1700000000.0,
        "type": 4,
        "route": 1,
        "rssi": -90,
        "snr": 7.5,
        "score": 0.5,
        "payload_length": 32,
        "packet_hash": "DEADBEEF" + "00" * 4,
        "raw_packet": raw_bytes.hex(),
        "airtime_ms": expected_airtime_ms,
    }

    # Step 3: serialize via the production code path.
    record = PacketRecord.from_packet_record(
        packet_record, origin="test-node", origin_id=public_key_hex.upper()
    )
    assert record is not None

    # Step 4: publish via the real publisher chain.
    pusher.publish_packet(record.to_dict())

    # Step 5: assertions on the wire output.
    assert len(captured) == 1, "expected exactly one paho publish call"
    publish = captured[0]

    # Topic follows the MC2MQTT family convention for letsmesh format.
    assert publish["topic"] == f"meshcore/LAX/{public_key_hex.upper()}/packets"

    # Payload is JSON; parse and verify duration is the Semtech value.
    payload_dict = json.loads(publish["payload"])
    assert payload_dict["duration"] == expected_duration
    assert payload_dict["duration"] != "0", "duration must not be hard-coded zero"
    assert 0 < int(payload_dict["duration"]) < 10_000, "duration should be a sane time-on-air in ms"

    # Sanity: other key fields flowed through correctly.
    assert payload_dict["origin"] == "test-node"
    assert payload_dict["origin_id"] == public_key_hex.upper()
    assert payload_dict["len"] == str(len(raw_bytes))
    assert payload_dict["raw"] == raw_bytes.hex()
    assert payload_dict["RSSI"] == "-90"
    assert payload_dict["type"] == "PACKET"
    assert payload_dict["direction"] == "rx"
    assert payload_dict["route"] == "F"  # route=1 -> "F" (flood)


def test_mqtt_published_packet_topic_uses_mc2mqtt_structure_for_waev_format():
    """The waev format flavor must publish on the same MC2MQTT topic structure
    as letsmesh. Locks the contract that all MC2MQTT family formats share
    ``meshcore/{IATA}/{PUBKEY}/packets``.
    """
    config = _make_config(format_value="waev", iata_code="SFO")
    public_key_hex = "CD" * 32
    identity = _FakeIdentity(public_key_hex)

    pusher = MeshCoreToMqttPusher(local_identity=identity, config=config)
    captured = _attach_capturing_client(pusher.connections[0])

    raw_bytes = bytes(range(20))
    airtime_mgr = AirtimeManager(config)
    packet_record = {
        "timestamp": 1700000000.0,
        "type": 1,
        "route": 2,
        "rssi": -75,
        "snr": 5.0,
        "score": 0.3,
        "payload_length": 14,
        "packet_hash": "CAFEBABE" + "00" * 4,
        "raw_packet": raw_bytes.hex(),
        "airtime_ms": airtime_mgr.calculate_airtime(len(raw_bytes)),
    }
    record = PacketRecord.from_packet_record(
        packet_record, origin="test", origin_id=public_key_hex.upper()
    )
    pusher.publish_packet(record.to_dict())

    assert len(captured) == 1
    assert captured[0]["topic"] == f"meshcore/SFO/{public_key_hex.upper()}/packets"
    payload_dict = json.loads(captured[0]["payload"])
    assert int(payload_dict["duration"]) > 0
    assert payload_dict["route"] == "D"  # route=2 -> "D" (direct)


def test_mqtt_published_packet_legacy_mqtt_format_uses_singular_packet_topic():
    """The legacy ``format: mqtt`` path keeps its custom topic shape:
    ``meshcore/repeater/{node_name}/packet`` (singular). This test locks the
    backward-compat behavior we explicitly preserve.
    """
    config = _make_config(format_value="mqtt", iata_code="LAX")
    public_key_hex = "EF" * 32
    identity = _FakeIdentity(public_key_hex)

    pusher = MeshCoreToMqttPusher(local_identity=identity, config=config)
    captured = _attach_capturing_client(pusher.connections[0])

    raw_bytes = bytes(range(16))
    airtime_mgr = AirtimeManager(config)
    packet_record = {
        "timestamp": 1700000000.0,
        "type": 1,
        "route": 1,
        "rssi": -80,
        "snr": 4.0,
        "score": 0.2,
        "payload_length": 10,
        "packet_hash": "BADC0DE0" + "00" * 4,
        "raw_packet": raw_bytes.hex(),
        "airtime_ms": airtime_mgr.calculate_airtime(len(raw_bytes)),
    }
    record = PacketRecord.from_packet_record(
        packet_record, origin="test-node", origin_id=public_key_hex.upper()
    )
    pusher.publish_packet(record.to_dict())

    assert len(captured) == 1
    # Legacy "mqtt" format: custom topic prefix + singular subtopic
    assert captured[0]["topic"] == "meshcore/repeater/test-node/packet"
    payload_dict = json.loads(captured[0]["payload"])
    # Duration still flows through correctly even on the legacy topic
    assert int(payload_dict["duration"]) > 0


def test_mqtt_published_packet_direction_uses_transmitted_flag_for_tx():
    """When engine marks packet_record.transmitted=True, published direction must be tx."""
    config = _make_config(format_value="letsmesh", iata_code="LAX")
    public_key_hex = "11" * 32
    identity = _FakeIdentity(public_key_hex)

    pusher = MeshCoreToMqttPusher(local_identity=identity, config=config)
    captured = _attach_capturing_client(pusher.connections[0])

    raw_bytes = bytes(range(12))
    airtime_mgr = AirtimeManager(config)
    packet_record = {
        "timestamp": 1700000000.0,
        "type": 8,
        "route": 1,
        "rssi": -88,
        "snr": 6.0,
        "score": 0.4,
        "payload_length": 6,
        "packet_hash": "FACEFEED" + "00" * 4,
        "raw_packet": raw_bytes.hex(),
        "airtime_ms": airtime_mgr.calculate_airtime(len(raw_bytes)),
        "transmitted": True,
    }

    record = PacketRecord.from_packet_record(
        packet_record, origin="test-node", origin_id=public_key_hex.upper()
    )
    assert record is not None

    pusher.publish_packet(record.to_dict())

    assert len(captured) == 1
    payload_dict = json.loads(captured[0]["payload"])
    assert payload_dict["direction"] == "tx"
