"""Tests for app-side trip time measurement and ACK hash collision handling."""

import builtins
import gettext
import time
from unittest.mock import MagicMock, patch

if not hasattr(builtins, "_"):
    gettext.install("meshy")

from meshy.frame_handler import FrameHandler
from meshy.message_controller import MessageController
from meshy.models import Contact, ContactType, MessageStatus
from meshy.protocol import PUSH_CODE_SEND_CONFIRMED
from meshy.storage import Storage


def _make_contact(path_length=-1, path=b""):
    return Contact(
        public_key=bytes(range(32)),
        name="Test",
        type=ContactType.CHAT,
        path_length=path_length,
        path=path,
    )


def _make_ctrl():
    storage = Storage(db_path=":memory:")
    win = MagicMock()
    win._storage = storage
    win._contacts_by_key = {}
    ctrl = MessageController(win)
    return ctrl, storage


# ─── _do_send_message records send time ──────────────────────


class TestSendTimeRecording:
    def setup_method(self):
        self.ctrl, self.storage = _make_ctrl()

    def teardown_method(self):
        self.storage.close()

    def test_do_send_message_records_last_send_mono(self):
        contact = _make_contact()
        msg_id = "msg-1"
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "text": "hello",
            "timestamp": int(time.time()),
            "attempt": 0,
            "strategy": "force_flood",
        }
        before = time.monotonic()
        self.ctrl._do_send_message(msg_id)
        after = time.monotonic()

        ctx = self.ctrl.retry_context[msg_id]
        assert "last_send_mono" in ctx
        assert before <= ctx["last_send_mono"] <= after


# ─── register_ack_key stores send time ───────────────────────


class TestRegisterAckKey:
    def setup_method(self):
        self.ctrl, self.storage = _make_ctrl()

    def teardown_method(self):
        self.storage.close()

    def test_register_ack_key_stores_send_time(self):
        msg_id = "msg-1"
        send_time = time.monotonic()
        self.ctrl.retry_context[msg_id] = {
            "attempt": 0,
            "attempt_paths": {0: {"path_length": 2, "path_bytes": "aabb"}},
            "last_send_mono": send_time,
        }
        self.ctrl.register_ack_key(msg_id, "deadbeef")

        ctx = self.ctrl.retry_context[msg_id]
        assert ctx["ack_send_times"]["deadbeef"] == send_time
        assert ctx["ack_paths"]["deadbeef"] == {"path_length": 2, "path_bytes": "aabb"}

    def test_collision_overwrites_with_latest_send_time(self):
        msg_id = "msg-1"
        t0 = time.monotonic()
        self.ctrl.retry_context[msg_id] = {
            "attempt": 0,
            "attempt_paths": {
                0: {"path_length": 2, "path_bytes": "aabb"},
            },
            "last_send_mono": t0,
        }
        self.ctrl.register_ack_key(msg_id, "deadbeef")

        t4 = time.monotonic()
        ctx = self.ctrl.retry_context[msg_id]
        ctx["attempt"] = 4
        ctx["attempt_paths"][4] = {"path_length": -1, "path_bytes": ""}
        ctx["last_send_mono"] = t4
        self.ctrl.register_ack_key(msg_id, "deadbeef")

        assert ctx["ack_send_times"]["deadbeef"] == t4
        assert ctx["ack_paths"]["deadbeef"]["path_length"] == -1


# ─── Grace context preserves send times ──────────────────────


class TestGraceContextSendTimes:
    def setup_method(self):
        self.ctrl, self.storage = _make_ctrl()

    def teardown_method(self):
        self.storage.close()

    @patch("meshy.message_controller.GLib")
    def test_grace_context_includes_ack_send_times(self, mock_glib):
        contact = _make_contact()
        msg_id = "msg-1"
        send_time = 12345.678
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "attempt": 2,
            "strategy": "force_flood",
            "ack_paths": {"aabb": {"path_length": -1}},
            "ack_send_times": {"aabb": send_time},
            "path_used": {"path_length": -1},
        }
        self.ctrl._acks_by_msg[msg_id] = {"aabb"}
        self.ctrl.pending_acks["aabb"] = msg_id
        self.ctrl._on_message_timeout(msg_id, "aabb")

        assert msg_id in self.ctrl.grace_contexts
        assert self.ctrl.grace_contexts[msg_id]["ack_send_times"]["aabb"] == send_time


# ─── Frame handler uses app-side trip time ───────────────────


def _make_fh():
    storage = Storage(db_path=":memory:")
    win = MagicMock()
    win._storage = storage
    win._self_info = {"name": "Me"}
    win._channels_view.on_path_variant_received.return_value = False
    win._channels = []
    win._channels_view_inst = None
    win._conn_ctrl.channel_sync_index = -1
    win._device_info.path_hash_mode = 0
    ctrl = MessageController(win)
    win._msg_ctrl = ctrl
    fh = FrameHandler(win)
    return fh, ctrl, storage


def _ack_frame(ack_key_hex, trip_time_ms):
    return (
        bytes([PUSH_CODE_SEND_CONFIRMED])
        + bytes.fromhex(ack_key_hex)
        + trip_time_ms.to_bytes(4, "little")
    )


class TestFrameHandlerAppTripTime:
    def setup_method(self):
        self.fh, self.ctrl, self.storage = _make_fh()

    def teardown_method(self):
        self.storage.close()

    def test_primary_ack_uses_app_side_trip_time(self):
        msg_id = "msg-1"
        ack_key = "deadbeef"
        send_mono = time.monotonic() - 2.5

        contact = _make_contact()
        self.ctrl.pending_acks[ack_key] = msg_id
        self.ctrl._acks_by_msg[msg_id] = {ack_key}
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "attempt": 0,
            "strategy": "force_flood",
            "path_used": {"path_length": -1, "path_bytes": "", "hop_count": 0},
            "ack_paths": {ack_key: {"path_length": -1, "path_bytes": ""}},
            "ack_send_times": {ack_key: send_mono},
        }

        self.fh.handle_frame(_ack_frame(ack_key, 99999))

        status_call = self.fh._win.set_message_status.call_args
        assert status_call[0][1] == MessageStatus.DELIVERED
        detail = status_call[0][2]
        # App-side ~2500ms, not firmware's 99999ms
        assert "99" not in detail

    def test_fallback_to_firmware_trip_time_without_send_time(self):
        msg_id = "msg-2"
        ack_key = "cafebabe"
        contact = _make_contact()
        self.ctrl.pending_acks[ack_key] = msg_id
        self.ctrl._acks_by_msg[msg_id] = {ack_key}
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "attempt": 0,
            "strategy": "force_flood",
            "path_used": {"path_length": -1, "path_bytes": "", "hop_count": 0},
        }

        self.fh.handle_frame(_ack_frame(ack_key, 5000))

        status_call = self.fh._win.set_message_status.call_args
        detail = status_call[0][2]
        assert "5.0" in detail and "s" in detail

    def test_grace_ack_uses_app_side_trip_time(self):
        msg_id = "msg-3"
        ack_key = "11223344"
        send_mono = time.monotonic() - 1.2

        self.ctrl.grace_acks[ack_key] = msg_id
        self.ctrl._grace_by_msg[msg_id] = {ack_key}
        self.ctrl.grace_contexts[msg_id] = {
            "contact_key": "00" * 32,
            "ack_paths": {ack_key: {"path_length": 2, "path_bytes": "aabb", "hop_count": 2}},
            "ack_send_times": {ack_key: send_mono},
            "path_used": {"path_length": 2, "path_bytes": "aabb", "hop_count": 2},
        }

        self.fh.handle_frame(_ack_frame(ack_key, 88888))

        status_call = self.fh._win.set_message_status.call_args
        detail = status_call[0][2]
        assert "late" in detail
        # App-side ~1200ms, not firmware's 88888ms
        assert "88" not in detail

    def test_collision_scenario_uses_latest_attempt_time(self):
        """Attempts 0 and 4 produce the same ACK hash (attempt & 3 collision).

        Attempt 0 was sent ~100s ago (timed out), attempt 4 was sent ~2s ago.
        The ACK arrives and should report ~2s, not ~100s.
        """
        msg_id = "msg-4"
        ack_key = "aabbccdd"
        now = time.monotonic()
        t4 = now - 2  # attempt 4: 2s ago (attempt 0's time was overwritten)

        contact = _make_contact()
        self.ctrl.pending_acks[ack_key] = msg_id
        self.ctrl._acks_by_msg[msg_id] = {ack_key}
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "attempt": 4,
            "strategy": "smart",
            "path_used": {"path_length": -1, "path_bytes": "", "hop_count": 0},
            "ack_paths": {ack_key: {"path_length": -1, "path_bytes": ""}},
            "ack_send_times": {ack_key: t4},
        }

        self.fh.handle_frame(_ack_frame(ack_key, 100000))

        status_call = self.fh._win.set_message_status.call_args
        detail = status_call[0][2]
        trip_val = float(detail.replace(" ", "").replace(" ", "").rstrip("s").replace(",", "."))
        assert trip_val < 10  # app-side ~2s, not firmware's 100s
