"""K1.3 端点测试：Kimi Code OAuth start/disconnect/status（内存替身，无 DB/无网络）。

覆盖：
- POST .../start → 202 + user_code/verification_uri + 创建 job；后台轮询 poll_token 成功
  → 存加密 oauth_enc + job done；**token 不进 job 结果/响应**；
- 后台轮询 authorization_pending → 继续轮询直到成功；
- POST .../disconnect → 清除凭据行；
- GET .../status → connected 真值（无 token 本体）。

后台 work 用 `run_job` 自建独立 session（请求 session 已关闭）——测试经依赖 override
注 fake http + fake store（共享实例，断后台落库）+ FakeSessionFactory；monkeypatch
`kimi_oauth.asyncio.sleep` 为 no-op（避免真等 interval 秒）。
"""

from __future__ import annotations

import os
from typing import Any

import pytest
from cryptography.fernet import Fernet
from fakes_providers import FakeCredentialStore
from fastapi.testclient import TestClient
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.kimi_oauth import decrypt_oauth_bundle
from ww_db import get_session


class _FakeResponse:
    def __init__(self, status_code: int, body: dict[str, Any]) -> None:
        self.status_code = status_code
        self._body = body

    def json(self) -> Any:
        return self._body


class _ScriptedHttp:
    """按调用序号返回预设响应（device auth → poll attempts）。aclose 无操作。"""

    def __init__(self, responses: list[_FakeResponse]) -> None:
        self._responses = responses
        self.calls: list[tuple[str, dict[str, str]]] = []

    async def post(self, url: str, *, data: dict[str, str]) -> _FakeResponse:
        idx = len(self.calls)
        self.calls.append((url, data))
        return self._responses[idx]

    async def aclose(self) -> None:
        return None


def _make_client(
    *,
    store: FakeCredentialStore,
    http: _ScriptedHttp,
    enc_key: str,
    monkeypatch: pytest.MonkeyPatch,
) -> TestClient:
    os.environ["CREDENTIAL_ENC_KEY"] = enc_key
    from ww_config import get_settings

    get_settings.cache_clear()

    from fakes_projects import FakeJobRepo, FakeSession, FakeSessionFactory
    from ww_api import routers
    from ww_api.main import create_app
    from ww_api.services import job_runner
    from ww_api.services.project_deps import (
        get_job_repo,
        get_session_factory,
    )
    from ww_api.services.provider_deps import get_credential_store

    # 后台轮询不真等 interval 秒。
    async def _no_sleep(_seconds: float) -> None:
        return None

    monkeypatch.setattr("ww_api.routers.kimi_oauth.asyncio.sleep", _no_sleep)
    # 后台 work 自建 SqlCredentialStore(session)——指到共享 fake store（断后台落库）。
    monkeypatch.setattr(routers.kimi_oauth, "SqlCredentialStore", lambda _session: store)
    # 后台 work 内调模块级 `_default_http_client()` 自建 http（非 dep）——monkeypatch 它指到
    # scripted fake，否则后台轮询会真联网（见 device-flow 注入纪律）。
    monkeypatch.setattr(routers.kimi_oauth, "_default_http_client", lambda: http)
    # `run_job` 默认 repo_factory 构 `SqlJobRepo(session)`——FakeSession 无 .execute，
    # 故 monkeypatch 模块级 `job_runner.SqlJobRepo` 指到共享 fake（见 memory/gotchas）。
    job_repo = FakeJobRepo()
    monkeypatch.setattr(job_runner, "SqlJobRepo", lambda _session: job_repo)

    app = create_app()
    app.dependency_overrides[get_credential_store] = lambda: store
    app.dependency_overrides[get_job_repo] = lambda: job_repo
    app.dependency_overrides[get_session] = lambda: FakeSession()
    app.dependency_overrides[get_session_factory] = lambda: FakeSessionFactory()
    app.dependency_overrides[routers.kimi_oauth._default_http_client] = lambda: http
    return TestClient(app)


def _device_auth_response() -> _FakeResponse:
    return _FakeResponse(
        200,
        {
            "device_code": "dev-1",
            "user_code": "WXYZ-9999",
            "verification_uri": "https://kimi.com/device",
            "verification_uri_complete": "https://kimi.com/device?code=WXYZ-9999",
            "expires_in": 600,
            "interval": 5,
        },
    )


def _token_response() -> _FakeResponse:
    return _FakeResponse(
        200, {"access_token": "secret-acc", "refresh_token": "secret-ref", "expires_in": 900}
    )


def test_start_returns_202_and_polls_to_success(monkeypatch: pytest.MonkeyPatch) -> None:
    enc_key = Fernet.generate_key().decode()
    store = FakeCredentialStore()
    # device auth → poll #1 success（TestClient 同步跑完 background task）。
    http = _ScriptedHttp([_device_auth_response(), _token_response()])
    client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)

    resp = client.post("/settings/providers/kimi-code/oauth/start")

    assert resp.status_code == 202
    body = resp.json()
    assert body["user_code"] == "WXYZ-9999"
    assert body["verification_uri"] == "https://kimi.com/device"
    assert body["interval"] == 5
    assert "job_id" in body
    # 响应绝不含 token。
    assert "secret-acc" not in resp.text
    assert "secret-ref" not in resp.text

    # 后台轮询成功 → 加密 oauth 凭据落库（共享 store）。
    cred = store.oauth.get((STUB_OWNER_ID, "kimi-code"))
    assert cred is not None
    token = decrypt_oauth_bundle(cred, key=enc_key)
    assert token.access_token == "secret-acc"
    assert token.refresh_token == "secret-ref"


def test_start_polls_through_authorization_pending(monkeypatch: pytest.MonkeyPatch) -> None:
    enc_key = Fernet.generate_key().decode()
    store = FakeCredentialStore()
    http = _ScriptedHttp(
        [
            _device_auth_response(),
            _FakeResponse(400, {"error": "authorization_pending"}),
            _token_response(),
        ]
    )
    client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)

    resp = client.post("/settings/providers/kimi-code/oauth/start")

    assert resp.status_code == 202
    # pending 后继续轮询 → 第二次成功落库。
    assert (STUB_OWNER_ID, "kimi-code") in store.oauth


def test_disconnect_clears_credential(monkeypatch: pytest.MonkeyPatch) -> None:
    enc_key = Fernet.generate_key().decode()
    store = FakeCredentialStore()
    store.oauth[(STUB_OWNER_ID, "kimi-code")] = b"blob"
    http = _ScriptedHttp([])
    client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)

    resp = client.post("/settings/providers/kimi-code/oauth/disconnect")

    assert resp.status_code == 200
    assert resp.json()["disconnected"] is True
    assert (STUB_OWNER_ID, "kimi-code") not in store.oauth


def test_status_reflects_connection(monkeypatch: pytest.MonkeyPatch) -> None:
    from datetime import UTC, datetime, timedelta

    from ww_api.services.kimi_oauth import TokenSet, encrypt_oauth_bundle

    enc_key = Fernet.generate_key().decode()
    store = FakeCredentialStore()
    expires = datetime.now(UTC) + timedelta(seconds=900)
    token = TokenSet(access_token="a", refresh_token="r", expires_at=expires)
    store.oauth[(STUB_OWNER_ID, "kimi-code")] = encrypt_oauth_bundle(token, key=enc_key)
    http = _ScriptedHttp([])
    client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)

    resp = client.get("/settings/providers/kimi-code/oauth/status")

    assert resp.status_code == 200
    body = resp.json()
    assert body["connected"] is True
    assert body["expires_at"] is not None
    # 状态绝不回 token 本体。
    assert "access_token" not in body
    assert "refresh_token" not in body


def test_status_not_connected_when_no_credential(monkeypatch: pytest.MonkeyPatch) -> None:
    enc_key = Fernet.generate_key().decode()
    store = FakeCredentialStore()
    http = _ScriptedHttp([])
    client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)

    resp = client.get("/settings/providers/kimi-code/oauth/status")

    assert resp.status_code == 200
    assert resp.json()["connected"] is False
