"""FEAT-010 数据生命周期单测 + 集成测试。

覆盖：
- unregister 软删 + 幂等 + 清 session/agent token
- reactivate 冷静期内成功 / 冷静期外拒绝 / 已 scrub 拒绝
- export_data 完整性
- purge_expired 真物理删 + 保 envelope_orders + scrub phone_hash
- hard_delete 立即清 + 保 orders
- FastAPI endpoints /account/unregister + /account/export + admin 三个
"""

from __future__ import annotations

import pytest
from fastapi.testclient import TestClient

from xiangqin import account, auth


def _user(phone: str = "13800001111") -> str:
    r = auth.register(phone)
    return auth.verify(r.request_id, "123456").user_id


def _three_users() -> tuple[str, str, str]:
    return (
        _user("13800001111"),
        _user("13800002222"),
        _user("13800003333"),
    )


# ============ unregister ============


def test_unregister_sets_deleted_at():
    u = _user()
    r = account.unregister(u)
    assert r["deleted_at"] is not None
    assert r["grace_until"] == r["deleted_at"] + 30 * 86400
    assert r["already_unregistered"] is False


def test_unregister_idempotent():
    u = _user()
    account.unregister(u)
    r2 = account.unregister(u)
    assert r2["already_unregistered"] is True


def test_unregister_clears_agent_token_and_sessions():
    from xiangqin import profile as profile_svc
    from xiangqin.db import get_conn

    u = _user()
    profile_svc.update(u, {"agent_gateway_url": "http://x", "agent_hooks_token": "t"})
    account.unregister(u)
    with get_conn() as conn:
        p = conn.execute(
            "SELECT agent_gateway_url, agent_hooks_token FROM profiles WHERE user_id = ?",
            (u,),
        ).fetchone()
        s = conn.execute("SELECT COUNT(*) AS n FROM sessions WHERE user_id = ?", (u,)).fetchone()
    assert p["agent_gateway_url"] is None
    assert p["agent_hooks_token"] is None
    assert s["n"] == 0


def test_unregister_unknown_user_raises():
    with pytest.raises(account.AccountError) as ei:
        account.unregister("no-such-user")
    assert ei.value.code == "user_not_found"


# ============ reactivate ============


def test_reactivate_within_grace():
    u = _user()
    account.unregister(u)
    r = account.reactivate(u)
    assert "reactivated_at" in r

    from xiangqin.db import get_conn

    with get_conn() as conn:
        row = conn.execute("SELECT deleted_at FROM users WHERE id = ?", (u,)).fetchone()
    assert row["deleted_at"] is None


def test_reactivate_not_unregistered_raises():
    u = _user()
    with pytest.raises(account.AccountError) as ei:
        account.reactivate(u)
    assert ei.value.code == "not_unregistered"


def test_reactivate_past_grace_raises(monkeypatch: pytest.MonkeyPatch):
    u = _user()
    account.unregister(u)

    # 伪造 now_ts 往后跳 31 天
    real_now = account.now_ts
    captured = real_now()
    monkeypatch.setattr(account, "now_ts", lambda: captured + 31 * 86400)
    with pytest.raises(account.AccountError) as ei:
        account.reactivate(u)
    assert ei.value.code == "grace_expired"


def test_reactivate_purged_user_raises():
    """phone_hash 已 scrub → reactivate 不可行（即使时间窗合适）。"""
    u = _user()
    account.unregister(u)
    account.hard_delete(u)  # scrub
    with pytest.raises(account.AccountError) as ei:
        account.reactivate(u)
    assert ei.value.code == "already_purged"


# ============ export_data ============


def test_export_data_schema():
    u = _user()
    data = account.export_data(u)
    assert data["schema_version"] == 1
    assert data["user"]["id"] == u
    # 核心字段都有 key（可能为空列表 / None）
    for k in (
        "profile",
        "messages_sent",
        "messages_received",
        "blocks_outgoing",
        "blocks_incoming",
        "reports_filed",
        "envelope_orders",
        "envelope_balance",
        "photos",
    ):
        assert k in data


def test_export_data_contains_messages():
    from xiangqin import mail

    a, b = _user("13800001111"), _user("13800002222")
    mail.send(a, b, "hello b")
    data_a = account.export_data(a)
    data_b = account.export_data(b)
    assert len(data_a["messages_sent"]) == 1
    assert data_a["messages_sent"][0]["body"] == "hello b"
    assert len(data_b["messages_received"]) == 1


def test_export_data_unknown_user_raises():
    with pytest.raises(account.AccountError) as ei:
        account.export_data("no-such-user")
    assert ei.value.code == "user_not_found"


def test_export_data_json_is_serializable():
    u = _user()
    s = account.export_data_json(u)
    assert isinstance(s, str)
    import json as _json

    data = _json.loads(s)
    assert data["user"]["id"] == u


# ============ purge_expired ============


def test_purge_expired_scrubs_users_and_deletes_children(monkeypatch: pytest.MonkeyPatch):
    from xiangqin import mail
    from xiangqin.db import get_conn

    a, b = _user("13800001111"), _user("13800002222")
    mail.send(a, b, "hello")  # 留条消息
    account.unregister(a)

    # 伪造 now 跳过 31 天
    captured = account.now_ts()
    monkeypatch.setattr(account, "now_ts", lambda: captured + 31 * 86400)
    r = account.purge_expired()
    assert r["purged_count"] == 1
    assert a in r["purged_ids"]

    with get_conn() as conn:
        u_row = conn.execute("SELECT phone_hash FROM users WHERE id = ?", (a,)).fetchone()
        p_row = conn.execute("SELECT * FROM profiles WHERE user_id = ?", (a,)).fetchone()
        m_row = conn.execute(
            "SELECT COUNT(*) AS n FROM messages WHERE from_user_id = ? OR to_user_id = ?",
            (a, a),
        ).fetchone()
    assert u_row["phone_hash"].startswith("DELETED:")
    assert p_row is None
    assert m_row["n"] == 0


def test_purge_expired_ignores_recent(monkeypatch: pytest.MonkeyPatch):
    u = _user()
    account.unregister(u)
    # 不跳时间：才刚注销，应跳过
    r = account.purge_expired()
    assert r["purged_count"] == 0

    from xiangqin.db import get_conn

    with get_conn() as conn:
        row = conn.execute("SELECT phone_hash FROM users WHERE id = ?", (u,)).fetchone()
    assert not row["phone_hash"].startswith("DELETED:")


def test_purge_expired_preserves_envelope_orders(monkeypatch: pytest.MonkeyPatch):
    """会计法 5 年留存：订单不删。"""
    from xiangqin import mail
    from xiangqin.db import get_conn

    u = _user()
    # 塞一条 envelope_order
    with get_conn() as conn:
        conn.execute(
            "INSERT INTO envelope_orders "
            "(id, user_id, count, amount_cents, mock, paid, created_at, status) "
            "VALUES ('ord1', ?, 10, 1000, 0, 1, 1000000, 'paid')",
            (u,),
        )
    account.unregister(u)
    captured = account.now_ts()
    monkeypatch.setattr(account, "now_ts", lambda: captured + 31 * 86400)
    account.purge_expired()

    with get_conn() as conn:
        o = conn.execute("SELECT id FROM envelope_orders WHERE user_id = ?", (u,)).fetchone()
    assert o is not None
    assert o["id"] == "ord1"
    # 释放引用
    del mail


def test_purge_expired_skips_already_purged(monkeypatch: pytest.MonkeyPatch):
    u = _user()
    account.unregister(u)
    account.hard_delete(u)  # 直接硬删，phone_hash 已 scrub

    captured = account.now_ts()
    monkeypatch.setattr(account, "now_ts", lambda: captured + 31 * 86400)
    r = account.purge_expired()
    # 已 scrub 的不再被 purge
    assert u not in r["purged_ids"]


# ============ hard_delete ============


def test_hard_delete_immediate():
    from xiangqin.db import get_conn

    u = _user()
    r = account.hard_delete(u)
    assert r["user_id"] == u
    with get_conn() as conn:
        row = conn.execute("SELECT phone_hash FROM users WHERE id = ?", (u,)).fetchone()
    assert row["phone_hash"].startswith("DELETED:")


def test_hard_delete_unknown_raises():
    with pytest.raises(account.AccountError) as ei:
        account.hard_delete("no-such-user")
    assert ei.value.code == "user_not_found"


# ============ FastAPI endpoints ============


@pytest.fixture
def client():
    from xiangqin.app import app

    return TestClient(app)


def _login(client: TestClient, phone: str) -> tuple[str, str]:
    r = client.post("/auth/register", json={"phone": phone})
    req = r.json()["request_id"]
    v = client.post("/auth/verify", json={"request_id": req, "code": "123456"})
    d = v.json()
    return d["session_token"], d["user_id"]


def test_account_unregister_endpoint(client):
    tok, uid = _login(client, "13800005001")
    r = client.post(
        "/account/unregister",
        headers={"Authorization": f"Bearer {tok}"},
    )
    assert r.status_code == 200
    d = r.json()
    assert d["user_id"] == uid
    assert d["deleted_at"] is not None
    assert d["grace_until"] == d["deleted_at"] + 30 * 86400


def test_account_export_endpoint(client):
    tok, uid = _login(client, "13800005002")
    r = client.get("/account/export", headers={"Authorization": f"Bearer {tok}"})
    assert r.status_code == 200
    data = r.json()
    assert data["user"]["id"] == uid


def test_admin_hard_delete_endpoint(client, monkeypatch: pytest.MonkeyPatch):
    admin_tok, admin_uid = _login(client, "13800005003")
    _, target_uid = _login(client, "13800005004")
    monkeypatch.setenv("XIANGQIN_ADMIN_USER_IDS", admin_uid)

    r = client.post(
        f"/admin/user/{target_uid}/hard-delete",
        headers={"Authorization": f"Bearer {admin_tok}"},
    )
    assert r.status_code == 200
    assert r.json()["user_id"] == target_uid


def test_admin_reactivate_endpoint(client, monkeypatch: pytest.MonkeyPatch):
    admin_tok, admin_uid = _login(client, "13800005005")
    tgt_tok, target_uid = _login(client, "13800005006")
    monkeypatch.setenv("XIANGQIN_ADMIN_USER_IDS", admin_uid)

    # target 先注销
    client.post(
        "/account/unregister",
        headers={"Authorization": f"Bearer {tgt_tok}"},
    )
    # admin 恢复
    r = client.post(
        f"/admin/user/{target_uid}/reactivate",
        headers={"Authorization": f"Bearer {admin_tok}"},
    )
    assert r.status_code == 200
    assert "reactivated_at" in r.json()


def test_admin_purge_expired_endpoint(client, monkeypatch: pytest.MonkeyPatch):
    admin_tok, admin_uid = _login(client, "13800005007")
    monkeypatch.setenv("XIANGQIN_ADMIN_USER_IDS", admin_uid)

    r = client.post(
        "/admin/account/purge-expired",
        headers={"Authorization": f"Bearer {admin_tok}"},
        json={"older_than_days": 30},
    )
    assert r.status_code == 200
    d = r.json()
    assert "purged_count" in d
    assert "purged_ids" in d


def test_unregister_then_cannot_login_with_old_session(client):
    """session 立即失效，注销后不能再用。"""
    tok, _ = _login(client, "13800005008")
    # 用 token 可正常访问
    r = client.get("/auth/me", headers={"Authorization": f"Bearer {tok}"})
    assert r.status_code == 200
    # 注销
    client.post("/account/unregister", headers={"Authorization": f"Bearer {tok}"})
    # 老 token 应无效
    r = client.get("/auth/me", headers={"Authorization": f"Bearer {tok}"})
    assert r.status_code == 401
