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 http_context(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv(
        "FINANCIAL_CONNECTOR_HMAC_KEYS_JSON",
        json.dumps({"connector/http-test": "http-server-secret"}),
    )
    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")
    user_box = {"current": _user("platform-admin", is_admin=True)}

    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: user_box["current"]
    with factory() as db:
        claim = _claim(db)
        db.commit()
    client = TestClient(app)
    try:
        yield client, user_box, claim
    finally:
        client.close()
        app.dependency_overrides.clear()
        Base.metadata.drop_all(engine)
        engine.dispose()
        get_settings.cache_clear()


def test_financial_connector_http_auth_config_and_reconciliation_scope(http_context) -> None:
    client, user_box, claim = http_context
    config = client.post(
        "/api/v1/financial-connectors/admin/tenants/default/configs",
        json={
            "provider": "http-bank",
            "environment": "mock",
            "key_version": "v1",
            "secret_ref": "connector/http-test",
            "allowed_event_types": ["payment_settled"],
            "clock_skew_seconds": 300,
            "status": "disabled",
            "request_id": "http-config-create-001",
            "reason": "创建 HTTP 回归测试连接器配置。",
        },
    )
    assert config.status_code == 201
    assert config.json()["status"] == "disabled"
    assert config.json()["version"] == 1
    assert "secret_ref" not in config.json()
    assert "secret" not in json.dumps(config.json())
    activated = client.post(
        "/api/v1/financial-connectors/admin/tenants/default/configs/"
        f"{config.json()['id']}/activate",
        json={
            "expected_version": 1,
            "request_id": "http-config-activate-001",
            "reason": "完成服务端密钥校验后启用模拟连接器。",
        },
    )
    assert activated.status_code == 200
    assert activated.json()["status"] == "active"
    assert activated.json()["version"] == 2

    envelope = FinancialEventEnvelope(
        tenant_id="default",
        external_event_id="http-settlement-001",
        event_type="payment_settled",
        occurred_at=datetime.now(UTC),
        correlation_id="http-correlation-001",
        payload={
            "claim_id": claim.id,
            "claim_reference": claim.claim_no,
            "amount": "66.00",
            "currency": "CNY",
            "external_payment_reference": "HTTP-PAYMENT-00001",
        },
    )
    timestamp = int(time.time())
    headers = {
        "X-Financial-Tenant": "default",
        "X-Financial-Provider": "http-bank",
        "X-Financial-Key-Version": "v1",
        "X-Financial-Timestamp": str(timestamp),
        "X-Financial-Signature": sign_financial_event(
            envelope,
            timestamp=timestamp,
            secret="http-server-secret",
            tenant_id="default",
            provider="http-bank",
            key_version="v1",
        ),
    }
    bad = client.post(
        "/api/v1/integrations/financial-events",
        json=envelope.model_dump(mode="json"),
        headers={**headers, "X-Financial-Signature": "sha256=" + "0" * 64},
    )
    assert bad.status_code == 401
    accepted = client.post(
        "/api/v1/integrations/financial-events",
        json=envelope.model_dump(mode="json"),
        headers=headers,
    )
    replay = client.post(
        "/api/v1/integrations/financial-events",
        json=envelope.model_dump(mode="json"),
        headers=headers,
    )
    conflicting_envelope = envelope.model_copy(
        update={"payload": {**envelope.payload, "amount": "66.01"}}
    )
    conflicting_headers = {
        **headers,
        "X-Financial-Signature": sign_financial_event(
            conflicting_envelope,
            timestamp=timestamp,
            secret="http-server-secret",
            tenant_id="default",
            provider="http-bank",
            key_version="v1",
        ),
    }
    conflict = client.post(
        "/api/v1/integrations/financial-events",
        json=conflicting_envelope.model_dump(mode="json"),
        headers=conflicting_headers,
    )
    assert accepted.status_code == 200
    assert accepted.json()["claim_status"] == "pending_payment"
    assert accepted.json()["evidence_classification"] == "simulated_connector"
    assert accepted.json()["projection_scope"] == "simulation_only"
    assert accepted.json()["reconciliation_case_id"] is None
    assert replay.status_code == 200 and replay.json()["replayed"] is True
    assert conflict.status_code == 409

    assert client.get("/api/v1/financial-reconciliation/cases").status_code == 403
    user_box["current"] = _user("finance-default", roles=["finance"])
    listed = client.get("/api/v1/financial-reconciliation/cases")
    observed = client.get("/api/v1/financial-connectors/observability")
    assert listed.status_code == 200
    assert listed.json()["total"] == 0
    assert observed.status_code == 200
    assert observed.json()["summary"]["retry_count"] == 1
    assert observed.json()["summary"]["auth_failure_count"] == 1
    assert observed.json()["summary"]["signature_failure_count"] == 1
    assert observed.json()["summary"]["payload_conflict_count"] == 1
    assert observed.json()["window_hours"] == 24
    assert observed.json()["source_revision"] == "20260716_0022"
    assert observed.json()["summary"]["latest_replay_at"] is not None
    assert observed.json()["summary"]["latest_auth_failure_at"] is not None
    assert observed.json()["summary"]["latest_payload_conflict_at"] is not None

    user_box["current"] = _user("finance-other", tenant_id="tenant-other", roles=["finance"])
    assert client.get("/api/v1/financial-reconciliation/cases").json()["total"] == 0
    other_observability = client.get("/api/v1/financial-connectors/observability")
    assert other_observability.json()["summary"]["auth_failure_count"] == 0
    assert other_observability.json()["summary"]["payload_conflict_count"] == 0

    user_box["current"] = _user("platform-admin", is_admin=True)
    events = client.get(
        "/api/v1/financial-connectors/admin/tenants/default/config-events"
    )
    assert events.status_code == 200
    assert [item["action"] for item in events.json()] == ["created", "activated"]
    assert "secret" not in json.dumps(events.json()).lower()


def _claim(db: Session) -> ExpenseClaim:
    row = ExpenseClaim(
        id=str(uuid.uuid4()),
        claim_no="BX-CONNECTOR-HTTP-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(row)
    db.flush()
    return row


def _user(
    username: str,
    *,
    tenant_id: str = "default",
    roles: list[str] | None = None,
    is_admin: bool = False,
) -> CurrentUserContext:
    return CurrentUserContext(
        username=username,
        name=username,
        role_codes=list(roles or []),
        is_admin=is_admin,
        tenant_id=tenant_id,
    )
