from unittest.mock import patch

from meshy.models import Channel, Contact, MessageStatus


def _insert_msg(storage, contact_hex, text, ts, is_outgoing=False):
    storage._db.execute(
        """INSERT INTO messages
        (contact_key_hex, sender_key_hex, text, timestamp, is_outgoing, is_cli, status, message_id, room_sender_name)
        VALUES (?, ?, ?, ?, ?, 0, 0, NULL, '')""",
        (contact_hex, contact_hex, text, ts, int(is_outgoing)),
    )
    storage._db.commit()


CONTACT_HEX = "aa" * 32
OTHER_HEX = "bb" * 32


# ─── has_duplicate_message ─────────────────────────────────────


class TestHasDuplicateMessage:
    def test_empty_db_returns_false(self, storage):
        assert storage.has_duplicate_message(CONTACT_HEX, 1000.0, "hello") is False

    def test_layer1_exact_timestamp_match(self, storage):
        _insert_msg(storage, CONTACT_HEX, "hello", 1000.0)
        assert storage.has_duplicate_message(CONTACT_HEX, 1000.0, "hello") is True

    def test_layer1_different_text_not_duplicate(self, storage):
        _insert_msg(storage, CONTACT_HEX, "hello", 1000.0)
        assert storage.has_duplicate_message(CONTACT_HEX, 1000.0, "world") is False

    @patch("meshy.storage.time")
    def test_layer2_manual_resend_within_30min(self, mock_time, storage):
        ts = 1000.0
        mock_time.time.return_value = ts + 600  # 10 min later
        _insert_msg(storage, CONTACT_HEX, "hello", ts)
        assert storage.has_duplicate_message(CONTACT_HEX, ts + 60, "hello") is True

    @patch("meshy.storage.time")
    def test_layer2_not_duplicate_after_30min(self, mock_time, storage):
        ts = 1000.0
        mock_time.time.return_value = ts + 2000  # > 30 min
        _insert_msg(storage, CONTACT_HEX, "hello", ts)
        assert storage.has_duplicate_message(CONTACT_HEX, ts + 60, "hello") is False

    @patch("meshy.storage.time")
    def test_layer2_not_duplicate_after_our_reply(self, mock_time, storage):
        ts = 1000.0
        mock_time.time.return_value = ts + 300
        _insert_msg(storage, CONTACT_HEX, "hello", ts)
        _insert_msg(storage, CONTACT_HEX, "reply", ts + 100, is_outgoing=True)
        assert storage.has_duplicate_message(CONTACT_HEX, ts + 200, "hello") is False

    @patch("meshy.storage.time")
    def test_layer2_different_text_not_duplicate(self, mock_time, storage):
        ts = 1000.0
        mock_time.time.return_value = ts + 300
        _insert_msg(storage, CONTACT_HEX, "hello", ts)
        assert storage.has_duplicate_message(CONTACT_HEX, ts + 60, "world") is False


# ─── message_exists ────────────────────────────────────────────


class TestMessageExists:
    def test_within_5s_window(self, storage):
        _insert_msg(storage, CONTACT_HEX, "test", 1000.0)
        assert storage.message_exists(CONTACT_HEX, 1003.0, "test") is True

    def test_outside_5s_window(self, storage):
        _insert_msg(storage, CONTACT_HEX, "test", 1000.0)
        assert storage.message_exists(CONTACT_HEX, 1010.0, "test") is False

    def test_different_text(self, storage):
        _insert_msg(storage, CONTACT_HEX, "test", 1000.0)
        assert storage.message_exists(CONTACT_HEX, 1000.0, "other") is False


# ─── count_unread_messages ─────────────────────────────────────


class TestCountUnread:
    def test_all_unread_without_last_read(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        _insert_msg(storage, CONTACT_HEX, "m1", 100.0)
        _insert_msg(storage, CONTACT_HEX, "m2", 200.0)
        _insert_msg(storage, CONTACT_HEX, "m3", 300.0, is_outgoing=True)
        assert storage.count_unread_messages(CONTACT_HEX) == 2

    def test_partial_unread_with_last_read(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        _insert_msg(storage, CONTACT_HEX, "m1", 100.0)
        _insert_msg(storage, CONTACT_HEX, "m2", 200.0)
        _insert_msg(storage, CONTACT_HEX, "m3", 300.0)
        storage.set_contact_last_read_at(CONTACT_HEX, 150.0)
        assert storage.count_unread_messages(CONTACT_HEX) == 2

    def test_channel_unread(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) VALUES (0, 'Bob', 'hi', 100.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) VALUES (0, 'Bob', 'there', 200.0, 0)",
        )
        storage._db.commit()
        assert storage.count_unread_channel_messages(0) == 2
        storage.set_channel_last_read_at(0, 150.0)
        assert storage.count_unread_channel_messages(0) == 1


# ─── upsert_path_record / get_path_history ─────────────────────


class TestPathHistory:
    def test_insert_new_record(self, storage):
        storage.upsert_path_record(CONTACT_HEX, "aabb", 1, 2, success=True, trip_ms=500)
        history = storage.get_path_history(CONTACT_HEX)
        assert len(history) == 1
        rec = history[0]
        assert rec["path_bytes"] == "aabb"
        assert rec["success_count"] == 1
        assert rec["failure_count"] == 0
        assert rec["last_trip_ms"] == 500

    def test_update_increments_counts(self, storage):
        storage.upsert_path_record(CONTACT_HEX, "aabb", 1, 2, success=True, trip_ms=500)
        storage.upsert_path_record(CONTACT_HEX, "aabb", 1, 2, success=True, trip_ms=300)
        storage.upsert_path_record(CONTACT_HEX, "aabb", 1, 2, success=False)
        history = storage.get_path_history(CONTACT_HEX)
        assert len(history) == 1
        assert history[0]["success_count"] == 2
        assert history[0]["failure_count"] == 1

    def test_history_limit_100(self, storage):
        for i in range(110):
            storage.upsert_path_record(CONTACT_HEX, f"path{i:03d}", 1, 1, success=True)
        history = storage.get_path_history(CONTACT_HEX)
        assert len(history) == 100


# ─── Migration / schema ───────────────────────────────────────


class TestMigration:
    def test_tables_created(self, storage):
        tables = {
            row[0]
            for row in storage._db.execute(
                "SELECT name FROM sqlite_master WHERE type='table'"
            ).fetchall()
        }
        for expected in (
            "contacts",
            "messages",
            "channels",
            "channel_messages",
            "path_history",
            "room_passwords",
            "settings",
            "regions",
            "blocked_channel_senders",
        ):
            assert expected in tables, f"Missing table: {expected}"

    def test_contacts_has_migrated_columns(self, storage):
        cols = {row[1] for row in storage._db.execute("PRAGMA table_info(contacts)").fetchall()}
        for col in (
            "last_read_at",
            "path_hash_size",
            "device_lastmod",
            "force_flood",
            "auto_reset_path",
            "smart_route_selection",
        ):
            assert col in cols, f"Missing column: {col}"


# ─── Blocked Channel Senders ─────────────────────────────────


class TestBlockedChannelSenders:
    def test_empty_by_default(self, storage):
        assert storage.get_blocked_channel_senders() == set()

    def test_block_sender(self, storage):
        storage.block_channel_sender("Spammer")
        assert storage.is_channel_sender_blocked("Spammer") is True
        assert "Spammer" in storage.get_blocked_channel_senders()

    def test_unblock_sender(self, storage):
        storage.block_channel_sender("Spammer")
        storage.unblock_channel_sender("Spammer")
        assert storage.is_channel_sender_blocked("Spammer") is False
        assert storage.get_blocked_channel_senders() == set()

    def test_block_duplicate_is_noop(self, storage):
        storage.block_channel_sender("Spammer")
        storage.block_channel_sender("Spammer")
        assert storage.get_blocked_channel_senders() == {"Spammer"}

    def test_unblock_nonexistent_is_noop(self, storage):
        storage.unblock_channel_sender("Nobody")
        assert storage.get_blocked_channel_senders() == set()

    def test_multiple_blocked(self, storage):
        storage.block_channel_sender("Alice")
        storage.block_channel_sender("Bob")
        blocked = storage.get_blocked_channel_senders()
        assert blocked == {"Alice", "Bob"}

    def test_get_channel_senders(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'hi', 100.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'hello', 200.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Bob', 'hey', 150.0, 0)",
        )
        storage._db.commit()
        senders = storage.get_channel_senders(0)
        assert len(senders) == 2
        assert senders[0]["sender_name"] == "Alice"
        assert senders[0]["message_count"] == 2
        assert senders[0]["last_seen"] == 200.0
        assert senders[1]["sender_name"] == "Bob"
        assert senders[1]["message_count"] == 1

    def test_get_channel_senders_excludes_outgoing(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Me', 'hi', 100.0, 1)",
        )
        storage._db.commit()
        senders = storage.get_channel_senders(0)
        assert len(senders) == 0

    def test_get_channel_participants_includes_blocked_without_messages(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'hi', 100.0, 0)",
        )
        storage._db.commit()
        storage.block_channel_sender("Alice")
        storage.block_channel_sender("Ghost")
        participants = storage.get_channel_participants(0)
        assert len(participants) == 2
        alice = next(p for p in participants if p["sender_name"] == "Alice")
        assert alice["message_count"] == 1
        assert alice["is_blocked"] is True
        ghost = next(p for p in participants if p["sender_name"] == "Ghost")
        assert ghost["message_count"] == 0
        assert ghost["last_seen"] is None
        assert ghost["is_blocked"] is True

    def test_get_channel_participants_unblocked_not_flagged(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Bob', 'hey', 100.0, 0)",
        )
        storage._db.commit()
        participants = storage.get_channel_participants(0)
        assert len(participants) == 1
        assert participants[0]["is_blocked"] is False

    def test_delete_messages_by_sender(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Spammer', 'spam1', 100.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Spammer', 'spam2', 200.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'hello', 150.0, 0)",
        )
        storage._db.commit()
        storage.delete_channel_messages_by_sender(0, "Spammer")
        msgs = storage.get_channel_messages(0)
        assert len(msgs) == 1
        assert msgs[0].sender_name == "Alice"


# ─── Channel Retention ──────────────────────────────────────


class TestChannelRetention:
    def test_default_retention_days(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        ch = storage.get_channel(0)
        assert ch.retention_days == 0

    def test_set_channel_retention_days(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage.set_channel_retention_days(0, 30)
        ch = storage.get_channel(0)
        assert ch.retention_days == 30

    def test_cleanup_prunes_old_messages(self, storage):
        import time as _time

        now = _time.time()
        storage.save_channel(Channel(index=0, name="Test", psk=bytes(16), retention_days=7))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'old', ?, 0)",
            (now - 8 * 86400,),
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Bob', 'recent', ?, 0)",
            (now - 3 * 86400,),
        )
        storage._db.commit()
        storage._cleanup_old_data()
        msgs = storage.get_channel_messages(0)
        assert len(msgs) == 1
        assert msgs[0].sender_name == "Bob"

    def test_cleanup_skips_forever_channels(self, storage):
        import time as _time

        now = _time.time()
        storage.save_channel(Channel(index=0, name="Permanent", psk=bytes(16), retention_days=0))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'ancient', ?, 0)",
            (now - 365 * 86400,),
        )
        storage._db.commit()
        storage._cleanup_old_data()
        msgs = storage.get_channel_messages(0)
        assert len(msgs) == 1

    def test_clear_channel_messages(self, storage):
        storage.save_channel(Channel(index=0, name="General", psk=bytes(16)))
        storage.save_channel(Channel(index=1, name="Other", psk=bytes(16)))
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Alice', 'hi', 100.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (0, 'Bob', 'hey', 200.0, 0)",
        )
        storage._db.execute(
            "INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) "
            "VALUES (1, 'Carol', 'hello', 150.0, 0)",
        )
        storage._db.commit()
        storage.clear_channel_messages(0)
        assert len(storage.get_channel_messages(0)) == 0
        assert len(storage.get_channel_messages(1)) == 1


# ─── find_contact_by_prefix ──────────────────────────────────


class TestFindContactByPrefix:
    def test_finds_matching_contact(self, storage):
        key = bytes.fromhex(CONTACT_HEX)
        storage.save_contact(Contact(public_key=key, name="Alice"))
        found = storage.find_contact_by_prefix(key[:6])
        assert found is not None
        assert found.public_key_hex == CONTACT_HEX

    def test_no_match_returns_none(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        found = storage.find_contact_by_prefix(bytes.fromhex("bb" * 6))
        assert found is None

    def test_prefix_ending_with_f(self, storage):
        key_hex = "ff" * 32
        storage.save_contact(Contact(public_key=bytes.fromhex(key_hex), name="Max"))
        found = storage.find_contact_by_prefix(bytes.fromhex(key_hex[:12]))
        assert found is not None
        assert found.name == "Max"

    def test_multiple_contacts_returns_one(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        storage.save_contact(Contact(public_key=bytes.fromhex(OTHER_HEX), name="Bob"))
        found = storage.find_contact_by_prefix(bytes.fromhex(CONTACT_HEX[:12]))
        assert found is not None
        assert found.public_key_hex == CONTACT_HEX


# ─── fail_orphaned_messages ──────────────────────────────────


def _insert_msg_with_status(storage, contact_hex, text, ts, status, is_outgoing=True):
    storage._db.execute(
        """INSERT INTO messages
        (contact_key_hex, sender_key_hex, text, timestamp, is_outgoing, is_cli, status, message_id, room_sender_name)
        VALUES (?, ?, ?, ?, ?, 0, ?, NULL, '')""",
        (contact_hex, contact_hex, text, ts, int(is_outgoing), int(status)),
    )
    storage._db.commit()


class TestFailOrphanedMessages:
    def test_pending_marked_as_failed(self, storage):
        _insert_msg_with_status(storage, CONTACT_HEX, "msg1", 100.0, MessageStatus.PENDING)
        storage.fail_orphaned_messages()
        row = storage._db.execute("SELECT status FROM messages").fetchone()
        assert row[0] == int(MessageStatus.FAILED)

    def test_sent_marked_as_failed(self, storage):
        _insert_msg_with_status(storage, CONTACT_HEX, "msg1", 100.0, MessageStatus.SENT)
        storage.fail_orphaned_messages()
        row = storage._db.execute("SELECT status FROM messages").fetchone()
        assert row[0] == int(MessageStatus.FAILED)

    def test_delivered_not_affected(self, storage):
        _insert_msg_with_status(storage, CONTACT_HEX, "msg1", 100.0, MessageStatus.DELIVERED)
        storage.fail_orphaned_messages()
        row = storage._db.execute("SELECT status FROM messages").fetchone()
        assert row[0] == int(MessageStatus.DELIVERED)

    def test_incoming_not_affected(self, storage):
        _insert_msg_with_status(
            storage,
            CONTACT_HEX,
            "msg1",
            100.0,
            MessageStatus.PENDING,
            is_outgoing=False,
        )
        storage.fail_orphaned_messages()
        row = storage._db.execute("SELECT status FROM messages").fetchone()
        assert row[0] == int(MessageStatus.PENDING)

    def test_mixed_messages(self, storage):
        _insert_msg_with_status(storage, CONTACT_HEX, "a", 100.0, MessageStatus.PENDING)
        _insert_msg_with_status(storage, CONTACT_HEX, "b", 200.0, MessageStatus.SENT)
        _insert_msg_with_status(storage, CONTACT_HEX, "c", 300.0, MessageStatus.DELIVERED)
        storage.fail_orphaned_messages()
        rows = storage._db.execute(
            "SELECT text, status FROM messages ORDER BY timestamp"
        ).fetchall()
        assert rows[0][1] == int(MessageStatus.FAILED)
        assert rows[1][1] == int(MessageStatus.FAILED)
        assert rows[2][1] == int(MessageStatus.DELIVERED)


# ─── Storage.batch() ─────────────────────────────────────────


class TestBatch:
    def test_defers_commit(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        with storage.batch():
            storage._db.execute(
                "UPDATE contacts SET name = ? WHERE public_key_hex = ?",
                ("Updated", CONTACT_HEX),
            )
            storage._commit()
            assert storage._batch_depth == 1

        assert storage._batch_depth == 0

    def test_nested_batch(self, storage):
        with storage.batch():
            assert storage._batch_depth == 1
            with storage.batch():
                assert storage._batch_depth == 2
            assert storage._batch_depth == 1
        assert storage._batch_depth == 0

    def test_depth_restores_after_exception(self, storage):
        try:
            with storage.batch():
                assert storage._batch_depth == 1
                raise ValueError("test error")
        except ValueError:
            pass
        assert storage._batch_depth == 0

    def test_commit_inside_batch_is_noop(self, storage):
        storage.save_contact(Contact(public_key=bytes.fromhex(CONTACT_HEX), name="Alice"))
        with storage.batch():
            storage._db.execute(
                "UPDATE contacts SET name = ? WHERE public_key_hex = ?",
                ("BatchUpdated", CONTACT_HEX),
            )
            storage._commit()
            assert storage._batch_depth == 1
