"""auth 模块单测。用 tmp DB 绕过默认 /opt/xiangqin/data/。"""

from __future__ import annotations

from pathlib import Path

import pytest


@pytest.fixture(autouse=True)
def tmp_db(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
    db_path = tmp_path / "xq.db"
    monkeypatch.setenv("XIANGQIN_DB_PATH", str(db_path))
    # 模块级常量需要 reload 才生效（因为已经 import 过）
    from xiangqin import db as db_module

    db_module.DEFAULT_DB_PATH = db_path
    db_module.init_db(db_path)
    yield db_path


def test_register_then_verify_new_user():
    from xiangqin import auth

    r = auth.register("13800001111")
    assert r.request_id
    assert r.masked_phone == "138****1111"
    assert r.expires_in_s == 60

    v = auth.verify(r.request_id, "123456")
    assert v.is_new_user is True
    assert v.user_id
    assert v.session_token


def test_register_reuses_active_request():
    from xiangqin import auth

    r1 = auth.register("13800001111")
    r2 = auth.register("13800001111")
    assert r1.request_id == r2.request_id  # 60s 内复用


def test_verify_wrong_code():
    from xiangqin import auth

    r = auth.register("13800001111")
    with pytest.raises(auth.AuthError) as e:
        auth.verify(r.request_id, "999999")
    assert e.value.code == "invalid_code"


def test_verify_twice_errors():
    from xiangqin import auth

    r = auth.register("13800001111")
    auth.verify(r.request_id, "123456")
    with pytest.raises(auth.AuthError) as e:
        auth.verify(r.request_id, "123456")
    assert e.value.code == "code_already_used"


def test_verify_expired():
    from xiangqin import auth, db as db_module

    r = auth.register("13800001111")
    with db_module.get_conn() as conn:
        conn.execute(
            "UPDATE auth_requests SET expires_at = 1 WHERE request_id = ?",
            (r.request_id,),
        )
    with pytest.raises(auth.AuthError) as e:
        auth.verify(r.request_id, "123456")
    assert e.value.code == "code_expired"


def test_invalid_phone():
    from xiangqin import auth

    with pytest.raises(auth.AuthError) as e:
        auth.register("12345")
    assert e.value.code == "invalid_phone"


def test_second_login_reuses_user():
    from xiangqin import auth

    r1 = auth.register("13800001111")
    v1 = auth.verify(r1.request_id, "123456")

    # 再登一次（等 60s+ 模拟，或直接伪造新 request）
    r2 = auth.register("13800001111")
    # 上面会复用 r1 的 request_id，因为 used=0。verify 一次后 used=1，再 register 会新开
    # 注意 r1 已经 used=1 了
    v2 = auth.verify(r2.request_id, "123456")

    assert v2.user_id == v1.user_id
    assert v2.is_new_user is False
    assert v2.session_token != v1.session_token


def test_resolve_session_happy():
    from xiangqin import auth

    r = auth.register("13800001111")
    v = auth.verify(r.request_id, "123456")

    uid = auth.resolve_session(v.session_token)
    assert uid == v.user_id


def test_resolve_session_invalid():
    from xiangqin import auth

    with pytest.raises(auth.AuthError) as e:
        auth.resolve_session("bogus-token")
    assert e.value.code == "unauthenticated"


def test_logout_clears_session():
    from xiangqin import auth

    r = auth.register("13800001111")
    v = auth.verify(r.request_id, "123456")
    auth.logout(v.session_token)

    with pytest.raises(auth.AuthError):
        auth.resolve_session(v.session_token)


def test_test_phone_whitelist_accepts_fixed_otp(monkeypatch):
    """白名单号 + 正确 TEST_OTP 可登录，且不调 SMS provider。"""
    monkeypatch.setenv("XIANGQIN_SMS_PROVIDER", "aliyun")
    monkeypatch.setenv("XIANGQIN_TEST_PHONES", "13900000001,13900000002")
    monkeypatch.setenv("XIANGQIN_TEST_OTP", "987654")

    from xiangqin import auth, sms

    called = []

    class _Boom:
        def send_code(self, *a, **kw):
            called.append(a)
            raise RuntimeError("aliyun should NOT be called for test phones")

    monkeypatch.setattr(sms, "get_provider", lambda: _Boom())

    r = auth.register("13900000001")
    v = auth.verify(r.request_id, "987654")
    assert v.user_id
    assert called == []


def test_test_phone_whitelist_rejects_wrong_code(monkeypatch):
    monkeypatch.setenv("XIANGQIN_TEST_PHONES", "13900000001")
    monkeypatch.setenv("XIANGQIN_TEST_OTP", "987654")

    from xiangqin import auth

    r = auth.register("13900000001")
    with pytest.raises(auth.AuthError) as e:
        auth.verify(r.request_id, "000000")
    assert e.value.code == "invalid_code"


def test_non_test_phone_unaffected_by_otp_env(monkeypatch):
    """非白名单号不受 TEST_OTP 影响，走正常 mock 路径（123456）。"""
    monkeypatch.setenv("XIANGQIN_TEST_PHONES", "13900000001")
    monkeypatch.setenv("XIANGQIN_TEST_OTP", "987654")

    from xiangqin import auth

    r = auth.register("13800001111")  # 非白名单
    with pytest.raises(auth.AuthError):
        auth.verify(r.request_id, "987654")  # TEST_OTP 对此号无效
    # mock provider 下真 code 还是 123456——但因为上面 verify 消耗了 request，重新 register
    r2 = auth.register("13800001111")
    v = auth.verify(r2.request_id, "123456")
    assert v.user_id


def test_empty_test_otp_disables_whitelist(monkeypatch):
    """TEST_OTP 未设时，即便号在白名单也不启用（prod 默认安全态）。"""
    monkeypatch.setenv("XIANGQIN_TEST_PHONES", "13900000001")
    monkeypatch.delenv("XIANGQIN_TEST_OTP", raising=False)

    from xiangqin import auth

    assert auth.is_test_phone("13900000001") is False
    # 走 mock 默认路径
    r = auth.register("13900000001")
    v = auth.verify(r.request_id, "123456")
    assert v.user_id


def test_phone_never_in_db_plain(tmp_db):
    """红线：手机号原文不能在 DB 任何一列出现。"""
    from xiangqin import auth

    phone = "13800001111"
    r = auth.register(phone)
    auth.verify(r.request_id, "123456")

    import sqlite3

    with sqlite3.connect(tmp_db) as conn:
        # dump 所有表内容，检查是否有手机号原文
        for table in [
            "users",
            "profiles",
            "auth_requests",
            "sessions",
            "messages",
            "envelope_usage",
            "envelope_balance",
            "envelope_orders",
            "reports",
            "blocks",
            "notify_state",
        ]:
            for row in conn.execute(f"SELECT * FROM {table}"):
                for v in row:
                    assert phone not in str(v), f"手机号原文泄漏在 {table}: {row}"
