"""DB migration / schema 测试（issue #025 规范）。

业界最佳实践 4 种模式：
1. Schema from scratch —— 空 db init_db 所有表 / 列 / 索引齐
2. Idempotent —— 同 migration 跑 2 次不破坏
3. Historical snapshot —— 模拟老库升级（阶段 2 做）
4. Round-trip —— 老数据不丢（带 DROP / 改列时必做）

本文件当前覆盖 1 + 2。历史 snapshot 用 fixture 后续补。
"""

from __future__ import annotations

import sqlite3
from pathlib import Path

import pytest

from xiangqin import db


# ─── helpers ───────────────────────────────────────────


def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
    row = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,)
    ).fetchone()
    return row is not None


def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
    return {r[1] for r in conn.execute(f"PRAGMA table_info({table})")}


def _index_exists(conn: sqlite3.Connection, name: str) -> bool:
    row = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='index' AND name=?", (name,)
    ).fetchone()
    return row is not None


# ─── Pattern 1: Schema from scratch ─────────────────────


def test_init_db_creates_all_expected_tables(tmp_path: Path) -> None:
    """空 db init_db 应建所有 0.4.5 schema 要求的表。"""
    db_path = tmp_path / "schema.db"
    db.init_db(db_path)

    expected_tables = {
        "users",
        "profiles",
        "auth_requests",
        "sessions",
        "messages",
        "envelope_usage",
        "envelope_balance",
        "envelope_orders",
        "reports",
        "blocks",
        "notify_state",
        "user_photos",  # FEAT-008 新表
    }
    with sqlite3.connect(db_path) as conn:
        for t in expected_tables:
            assert _table_exists(conn, t), f"表 {t} 未建"


def test_user_photos_schema_fields(tmp_path: Path) -> None:
    """FEAT-008 新表 user_photos 字段齐全。"""
    db_path = tmp_path / "photos.db"
    db.init_db(db_path)

    expected_cols = {
        "id",
        "user_id",
        "purpose",
        "oss_key",
        "sha256",
        "content_type",
        "size_bytes",
        "uploaded_at",
        "moderation_status",
        "moderation_reason",
    }
    with sqlite3.connect(db_path) as conn:
        cols = _column_names(conn, "user_photos")
    assert expected_cols.issubset(cols), f"user_photos 缺字段: {expected_cols - cols}"


def test_profiles_has_avatar_photo_id_column(tmp_path: Path) -> None:
    """FEAT-008 迁移：profiles 必须有 avatar_photo_id 列。"""
    db_path = tmp_path / "profile.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        cols = _column_names(conn, "profiles")
    assert "avatar_photo_id" in cols


def test_expected_indexes_created(tmp_path: Path) -> None:
    """性能关键索引必须建（profiles gender_city_age、user_photos user_purpose 等）。"""
    db_path = tmp_path / "idx.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        assert _index_exists(conn, "idx_profiles_gender_city_age")
        assert _index_exists(conn, "idx_user_photos_user_purpose")


# ─── Pattern 2: Idempotent ─────────────────────────────


def test_init_db_idempotent(tmp_path: Path) -> None:
    """init_db 跑 2 次不报错不重复建（CREATE TABLE IF NOT EXISTS 幂等）。"""
    db_path = tmp_path / "idem.db"
    db.init_db(db_path)
    # 跑第 2 次应不抛
    db.init_db(db_path)
    # 且表还在 + 无重复
    with sqlite3.connect(db_path) as conn:
        rows = conn.execute(
            "SELECT name, COUNT(*) FROM sqlite_master WHERE type='table' GROUP BY name"
        ).fetchall()
        for name, count in rows:
            assert count == 1, f"表 {name} 重复建了 {count} 次"


def test_migrate_profile_columns_idempotent(tmp_path: Path) -> None:
    """_migrate_profile_columns 跑 2 次不破坏（ALTER TABLE 不能重复加列，函数自己判断）。"""
    db_path = tmp_path / "mig.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        # 第 1 次已经在 init 里跑了，再手动跑一次应该 no-op
        db._migrate_profile_columns(conn)
        db._migrate_profile_columns(conn)
    # assert 没报错 + 列还在
    with sqlite3.connect(db_path) as conn:
        cols = _column_names(conn, "profiles")
    for expected in [
        "height",
        "education",
        "agent_gateway_url",
        "agent_hooks_token",
        "notify_on_new_mail",
        "avatar_photo_id",
    ]:
        assert expected in cols, f"列 {expected} 未加"


def test_migrate_users_realname_columns(tmp_path: Path) -> None:
    """FEAT-033：users 加 verified_at / cert_id_hash / legal_name + 唯一索引。"""
    db_path = tmp_path / "realname.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        cols = _column_names(conn, "users")
    for expected in ["verified_at", "cert_id_hash", "legal_name"]:
        assert expected in cols, f"列 {expected} 未加"
    # 唯一索引防一人多注册
    with sqlite3.connect(db_path) as conn:
        assert _index_exists(conn, "idx_users_cert_id_hash")


def test_migrate_users_realname_idempotent(tmp_path: Path) -> None:
    """realname 迁移跑 2 次不报错。"""
    db_path = tmp_path / "realname-idem.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        db._migrate_users_realname(conn)
        db._migrate_users_realname(conn)


def test_cert_id_hash_unique_constraint(tmp_path: Path) -> None:
    """cert_id_hash 唯一索引：同一身份证号绑第 2 个 user 应 IntegrityError。"""
    import pytest

    db_path = tmp_path / "unique.db"
    db.init_db(db_path)
    h = db.hash_cert_id("110101199001011237")
    with sqlite3.connect(db_path) as conn:
        conn.execute(
            "INSERT INTO users (id, phone_hash, created_at, verified_at, cert_id_hash, legal_name) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("u1", db.hash_phone("13800001111"), 1000, 1001, h, "张三"),
        )
        conn.commit()
        # 第二个 user 绑同一 cert_id_hash 应抛
        with pytest.raises(sqlite3.IntegrityError):
            conn.execute(
                "INSERT INTO users (id, phone_hash, created_at, verified_at, cert_id_hash, legal_name) "
                "VALUES (?, ?, ?, ?, ?, ?)",
                ("u2", db.hash_phone("13800002222"), 2000, 2001, h, "张三"),
            )
            conn.commit()


def test_hash_cert_id_deterministic_but_salted(monkeypatch):
    """hash_cert_id：同 salt 同输入输出一致；salt 变了 hash 变。"""
    monkeypatch.setenv("XIANGQIN_CERT_ID_SALT", "salt-a")
    import importlib
    from xiangqin import db as db_mod

    importlib.reload(db_mod)
    h1 = db_mod.hash_cert_id("110101199001011237")
    h2 = db_mod.hash_cert_id("110101199001011237")
    assert h1 == h2
    monkeypatch.setenv("XIANGQIN_CERT_ID_SALT", "salt-b")
    importlib.reload(db_mod)
    h3 = db_mod.hash_cert_id("110101199001011237")
    assert h3 != h1
    # restore
    monkeypatch.delenv("XIANGQIN_CERT_ID_SALT", raising=False)
    importlib.reload(db_mod)


def test_drop_exposure_tables_idempotent(tmp_path: Path) -> None:
    """DROP 不存在的 exposure 表不报错（DROP TABLE IF EXISTS 幂等）。"""
    db_path = tmp_path / "drop.db"
    db.init_db(db_path)
    with sqlite3.connect(db_path) as conn:
        db._drop_exposure_tables(conn)
        db._drop_exposure_tables(conn)
    # 验证表真没了
    with sqlite3.connect(db_path) as conn:
        assert not _table_exists(conn, "exposure_orders")
        assert not _table_exists(conn, "exposure_usage")


# ─── Pattern 3: Historical snapshot（阶段 2 TODO）──────


@pytest.mark.skip(reason="待 fixtures/db-snapshots/ 准备好后启用")
def test_upgrade_from_v030_pre_m4_preserves_users_data() -> None:
    """模拟 0.3.0 老 schema（只有 users / profiles 4 列 / exposure_orders）+ 有数据，
    跑完 init_db 迁移后，users 数据不丢 + 新列是 NULL + exposure 表被 DROP。"""
    ...


# ─── Pattern 4: Round-trip（阶段 2 TODO）───────────────


@pytest.mark.skip(reason="待 Pattern 3 的 snapshot 准备好后启用")
def test_migrate_preserves_profile_data() -> None:
    """老 profile 数据（gender / age / city / tags / bio）migration 后不变。"""
    ...
