from __future__ import annotations

import json
import time
import uuid
from collections.abc import Generator
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

import app.models  # noqa: F401 - 注册完整 metadata
from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.api.v1.endpoints.financial_connectors import router
from app.core.config import get_settings
from app.db.base_class import Base
from app.models.financial_record import ExpenseClaim
from app.schemas.financial_connector import FinancialEventEnvelope
from app.services.financial_connector_auth import sign_financial_event


@pytest.fixture()
def lifecycle_http(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv(
        "FINANCIAL_CONNECTOR_HMAC_KEYS_JSON",
        json.dumps(
            {
                "connector/valid": "v" * 32,
                "connector/next": "n" * 32,
                "connector/weak": "short",
                "connector/blank": " " * 32,
            }
        ),
    )
    get_settings.cache_clear()
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    app = FastAPI()
    app.include_router(router, prefix="/api/v1")

    def override_db() -> Generator[Session, None, None]:
        with factory() as db:
            yield db

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[get_current_user] = lambda: CurrentUserContext(
        username="lifecycle-admin",
        name="连接器管理员",
        role_codes=["platform_admin"],
        is_admin=True,
        tenant_id="default",
    )
    with factory() as db:
        claim = ExpenseClaim(
            id=str(uuid.uuid4()),
            claim_no="BX-CONFIG-LIFECYCLE-001",
            employee_name="配置回归员工",
            department_name="财务测试部",
            expense_type="travel",
            reason="连接器配置生命周期回归",
            location="上海",
            amount=Decimal("66.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime.now(UTC),
            submitted_at=datetime.now(UTC),
            status="pending_payment",
            approval_stage="待付款",
            risk_flags_json=[],
        )
        db.add(claim)
        db.commit()
    client = TestClient(app)
    try:
        yield client, claim
    finally:
        client.close()
        app.dependency_overrides.clear()
        Base.metadata.drop_all(engine)
        engine.dispose()
        get_settings.cache_clear()


def test_activate_rotate_disable_is_versioned_and_audited(lifecycle_http) -> None:
    client, claim = lifecycle_http
    bypass = client.post(
        "/api/v1/financial-connectors/admin/tenants/default/configs",
        json={
            "provider": "bypass-bank",
            "environment": "production",
            "key_version": "v1",
            "secret_ref": "connector/valid",
            "allowed_event_types": ["payment_settled"],
            "status": "active",
            "request_id": "lifecycle-bypass-001",
            "reason": "创建时不得绕过显式激活和服务端密钥校验。",
        },
    )
    assert bypass.status_code == 422
    created = _create_config(client, secret_ref="connector/valid")
    config_id = created["id"]
    assert created["status"] == "disabled" and created["version"] == 1

    activated = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/activate",
        json={
            "expected_version": 1,
            "request_id": "lifecycle-activate-001",
            "reason": "生产切换前完成服务端密钥解析与强度校验。",
        },
    )
    assert activated.status_code == 200
    assert activated.json()["status"] == "active"
    assert activated.json()["version"] == 2

    stale = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/disable",
        json={
            "expected_version": 1,
            "request_id": "lifecycle-disable-stale-001",
            "reason": "使用过期版本停用必须被拒绝。",
        },
    )
    assert stale.status_code == 409

    rotated = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/rotate",
        json={
            "expected_version": 2,
            "request_id": "lifecycle-rotate-001",
            "reason": "按季度轮换服务端 HMAC 密钥。",
            "new_key_version": "v2",
            "new_secret_ref": "connector/next",
        },
    )
    assert rotated.status_code == 200
    previous = rotated.json()["previous"]
    replacement = rotated.json()["replacement"]
    assert previous["status"] == "rotating" and previous["version"] == 3
    assert replacement["status"] == "active" and replacement["version"] == 1
    assert replacement["key_version"] == "v2"

    envelope = FinancialEventEnvelope(
        tenant_id="default",
        external_event_id="lifecycle-simulation-001",
        event_type="payment_settled",
        occurred_at=datetime.now(UTC),
        correlation_id="lifecycle-correlation-001",
        payload={
            "claim_id": claim.id,
            "claim_reference": claim.claim_no,
            "amount": "66.00",
            "currency": "CNY",
            "external_payment_reference": "LIFECYCLE-PAYMENT-001",
        },
    )
    timestamp = int(time.time())
    old_response = client.post(
        "/api/v1/integrations/financial-events",
        json=envelope.model_dump(mode="json"),
        headers=_headers(envelope, timestamp, version="v1", secret="v" * 32),
    )
    assert old_response.status_code == 401
    assert old_response.json()["detail"]["code"] == "connector_inactive"
    new_response = client.post(
        "/api/v1/integrations/financial-events",
        json=envelope.model_dump(mode="json"),
        headers=_headers(envelope, timestamp, version="v2", secret="n" * 32),
    )
    assert new_response.status_code == 200
    assert new_response.json()["projection_scope"] == "simulation_only"
    assert new_response.json()["claim_status"] == "pending_payment"

    disabled = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/disable",
        json={
            "expected_version": 3,
            "request_id": "lifecycle-disable-001",
            "reason": "新版本已稳定接管，停用旧轮换版本。",
        },
    )
    assert disabled.status_code == 200
    assert disabled.json()["status"] == "disabled"
    assert disabled.json()["version"] == 4

    events = client.get(
        "/api/v1/financial-connectors/admin/tenants/default/config-events"
    )
    assert events.status_code == 200
    payload = events.json()
    assert [item["action"] for item in payload] == [
        "created",
        "activated",
        "rotation_started",
        "rotation_replacement_created",
        "disabled",
    ]
    assert all(item["actor_id"] == "lifecycle-admin" for item in payload)
    serialized = json.dumps(payload).lower()
    assert "secret_ref" not in serialized
    assert "connector/valid" not in serialized
    assert "connector/next" not in serialized


@pytest.mark.parametrize(
    ("secret_ref", "error_code"),
    [
        ("connector/missing", "secret_unavailable"),
        ("connector/weak", "secret_too_short"),
        ("connector/blank", "secret_too_short"),
    ],
)
def test_activate_fails_closed_when_server_secret_is_unusable(
    lifecycle_http,
    secret_ref: str,
    error_code: str,
) -> None:
    client, _ = lifecycle_http
    created = _create_config(
        client,
        provider=f"invalid-{error_code}",
        secret_ref=secret_ref,
        request_id=f"create-{error_code}-001",
    )
    response = client.post(
        "/api/v1/financial-connectors/admin/tenants/default/configs/"
        f"{created['id']}/activate",
        json={
            "expected_version": 1,
            "request_id": f"activate-{error_code}-001",
            "reason": "不可用的服务端密钥不得进入激活态。",
        },
    )
    assert response.status_code == 400
    assert response.json()["detail"]["code"] == error_code
    configs = client.get(
        "/api/v1/financial-connectors/admin/tenants/default/configs"
    ).json()
    current = next(item for item in configs if item["id"] == created["id"])
    assert current["status"] == "disabled"
    assert current["version"] == 1


def _create_config(
    client: TestClient,
    *,
    provider: str = "lifecycle-bank",
    secret_ref: str,
    request_id: str = "lifecycle-create-001",
) -> dict[str, object]:
    response = client.post(
        "/api/v1/financial-connectors/admin/tenants/default/configs",
        json={
            "provider": provider,
            "environment": "mock",
            "key_version": "v1",
            "secret_ref": secret_ref,
            "allowed_event_types": ["payment_settled"],
            "clock_skew_seconds": 300,
            "status": "disabled",
            "request_id": request_id,
            "reason": "创建连接器配置并等待显式激活。",
        },
    )
    assert response.status_code == 201
    return response.json()


def _headers(
    envelope: FinancialEventEnvelope,
    timestamp: int,
    *,
    version: str,
    secret: str,
) -> dict[str, str]:
    return {
        "X-Financial-Tenant": "default",
        "X-Financial-Provider": "lifecycle-bank",
        "X-Financial-Key-Version": version,
        "X-Financial-Timestamp": str(timestamp),
        "X-Financial-Signature": sign_financial_event(
            envelope,
            timestamp=timestamp,
            secret=secret,
            tenant_id="default",
            provider="lifecycle-bank",
            key_version=version,
        ),
    }
