import base64
import hashlib
from datetime import datetime, timezone

import aiosqlite
import pytest
from cryptography.fernet import Fernet

from src.database import Database
from src.models import (
    Account,
    Channel,
    CollectionTaskStatus,
    CollectionTaskType,
    Message,
    SearchQuery,
    StatsAllTaskPayload,
)


def _encrypt_v1(secret: str, plaintext: str) -> str:
    digest = hashlib.sha256(secret.encode("utf-8")).digest()
    key = base64.urlsafe_b64encode(digest)
    token = Fernet(key).encrypt(plaintext.encode("utf-8")).decode("ascii")
    return f"enc:v1:{token}"


@pytest.mark.asyncio
async def test_add_and_get_accounts(db):
    acc = Account(phone="+71234567890", session_string="session1", is_primary=True)
    await db.add_account(acc)

    accounts = await db.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].phone == "+71234567890"
    assert accounts[0].is_primary is True


@pytest.mark.asyncio
async def test_account_upsert(db):
    acc = Account(phone="+71234567890", session_string="session1")
    await db.add_account(acc)
    acc2 = Account(phone="+71234567890", session_string="session2")
    await db.add_account(acc2)

    accounts = await db.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].session_string == "session2"


@pytest.mark.asyncio
async def test_account_upsert_reactivates_without_overwriting_primary(db):
    await db.add_account(
        Account(
            phone="+71234567891",
            session_string="session1",
            is_primary=True,
            is_active=False,
        )
    )

    await db.add_account(
        Account(
            phone="+71234567891",
            session_string="session2",
            is_primary=False,
            is_active=True,
        )
    )

    accounts = await db.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].session_string == "session2"
    assert accounts[0].is_active is True
    assert accounts[0].is_primary is True


@pytest.mark.asyncio
async def test_account_session_encrypted_at_rest(tmp_path):
    db_path = str(tmp_path / "encrypted.db")
    database = Database(db_path, session_encryption_secret="test-encryption-secret")
    await database.initialize()

    await database.add_account(Account(phone="+71230000000", session_string="session_plain"))

    cur = await database.execute(
        "SELECT session_string FROM accounts WHERE phone = ?",
        ("+71230000000",),
    )
    row = await cur.fetchone()
    assert row is not None
    assert row["session_string"] != "session_plain"
    assert row["session_string"].startswith("enc:v2:")

    accounts = await database.get_accounts()
    assert accounts[0].session_string == "session_plain"
    await database.close()


@pytest.mark.asyncio
async def test_plaintext_sessions_migrate_on_init(tmp_path):
    """Plaintext sessions are encrypted during initialize(), not get_accounts()."""
    db_path = str(tmp_path / "plaintext_migration.db")

    legacy_db = Database(db_path)
    await legacy_db.initialize()
    await legacy_db.add_account(Account(phone="+71230000001", session_string="legacy_plaintext"))
    await legacy_db.close()

    encrypted_db = Database(db_path, session_encryption_secret="migration-secret")
    await encrypted_db.initialize()

    # Migration already happened during initialize(); verify DB state first.
    cur = await encrypted_db.execute(
        "SELECT session_string FROM accounts WHERE phone = ?",
        ("+71230000001",),
    )
    row = await cur.fetchone()
    assert row is not None
    assert row["session_string"].startswith("enc:v2:")

    # get_accounts() returns decrypted value.
    accounts = await encrypted_db.get_accounts()

    assert accounts[0].session_string == "legacy_plaintext"
    await encrypted_db.close()


@pytest.mark.asyncio
async def test_legacy_v1_sessions_migrate_to_v2_on_init(tmp_path):
    db_path = str(tmp_path / "v1_migration.db")
    legacy_secret = "legacy-key"

    legacy_db = Database(db_path)
    await legacy_db.initialize()
    legacy_v1 = _encrypt_v1(legacy_secret, "legacy_v1_session")
    await legacy_db.execute(
        "INSERT INTO accounts (phone, session_string) VALUES (?, ?)",
        ("+71230000002", legacy_v1),
    )
    await legacy_db.db.commit()
    await legacy_db.close()

    encrypted_db = Database(db_path, session_encryption_secret=legacy_secret)
    await encrypted_db.initialize()

    cur = await encrypted_db.execute(
        "SELECT session_string FROM accounts WHERE phone = ?",
        ("+71230000002",),
    )
    row = await cur.fetchone()
    assert row is not None
    assert row["session_string"].startswith("enc:v2:")
    assert row["session_string"] != legacy_v1

    accounts = await encrypted_db.get_accounts()
    assert accounts[0].session_string == "legacy_v1_session"
    await encrypted_db.close()


@pytest.mark.asyncio
async def test_migrate_sessions_rollback_on_bad_row(tmp_path):
    """Migration rolls back when a row has an unsupported encryption version."""
    db_path = str(tmp_path / "rollback_migration.db")

    db = Database(db_path)
    await db.initialize()
    await db.add_account(Account(phone="+71230000010", session_string="good_session"))
    # Insert a bad row directly — unsupported enc version
    await db.execute(
        "INSERT INTO accounts (phone, session_string) VALUES (?, ?)",
        ("+71230000011", "enc:v99:garbage"),
    )
    await db.db.commit()
    await db.close()

    encrypted_db = Database(db_path, session_encryption_secret="some-key")
    try:
        with pytest.raises(RuntimeError, match="Failed to migrate session"):
            await encrypted_db.initialize()
    finally:
        await encrypted_db.close()

    # Verify rollback: both rows should be unchanged
    conn = await aiosqlite.connect(db_path)
    conn.row_factory = aiosqlite.Row
    cur = await conn.execute("SELECT phone, session_string FROM accounts ORDER BY phone")
    rows = await cur.fetchall()
    assert len(rows) == 2
    assert rows[0]["session_string"] == "good_session"
    assert rows[1]["session_string"] == "enc:v99:garbage"
    await conn.close()


@pytest.mark.asyncio
async def test_initialize_fails_without_key_when_encrypted_sessions_exist(tmp_path):
    db_path = str(tmp_path / "no_key_fail_fast.db")

    encrypted_db = Database(db_path, session_encryption_secret="strict-key")
    await encrypted_db.initialize()
    await encrypted_db.add_account(Account(phone="+71230000003", session_string="encrypted"))
    await encrypted_db.close()

    db_without_key = Database(db_path)
    try:
        with pytest.raises(RuntimeError, match="SESSION_ENCRYPTION_KEY"):
            await db_without_key.initialize()
    finally:
        await db_without_key.close()


@pytest.mark.asyncio
async def test_add_and_get_channels(db):
    ch = Channel(channel_id=-1001234567890, title="Test Channel", username="@test")
    await db.add_channel(ch)

    channels = await db.get_channels()
    assert len(channels) == 1
    assert channels[0].channel_id == -1001234567890


@pytest.mark.asyncio
async def test_get_channel_by_pk(db):
    ch = Channel(channel_id=-1002233445566, title="Lookup Channel", username="@lookup")
    await db.add_channel(ch)

    channels = await db.get_channels()
    found = await db.get_channel_by_pk(channels[0].id)
    assert found is not None
    assert found.channel_id == -1002233445566
    assert found.title == "Lookup Channel"


@pytest.mark.asyncio
async def test_get_channel_by_pk_returns_none_for_unknown_id(db):
    found = await db.get_channel_by_pk(999999)
    assert found is None


@pytest.mark.asyncio
async def test_set_channels_filtered_bulk_and_reset(db):
    await db.add_channel(Channel(channel_id=-1007001, title="One", username="one"))
    await db.add_channel(Channel(channel_id=-1007002, title="Two", username="two"))

    updated = await db.set_channels_filtered_bulk(
        [(-1007001, "low_uniqueness"), (-1007002, "non_cyrillic,chat_noise")]
    )
    assert updated == 2

    channels = await db.get_channels()
    by_channel_id = {channel.channel_id: channel for channel in channels}
    assert by_channel_id[-1007001].is_filtered is True
    assert by_channel_id[-1007001].filter_flags == "low_uniqueness"
    assert by_channel_id[-1007002].is_filtered is True
    assert by_channel_id[-1007002].filter_flags == "non_cyrillic,chat_noise"

    reset_count = await db.reset_all_channel_filters()
    assert reset_count >= 2

    channels = await db.get_channels()
    by_channel_id = {channel.channel_id: channel for channel in channels}
    assert by_channel_id[-1007001].is_filtered is False
    assert by_channel_id[-1007001].filter_flags == ""
    assert by_channel_id[-1007002].is_filtered is False
    assert by_channel_id[-1007002].filter_flags == ""


@pytest.mark.asyncio
async def test_insert_message_deduplication(db):
    msg = Message(
        channel_id=-1001234567890,
        message_id=1,
        text="Hello world",
        date=datetime.now(timezone.utc),
    )
    first = await db.insert_message(msg)
    second = await db.insert_message(msg)
    assert first is True
    assert second is False
    # Second insert should be ignored (dedup)
    messages, total = await db.search_messages()
    assert total == 1


@pytest.mark.asyncio
async def test_batch_insert(db):
    messages = [
        Message(
            channel_id=-100123,
            message_id=i,
            text=f"Message {i}",
            date=datetime.now(timezone.utc),
        )
        for i in range(10)
    ]
    count = await db.insert_messages_batch(messages)
    assert count == 10

    results, total = await db.search_messages()
    assert total == 10


@pytest.mark.asyncio
async def test_search_messages(db):
    messages = [
        Message(
            channel_id=-100123,
            message_id=1,
            text="Bitcoin price is rising",
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100123,
            message_id=2,
            text="Ethereum update today",
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100123,
            message_id=3,
            text="Bitcoin hits new ATH",
            date=datetime.now(timezone.utc),
        ),
    ]
    await db.insert_messages_batch(messages)

    results, total = await db.search_messages(query="Bitcoin")
    assert total == 2
    assert all("Bitcoin" in (m.text or "") for m in results)


@pytest.mark.asyncio
async def test_search_messages_date_to_includes_entire_day(db):
    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100123,
                message_id=1,
                text="Morning",
                date=datetime(2025, 3, 6, 0, 0, tzinfo=timezone.utc),
            ),
            Message(
                channel_id=-100123,
                message_id=2,
                text="Evening",
                date=datetime(2025, 3, 6, 23, 59, tzinfo=timezone.utc),
            ),
            Message(
                channel_id=-100123,
                message_id=3,
                text="Next day",
                date=datetime(2025, 3, 7, 0, 0, tzinfo=timezone.utc),
            ),
        ]
    )

    results, total = await db.search_messages(date_to="2025-03-06")

    assert total == 2
    assert {message.message_id for message in results} == {1, 2}


@pytest.mark.asyncio
async def test_search_messages_full_iso_date_to_remains_precise(db):
    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100123,
                message_id=1,
                text="Before cutoff",
                date=datetime(2025, 3, 6, 11, 59, tzinfo=timezone.utc),
            ),
            Message(
                channel_id=-100123,
                message_id=2,
                text="After cutoff",
                date=datetime(2025, 3, 6, 12, 1, tzinfo=timezone.utc),
            ),
        ]
    )

    results, total = await db.search_messages(date_to="2025-03-06T12:00:00+00:00")

    assert total == 1
    assert results[0].message_id == 1


@pytest.mark.asyncio
async def test_notification_queries_crud(db):
    sq = SearchQuery(query="bitcoin", is_regex=False, notify_on_collect=True)
    repo = db.repos.search_queries
    sq_id = await repo.add(sq)
    assert sq_id > 0

    queries = await db.get_notification_queries(active_only=False)
    assert len(queries) == 1
    assert queries[0].query == "bitcoin"

    await repo.delete(sq_id)
    queries = await db.get_notification_queries(active_only=False)
    assert len(queries) == 0


@pytest.mark.asyncio
async def test_stats(db):
    stats = await db.get_stats()
    assert stats["accounts"] == 0
    assert stats["channels"] == 0
    assert stats["messages"] == 0
    assert stats["search_queries"] == 0


@pytest.mark.asyncio
async def test_get_set_setting(db):
    assert await db.get_setting("nonexistent") is None
    await db.set_setting("tg_api_id", "12345")
    assert await db.get_setting("tg_api_id") == "12345"
    await db.set_setting("tg_api_id", "99999")  # upsert
    assert await db.get_setting("tg_api_id") == "99999"


@pytest.mark.asyncio
async def test_stats_with_data(db):
    acc = Account(phone="+71234567890", session_string="s1", is_primary=True)
    await db.add_account(acc)
    ch = Channel(channel_id=-100123, title="Test Channel")
    await db.add_channel(ch)
    msgs = [
        Message(
            channel_id=-100123,
            message_id=i,
            text=f"Msg {i}",
            date=datetime.now(timezone.utc),
        )
        for i in range(1, 4)
    ]
    await db.insert_messages_batch(msgs)
    repo = db.repos.search_queries
    await repo.add(SearchQuery(query="test"))

    stats = await db.get_stats()
    assert stats["accounts"] == 1
    assert stats["channels"] == 1
    assert stats["messages"] == 3
    assert stats["search_queries"] == 1


@pytest.mark.asyncio
async def test_account_premium_field(db):
    acc = Account(phone="+71234567890", session_string="s1", is_premium=True)
    await db.add_account(acc)

    accounts = await db.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].is_premium is True


@pytest.mark.asyncio
async def test_update_account_premium(db):
    acc = Account(phone="+71234567890", session_string="s1", is_premium=False)
    await db.add_account(acc)

    await db.update_account_premium("+71234567890", True)
    accounts = await db.get_accounts()
    assert accounts[0].is_premium is True

    await db.update_account_premium("+71234567890", False)
    accounts = await db.get_accounts()
    assert accounts[0].is_premium is False


@pytest.mark.asyncio
async def test_account_upsert_updates_premium(db):
    acc = Account(phone="+71234567890", session_string="s1", is_premium=False)
    await db.add_account(acc)

    acc2 = Account(phone="+71234567890", session_string="s2", is_premium=True)
    await db.add_account(acc2)

    accounts = await db.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].is_premium is True
    assert accounts[0].session_string == "s2"


@pytest.mark.asyncio
async def test_insert_message_with_media_type(db):
    msg = Message(
        channel_id=-100123,
        message_id=1,
        text=None,
        media_type="photo",
        date=datetime.now(timezone.utc),
    )
    inserted = await db.insert_message(msg)
    assert inserted is True

    messages, total = await db.search_messages()
    assert total == 1
    assert messages[0].media_type == "photo"
    assert messages[0].text is None


@pytest.mark.asyncio
async def test_batch_insert_with_media_type(db):
    messages = [
        Message(
            channel_id=-100123,
            message_id=1,
            text="Hello",
            media_type=None,
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100123,
            message_id=2,
            text=None,
            media_type="video",
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100123,
            message_id=3,
            text="With photo",
            media_type="photo",
            date=datetime.now(timezone.utc),
        ),
    ]
    count = await db.insert_messages_batch(messages)
    assert count > 0

    results, total = await db.search_messages()
    assert total == 3
    media_types = {m.message_id: m.media_type for m in results}
    assert media_types[1] is None
    assert media_types[2] == "video"
    assert media_types[3] == "photo"


@pytest.mark.asyncio
async def test_migrate_adds_media_type_column(tmp_path):
    """Migration adds media_type column to existing DB without it."""
    db_path = str(tmp_path / "migrate_test.db")

    # Create DB with old schema (no media_type)
    conn = await aiosqlite.connect(db_path)
    await conn.executescript("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER NOT NULL,
            message_id INTEGER NOT NULL,
            sender_id INTEGER,
            sender_name TEXT,
            text TEXT,
            date TEXT NOT NULL,
            collected_at TEXT DEFAULT (datetime('now')),
            UNIQUE(channel_id, message_id)
        );
        CREATE TABLE IF NOT EXISTS accounts (
            id INTEGER PRIMARY KEY,
            phone TEXT UNIQUE NOT NULL,
            session_string TEXT NOT NULL,
            is_primary INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1,
            flood_wait_until TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS channels (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER UNIQUE NOT NULL,
            title TEXT,
            username TEXT,
            is_active INTEGER DEFAULT 1,
            last_collected_id INTEGER DEFAULT 0,
            added_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS keywords (
            id INTEGER PRIMARY KEY,
            pattern TEXT NOT NULL,
            is_regex INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1
        );
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );
    """)
    await conn.commit()
    await conn.close()

    # Now initialize with Database — should run migration
    database = Database(db_path)
    await database.initialize()

    # Verify media_type column exists
    cur = await database.execute("PRAGMA table_info(messages)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "media_type" in columns

    # Verify we can insert with media_type
    msg = Message(
        channel_id=-100123,
        message_id=1,
        text="test",
        media_type="sticker",
        date=datetime.now(timezone.utc),
    )
    await database.insert_message(msg)
    messages, total = await database.search_messages()
    assert total == 1
    assert messages[0].media_type == "sticker"

    await database.close()


@pytest.mark.asyncio
async def test_add_channel_with_channel_type(db):
    ch = Channel(channel_id=-100123, title="Test", username="test", channel_type="channel")
    await db.add_channel(ch)

    channels = await db.get_channels()
    assert len(channels) == 1
    assert channels[0].channel_type == "channel"


@pytest.mark.asyncio
async def test_channel_type_upsert(db):
    ch = Channel(channel_id=-100123, title="Test", username="test", channel_type=None)
    await db.add_channel(ch)

    ch2 = Channel(channel_id=-100123, title="Test Updated", username="test", channel_type="group")
    await db.add_channel(ch2)

    channels = await db.get_channels()
    assert len(channels) == 1
    assert channels[0].channel_type == "group"
    assert channels[0].title == "Test Updated"


@pytest.mark.asyncio
async def test_channel_upsert_reactivates_existing_channel(db):
    await db.add_channel(
        Channel(channel_id=-100124, title="Test", username="test", is_active=False)
    )
    await db.add_channel(
        Channel(channel_id=-100124, title="Test Updated", username="test", is_active=True)
    )

    channels = await db.get_channels()
    assert len(channels) == 1
    assert channels[0].title == "Test Updated"
    assert channels[0].is_active is True


@pytest.mark.asyncio
async def test_migrate_adds_channel_type_column(tmp_path):
    """Migration adds channel_type column to existing channels table."""
    db_path = str(tmp_path / "migrate_channel_type_test.db")

    conn = await aiosqlite.connect(db_path)
    await conn.executescript("""
        CREATE TABLE IF NOT EXISTS accounts (
            id INTEGER PRIMARY KEY,
            phone TEXT UNIQUE NOT NULL,
            session_string TEXT NOT NULL,
            is_primary INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1,
            is_premium INTEGER DEFAULT 0,
            flood_wait_until TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS channels (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER UNIQUE NOT NULL,
            title TEXT,
            username TEXT,
            is_active INTEGER DEFAULT 1,
            last_collected_id INTEGER DEFAULT 0,
            added_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER NOT NULL,
            message_id INTEGER NOT NULL,
            sender_id INTEGER,
            sender_name TEXT,
            text TEXT,
            media_type TEXT,
            date TEXT NOT NULL,
            collected_at TEXT DEFAULT (datetime('now')),
            UNIQUE(channel_id, message_id)
        );
        CREATE TABLE IF NOT EXISTS keywords (
            id INTEGER PRIMARY KEY,
            pattern TEXT NOT NULL,
            is_regex INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1
        );
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );
    """)
    await conn.execute(
        "INSERT INTO channels (channel_id, title) VALUES (?, ?)",
        (-100123, "Old Channel"),
    )
    await conn.commit()
    await conn.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute("PRAGMA table_info(channels)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "channel_type" in columns
    assert "is_filtered" in columns
    assert "filter_flags" in columns

    channels = await database.get_channels()
    assert len(channels) == 1
    assert channels[0].channel_type is None
    assert channels[0].is_filtered is False
    assert channels[0].filter_flags == ""

    ch = Channel(channel_id=-100456, title="New", channel_type="group")
    await database.add_channel(ch)
    channels = await database.get_channels()
    assert any(c.channel_type == "group" for c in channels)

    await database.close()


@pytest.mark.asyncio
async def test_migrate_filter_columns_idempotent(tmp_path):
    db_path = str(tmp_path / "migrate_filter_columns_idempotent.db")

    conn = await aiosqlite.connect(db_path)
    await conn.executescript("""
        CREATE TABLE IF NOT EXISTS accounts (
            id INTEGER PRIMARY KEY,
            phone TEXT UNIQUE NOT NULL,
            session_string TEXT NOT NULL,
            is_primary INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1,
            flood_wait_until TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS channels (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER UNIQUE NOT NULL,
            title TEXT,
            username TEXT,
            is_active INTEGER DEFAULT 1,
            last_collected_id INTEGER DEFAULT 0,
            added_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER NOT NULL,
            message_id INTEGER NOT NULL,
            sender_id INTEGER,
            sender_name TEXT,
            text TEXT,
            media_type TEXT,
            date TEXT NOT NULL,
            collected_at TEXT DEFAULT (datetime('now')),
            UNIQUE(channel_id, message_id)
        );
        CREATE TABLE IF NOT EXISTS keywords (
            id INTEGER PRIMARY KEY,
            pattern TEXT NOT NULL,
            is_regex INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1
        );
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );
    """)
    await conn.commit()
    await conn.close()

    database = Database(db_path)
    await database.initialize()
    await database.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute("PRAGMA table_info(channels)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "is_filtered" in columns
    assert "filter_flags" in columns

    await database.close()


@pytest.mark.asyncio
async def test_migrate_adds_is_premium_column(tmp_path):
    """Migration adds is_premium column to existing accounts table without it."""
    db_path = str(tmp_path / "migrate_premium_test.db")

    conn = await aiosqlite.connect(db_path)
    await conn.executescript("""
        CREATE TABLE IF NOT EXISTS accounts (
            id INTEGER PRIMARY KEY,
            phone TEXT UNIQUE NOT NULL,
            session_string TEXT NOT NULL,
            is_primary INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1,
            flood_wait_until TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS channels (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER UNIQUE NOT NULL,
            title TEXT,
            username TEXT,
            is_active INTEGER DEFAULT 1,
            last_collected_id INTEGER DEFAULT 0,
            added_at TEXT DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY,
            channel_id INTEGER NOT NULL,
            message_id INTEGER NOT NULL,
            sender_id INTEGER,
            sender_name TEXT,
            text TEXT,
            media_type TEXT,
            date TEXT NOT NULL,
            collected_at TEXT DEFAULT (datetime('now')),
            UNIQUE(channel_id, message_id)
        );
        CREATE TABLE IF NOT EXISTS keywords (
            id INTEGER PRIMARY KEY,
            pattern TEXT NOT NULL,
            is_regex INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1
        );
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );
    """)
    # Insert an account without is_premium column
    await conn.execute(
        "INSERT INTO accounts (phone, session_string) VALUES (?, ?)",
        ("+71111111111", "session_old"),
    )
    await conn.commit()
    await conn.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute("PRAGMA table_info(accounts)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "is_premium" in columns

    accounts = await database.get_accounts()
    assert len(accounts) == 1
    assert accounts[0].is_premium is False

    await database.close()


@pytest.mark.asyncio
async def test_stats_task_claim_and_continuation(db):
    now = datetime.now(timezone.utc)
    payload = StatsAllTaskPayload(channel_ids=[-1001, -1002])
    tid = await db.create_stats_task(
        payload,
        run_after=now,
    )

    claimed = await db.repos.tasks.claim_next_due_generic_task(
        now, [CollectionTaskType.STATS_ALL.value]
    )
    assert claimed is not None
    assert claimed.id == tid
    assert claimed.task_type == CollectionTaskType.STATS_ALL
    assert claimed.status == CollectionTaskStatus.RUNNING
    assert isinstance(claimed.payload, StatsAllTaskPayload)
    assert claimed.payload.task_kind == CollectionTaskType.STATS_ALL.value

    continuation_id = await db.create_stats_continuation_task(
        payload=StatsAllTaskPayload(channel_ids=[-1001, -1002], next_index=1),
        run_after=now,
        parent_task_id=tid,
    )
    continuation = await db.get_collection_task(continuation_id)
    assert continuation is not None
    assert continuation.parent_task_id == tid
    assert continuation.status == CollectionTaskStatus.PENDING
    assert isinstance(continuation.payload, StatsAllTaskPayload)
    assert continuation.payload.next_index == 1


@pytest.mark.asyncio
async def test_requeue_running_stats_tasks_on_startup(db):
    tid = await db.create_stats_task(StatsAllTaskPayload(channel_ids=[]))
    await db.update_collection_task(tid, CollectionTaskStatus.RUNNING)

    requeued = await db.repos.tasks.requeue_running_generic_tasks_on_startup(
        datetime.now(timezone.utc), [CollectionTaskType.STATS_ALL.value]
    )
    assert requeued == 1

    task = await db.get_collection_task(tid)
    assert task is not None
    assert task.status == CollectionTaskStatus.PENDING
    assert task.run_after is not None


@pytest.mark.asyncio
async def test_stats_payload_round_trip_serialization(db):
    payload = StatsAllTaskPayload(channel_ids=[-1001], next_index=3, channels_ok=2, channels_err=1)
    task_id = await db.create_stats_task(payload)

    task = await db.get_collection_task(task_id)
    assert task is not None
    assert task.task_type == CollectionTaskType.STATS_ALL
    assert isinstance(task.payload, StatsAllTaskPayload)
    assert task.payload.model_dump() == payload.model_dump()


@pytest.mark.asyncio
async def test_cancel_collection_task_preserves_typed_status(db):
    task_id = await db.create_collection_task(-100123, "Channel")
    cancelled = await db.cancel_collection_task(task_id, note="cancelled in test")

    task = await db.get_collection_task(task_id)
    assert cancelled is True
    assert task is not None
    assert task.task_type == CollectionTaskType.CHANNEL_COLLECT
    assert task.status == CollectionTaskStatus.CANCELLED
    assert task.note == "cancelled in test"


@pytest.mark.asyncio
async def test_migration_backfills_collection_task_type(tmp_path):
    db_path = tmp_path / "legacy_tasks.db"
    stats_payload = (
        '{"task_kind":"stats_all","channel_ids":[],'
        '"next_index":0,"batch_size":20,"channels_ok":0,"channels_err":0}'
    )
    conn = await aiosqlite.connect(db_path)
    try:
        conn.row_factory = aiosqlite.Row
        await conn.executescript("""
            CREATE TABLE collection_tasks (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER NOT NULL,
                channel_title TEXT,
                status TEXT DEFAULT 'pending',
                messages_collected INTEGER DEFAULT 0,
                error TEXT,
                run_after TEXT,
                payload TEXT,
                parent_task_id INTEGER,
                created_at TEXT DEFAULT (datetime('now')),
                started_at TEXT,
                completed_at TEXT
            );
            """)
        await conn.execute(
            "INSERT INTO collection_tasks"
            " (id, channel_id, channel_title, status, payload)"
            " VALUES (?, ?, ?, ?, ?)",
            (1, 0, "Stats", "pending", stats_payload),
        )
        await conn.execute(
            "INSERT INTO collection_tasks"
            " (id, channel_id, channel_title, status, payload)"
            " VALUES (?, ?, ?, ?, ?)",
            (2, -1001, "Channel", "pending", None),
        )
        await conn.commit()
    finally:
        await conn.close()

    database = Database(str(db_path))
    await database.initialize()

    stats_task = await database.get_collection_task(1)
    channel_task = await database.get_collection_task(2)
    assert stats_task is not None
    assert channel_task is not None
    assert stats_task.task_type == CollectionTaskType.STATS_ALL
    assert stats_task.channel_id is None
    assert channel_task.task_type == CollectionTaskType.CHANNEL_COLLECT
    assert channel_task.channel_id == -1001

    await database.close()


@pytest.mark.asyncio
async def test_delete_messages_for_channel(db):
    ch1 = Channel(channel_id=-100301, title="Channel 1")
    ch2 = Channel(channel_id=-100302, title="Channel 2")
    await db.add_channel(ch1)
    await db.add_channel(ch2)

    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100301, message_id=i, text=f"msg {i}", date=datetime.now(timezone.utc)
            )
            for i in range(1, 6)
        ]
    )
    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100302, message_id=i, text=f"msg {i}", date=datetime.now(timezone.utc)
            )
            for i in range(1, 4)
        ]
    )

    deleted = await db.delete_messages_for_channel(-100301)
    assert deleted == 5

    # Only ch2 messages remain
    results, total = await db.search_messages()
    assert total == 3
    assert all(m.channel_id == -100302 for m in results)


@pytest.mark.asyncio
async def test_search_excludes_filtered_channels(db):
    ch_ok = Channel(channel_id=-100311, title="Good Channel")
    ch_bad = Channel(channel_id=-100312, title="Filtered Channel")
    await db.add_channel(ch_ok)
    await db.add_channel(ch_bad)
    await db.set_channels_filtered_bulk([(-100312, "low_uniqueness")])

    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100311,
                message_id=1,
                text="good message",
                date=datetime.now(timezone.utc),
            ),
        ]
    )
    await db.insert_messages_batch(
        [
            Message(
                channel_id=-100312,
                message_id=1,
                text="filtered message",
                date=datetime.now(timezone.utc),
            ),
        ]
    )

    results, total = await db.search_messages()
    assert total == 1
    assert results[0].channel_id == -100311

    results_fts, total_fts = await db.search_messages(query="message")
    assert total_fts == 1
    assert results_fts[0].channel_id == -100311


@pytest.mark.asyncio
async def test_update_channel_meta(db):
    """update_channel_meta updates username and title, leaving other fields intact."""
    ch = Channel(channel_id=9999001, title="Original Title", username="original_user")
    await db.add_channel(ch)

    await db.update_channel_meta(9999001, username="new_user", title="New Title")

    stored = await db.get_channel_by_channel_id(9999001)
    assert stored is not None
    assert stored.username == "new_user"
    assert stored.title == "New Title"
    # Other fields must remain untouched
    assert stored.is_active is True
    assert stored.is_filtered is False


@pytest.mark.aiosqlite_serial
@pytest.mark.asyncio
async def test_migrate_adds_reactions_json_column(tmp_path):
    """Migration adds reactions_json column to existing DB without it."""
    db_path = str(tmp_path / "migrate_reactions.db")

    conn = await aiosqlite.connect(db_path)
    try:
        await conn.executescript("""
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER NOT NULL,
                message_id INTEGER NOT NULL,
                sender_id INTEGER,
                sender_name TEXT,
                text TEXT,
                media_type TEXT,
                topic_id INTEGER,
                date TEXT NOT NULL,
                collected_at TEXT DEFAULT (datetime('now')),
                UNIQUE(channel_id, message_id)
            );
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY,
                phone TEXT UNIQUE NOT NULL,
                session_string TEXT NOT NULL,
                is_primary INTEGER DEFAULT 0,
                is_active INTEGER DEFAULT 1,
                flood_wait_until TEXT,
                created_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS channels (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER UNIQUE NOT NULL,
                title TEXT,
                username TEXT,
                is_active INTEGER DEFAULT 1,
                last_collected_id INTEGER DEFAULT 0,
                added_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
        """)
        await conn.commit()
    finally:
        await conn.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute("PRAGMA table_info(messages)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "reactions_json" in columns

    msg = Message(
        channel_id=-100123,
        message_id=1,
        text="test",
        reactions_json='[{"emoji": "👍", "count": 5}]',
        date=datetime.now(timezone.utc),
    )
    await database.insert_message(msg)
    messages, total = await database.search_messages()
    assert total == 1
    assert messages[0].reactions_json == '[{"emoji": "👍", "count": 5}]'

    await database.close()


@pytest.mark.asyncio
async def test_reactions_json_roundtrip(db):
    """reactions_json is persisted and retrieved via search_messages."""
    await db.add_channel(Channel(channel_id=-100999, title="ReactTest"))
    msg = Message(
        channel_id=-100999,
        message_id=42,
        text="hello",
        reactions_json='[{"emoji": "❤️", "count": 3}]',
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    messages, total = await db.search_messages(channel_id=-100999)
    assert total == 1
    assert messages[0].reactions_json == '[{"emoji": "❤️", "count": 3}]'


@pytest.mark.asyncio
async def test_reactions_json_null_when_absent(db):
    """reactions_json is None when message has no reactions."""
    await db.add_channel(Channel(channel_id=-100998, title="NoReact"))
    msg = Message(
        channel_id=-100998,
        message_id=1,
        text="no reactions",
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    messages, _ = await db.search_messages(channel_id=-100998)
    assert messages[0].reactions_json is None


# ---------------------------------------------------------------------------
# message_reactions table tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_message_reactions_populated_on_insert(db):
    """insert_message populates message_reactions from reactions_json."""
    await db.add_channel(Channel(channel_id=-100111, title="ReactNorm"))
    msg = Message(
        channel_id=-100111,
        message_id=1,
        text="react test",
        reactions_json='[{"emoji": "👍", "count": 5}, {"emoji": "❤️", "count": 2}]',
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    cur = await db.execute(
        "SELECT emoji, count FROM message_reactions WHERE channel_id = ? AND message_id = ?",
        (-100111, 1),
    )
    rows = await cur.fetchall()
    result = {r["emoji"]: r["count"] for r in rows}
    assert result == {"👍": 5, "❤️": 2}


@pytest.mark.asyncio
async def test_message_reactions_not_populated_when_no_reactions(db):
    """insert_message does not create message_reactions rows when reactions_json is None."""
    await db.add_channel(Channel(channel_id=-100112, title="NoReactNorm"))
    msg = Message(
        channel_id=-100112,
        message_id=1,
        text="no reactions",
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    cur = await db.execute(
        "SELECT COUNT(*) AS cnt FROM message_reactions WHERE channel_id = ?", (-100112,)
    )
    row = await cur.fetchone()
    assert row["cnt"] == 0


@pytest.mark.asyncio
async def test_message_reactions_batch_insert(db):
    """insert_messages_batch populates message_reactions for all messages with reactions."""
    await db.add_channel(Channel(channel_id=-100113, title="BatchReact"))
    messages = [
        Message(
            channel_id=-100113,
            message_id=i,
            text=f"msg {i}",
            reactions_json=f'[{{"emoji": "🔥", "count": {i * 10}}}]',
            date=datetime.now(timezone.utc),
        )
        for i in range(1, 4)
    ]
    await db.insert_messages_batch(messages)

    cur = await db.execute(
        "SELECT SUM(count) AS total FROM message_reactions WHERE channel_id = ?",
        (-100113,),
    )
    row = await cur.fetchone()
    assert row["total"] == 10 + 20 + 30


@pytest.mark.asyncio
async def test_get_trending_emojis(db):
    """get_trending_emojis returns emojis sorted by total count."""
    await db.add_channel(Channel(channel_id=-100114, title="Trending"))
    messages = [
        Message(
            channel_id=-100114,
            message_id=1,
            text="a",
            reactions_json='[{"emoji": "👍", "count": 10}, {"emoji": "❤️", "count": 3}]',
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100114,
            message_id=2,
            text="b",
            reactions_json='[{"emoji": "👍", "count": 5}, {"emoji": "🔥", "count": 20}]',
            date=datetime.now(timezone.utc),
        ),
    ]
    await db.insert_messages_batch(messages)

    trending = await db.get_trending_emojis(limit=3)
    assert len(trending) == 3
    emojis = [t["emoji"] for t in trending]
    # 🔥=20, 👍=15, ❤️=3
    assert emojis[0] == "🔥"
    assert emojis[1] == "👍"
    assert trending[0]["count"] == 20
    assert trending[1]["count"] == 15


@pytest.mark.asyncio
async def test_get_top_reacted_messages(db):
    """get_top_reacted_messages returns messages ordered by total reactions."""
    await db.add_channel(Channel(channel_id=-100115, title="TopReact"))
    messages = [
        Message(
            channel_id=-100115,
            message_id=1,
            text="low",
            reactions_json='[{"emoji": "👍", "count": 2}]',
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100115,
            message_id=2,
            text="high",
            reactions_json='[{"emoji": "🔥", "count": 50}, {"emoji": "❤️", "count": 10}]',
            date=datetime.now(timezone.utc),
        ),
        Message(
            channel_id=-100115,
            message_id=3,
            text="mid",
            reactions_json='[{"emoji": "😂", "count": 7}]',
            date=datetime.now(timezone.utc),
        ),
    ]
    await db.insert_messages_batch(messages)

    top = await db.get_top_reacted_messages(limit=2, channel_id=-100115)
    assert len(top) == 2
    # message 2 has 60 total, message 3 has 7
    assert top[0].message_id == 2
    assert top[1].message_id == 3


@pytest.mark.asyncio
async def test_get_top_messages_empty(db):
    """get_top_messages returns empty list when no messages with reactions exist."""
    result = await db.get_top_messages(limit=10)
    assert result == []


@pytest.mark.asyncio
async def test_get_top_messages_sorted_by_reactions(db):
    """get_top_messages returns messages sorted by total reaction count descending."""
    await db.add_channel(Channel(channel_id=-100500, title="TopTest"))
    msgs = [
        Message(
            channel_id=-100500,
            message_id=1,
            text="low",
            reactions_json='[{"emoji": "👍", "count": 2}]',
            date=datetime(2025, 1, 1, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100500,
            message_id=2,
            text="high",
            reactions_json='[{"emoji": "❤️", "count": 10}, {"emoji": "🔥", "count": 5}]',
            date=datetime(2025, 1, 2, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100500,
            message_id=3,
            text="no reactions",
            date=datetime(2025, 1, 3, tzinfo=timezone.utc),
        ),
    ]
    for m in msgs:
        await db.insert_message(m)

    result = await db.get_top_messages(limit=10)
    assert len(result) == 2  # message without reactions excluded
    assert result[0]["message_id"] == 2
    assert result[0]["total_reactions"] == 15
    assert result[1]["message_id"] == 1
    assert result[1]["total_reactions"] == 2


@pytest.mark.asyncio
async def test_get_top_messages_date_filter(db):
    """get_top_messages respects date_from/date_to filters."""
    await db.add_channel(Channel(channel_id=-100501, title="DateFilter"))
    msgs = [
        Message(
            channel_id=-100501,
            message_id=1,
            text="old",
            reactions_json='[{"emoji": "👍", "count": 5}]',
            date=datetime(2024, 6, 1, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100501,
            message_id=2,
            text="new",
            reactions_json='[{"emoji": "👍", "count": 3}]',
            date=datetime(2025, 3, 1, tzinfo=timezone.utc),
        ),
    ]
    for m in msgs:
        await db.insert_message(m)

    result = await db.get_top_messages(limit=10, date_from="2025-01-01")
    assert len(result) == 1
    assert result[0]["message_id"] == 2


@pytest.mark.asyncio
async def test_get_engagement_by_media_type(db):
    """get_engagement_by_media_type groups messages by media type with counts."""
    await db.add_channel(Channel(channel_id=-100502, title="MediaTypes"))
    msgs = [
        Message(
            channel_id=-100502,
            message_id=1,
            text="text msg",
            media_type=None,
            reactions_json='[{"emoji": "👍", "count": 4}]',
            date=datetime(2025, 1, 1, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100502,
            message_id=2,
            text="photo msg",
            media_type="photo",
            reactions_json='[{"emoji": "❤️", "count": 6}]',
            date=datetime(2025, 1, 2, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100502,
            message_id=3,
            text="another text",
            media_type=None,
            date=datetime(2025, 1, 3, tzinfo=timezone.utc),
        ),
    ]
    for m in msgs:
        await db.insert_message(m)

    result = await db.get_engagement_by_media_type()
    content_types = {r["content_type"]: r for r in result}
    assert "text" in content_types
    assert "photo" in content_types
    assert content_types["text"]["message_count"] == 2
    assert content_types["photo"]["message_count"] == 1


@pytest.mark.asyncio
async def test_get_hourly_activity(db):
    """get_hourly_activity groups messages by hour with correct counts."""
    await db.add_channel(Channel(channel_id=-100503, title="Hourly"))
    msgs = [
        Message(
            channel_id=-100503,
            message_id=1,
            text="morning",
            date=datetime(2025, 1, 1, 9, 0, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100503,
            message_id=2,
            text="morning2",
            date=datetime(2025, 1, 2, 9, 30, tzinfo=timezone.utc),
        ),
        Message(
            channel_id=-100503,
            message_id=3,
            text="evening",
            date=datetime(2025, 1, 1, 20, 0, tzinfo=timezone.utc),
        ),
    ]
    for m in msgs:
        await db.insert_message(m)

    result = await db.get_hourly_activity()
    hours = {r["hour"]: r for r in result}
    assert 9 in hours
    assert hours[9]["message_count"] == 2
    assert 20 in hours
    assert hours[20]["message_count"] == 1


@pytest.mark.aiosqlite_serial
@pytest.mark.asyncio
async def test_migration_backfills_message_reactions(tmp_path):
    """Migration backfills message_reactions from existing reactions_json data."""
    db_path = str(tmp_path / "migrate_reactions_norm.db")

    # Build a DB without the message_reactions table but with reactions_json data
    conn = await aiosqlite.connect(db_path)
    try:
        await conn.executescript("""
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER NOT NULL,
                message_id INTEGER NOT NULL,
                sender_id INTEGER,
                sender_name TEXT,
                text TEXT,
                media_type TEXT,
                topic_id INTEGER,
                reactions_json TEXT,
                date TEXT NOT NULL,
                collected_at TEXT DEFAULT (datetime('now')),
                UNIQUE(channel_id, message_id)
            );
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY,
                phone TEXT UNIQUE NOT NULL,
                session_string TEXT NOT NULL,
                is_primary INTEGER DEFAULT 0,
                is_active INTEGER DEFAULT 1,
                flood_wait_until TEXT,
                created_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS channels (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER UNIQUE NOT NULL,
                title TEXT,
                username TEXT,
                is_active INTEGER DEFAULT 1,
                last_collected_id INTEGER DEFAULT 0,
                added_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
        """)
        await conn.execute(
            "INSERT INTO messages (channel_id, message_id, text, reactions_json, date) "
            "VALUES (?, ?, ?, ?, datetime('now'))",
            (-100200, 1, "backfill test", '[{"emoji": "🎉", "count": 7}]'),
        )
        await conn.commit()
    finally:
        await conn.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute(
        "SELECT emoji, count FROM message_reactions WHERE channel_id = ? AND message_id = ?",
        (-100200, 1),
    )
    rows = await cur.fetchall()
    result = {r["emoji"]: r["count"] for r in rows}
    assert result == {"🎉": 7}

    await database.close()


@pytest.mark.aiosqlite_serial
@pytest.mark.asyncio
async def test_migrate_adds_engagement_columns(tmp_path):
    """Migration adds views, forwards, reply_count columns to existing DB without them."""
    db_path = str(tmp_path / "migrate_engagement.db")

    conn = await aiosqlite.connect(db_path)
    try:
        await conn.executescript("""
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER NOT NULL,
                message_id INTEGER NOT NULL,
                sender_id INTEGER,
                sender_name TEXT,
                text TEXT,
                media_type TEXT,
                topic_id INTEGER,
                reactions_json TEXT,
                date TEXT NOT NULL,
                collected_at TEXT DEFAULT (datetime('now')),
                UNIQUE(channel_id, message_id)
            );
            CREATE TABLE IF NOT EXISTS accounts (
                id INTEGER PRIMARY KEY,
                phone TEXT UNIQUE NOT NULL,
                session_string TEXT NOT NULL,
                is_primary INTEGER DEFAULT 0,
                is_active INTEGER DEFAULT 1,
                flood_wait_until TEXT,
                created_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS channels (
                id INTEGER PRIMARY KEY,
                channel_id INTEGER UNIQUE NOT NULL,
                title TEXT,
                username TEXT,
                is_active INTEGER DEFAULT 1,
                last_collected_id INTEGER DEFAULT 0,
                added_at TEXT DEFAULT (datetime('now'))
            );
            CREATE TABLE IF NOT EXISTS settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
        """)
        await conn.commit()
    finally:
        await conn.close()

    database = Database(db_path)
    await database.initialize()

    cur = await database.execute("PRAGMA table_info(messages)")
    columns = {row["name"] for row in await cur.fetchall()}
    assert "views" in columns
    assert "forwards" in columns
    assert "reply_count" in columns

    await database.close()


@pytest.mark.asyncio
async def test_engagement_stats_roundtrip(db):
    """views, forwards, reply_count are persisted and retrieved via search_messages."""
    await db.add_channel(Channel(channel_id=-100777, title="EngageTest"))
    msg = Message(
        channel_id=-100777,
        message_id=10,
        text="popular post",
        views=1234,
        forwards=56,
        reply_count=7,
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    messages, total = await db.search_messages(channel_id=-100777)
    assert total == 1
    assert messages[0].views == 1234
    assert messages[0].forwards == 56
    assert messages[0].reply_count == 7


@pytest.mark.asyncio
async def test_engagement_stats_null_when_absent(db):
    """views, forwards, reply_count are None when not provided."""
    await db.add_channel(Channel(channel_id=-100776, title="NoEngageTest"))
    msg = Message(
        channel_id=-100776,
        message_id=1,
        text="plain post",
        date=datetime.now(timezone.utc),
    )
    await db.insert_message(msg)

    messages, _ = await db.search_messages(channel_id=-100776)
    assert messages[0].views is None
    assert messages[0].forwards is None
    assert messages[0].reply_count is None
