from __future__ import annotations

from collections.abc import Generator
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.api.v1.endpoints import steward as steward_endpoint
from app.main import create_app
from app.models.agent_conversation import AgentConversation
from app.models.financial_record import ExpenseClaim
from app.schemas.steward import (
    StewardPlanResponse,
    StewardRuntimeDecisionResponse,
    StewardSlotDecisionResponse,
)
from app.services.expense_cases import ExpenseCaseService
from app.test_helpers.db import build_in_memory_session_factory


def _current_user(*, tenant_id: str = "tenant-a") -> CurrentUserContext:
    return CurrentUserContext(
        username="trusted.user@example.com",
        name="可信用户",
        role_codes=["user"],
        is_admin=False,
        tenant_id=tenant_id,
        department_name="交付部",
        department_id="dept-delivery",
        cost_center="CC-TRUSTED",
        position="实施顾问",
        grade="P6",
        employee_no="E-TRUSTED",
        employee_id="employee-trusted",
        manager_name="可信经理",
        auth_session_id="session-secret",
    )


def _build_client(
    *,
    current_user: CurrentUserContext | None,
) -> tuple[TestClient, object, object]:
    session_factory = build_in_memory_session_factory()
    app = create_app()

    def override_db() -> Generator[Session, None, None]:
        db = session_factory()
        try:
            yield db
        finally:
            db.close()

    app.dependency_overrides[get_db] = override_db
    if current_user is not None:
        app.dependency_overrides[get_current_user] = lambda: current_user
    return TestClient(app), session_factory, app


@pytest.mark.parametrize(
    ("path", "payload"),
    [
        ("/api/v1/steward/plans", {"message": "报销交通费"}),
        ("/api/v1/steward/plans/stream", {"message": "报销交通费"}),
        (
            "/api/v1/steward/slot-decisions",
            {"task_type": "reimbursement", "user_message": "报销交通费"},
        ),
        (
            "/api/v1/steward/runtime-decisions",
            {"user_message": "继续处理"},
        ),
    ],
)
def test_steward_ai_endpoints_require_authenticated_user(
    path: str,
    payload: dict[str, object],
) -> None:
    client, _session_factory, app = _build_client(current_user=None)
    try:
        response = client.post(path, json=payload)
        assert response.status_code == 401
    finally:
        app.dependency_overrides.clear()


@pytest.mark.parametrize(
    ("path", "payload"),
    [
        ("/api/v1/steward/plans", {"message": "报销交通费"}),
        ("/api/v1/steward/plans/stream", {"message": "报销交通费"}),
        (
            "/api/v1/steward/slot-decisions",
            {"task_type": "reimbursement", "user_message": "报销交通费"},
        ),
        (
            "/api/v1/steward/runtime-decisions",
            {"user_message": "继续处理"},
        ),
    ],
)
def test_steward_ai_endpoints_fail_closed_without_tenant(
    path: str,
    payload: dict[str, object],
) -> None:
    client, _session_factory, app = _build_client(
        current_user=_current_user(tenant_id=""),
    )
    try:
        response = client.post(path, json=payload)
        assert response.status_code == 403
        assert "缺少租户归属" in response.json()["detail"]
    finally:
        app.dependency_overrides.clear()


def test_steward_plan_and_stream_use_authenticated_identity(monkeypatch) -> None:
    captured_payloads = []

    class CapturingPlanner:
        @staticmethod
        def _clean_text(value):
            return str(value or "").strip()

        @staticmethod
        def _resolve_base_date(_client_now_iso, _context_json):
            return datetime(2026, 7, 16, tzinfo=UTC).date()

        @staticmethod
        def _looks_like_ambiguous_travel_flow(_message, _base_date, _payload):
            return False

        @staticmethod
        def build_plan(payload):
            captured_payloads.append(payload)
            return StewardPlanResponse(
                plan_id=f"plan-auth-{len(captured_payloads)}",
                summary="已按可信登录身份生成计划。",
            )

    monkeypatch.setattr(
        steward_endpoint,
        "_build_steward_planner",
        lambda _db: CapturingPlanner(),
    )
    current_user = _current_user()
    client, session_factory, app = _build_client(current_user=current_user)
    forged_context = {
        "tenant_id": "tenant-forged",
        "tenantId": "tenant-forged-camel",
        "user_id": "forged-user",
        "userId": "forged-user-camel",
        "username": "forged@example.com",
        "role_codes": ["admin"],
        "is_admin": True,
        "auth_session_id": "forged-session",
        "session_type": "steward",
    }
    try:
        plan_response = client.post(
            "/api/v1/steward/plans",
            json={
                "message": "报销交通费",
                "user_id": "forged-user",
                "context_json": forged_context,
            },
        )
        assert plan_response.status_code == 200

        with client.stream(
            "POST",
            "/api/v1/steward/plans/stream",
            json={
                "message": "继续报销交通费",
                "user_id": "forged-user",
                "context_json": forged_context,
            },
        ) as stream_response:
            assert stream_response.status_code == 200
            assert any(stream_response.iter_lines())

        assert len(captured_payloads) == 2
        for captured in captured_payloads:
            assert captured.user_id == current_user.username
            assert captured.context_json["tenant_id"] == "tenant-a"
            assert captured.context_json["user_id"] == current_user.username
            assert captured.context_json["username"] == current_user.username
            assert captured.context_json["role_codes"] == ["user"]
            assert captured.context_json["is_admin"] is False
            assert "tenantId" not in captured.context_json
            assert "userId" not in captured.context_json
            assert "auth_session_id" not in captured.context_json

        with session_factory() as db:
            conversations = list(db.scalars(select(AgentConversation)).all())
            assert len(conversations) == 2
            assert all(item.user_id == current_user.username for item in conversations)
            assert all(
                item.state_json.get("tenant_id") == "tenant-a"
                for item in conversations
            )
    finally:
        app.dependency_overrides.clear()


def test_steward_slot_and_runtime_ignore_forged_identity(monkeypatch) -> None:
    captured_slot_payloads = []
    captured_runtime_payloads = []

    def fake_slot_decision(payload, _runtime_chat):
        captured_slot_payloads.append(payload)
        return StewardSlotDecisionResponse(
            next_action="ask_user",
            question="请补充金额。",
        )

    def fake_runtime_decision(payload, _runtime_chat):
        captured_runtime_payloads.append(payload)
        return StewardRuntimeDecisionResponse(next_action="no_op")

    monkeypatch.setattr(steward_endpoint, "_decide_steward_slot", fake_slot_decision)
    monkeypatch.setattr(
        steward_endpoint,
        "_decide_steward_runtime",
        fake_runtime_decision,
    )
    client, _session_factory, app = _build_client(current_user=_current_user())
    try:
        slot_response = client.post(
            "/api/v1/steward/slot-decisions",
            json={
                "task_type": "reimbursement",
                "user_message": "报销交通费",
                "task_context": {
                    "tenant_id": "tenant-forged",
                    "tenantId": "tenant-forged-camel",
                    "username": "forged@example.com",
                    "is_admin": True,
                },
            },
        )
        runtime_response = client.post(
            "/api/v1/steward/runtime-decisions",
            json={
                "user_message": "继续处理",
                "context_json": {
                    "tenant_id": "tenant-forged",
                    "tenantId": "tenant-forged-camel",
                    "username": "forged@example.com",
                },
                "runtime_state": {
                    "tenant_id": "tenant-forged-runtime",
                    "user_id": "forged-runtime-user",
                },
            },
        )

        assert slot_response.status_code == 200
        assert runtime_response.status_code == 200
        assert len(captured_slot_payloads) == 1
        assert len(captured_runtime_payloads) == 1
        slot_context = captured_slot_payloads[0].task_context
        assert slot_context["tenant_id"] == "tenant-a"
        assert slot_context["username"] == "trusted.user@example.com"
        assert slot_context["is_admin"] is False
        assert "tenantId" not in slot_context

        runtime_payload = captured_runtime_payloads[0]
        assert runtime_payload.context_json["tenant_id"] == "tenant-a"
        assert runtime_payload.runtime_state["tenant_id"] == "tenant-a"
        assert runtime_payload.context_json["user_id"] == "trusted.user@example.com"
        assert runtime_payload.runtime_state["user_id"] == "trusted.user@example.com"
        assert "tenantId" not in runtime_payload.context_json
    finally:
        app.dependency_overrides.clear()


def test_steward_runtime_rejects_cross_tenant_conversation(monkeypatch) -> None:
    client, session_factory, app = _build_client(current_user=_current_user())
    with session_factory() as db:
        db.add(
            AgentConversation(
                conversation_id="conv-tenant-b",
                user_id="trusted.user@example.com",
                source="user_message",
                state_json={
                    "tenant_id": "tenant-b",
                    "session_type": "steward",
                    "steward_state": {"active_flow": "travel_reimbursement"},
                },
            )
        )
        db.commit()

    monkeypatch.setattr(
        steward_endpoint,
        "_decide_steward_runtime",
        lambda *_args, **_kwargs: pytest.fail("越权会话不应进入决策服务"),
    )
    try:
        response = client.post(
            "/api/v1/steward/runtime-decisions",
            json={
                "user_message": "继续处理",
                "context_json": {"conversation_id": "conv-tenant-b"},
            },
        )
        assert response.status_code == 403
        assert "不属于登录用户或租户" in response.json()["detail"]
    finally:
        app.dependency_overrides.clear()


def test_steward_application_candidates_are_tenant_scoped() -> None:
    _client, session_factory, app = _build_client(current_user=_current_user())
    try:
        with session_factory() as db:
            for tenant_id, suffix in (("tenant-a", "A"), ("tenant-b", "B")):
                claim = ExpenseClaim(
                    id=f"application-tenant-{suffix.lower()}",
                    tenant_id=tenant_id,
                    claim_no=f"AP-TENANT-{suffix}",
                    employee_id="employee-trusted",
                    employee_name="可信用户",
                    department_name="交付部",
                    expense_type="travel_application",
                    reason="上海客户现场部署",
                    location="上海",
                    amount=Decimal("1200.00"),
                    currency="CNY",
                    invoice_count=0,
                    occurred_at=datetime(2026, 7, 20, tzinfo=UTC),
                    submitted_at=datetime(2026, 7, 16, tzinfo=UTC),
                    status="approved",
                    approval_stage="已完成",
                    risk_flags_json=[],
                )
                db.add(claim)
                db.flush()
                ExpenseCaseService(db).ensure_case_for_claim(
                    claim,
                    tenant_id=tenant_id,
                )
            db.commit()

            payload = steward_endpoint._bind_authenticated_plan_request(
                steward_endpoint.StewardPlanRequest(
                    message="7月20日去上海出差，继续发起报销",
                    context_json={},
                ),
                _current_user(),
            )
            candidates = steward_endpoint._query_required_application_gate_candidates(
                db,
                payload,
                payload.context_json,
                tenant_id="tenant-a",
            )

        assert [item["claim_no"] for item in candidates] == ["AP-TENANT-A"]
    finally:
        app.dependency_overrides.clear()
