import re
from datetime import UTC, datetime

from fastapi.testclient import TestClient

from zbt.core.config import Settings
from zbt.core.errors import AppError
from zbt.core.passwords import PasswordService
from zbt.domains.agent.repository import InMemoryAgentRepository
from zbt.domains.agent.runtime import AgentReply
from zbt.domains.catalog.repository import InMemoryCatalogRepository
from zbt.domains.enrollment.repository import InMemoryEnrollmentRepository
from zbt.domains.identity.models import AdminUser
from zbt.domains.identity.repository import InMemoryIdentityRepository
from zbt.harness.kernel import AgentInvocation
from zbt.infrastructure.redis.agent_state import InMemoryAgentStateStore
from zbt.main import create_app


class FixedAgentRuntime:
    def reply(self, invocation: AgentInvocation) -> AgentReply:
        assert "65岁" in invocation.message
        assert invocation.persona == "customer"
        return AgentReply(
            text="可以优先考虑银龄守护医疗险，我会继续核对地区和职业。",
            cards=[],
            trace_id="trace_test_001",
        )


class FixedOperationRuntime:
    def reply(self, invocation: AgentInvocation) -> AgentReply:
        assert invocation.persona == "operation"
        assert invocation.admin_user is not None
        assert "订单" in invocation.message
        return AgentReply(
            text="当前共有24笔订单，其中18笔已出单。",
            cards=[
                {
                    "type": "metrics",
                    "version": "1.0",
                    "title": "经营总览",
                    "items": [
                        {"label": "订单总量", "value": 24, "unit": "笔"},
                    ],
                }
            ],
            trace_id="00000000-0000-0000-0000-000000000001",
            trace_url="https://smith.langchain.com/example-trace",
            invoked_tools=("get_operation_overview",),
        )


class StreamingAgentRuntime:
    def reply(self, invocation: AgentInvocation) -> AgentReply:
        assert invocation.h5_user is not None
        return AgentReply(
            text="已为你找到银龄守护医疗险。",
            cards=[
                {
                    "type": "product_recommendations",
                    "version": "1.0",
                    "title": "为你匹配的保障方案",
                    "items": [],
                }
            ],
            trace_id="00000000-0000-0000-0000-000000000002",
            actions=[],
            invoked_tools=("list_available_products",),
        )


class FailingAgentRuntime:
    def reply(self, invocation: AgentInvocation) -> AgentReply:
        raise AppError(
            "KNOWLEDGE_RERANK_FAILED",
            "知识重排失败",
            503,
            retryable=True,
        )


def test_h5_user_can_create_customer_agent_thread() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        agent_repository=InMemoryAgentRepository(),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        response = client.post(
            "/api/v1/agent/threads",
            headers={"Authorization": f"Bearer {token}"},
            json={"title": "给父亲配置医疗险"},
        )

    assert response.status_code == 201
    assert response.json()["data"]["persona"] == "customer"
    assert response.json()["data"]["title"] == "给父亲配置医疗险"


def test_h5_user_can_send_message_and_receive_agent_reply() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        agent_repository=InMemoryAgentRepository(),
        agent_runtime=FixedAgentRuntime(),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread_id = client.post(
            "/api/v1/agent/threads",
            headers=headers,
            json={"title": "给父亲配置医疗险"},
        ).json()["data"]["id"]
        response = client.post(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
            json={
                "content": {
                    "type": "text",
                    "text": "想给65岁的父亲买医疗险",
                },
                "client_message_id": "client-message-001",
            },
        )

    assert response.status_code == 202
    assert response.json()["data"]["status"] == "COMPLETED"
    assert "银龄守护医疗险" in response.json()["data"]["assistant_message"]["text"]
    assert re.fullmatch(
        r"ZBT-\d{8}-\d{4}",
        response.json()["data"]["service_no"],
    )
    assert (
        response.json()["data"]["assistant_message"]["service_no"]
        == response.json()["data"]["service_no"]
    )
    assert response.json()["data"]["trace_id"] == "trace_test_001"
    run_id = response.json()["data"]["run_id"]

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        messages = client.get(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
        )
        run = client.get(f"/api/v1/agent/runs/{run_id}", headers=headers)

    assert [item["role"] for item in messages.json()["data"]["items"]] == [
        "USER",
        "ASSISTANT",
    ]
    assert run.json()["data"]["status"] == "COMPLETED"
    assert run.json()["data"]["trace_id"] == "trace_test_001"


def test_h5_user_can_list_and_restore_own_conversation_history() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    repository = InMemoryAgentRepository()
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        agent_repository=repository,
        agent_runtime=FixedAgentRuntime(),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread_id = client.post(
            "/api/v1/agent/threads",
            headers=headers,
            json={"title": "父母医疗险咨询"},
        ).json()["data"]["id"]
        client.post(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
            json={
                "content": {"type": "text", "text": "想给65岁的父亲买医疗险"},
                "client_message_id": "history-message-001",
            },
        )
        threads = client.get("/api/v1/agent/threads", headers=headers)
        messages = client.get(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
        )

    assert threads.status_code == 200
    assert threads.json()["data"]["total"] == 1
    assert threads.json()["data"]["items"][0]["title"] == "父母医疗险咨询"
    assert threads.json()["data"]["items"][0]["message_count"] == 2
    assert threads.json()["data"]["items"][0]["last_message"]["role"] == "ASSISTANT"
    assert len(messages.json()["data"]["items"]) == 2


def test_admin_can_use_operation_persona_with_the_same_agent_kernel_contract() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    identities = InMemoryIdentityRepository()
    identities.save_admin_user(
        AdminUser(
            id="01ADMIN0000000000000000001",
            username="admin",
            password_hash=PasswordService().hash("zaq1XSW@"),
            display_name="系统管理员",
            status="ACTIVE",
            roles=("SUPER_ADMIN",),
            permissions=("*",),
            data_scope="ALL",
        )
    )
    app = create_app(
        settings=settings,
        identity_repository=identities,
        catalog_repository=InMemoryCatalogRepository(),
        agent_repository=InMemoryAgentRepository(),
        agent_runtime=FixedOperationRuntime(),
        enrollment_repository=InMemoryEnrollmentRepository(),
        clock=lambda: datetime(2026, 7, 26, 8, tzinfo=UTC),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/admin/auth/login",
            json={"username": "admin", "password": "zaq1XSW@"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread = client.post(
            "/api/v1/admin/agent/threads",
            headers=headers,
            json={"title": "订单经营分析"},
        )
        response = client.post(
            f"/api/v1/admin/agent/threads/{thread.json()['data']['id']}/messages",
            headers=headers,
            json={
                "content": {"type": "text", "text": "分析一下当前订单情况"},
                "client_message_id": "operation-message-001",
            },
        )
        runs = client.get("/api/v1/admin/agent/runs", headers=headers)

    assert thread.status_code == 201
    assert thread.json()["data"]["persona"] == "operation"
    assert response.status_code == 202
    data = response.json()["data"]
    assert data["invoked_tools"] == ["get_operation_overview"]
    assert data["trace_url"] == "https://smith.langchain.com/example-trace"
    assert data["assistant_message"]["cards"][0]["type"] == "metrics"
    assert runs.status_code == 200
    assert runs.json()["data"]["total"] == 1
    assert runs.json()["data"]["items"][0]["id"] == data["run_id"]


def test_admin_can_list_only_own_operation_conversations() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    identities = InMemoryIdentityRepository()
    for user_id, username in (
        ("01ADMIN0000000000000000011", "admin-alpha"),
        ("01ADMIN0000000000000000012", "admin-beta"),
    ):
        identities.save_admin_user(
            AdminUser(
                id=user_id,
                username=username,
                password_hash=PasswordService().hash("zaq1XSW@"),
                display_name=username,
                status="ACTIVE",
                roles=("OPERATOR",),
                permissions=("dashboard:read",),
                data_scope="ALL",
            )
        )
    app = create_app(
        settings=settings,
        identity_repository=identities,
        catalog_repository=InMemoryCatalogRepository(),
        agent_repository=InMemoryAgentRepository(),
        enrollment_repository=InMemoryEnrollmentRepository(),
    )

    with TestClient(app) as client:
        alpha_token = client.post(
            "/api/v1/admin/auth/login",
            json={"username": "admin-alpha", "password": "zaq1XSW@"},
        ).json()["data"]["tokens"]["access_token"]
        beta_token = client.post(
            "/api/v1/admin/auth/login",
            json={"username": "admin-beta", "password": "zaq1XSW@"},
        ).json()["data"]["tokens"]["access_token"]
        alpha_headers = {"Authorization": f"Bearer {alpha_token}"}
        beta_headers = {"Authorization": f"Bearer {beta_token}"}
        alpha_thread = client.post(
            "/api/v1/admin/agent/threads",
            headers=alpha_headers,
            json={"title": "Alpha订单趋势"},
        ).json()["data"]
        client.post(
            "/api/v1/admin/agent/threads",
            headers=alpha_headers,
            json={"title": "评测-operation-analytics-trend"},
        )
        client.post(
            "/api/v1/admin/agent/threads",
            headers=alpha_headers,
            json={"title": "__EVAL__:operation-overview"},
        )
        beta_thread = client.post(
            "/api/v1/admin/agent/threads",
            headers=beta_headers,
            json={"title": "Beta保单趋势"},
        ).json()["data"]

        alpha_threads = client.get(
            "/api/v1/admin/agent/threads",
            headers=alpha_headers,
        )
        beta_threads = client.get(
            "/api/v1/admin/agent/threads",
            headers=beta_headers,
        )
        cross_account_messages = client.get(
            f"/api/v1/admin/agent/threads/{alpha_thread['id']}/messages",
            headers=beta_headers,
        )

    assert alpha_threads.status_code == 200
    assert [item["id"] for item in alpha_threads.json()["data"]["items"]] == [
        alpha_thread["id"]
    ]
    assert beta_threads.status_code == 200
    assert [item["id"] for item in beta_threads.json()["data"]["items"]] == [
        beta_thread["id"]
    ]
    assert cross_account_messages.status_code == 404
    assert cross_account_messages.json()["error"]["code"] == "AGENT_THREAD_NOT_FOUND"


def test_h5_agent_stream_emits_and_replays_runtime_events() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    state_store = InMemoryAgentStateStore()
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        catalog_repository=InMemoryCatalogRepository(),
        agent_repository=InMemoryAgentRepository(),
        agent_runtime=StreamingAgentRuntime(),
        enrollment_repository=InMemoryEnrollmentRepository(),
        agent_state_store=state_store,
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread_id = client.post(
            "/api/v1/agent/threads",
            headers=headers,
            json={"title": "SSE测试"},
        ).json()["data"]["id"]
        response = client.post(
            f"/api/v1/agent/threads/{thread_id}/messages/stream",
            headers=headers,
            json={
                "content": {"type": "text", "text": "推荐父母医疗险"},
                "client_message_id": "stream-message-001",
            },
        )

        assert response.status_code == 200
        assert response.headers["content-type"].startswith("text/event-stream")
        assert response.text.index("event: run.started") < response.text.index(
            "event: tool.completed"
        )
        assert response.text.index("event: tool.completed") < response.text.index("event: ui.ready")
        assert response.text.index("event: ui.ready") < response.text.index("event: run.completed")
        run_id = next(
            line.removeprefix("id: ").split(":")[0]
            for line in response.text.splitlines()
            if line.startswith("id: ")
        )
        replay = client.get(
            f"/api/v1/agent/runs/{run_id}/events",
            headers=headers,
        )

    assert replay.status_code == 200
    assert replay.text.count("event: ") == 4
    assert "list_available_products" in replay.text


def test_failed_agent_run_does_not_leave_a_dangling_user_message() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        catalog_repository=InMemoryCatalogRepository(),
        agent_repository=InMemoryAgentRepository(),
        agent_runtime=FailingAgentRuntime(),
        enrollment_repository=InMemoryEnrollmentRepository(),
        agent_state_store=InMemoryAgentStateStore(),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread_id = client.post(
            "/api/v1/agent/threads",
            headers=headers,
            json={"title": "失败消息回滚"},
        ).json()["data"]["id"]
        response = client.post(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
            json={
                "content": {"type": "text", "text": "医疗险等待期是多少天？"},
                "client_message_id": "failed-message-001",
            },
        )
        messages = client.get(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
        )

    assert response.status_code == 503
    assert response.json()["error"]["code"] == "KNOWLEDGE_RERANK_FAILED"
    assert messages.json()["data"]["items"] == []


def test_agent_rate_limit_is_enforced_before_model_invocation() -> None:
    settings = Settings(
        app_env="test",
        jwt_access_secret="a" * 32,
        jwt_refresh_secret="b" * 32,
        field_encryption_key="c" * 32,
    )
    app = create_app(
        settings=settings,
        identity_repository=InMemoryIdentityRepository(),
        catalog_repository=InMemoryCatalogRepository(),
        agent_repository=InMemoryAgentRepository(),
        agent_runtime=StreamingAgentRuntime(),
        enrollment_repository=InMemoryEnrollmentRepository(),
        agent_state_store=InMemoryAgentStateStore(rate_limit=1),
    )

    with TestClient(app) as client:
        token = client.post(
            "/api/v1/h5/auth/login",
            json={"mobile": "18800000001", "code": "147258"},
        ).json()["data"]["tokens"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}
        thread_id = client.post(
            "/api/v1/agent/threads",
            headers=headers,
            json={"title": "限流测试"},
        ).json()["data"]["id"]
        payload = {
            "content": {"type": "text", "text": "推荐医疗险"},
            "client_message_id": "rate-message",
        }
        first = client.post(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
            json=payload,
        )
        second = client.post(
            f"/api/v1/agent/threads/{thread_id}/messages",
            headers=headers,
            json=payload,
        )

    assert first.status_code == 202
    assert second.status_code == 429
    assert second.json()["error"]["code"] == "AGENT_RATE_LIMITED"
