"""Tests for MessageController routing logic."""

from unittest.mock import MagicMock, patch

from meshy.message_controller import (
    FLOOD_RETRIES,
    PATH_RETRIES,
    MessageController,
)
from meshy.models import Contact, ContactType
from meshy.storage import Storage


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


def _make_win(storage):
    win = MagicMock()
    win._storage = storage
    return win


def _make_ctrl(storage=None):
    if storage is None:
        storage = Storage(db_path=":memory:")
    win = _make_win(storage)
    ctrl = MessageController(win)
    return ctrl, storage


# ─── _determine_strategy() ───────────────────────────────────


class TestDetermineStrategy:
    def test_override_direct(self):
        c = _make_contact(smart=True)
        assert MessageController._determine_strategy(c, "direct") == "override_direct"

    def test_override_flood(self):
        c = _make_contact(smart=True)
        assert MessageController._determine_strategy(c, "flood") == "override_flood"

    def test_force_flood(self):
        c = _make_contact(smart=False, force_flood=True)
        assert MessageController._determine_strategy(c, None) == "force_flood"

    def test_smart(self):
        c = _make_contact(smart=True)
        assert MessageController._determine_strategy(c, None) == "smart"

    def test_traditional(self):
        c = _make_contact(smart=False)
        assert MessageController._determine_strategy(c, None) == "traditional"

    def test_override_takes_precedence_over_force_flood(self):
        c = _make_contact(smart=True, force_flood=True)
        assert MessageController._determine_strategy(c, "direct") == "override_direct"


# ─── max_retries_for() ────────────────────────────────────────


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

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

    def test_override_direct_returns_1(self):
        ctx = {"strategy": "override_direct"}
        assert self.ctrl.max_retries_for(ctx) == 1

    def test_override_flood_returns_flood_retries(self):
        ctx = {"strategy": "override_flood"}
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES

    def test_force_flood_returns_flood_retries(self):
        ctx = {"strategy": "force_flood"}
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES

    def test_smart_no_history_returns_flood_retries(self):
        contact = _make_contact(smart=True)
        ctx = {"strategy": "smart", "contact": contact}
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES

    def test_smart_with_history_returns_path_plus_one(self):
        contact = _make_contact(smart=True)
        ctx = {"strategy": "smart", "contact": contact}
        self.storage.upsert_path_record(
            contact.public_key_hex,
            "aabb",
            1,
            2,
            success=True,
            trip_ms=100,
        )
        assert self.ctrl.max_retries_for(ctx) == PATH_RETRIES + 1

    def test_smart_retries_update_live_after_path_added(self):
        contact = _make_contact(smart=True)
        ctx = {"strategy": "smart", "contact": contact}
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES
        self.storage.upsert_path_record(
            contact.public_key_hex,
            "aabb",
            1,
            2,
            success=True,
            trip_ms=100,
        )
        assert self.ctrl.max_retries_for(ctx) == PATH_RETRIES + 1

    def test_smart_retries_drop_after_weight_zero(self):
        contact = _make_contact(smart=True)
        ctx = {"strategy": "smart", "contact": contact}
        self.storage.upsert_path_record(
            contact.public_key_hex,
            "aabb",
            1,
            2,
            success=True,
        )
        assert self.ctrl.max_retries_for(ctx) == PATH_RETRIES + 1
        for _ in range(20):
            self.storage.upsert_path_record(
                contact.public_key_hex,
                "aabb",
                1,
                2,
                success=False,
                weight_increment=-0.2,
            )
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES

    def test_traditional_with_path_returns_path_plus_one(self):
        ctx = {"strategy": "traditional", "started_with_path": True}
        assert self.ctrl.max_retries_for(ctx) == PATH_RETRIES + 1

    def test_traditional_flood_returns_flood_retries(self):
        ctx = {"strategy": "traditional", "started_with_path": False}
        assert self.ctrl.max_retries_for(ctx) == FLOOD_RETRIES


# ─── _apply_smart_route() ────────────────────────────────────


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

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

    def test_no_history_resets_to_flood(self):
        contact = _make_contact(smart=True, path_length=2, path=b"\xaa\xbb")
        ctx = {"contact": contact, "attempt": 0}
        self.ctrl._apply_smart_route(ctx, contact)
        assert contact.path_length == -1
        assert contact.path == b""

    def test_attempt_past_path_retries_resets_to_flood(self):
        contact = _make_contact(smart=True, path_length=2, path=b"\xaa\xbb")
        self.storage.upsert_path_record(
            contact.public_key_hex,
            "aabb",
            1,
            2,
            success=True,
            trip_ms=100,
        )
        ctx = {"contact": contact, "attempt": PATH_RETRIES}
        self.ctrl._apply_smart_route(ctx, contact)
        assert contact.path_length == -1

    def test_selects_best_path(self):
        contact = _make_contact(smart=True)
        path_hex = "cc" * 2
        self.storage.upsert_path_record(
            contact.public_key_hex,
            path_hex,
            1,
            2,
            success=True,
            trip_ms=100,
        )
        ctx = {"contact": contact, "attempt": 0}
        self.ctrl._apply_smart_route(ctx, contact)
        self.ctrl._win.set_contact_path.assert_called_once()
        args = self.ctrl._win.set_contact_path.call_args[0]
        assert args[0] is contact

    def test_excludes_tried_paths(self):
        contact = _make_contact(smart=True)
        for i in range(3):
            path_hex = f"{i:02x}" * 2
            for _ in range(5):
                self.storage.upsert_path_record(
                    contact.public_key_hex,
                    path_hex,
                    1,
                    2,
                    success=True,
                    trip_ms=100 + i * 100,
                )

        ctx = {"contact": contact, "attempt": 0, "paths_tried": ["0000"]}
        self.ctrl._apply_smart_route(ctx, contact)
        set_path_args = self.ctrl._win.set_contact_path.call_args[0]
        set_path_bytes = set_path_args[2]
        assert set_path_bytes != bytes.fromhex("0000")

    def test_resets_tried_when_all_exhausted(self):
        contact = _make_contact(smart=True)
        path_hex = "aabb"
        self.storage.upsert_path_record(
            contact.public_key_hex,
            path_hex,
            1,
            2,
            success=True,
            trip_ms=100,
        )
        ctx = {"contact": contact, "attempt": 1, "paths_tried": [path_hex]}
        self.ctrl._apply_smart_route(ctx, contact)
        assert ctx["paths_tried"] == []
        self.ctrl._win.set_contact_path.assert_called_once()

    def test_does_not_reset_path_when_already_set(self):
        contact = _make_contact(smart=True, path_length=2, path=b"\xaa\xbb")
        self.storage.upsert_path_record(
            contact.public_key_hex,
            "aabb",
            1,
            2,
            success=True,
            trip_ms=100,
        )
        ctx = {"contact": contact, "attempt": 0, "paths_tried": ["aabb"]}
        self.ctrl._apply_smart_route(ctx, contact)
        assert contact.path_length == 2
        self.ctrl._win.set_contact_path.assert_not_called()


# ─── _send_message_attempt() pre-send refresh ────────────────


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

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

    def test_first_smart_attempt_triggers_refresh(self):
        contact = _make_contact(smart=True)
        msg_id = "test-msg-1"
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "strategy": "smart",
            "attempt": 0,
        }
        self.ctrl._send_message_attempt(msg_id)
        self.ctrl._win._frame_handler.do_contacts_refetch.assert_called_once()
        assert self.ctrl.retry_context[msg_id]["_refreshed"] is True

    @patch.object(MessageController, "_do_send_message", return_value=False)
    def test_refresh_only_once(self, mock_send):
        contact = _make_contact(smart=True)
        msg_id = "test-msg-2"
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "strategy": "smart",
            "attempt": 0,
            "_refreshed": True,
        }
        self.ctrl._send_message_attempt(msg_id)
        self.ctrl._win._frame_handler.do_contacts_refetch.assert_not_called()
        mock_send.assert_called_once_with(msg_id)

    @patch.object(MessageController, "_do_send_message", return_value=False)
    def test_non_smart_skips_refresh(self, mock_send):
        contact = _make_contact(smart=False)
        msg_id = "test-msg-3"
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "strategy": "traditional",
            "attempt": 0,
        }
        self.ctrl._send_message_attempt(msg_id)
        self.ctrl._win._frame_handler.do_contacts_refetch.assert_not_called()
        mock_send.assert_called_once_with(msg_id)

    @patch.object(MessageController, "_do_send_message", return_value=False)
    def test_retry_attempt_skips_refresh(self, mock_send):
        contact = _make_contact(smart=True)
        msg_id = "test-msg-4"
        self.ctrl.retry_context[msg_id] = {
            "contact": contact,
            "strategy": "smart",
            "attempt": 2,
        }
        self.ctrl._send_message_attempt(msg_id)
        self.ctrl._win._frame_handler.do_contacts_refetch.assert_not_called()
        mock_send.assert_called_once_with(msg_id)
