"""
配置表、用户表与 PassKey 表的查询行为。

这几张表决定「谁能登录、看到什么配置」，查错一行的后果是越权或配置串用，而不是
一个能被日志发现的异常。同步方法都有一个已是 2.0 写法的异步孪生方法，这里对同一
批数据同时跑两条路径并要求结果一致——同步侧改写后若有偏差，这个断言会直接暴露。
"""
import asyncio

import pytest

from app.db import base as db_base
from app.db.models.passkey import PassKey
from app.db.models.systemconfig import SystemConfig
from app.db.models.user import User
from app.db.models.userconfig import UserConfig
from app.db.oper.passkey import PassKeyOper
from app.db.oper.user import UserOper
from app.db.session import async_session_scope


@pytest.fixture(autouse=True)
def _track(db):
    """把本文件涉及的表纳入用例级回收。"""
    db.watermark(SystemConfig, UserConfig, User, PassKey)


# --------------------------------------------------------------------------- #
# SystemConfig
# --------------------------------------------------------------------------- #

def test_systemconfig_get_by_key_matches_async_twin(db):
    """
    按键取配置的同步与异步结果必须一致，且只命中同名键。
    """
    db.add(SystemConfig(key="mp-test-a", value={"n": 1}),
           SystemConfig(key="mp-test-b", value={"n": 2}))

    found = SystemConfig.get_by_key(db.session, "mp-test-a")
    assert found.value == {"n": 1}

    async_found = db.run_async_session(
        lambda session: SystemConfig.async_get_by_key(session, "mp-test-a")
    )
    assert async_found.value == found.value


def test_systemconfig_get_by_key_returns_none_when_absent(db):
    """
    键不存在时返回 None——调用方据此决定是否落默认值。
    """
    assert SystemConfig.get_by_key(db.session, "mp-test-missing") is None


def test_systemconfig_queries_reuse_explicit_sessions(db, monkeypatch):
    """SystemConfig 显式同步与异步会话不得触发兼容会话。"""
    db.add(SystemConfig(key="mp-explicit-config", value=True))
    monkeypatch.setattr(
        db_base,
        "run_sync_transaction",
        lambda _operation: (_ for _ in ()).throw(
            AssertionError("不应创建额外同步事务")
        ),
    )
    assert SystemConfig.get_by_key(db.session, "mp-explicit-config") is not None

    async def check() -> None:
        """验证异步配置查询复用显式 AsyncSession。"""
        async with async_session_scope() as session:
            monkeypatch.setattr(
                db_base,
                "run_async_transaction",
                lambda _operation: (_ for _ in ()).throw(
                    AssertionError("不应创建额外异步事务")
                ),
            )
            assert await SystemConfig.async_get_by_key(
                session,
                "mp-explicit-config",
            ) is not None

    asyncio.run(check())


def test_systemconfig_delete_by_key_removes_only_that_key(db):
    """
    按键删除只能删掉那一个键，误删会静默丢失其他配置。
    """
    db.add(SystemConfig(key="mp-test-del", value={"n": 1}),
           SystemConfig(key="mp-test-keep", value={"n": 2}))

    assert SystemConfig().delete_by_key(db.session, "mp-test-del") is True

    assert SystemConfig.get_by_key(db.session, "mp-test-del") is None
    assert SystemConfig.get_by_key(db.session, "mp-test-keep").value == {"n": 2}


def test_systemconfig_delete_by_key_tolerates_missing_key(db):
    """
    删除不存在的键返回 True 而不抛异常，保持调用方的幂等语义。
    """
    assert SystemConfig().delete_by_key(db.session, "mp-test-missing") is True


# --------------------------------------------------------------------------- #
# UserConfig
# --------------------------------------------------------------------------- #

def test_userconfig_get_by_key_scopes_by_username(db):
    """
    用户配置必须同时按用户名和键命中——只按键会把别人的配置读给当前用户。
    """
    db.add(UserConfig(username="alice", key="theme", value="dark"),
           UserConfig(username="bob", key="theme", value="light"))

    assert UserConfig.get_by_key(db.session, username="alice", key="theme").value == "dark"
    assert UserConfig.get_by_key(db.session, username="bob", key="theme").value == "light"
    assert UserConfig.get_by_key(db.session, username="carol", key="theme") is None


def test_userconfig_delete_by_key_removes_only_that_user(db):
    """
    删除某用户的配置不能波及同名键的其他用户。
    """
    db.add(UserConfig(username="alice", key="theme", value="dark"),
           UserConfig(username="bob", key="theme", value="light"))

    assert UserConfig().delete_by_key(db.session, username="alice", key="theme") is True

    assert UserConfig.get_by_key(db.session, username="alice", key="theme") is None
    assert UserConfig.get_by_key(db.session, username="bob", key="theme").value == "light"


def test_userconfig_delete_by_key_tolerates_missing_row(db):
    """
    删除不存在的用户配置返回 True，不抛异常。
    """
    assert UserConfig().delete_by_key(db.session, username="nobody", key="theme") is True


# --------------------------------------------------------------------------- #
# User
# --------------------------------------------------------------------------- #

def test_user_lookup_by_name_and_id_matches_async_twin(db):
    """
    按名与按 ID 取用户的同步、异步结果必须指向同一行。

    登录链路走同步、API 依赖注入走异步，两者不一致会表现为「能登录但查不到自己」。
    """
    created = db.add(User(name="mp-test-user", email="u@example.com",
                          hashed_password="x", is_active=True))

    by_name = User.get_by_name(db.session, "mp-test-user")
    by_id = User.get_by_id(db.session, created.id)
    assert by_name.id == by_id.id == created.id

    assert db.run_async_session(
        lambda session: User.async_get_by_name(session, "mp-test-user")
    ).id == created.id
    assert db.run_async_session(
        lambda session: User.async_get_by_id(session, created.id)
    ).id == created.id


def test_user_lookup_returns_none_when_absent(db):
    """
    查无此人时返回 None，而不是抛异常或返回任意一行。
    """
    assert User.get_by_name(db.session, "mp-test-nobody") is None
    assert User.get_by_id(db.session, -1) is None


def test_user_delete_by_name_and_by_id_remove_only_the_target(db):
    """
    按名、按 ID 删除都只能删掉目标用户。
    """
    keep = db.add(User(name="mp-test-keep", hashed_password="x"))
    drop_by_name = db.add(User(name="mp-test-drop-name", hashed_password="x"))
    drop_by_id = db.add(User(name="mp-test-drop-id", hashed_password="x"))

    assert User().delete_by_name(db.session, drop_by_name.name) is True
    assert User().delete_by_id(db.session, drop_by_id.id) is True

    assert User.get_by_name(db.session, "mp-test-drop-name") is None
    assert User.get_by_id(db.session, drop_by_id.id) is None
    assert User.get_by_id(db.session, keep.id) is not None


def test_user_update_otp_reports_whether_user_existed(db):
    """
    更新 OTP 必须如实反馈用户是否存在。

    对不存在的用户返回 True 会让上层以为二次验证已开启，实际并没有。
    """
    db.add(User(name="mp-test-otp", hashed_password="x", is_otp=False))

    assert User().update_otp_by_name(db.session, "mp-test-otp", True, "SECRET") is True
    assert User().update_otp_by_name(db.session, "mp-test-nobody", True, "SECRET") is False

    updated = User.get_by_name(db.session, "mp-test-otp")
    assert (updated.is_otp, updated.otp_secret) == (True, "SECRET")


def test_user_async_mutations_match_sync_behaviour(db):
    """
    异步的删除与 OTP 更新必须与同步路径给出相同的存在性判断。
    """
    async_id_user = db.add(User(name="mp-test-async-id", hashed_password="x"))
    db.add(User(name="mp-test-async-otp", hashed_password="x", is_otp=False))
    oper = UserOper()

    assert asyncio.run(oper.async_update_otp_by_name(
        name="mp-test-async-otp", otp=True, secret="S2")) is True
    assert asyncio.run(oper.async_update_otp_by_name(
        name="mp-test-nobody", otp=True, secret="S2")) is False

    assert asyncio.run(oper.async_delete_by_name(name="mp-test-async-otp")) is True
    assert User.get_by_name(db.session, "mp-test-async-otp") is None

    asyncio.run(oper.async_delete(async_id_user.id))
    assert User.get_by_id(db.session, async_id_user.id) is None


# --------------------------------------------------------------------------- #
# PassKey
# --------------------------------------------------------------------------- #

def _passkey(user_id: int, credential_id: str, is_active: bool = True) -> PassKey:
    """构造一条 PassKey 记录。"""
    return PassKey(user_id=user_id, credential_id=credential_id,
                   public_key="pk", sign_count=0, is_active=is_active)


def test_passkey_listing_excludes_inactive_credentials(db):
    """
    列出用户凭据时必须排除已停用的。

    停用的凭据仍能被列出意味着它还会出现在登录选项里，等于停用没生效。
    """
    db.add(_passkey(9001, "cred-active-1"),
           _passkey(9001, "cred-active-2"),
           _passkey(9001, "cred-inactive", is_active=False),
           _passkey(9002, "cred-other"))

    listed = PassKey.get_by_user_id(db.session, 9001)

    assert {p.credential_id for p in listed} == {"cred-active-1", "cred-active-2"}
    assert {p.credential_id for p in db.run_async_session(
        lambda session: PassKey.async_get_by_user_id(session, 9001)
    )} == \
        {"cred-active-1", "cred-active-2"}


def test_passkey_oper_queries_use_explicit_session(db, monkeypatch):
    """PassKeyOper 的宿主查询使用调用方 Session，不创建兼容事务。"""
    db.add(_passkey(9002, "cred-oper"), _passkey(9002, "cred-oper-inactive", is_active=False))
    monkeypatch.setattr(
        "app.db.base.run_sync_transaction",
        lambda _query: pytest.fail("显式 Session 查询不应创建兼容事务"),
    )

    oper = PassKeyOper(db.session)

    assert {item.credential_id for item in oper.list()} == {
        "cred-oper",
        "cred-oper-inactive",
    }
    assert [item.credential_id for item in oper.list_by_user_id(9002)] == ["cred-oper"]
    assert oper.get_by_credential_id("cred-oper").user_id == 9002
    assert oper.get_by_credential_id("cred-oper-inactive") is None


def test_passkey_lookup_by_credential_id_skips_inactive(db):
    """
    按凭据 ID 查找同样必须忽略停用记录，否则停用的密钥仍可完成认证。
    """
    db.add(_passkey(9003, "cred-live"), _passkey(9003, "cred-dead", is_active=False))

    assert PassKey.get_by_credential_id(db.session, "cred-live").user_id == 9003
    assert PassKey.get_by_credential_id(db.session, "cred-dead") is None
    assert db.run_async_session(
        lambda session: PassKey.async_get_by_credential_id(session, "cred-dead")
    ) is None


def test_passkey_remaining_queries_reuse_explicit_sessions(db, monkeypatch):
    """PassKey 其余同步/异步查询必须复用调用方会话。"""
    key = db.add(_passkey(9008, "cred-explicit"))
    monkeypatch.setattr(
        db_base,
        "run_sync_transaction",
        lambda _operation: (_ for _ in ()).throw(
            AssertionError("不应创建额外同步事务")
        ),
    )
    assert PassKey.get_by_id(db.session, key.id).credential_id == "cred-explicit"

    async def check() -> None:
        """验证三个异步查询都复用显式 AsyncSession。"""
        async with async_session_scope() as session:
            monkeypatch.setattr(
                db_base,
                "run_async_transaction",
                lambda _operation: (_ for _ in ()).throw(
                    AssertionError("不应创建额外异步事务")
                ),
            )
            assert [item.credential_id for item in await PassKey.async_get_by_user_id(
                session,
                9008,
            )] == ["cred-explicit"]
            assert await PassKey.async_get_by_credential_id(
                session,
                "cred-explicit",
            ) is not None
            assert await PassKey.async_get_by_id(session, key.id) is not None

    asyncio.run(check())


def test_passkey_get_by_id_ignores_active_flag(db):
    """
    按主键取记录是管理用途，不应过滤停用状态——否则管理端看不到自己刚停用的凭据。
    """
    dead = db.add(_passkey(9004, "cred-admin", is_active=False))

    assert PassKey.get_by_id(db.session, dead.id).credential_id == "cred-admin"
    assert db.run_async_session(
        lambda session: PassKey.async_get_by_id(session, dead.id)
    ).credential_id == "cred-admin"


def test_passkey_delete_requires_matching_owner(db):
    """
    删除必须同时匹配主键与所属用户。

    只按主键删除即为越权：任意登录用户都能删掉别人的凭据。
    """
    victim = db.add(_passkey(9005, "cred-victim"))

    assert PassKey.delete_by_id(db.session, passkey_id=victim.id, user_id=9999) is False
    assert PassKey.get_by_id(db.session, victim.id) is not None

    assert PassKey.delete_by_id(db.session, passkey_id=victim.id, user_id=9005) is True
    assert PassKey.get_by_id(db.session, victim.id) is None


def test_passkey_async_delete_enforces_the_same_ownership_rule(db):
    """
    异步删除必须与同步路径使用同一套归属判定。
    """
    victim = db.add(_passkey(9006, "cred-async-victim"))

    oper = PassKeyOper()
    assert asyncio.run(oper.async_delete_by_id(passkey_id=victim.id, user_id=9999)) is False
    assert asyncio.run(oper.async_delete_by_id(passkey_id=victim.id, user_id=9006)) is True
    assert PassKey.get_by_id(db.session, victim.id) is None


def test_passkey_update_last_used_persists_sign_count(db):
    """
    签名计数必须落库——它是防重放的依据，不落库等于校验形同虚设。
    """
    key = db.add(_passkey(9007, "cred-count"))

    assert key.update_last_used(db.session, sign_count=42) is True

    assert PassKey.get_by_id(db.session, key.id).sign_count == 42
