"""Tests for room server support: protocol builders, storage, frame handler, ACL parsing."""

import struct
import time
from datetime import datetime
from unittest.mock import MagicMock, patch

from meshy.frame_handler import FrameHandler
from meshy.models import Contact, ContactType, Message, MessageStatus
from meshy.protocol import (
    AUTO_ADD_CHAT,
    AUTO_ADD_ROOM_SERVER,
    CMD_LOGOUT,
    CMD_SEND_BINARY_REQ,
    CMD_SEND_LOGIN,
    CMD_SET_AUTO_ADD_CONFIG,
    PUSH_CODE_LOGIN_FAILED,
    PUSH_CODE_LOGIN_SUCCESS,
    REQ_TYPE_ACL,
    build_get_acl,
    build_logout,
    build_send_login,
    build_set_auto_add_config,
)
from meshy.repeater_helpers import (
    ACL_ROLES,
    ROLE_PERMS,
    parse_acl_response,
    parse_region_text,
)

_PUB_KEY = bytes(range(32))
_PREFIX = _PUB_KEY[:6]


def _make_contact(name="TestRoom", type_=ContactType.ROOM, key_hex=None):
    if key_hex is None:
        key_hex = "aa" * 32
    return Contact(public_key=bytes.fromhex(key_hex), name=name, type=type_)


def _make_win(storage):
    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._login_callback = None
    win._login_target_prefix = None
    win._login_pub_key = None
    win._login_was_flood_retry = False
    win._login_is_admin = False
    win._contacts = []
    win._pending_binary_requests = {}
    win._cli_pending_prefixes = {}
    win._cli_response_callback = None
    return win


def _build_login_success(prefix, perms=0x01, timestamp=1700000000, acl_perm=3, fw_ver=1):
    data = bytes([PUSH_CODE_LOGIN_SUCCESS, perms])
    data += prefix
    data += struct.pack("<I", timestamp)
    data += bytes([acl_perm, fw_ver])
    return data


def _build_login_failed(prefix):
    return bytes([PUSH_CODE_LOGIN_FAILED, 0x00]) + prefix


def _build_acl_response(entries):
    """Build ACL binary from list of (key_hex_12chars, perm) tuples."""
    data = bytes(6)  # code + reserved + tag
    for key_hex, perm in entries:
        data += bytes.fromhex(key_hex) + bytes([perm])
    return data


# ─── Protocol Builders ─────────────────────────────────────


class TestProtocolRoomBuilders:
    def test_build_send_login_layout(self):
        frame = build_send_login(_PUB_KEY, "secret")
        assert frame[0] == CMD_SEND_LOGIN
        assert frame[1:33] == _PUB_KEY
        assert frame[33:-1] == b"secret"
        assert frame[-1] == 0x00

    def test_build_send_login_length(self):
        pwd = "test123"
        frame = build_send_login(_PUB_KEY, pwd)
        assert len(frame) == 1 + 32 + len(pwd.encode()) + 1

    def test_build_send_login_unicode(self):
        frame = build_send_login(_PUB_KEY, "héslo")
        assert "héslo".encode() in frame

    def test_build_logout_layout(self):
        frame = build_logout(_PUB_KEY)
        assert frame[0] == CMD_LOGOUT
        assert frame[1:] == _PUB_KEY
        assert len(frame) == 33

    def test_build_get_acl_layout(self):
        frame = build_get_acl(_PUB_KEY)
        assert frame[0] == CMD_SEND_BINARY_REQ
        assert frame[1:33] == _PUB_KEY
        assert frame[33] == REQ_TYPE_ACL
        assert frame[34:36] == bytes([0x00, 0x00])
        assert len(frame) == 36

    def test_auto_add_room_only(self):
        frame = build_set_auto_add_config(room=True)
        assert frame[0] == CMD_SET_AUTO_ADD_CONFIG
        assert frame[1] == AUTO_ADD_ROOM_SERVER  # 0x08

    def test_auto_add_room_and_chat(self):
        frame = build_set_auto_add_config(room=True, chat=True)
        assert frame[1] == AUTO_ADD_ROOM_SERVER | AUTO_ADD_CHAT

    def test_auto_add_no_flags(self):
        frame = build_set_auto_add_config()
        assert frame[1] == 0x00

    def test_auto_add_max_hops_clamped(self):
        frame = build_set_auto_add_config(max_hops=100)
        assert frame[2] == 64


# ─── Storage: Room Passwords ──────────────────────────────


class TestStorageRoomPasswords:
    def test_save_and_get(self, storage):
        storage.save_room_password("aabbcc", "secret")
        assert storage.get_room_password("aabbcc") == "secret"

    def test_get_nonexistent(self, storage):
        assert storage.get_room_password("unknown") is None

    def test_update(self, storage):
        storage.save_room_password("aabbcc", "old")
        storage.save_room_password("aabbcc", "new")
        assert storage.get_room_password("aabbcc") == "new"

    def test_remove(self, storage):
        storage.save_room_password("aabbcc", "secret")
        storage.remove_room_password("aabbcc")
        assert storage.get_room_password("aabbcc") is None

    def test_remove_nonexistent(self, storage):
        storage.remove_room_password("nonexistent")

    def test_multiple_rooms(self, storage):
        storage.save_room_password("room1a", "pw1")
        storage.save_room_password("room2b", "pw2")
        assert storage.get_room_password("room1a") == "pw1"
        assert storage.get_room_password("room2b") == "pw2"


# ─── Storage: Room Sender Name ────────────────────────────


class TestStorageRoomSenderName:
    def test_room_sender_name_roundtrip(self, storage):
        contact = _make_contact()
        storage.save_contact(contact)
        msg = Message(
            sender_key=contact.public_key,
            text="hello",
            timestamp=datetime.now(),
            is_outgoing=False,
            status=MessageStatus.DELIVERED,
            room_sender_name="Alice",
        )
        storage.save_message(contact.public_key_hex, msg)
        messages = storage.get_messages(contact.public_key_hex)
        assert len(messages) == 1
        assert messages[0].room_sender_name == "Alice"

    def test_room_sender_name_default_empty(self, storage):
        contact = _make_contact()
        storage.save_contact(contact)
        msg = Message(
            sender_key=contact.public_key,
            text="hello",
            timestamp=datetime.now(),
            is_outgoing=False,
            status=MessageStatus.DELIVERED,
        )
        storage.save_message(contact.public_key_hex, msg)
        messages = storage.get_messages(contact.public_key_hex)
        assert messages[0].room_sender_name == ""


# ─── Frame Handler: Login Success ─────────────────────────


class TestFrameHandlerLoginSuccess:
    def setup_method(self, storage=None):
        self._storage_backup = storage

    @patch("meshy.frame_handler.GLib")
    def test_admin_login(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX

        frame = _build_login_success(_PREFIX, perms=0x01, timestamp=1700000000, acl_perm=3)
        fh.handle_frame(frame)

        cb.assert_called_once_with(True, 1700000000, True, 3)

    @patch("meshy.frame_handler.GLib")
    def test_non_admin_login(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX

        frame = _build_login_success(_PREFIX, perms=0x00, acl_perm=2)
        fh.handle_frame(frame)

        cb.assert_called_once()
        args = cb.call_args[0]
        assert args[0] is True
        assert args[2] is False  # is_admin
        assert args[3] == 2  # acl_permissions

    @patch("meshy.frame_handler.GLib")
    def test_short_frame_admin_defaults_to_3(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX

        # 12 bytes: code + perms + prefix(6) + timestamp(4) — no ACL byte
        frame = bytes([PUSH_CODE_LOGIN_SUCCESS, 0x01]) + _PREFIX + struct.pack("<I", 100)
        fh.handle_frame(frame)

        args = cb.call_args[0]
        assert args[3] == 3  # admin defaults to 3

    @patch("meshy.frame_handler.GLib")
    def test_short_frame_non_admin_defaults_to_2(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX

        frame = bytes([PUSH_CODE_LOGIN_SUCCESS, 0x00]) + _PREFIX + struct.pack("<I", 100)
        fh.handle_frame(frame)

        args = cb.call_args[0]
        assert args[3] == 2  # non-admin defaults to 2

    @patch("meshy.frame_handler.GLib")
    def test_prefix_mismatch(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = b"\xff" * 6

        frame = _build_login_success(_PREFIX)
        fh.handle_frame(frame)

        cb.assert_not_called()

    @patch("meshy.frame_handler.GLib")
    def test_no_callback(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        win._login_callback = None

        frame = _build_login_success(_PREFIX)
        fh.handle_frame(frame)  # should not raise

    @patch("meshy.frame_handler.GLib")
    def test_flood_retry_resets_path(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX
        win._login_was_flood_retry = True
        win._login_pub_key = _PUB_KEY

        frame = _build_login_success(_PREFIX)
        fh.handle_frame(frame)

        cb.assert_called_once()
        win.send_frame.assert_called_once()

    @patch("meshy.frame_handler.GLib")
    def test_too_short_frame(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        win._login_callback = MagicMock()
        win._login_target_prefix = _PREFIX

        frame = bytes([PUSH_CODE_LOGIN_SUCCESS, 0x01, 0x00, 0x00])
        result = fh.handle_frame(frame)
        assert result is False


# ─── Frame Handler: Login Failed ──────────────────────────


class TestFrameHandlerLoginFailed:
    @patch("meshy.frame_handler.GLib")
    def test_prefix_match(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = _PREFIX

        frame = _build_login_failed(_PREFIX)
        fh.handle_frame(frame)

        cb.assert_called_once_with(False, None, False, 0)

    @patch("meshy.frame_handler.GLib")
    def test_prefix_mismatch(self, mock_glib, storage):
        win = _make_win(storage)
        fh = FrameHandler(win)
        cb = MagicMock()
        win._login_callback = cb
        win._login_target_prefix = b"\xff" * 6

        frame = _build_login_failed(_PREFIX)
        fh.handle_frame(frame)

        cb.assert_not_called()


# ─── Frame Handler: Room Sender Resolution ────────────────


class TestFrameHandlerRoomSender:
    @patch("meshy.frame_handler.GLib")
    def test_resolves_sender_name(self, mock_glib, storage):
        room_key = "bb" * 32
        sender_key = "cc" * 32
        room = _make_contact("MyRoom", ContactType.ROOM, room_key)
        sender = _make_contact("Alice", ContactType.CHAT, sender_key)
        storage.save_contact(room)
        storage.save_contact(sender)

        win = _make_win(storage)
        win._contacts = [room, sender]
        fh = FrameHandler(win)

        sig = sender_key[:8]  # 4-byte prefix as hex
        fh.handle_incoming_message(
            {
                "pubkey_prefix": room_key[:12],
                "txt_type": 2,
                "sender_timestamp": time.time(),
                "text": "hello",
                "signature": sig,
            }
        )

        saved_msg = storage.get_messages(room_key)
        assert len(saved_msg) == 1
        assert saved_msg[0].room_sender_name == "Alice"

    @patch("meshy.frame_handler.GLib")
    def test_unknown_sender_uses_signature(self, mock_glib, storage):
        room_key = "bb" * 32
        room = _make_contact("MyRoom", ContactType.ROOM, room_key)
        storage.save_contact(room)

        win = _make_win(storage)
        win._contacts = [room]
        fh = FrameHandler(win)

        fh.handle_incoming_message(
            {
                "pubkey_prefix": room_key[:12],
                "txt_type": 2,
                "sender_timestamp": time.time(),
                "text": "hello",
                "signature": "deadbeef",
            }
        )

        saved_msg = storage.get_messages(room_key)
        assert len(saved_msg) == 1
        assert saved_msg[0].room_sender_name == "deadbeef"

    @patch("meshy.frame_handler.GLib")
    def test_non_room_no_resolution(self, mock_glib, storage):
        chat_key = "dd" * 32
        contact = _make_contact("Bob", ContactType.CHAT, chat_key)
        storage.save_contact(contact)

        win = _make_win(storage)
        win._contacts = [contact]
        fh = FrameHandler(win)

        fh.handle_incoming_message(
            {
                "pubkey_prefix": chat_key[:12],
                "txt_type": 2,
                "sender_timestamp": time.time(),
                "text": "hi",
                "signature": "aabb",
            }
        )

        saved_msg = storage.get_messages(chat_key)
        assert len(saved_msg) == 1
        assert saved_msg[0].room_sender_name == ""

    @patch("meshy.frame_handler.GLib")
    def test_cli_message_no_resolution(self, mock_glib, storage):
        room_key = "bb" * 32
        sender_key = "cc" * 32
        room = _make_contact("MyRoom", ContactType.ROOM, room_key)
        sender = _make_contact("Alice", ContactType.CHAT, sender_key)
        storage.save_contact(room)

        win = _make_win(storage)
        win._contacts = [room, sender]
        win._cli_response_callback = None
        fh = FrameHandler(win)

        fh.handle_incoming_message(
            {
                "pubkey_prefix": room_key[:12],
                "txt_type": 1,  # CLI
                "sender_timestamp": time.time(),
                "text": "> OK",
                "signature": sender_key[:8],
            }
        )

        saved_msg = storage.get_messages(room_key)
        assert len(saved_msg) == 1
        assert saved_msg[0].room_sender_name == ""


# ─── ACL Response Parsing ─────────────────────────────────


class TestParseAclResponse:
    def test_single_entry(self):
        data = _build_acl_response([("aabbccddeeff", 3)])
        result = parse_acl_response(data)
        assert result == [{"key": "aabbccddeeff", "perm": 3}]

    def test_multiple_entries(self):
        data = _build_acl_response(
            [
                ("aabbccddeeff", 3),
                ("112233445566", 2),
                ("ffeeddccbbaa", 1),
            ]
        )
        result = parse_acl_response(data)
        assert len(result) == 3
        assert result[0]["key"] == "aabbccddeeff"
        assert result[1]["perm"] == 2
        assert result[2]["key"] == "ffeeddccbbaa"

    def test_zero_key_filtered(self):
        data = _build_acl_response(
            [
                ("aabbccddeeff", 3),
                ("000000000000", 1),
            ]
        )
        result = parse_acl_response(data)
        assert len(result) == 1

    def test_empty_payload(self):
        data = bytes(6)  # header only
        assert parse_acl_response(data) == []

    def test_partial_entry_ignored(self):
        data = bytes(6) + bytes(5)  # header + incomplete entry
        assert parse_acl_response(data) == []

    def test_permission_bitmask(self):
        data = _build_acl_response([("aabbccddeeff", 0x07)])
        result = parse_acl_response(data)
        assert ACL_ROLES[result[0]["perm"] & 3] == "Admin"


# ─── ACL Constants ─────────────────────────────────────────


class TestAclConstants:
    def test_acl_roles_complete(self):
        assert set(ACL_ROLES.keys()) == {1, 2, 3}

    def test_role_perms_mapping(self):
        assert ROLE_PERMS[0] == 3  # Admin
        assert ROLE_PERMS[1] == 2  # Read-Write
        assert ROLE_PERMS[2] == 1  # Read Only


# ─── Region Text Parsing ──────────────────────────────────


class TestParseRegionText:
    def test_simple_region(self):
        entries, _ = parse_region_text("US")
        assert entries == [{"name": "US", "indent": 0, "home": False, "flood": False}]

    def test_flood_suffix(self):
        entries, _ = parse_region_text("EU F")
        assert entries[0]["flood"] is True
        assert entries[0]["name"] == "EU"

    def test_home_suffix(self):
        entries, _ = parse_region_text("US^")
        assert entries[0]["home"] is True
        assert entries[0]["name"] == "US"

    def test_flood_and_home(self):
        entries, _ = parse_region_text("US^ F")
        assert entries[0]["flood"] is True
        assert entries[0]["home"] is True
        assert entries[0]["name"] == "US"

    def test_indented_regions(self):
        text = "*\n  US\n    East"
        entries, _ = parse_region_text(text)
        assert len(entries) == 3
        assert entries[0]["indent"] == 0
        assert entries[1]["indent"] == 2
        assert entries[2]["indent"] == 4

    def test_empty_lines_skipped(self):
        entries, _ = parse_region_text("US\n\nEU")
        assert len(entries) == 2

    def test_default_scope_parsed(self):
        _, default = parse_region_text("US", "blah blah default scope is US")
        assert default == "US"

    def test_default_scope_null(self):
        _, default = parse_region_text("US", "default scope is <null>")
        assert default == ""

    def test_default_scope_missing(self):
        _, default = parse_region_text("US", "")
        assert default == ""

    def test_empty_region_text(self):
        entries, default = parse_region_text("", "default scope is US")
        assert entries == []
        assert default == "US"
