from unittest.mock import patch

from meshy.models import Channel, Contact


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",
        ):
            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}"
