"""P0-1 原子性：`PUT /settings/providers` 多凭据一请求，中途失败整体回滚。

不变量：仓储写方法只 `flush()`，端点末尾**统一一次** `commit()`。若某条 upsert 中途
抛错，端点绝不到达 `commit()` → 请求 session 回滚，DB 不留半更新（无部分提交）。

用可注入 fake：store 在第 N 次 upsert 抛错；fake session 记录 `commit()` 调用次数。
断言：① 请求返回 500（兜底信封）；② `session.commit()` **从未被调用**（无半提交）。
"""

from __future__ import annotations

import os
import uuid
from typing import Any

import httpx
import pytest
from cryptography.fernet import Fernet
from fastapi import FastAPI
from ww_api.services.credentials import StoredRouting


class _ExplodingStore:
    """凭据 store fake：前 N 次 upsert 成功，第 fail_at 次（1-based）抛错。

    模拟「多凭据一请求」里第 K 条写入失败——验证端点不会半提交。
    """

    def __init__(self, *, fail_at: int) -> None:
        self._fail_at = fail_at
        self._upsert_calls = 0
        self.creds: dict[tuple[uuid.UUID, str], bytes] = {}

    async def list_credentials(self, owner_id: uuid.UUID) -> list[Any]:
        return []

    async def list_routing(self) -> list[Any]:
        return []

    async def get_credential(self, owner_id: uuid.UUID, provider: str) -> Any:
        return None

    async def upsert_credential(
        self, owner_id: uuid.UUID, provider: str, api_key_enc: bytes
    ) -> None:
        self._upsert_calls += 1
        if self._upsert_calls == self._fail_at:
            raise RuntimeError("simulated mid-request write failure")
        self.creds[(owner_id, provider)] = api_key_enc

    async def upsert_oauth_credential(
        self, owner_id: uuid.UUID, provider: str, oauth_enc: bytes
    ) -> None:  # pragma: no cover - 不用于本测试
        raise NotImplementedError

    async def delete_credential(self, owner_id: uuid.UUID, provider: str) -> bool:
        return False

    async def upsert_routing(self, routing: StoredRouting) -> None:
        self.creds[(uuid.UUID(int=0), routing.tier)] = b""


class _RecordingSession:
    """fake AsyncSession：只记 commit/rollback 调用次数（端点统一提交的探针）。"""

    def __init__(self) -> None:
        self.commits = 0
        self.rollbacks = 0

    async def commit(self) -> None:
        self.commits += 1

    async def rollback(self) -> None:
        self.rollbacks += 1


def _build_app(store: _ExplodingStore, session: _RecordingSession) -> FastAPI:
    os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
    from ww_config import get_settings

    get_settings.cache_clear()
    from ww_api.main import create_app
    from ww_api.services.provider_deps import get_credential_store
    from ww_db import get_session

    app = create_app()
    app.dependency_overrides[get_credential_store] = lambda: store
    app.dependency_overrides[get_session] = lambda: session
    return app


@pytest.mark.asyncio
async def test_upsert_providers_rolls_back_on_mid_request_failure() -> None:
    # Arrange：第 2 条凭据写入时抛错（前一条已 flush 到 session，但尚未 commit）。
    store = _ExplodingStore(fail_at=2)
    session = _RecordingSession()
    app = _build_app(store, session)

    payload = {
        "credentials": [
            {"provider": "deepseek", "api_key": "sk-a"},
            {"provider": "kimi", "api_key": "sk-b"},
            {"provider": "qwen", "api_key": "sk-c"},
        ]
    }

    transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        resp = await client.put("/settings/providers", json=payload)

    # Assert：兜底信封（catch-all → 500 INTERNAL，不回显原始异常），且**从未提交**（无半提交）。
    assert resp.status_code == 500
    assert resp.json()["error"]["code"] == "INTERNAL"
    assert "simulated mid-request write failure" not in resp.text
    assert session.commits == 0


@pytest.mark.asyncio
async def test_upsert_providers_commits_once_on_success() -> None:
    # Arrange：全部成功（fail_at 远大于条数）。
    store = _ExplodingStore(fail_at=99)
    session = _RecordingSession()
    app = _build_app(store, session)

    payload = {
        "credentials": [
            {"provider": "deepseek", "api_key": "sk-a"},
            {"provider": "kimi", "api_key": "sk-b"},
        ]
    }

    transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        resp = await client.put("/settings/providers", json=payload)

    # Assert：成功路径**恰好一次** commit（所有写入统一提交）。
    assert resp.status_code == 200
    assert session.commits == 1
