import asyncio
from decimal import Decimal
import json
from unittest.mock import AsyncMock, patch

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.api.admin_settings import _debug_agent_stream, get_agent_runtime_config, save_agent_runtime_config
from app.models import Base
from app.models.admin import Admin
from app.models.ai_config import ModelConfig, SystemConfig
from app.schemas.admin import AgentDebugRequest, AgentRuntimeConfigSaveRequest
from app.services.agent_debug_service import AgentDebugService
from app.services.model_stream_service import (
    AsyncStreamingModelResponse,
    ModelStreamService,
    _openai_stream_payload,
    _stream_configured_model_async,
)
from app.services.model_service import ModelClientService, _copy_model_with_overrides, _max_output_tokens
from app.services.rag_service import PromptService, RagResult, RetrievedChunk


def _database() -> Session:
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    return Session(engine)


def _admin() -> Admin:
    return Admin(id=1, username="admin", password="hash", name="系统管理员", status=1)


def _model() -> ModelConfig:
    return ModelConfig(
        id=1,
        provider="openai-compatible",
        display_name="正式模型",
        api_type="openai_compatible",
        model_name="production-model",
        base_url="https://example.com/v1",
        api_url="",
        api_key="encrypted",
        auth_type="bearer",
        max_token=None,
        stream_enabled=1,
        timeout_second=30,
        enabled=1,
    )


def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
    with _database() as db:
        admin = _admin()
        model = _model()
        db.add_all([
            admin,
            model,
            SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
        ])
        db.commit()

        result = get_agent_runtime_config(db=db, current_admin=admin)["data"]

        assert result["modelId"] == model.id
        assert result["modelName"] == "正式模型"
        assert result["maxToken"] == 8192
        assert result["streamEnabled"] == 1
        assert result["reasoningVisible"] == 0
        assert result["responseDepth"] == 35
        assert _max_output_tokens(model) == 8192


def test_saved_runtime_config_is_persisted_on_enabled_model():
    with _database() as db:
        admin = _admin()
        model = _model()
        db.add_all([
            admin,
            model,
            SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
        ])
        db.commit()

        result = save_agent_runtime_config(
            AgentRuntimeConfigSaveRequest(
                temperature=0.3,
                topP=0.9,
                topK=40,
                presencePenalty=0.2,
                frequencyPenalty=0.4,
                maxToken=12000,
                streamEnabled=0,
                reasoningVisible=1,
                responseDepth=20,
            ),
            db=db,
            current_admin=admin,
        )["data"]

        db.refresh(model)
        assert result["maxToken"] == 12000
        assert model.max_token == 12000
        assert model.temperature == Decimal("0.30")
        assert model.top_p == Decimal("0.90")
        assert model.top_k == 40
        assert model.presence_penalty == Decimal("0.20")
        assert model.frequency_penalty == Decimal("0.40")
        assert model.stream_enabled == 0
        assert result["streamEnabled"] == 0
        assert result["reasoningVisible"] == 1
        assert result["responseDepth"] == 20
        assert db.query(SystemConfig).filter_by(config_key="show_model_reasoning").one().config_value == "true"
        assert db.query(SystemConfig).filter_by(config_key="agent_response_depth").one().config_value == "20"

        payload = _openai_stream_payload(
            model,
            RagResult(question="测试", knowledge_scopes=[], chunks=[], prompt="测试", allow_general_knowledge=True),
        )
        assert payload["max_tokens"] == 12000
        assert payload["temperature"] == 0.3
        assert payload["top_p"] == 0.9
        assert payload["presence_penalty"] == 0.2
        assert payload["frequency_penalty"] == 0.4


def test_disabled_stream_returns_one_complete_chunk():
    model = _model()
    model.stream_enabled = 0
    answer = "完整回答" * 20
    rag_result = RagResult(
        question="测试",
        knowledge_scopes=[],
        chunks=[],
        prompt="测试",
        allow_general_knowledge=True,
    )

    async def collect():
        return [item async for item in _stream_configured_model_async(model, rag_result)]

    with patch("app.services.model_stream_service._call_configured_model", return_value=answer):
        chunks = asyncio.run(collect())
    assert chunks == [answer]


def test_agent_preview_uses_same_fixed_info_route_when_default_model_is_selected(monkeypatch):
    async def fixed_chunks():
        yield "本周三晚八点上课。"

    async def build_result(_db, _payload):
        return RagResult(
            question="本周什么时候上课？",
            knowledge_scopes=[],
            chunks=[
                RetrievedChunk(
                    knowledge_id=1,
                    knowledge_name="当前安排",
                    title="本周安排",
                    content="本周三晚八点上课。",
                    knowledge_type="fixed",
                )
            ],
            prompt="本周什么时候上课？",
            tool_trace=[],
        )

    captured = {}

    def debug_stream(model, _rag_result, _overrides, **kwargs):
        captured["model"] = model.model_name
        captured["fallback"] = kwargs["fallback_model"].model_name
        captured["routeReason"] = kwargs["route_reason"]
        return AsyncStreamingModelResponse(
            model_id=model.id,
            model_name=model.model_name,
            input_token=10,
            chunks=fixed_chunks(),
            route_reason=kwargs["route_reason"],
            question_type=kwargs["question_type"],
        )

    with _database() as db:
        admin = _admin()
        main = _model()
        main.is_default = 1
        main.allow_fixed_info = 0
        fixed = ModelConfig(
            id=2,
            provider="fixed",
            display_name="固定信息模型",
            api_type="openai_compatible",
            model_name="fixed-model",
            base_url="https://example.com/v1",
            api_url="",
            api_key="encrypted",
            auth_type="bearer",
            max_token=8192,
            stream_enabled=1,
            timeout_second=30,
            enabled=1,
            is_default=0,
            allow_fixed_info=1,
        )
        db.add_all([admin, main, fixed])
        db.commit()
        monkeypatch.setattr(AgentDebugService, "build_result", build_result)
        monkeypatch.setattr(ModelStreamService, "debug_stream_async", debug_stream)
        payload = AgentDebugRequest(
            promptContent="你是测试助手",
            modelId=main.id,
            question="本周什么时候上课？",
        )

        async def collect_events():
            return [item async for item in AgentDebugService.stream(payload, db, admin)]

        events = asyncio.run(collect_events())

        complete = next(item for item in events if item["type"] == "complete")
        assert captured["model"] == "fixed-model"
        assert captured["fallback"] == "production-model"
        assert "仅召回固定信息类" in captured["routeReason"]
        assert complete["modelName"] == "fixed-model"
        assert complete["questionType"] == "fixed_info"
        assert any(item.get("tool") == "model_route" for item in complete["retrievalTrace"])


def test_agent_preview_uses_conservative_simple_knowledge_route(monkeypatch):
    async def answer_chunks():
        yield "原生课程包含静水流深等练习。"

    async def build_result(_db, _payload):
        return RagResult(
            question="原生里的作业都有哪些",
            knowledge_scopes=[],
            chunks=[
                RetrievedChunk(
                    knowledge_id=1,
                    knowledge_name="原生课程",
                    title="课程作业",
                    content="静水流深静心。",
                    knowledge_type="course",
                )
            ],
            prompt="原生里的作业都有哪些",
            tool_trace=[],
        )

    captured = {}

    def debug_stream(model, _rag_result, _overrides, **kwargs):
        captured["model"] = model.model_name
        captured["questionType"] = kwargs["question_type"]
        return AsyncStreamingModelResponse(
            model_id=model.id,
            model_name=model.model_name,
            input_token=10,
            chunks=answer_chunks(),
            route_reason=kwargs["route_reason"],
            question_type=kwargs["question_type"],
        )

    with _database() as db:
        admin = _admin()
        main = _model()
        main.is_default = 1
        main.allow_simple_knowledge = 0
        simple = ModelConfig(
            id=2,
            provider="simple",
            display_name="简单知识模型",
            api_type="openai_compatible",
            model_name="simple-model",
            base_url="https://example.com/v1",
            api_url="",
            api_key="encrypted",
            auth_type="bearer",
            max_token=8192,
            stream_enabled=1,
            timeout_second=30,
            enabled=1,
            is_default=0,
            allow_simple_knowledge=1,
        )
        db.add_all(
            [
                admin,
                main,
                simple,
                SystemConfig(config_key="chat_model_routing_mode", config_value="conservative"),
            ]
        )
        db.commit()
        monkeypatch.setattr(AgentDebugService, "build_result", build_result)
        monkeypatch.setattr(ModelStreamService, "debug_stream_async", debug_stream)
        payload = AgentDebugRequest(
            promptContent="你是测试助手",
            modelId=main.id,
            question="原生里的作业都有哪些",
        )

        async def collect_events():
            return [item async for item in AgentDebugService.stream(payload, db, admin)]

        events = asyncio.run(collect_events())

        complete = next(item for item in events if item["type"] == "complete")
        assert captured == {"model": "simple-model", "questionType": "simple_knowledge"}
        assert complete["modelName"] == "simple-model"
        assert complete["questionType"] == "simple_knowledge"


def test_debug_stream_setting_overrides_model_without_changing_it():
    model = _model()
    debug_model = _copy_model_with_overrides(model, {"stream_enabled": 0})

    assert model.stream_enabled == 1
    assert debug_model.stream_enabled == 0

    payload = AgentDebugRequest(
        promptContent="测试",
        modelId=1,
        question="测试",
        streamEnabled=0,
        reasoningVisible=1,
    )
    assert AgentDebugService.overrides(payload)["stream_enabled"] == 0


def test_debug_preview_passes_conversation_history_and_replaces_saved_prompt():
    rag_result = RagResult(
        question="再回答上面的问题",
        knowledge_scopes=[],
        chunks=[],
        prompt="调试提示词渲染结果",
        allow_general_knowledge=True,
        messages=[{"role": "system", "content": "调试主提示词"}],
    )
    payload = AgentDebugRequest(
        promptContent="调试主提示词",
        modelId=1,
        question="再回答上面的问题",
        history=[
            {"role": "user", "content": "最开始的问题"},
            {"role": "assistant", "content": "第一次回答"},
        ],
    )

    with _database() as db:
        build_result = AsyncMock(return_value=rag_result)
        with patch("app.services.agent_debug_service.KnowledgeAgentService.build_result", build_result):
            result = asyncio.run(AgentDebugService.build_result(db, payload))

    kwargs = build_result.await_args.kwargs
    assert [item.content for item in kwargs["history"]] == ["最开始的问题", "第一次回答"]
    assert kwargs["preview_knowledge_ids"] is None
    assert kwargs["version_overrides"] is None
    assert kwargs["prompt_override"] == "调试主提示词"
    assert kwargs["response_depth"] == 35
    assert result.messages == rag_result.messages
    assert result.prompt == "调试提示词渲染结果"


def test_agent_debug_empty_knowledge_selection_uses_formal_open_catalog():
    payload = AgentDebugRequest(
        promptContent="调试主提示词",
        modelId=1,
        knowledgeIds=[],
        knowledgeVersions={},
        question="测试问题",
    )
    rag_result = RagResult(
        question="测试问题",
        knowledge_scopes=[],
        chunks=[],
        prompt="测试",
        allow_general_knowledge=True,
    )

    with _database() as db:
        build_result = AsyncMock(return_value=rag_result)
        with patch("app.services.agent_debug_service.KnowledgeAgentService.build_result", build_result):
            asyncio.run(AgentDebugService.build_result(db, payload))

    kwargs = build_result.await_args.kwargs
    assert kwargs["preview_knowledge_ids"] is None
    assert kwargs["version_overrides"] is None


def test_user_stream_no_hit_still_calls_model_for_cautious_answer():
    async def configured_chunks(_model, _rag_result):
        yield "我暂时没有可靠课程依据，但可以先帮你整理需要确认的点。"

    async def collect():
        with _database() as db:
            model = _model()
            db.add_all([
                model,
                SystemConfig(config_key="mock_model_enabled", config_value="false"),
            ])
            db.commit()
            rag_result = RagResult(
                question="课程里的特殊练习怎么做？",
                knowledge_scopes=[],
                chunks=[],
                prompt="本轮没有可靠的正式知识章节。",
                allow_general_knowledge=False,
            )
            with patch(
                "app.services.model_stream_service._stream_configured_model_async",
                configured_chunks,
            ):
                response = ModelStreamService.stream_async(db, rag_result)
                return [chunk async for chunk in response.chunks]

    chunks = asyncio.run(collect())

    assert chunks == ["我暂时没有可靠课程依据，但可以先帮你整理需要确认的点。"]


def test_prompt_includes_response_depth_instruction():
    with _database() as db:
        db.add(SystemConfig(config_key="agent_response_depth", config_value="20"))
        db.commit()

        messages = PromptService.build_messages(db, "做功课没反应怎么办？", [])

    system = messages[0]["content"]
    assert "回答深度：20/100（精简）" in system
    assert "不展开心理分析" in system
    assert "回到当下、身体感受和情绪觉察" in system


@pytest.mark.parametrize("reasoning_visible", [True, False])
def test_agent_debug_stream_respects_reasoning_visibility(reasoning_visible):
    async def chunks():
        yield "<think>内部思考</think>"
        yield "- **第一步**：停一下"

    rag_result = RagResult(
        question="怎么冷静",
        knowledge_scopes=[],
        chunks=[],
        prompt="怎么冷静",
        allow_general_knowledge=True,
        retrieval_log_id=9,
        tool_trace=[{"tool": "KnowledgeSearch", "status": "success"}],
        messages=[{"role": "user", "content": "怎么冷静"}],
    )
    model_response = AsyncStreamingModelResponse(
        model_id=1,
        model_name="production-model",
        input_token=10,
        chunks=chunks(),
    )

    async def collect(payload, db, admin):
        return [event async for event in _debug_agent_stream(payload, db, admin)]

    with _database() as db:
        admin = _admin()
        model = _model()
        db.add_all([admin, model])
        db.add(SystemConfig(
            id=1,
            config_key="show_model_reasoning",
            config_value="false" if reasoning_visible else "true",
        ))
        db.commit()
        payload = AgentDebugRequest(
            promptContent="你是测试助手",
            modelId=model.id,
            knowledgeIds=[],
            question="怎么冷静",
            maxToken=8192,
            reasoningVisible=1 if reasoning_visible else 0,
        )
        with (
            patch(
                "app.services.agent_debug_service.KnowledgeAgentService.build_result",
                return_value=rag_result,
            ),
            patch(
                "app.services.agent_debug_service.ModelStreamService.debug_stream_async",
                return_value=model_response,
            ),
        ):
            events = asyncio.run(collect(payload, db, admin))

    decoded = [
        json.loads(event.removeprefix("data: "))
        for event in events
        if event != "data: [DONE]\n\n"
    ]
    expected_types = (
        ["status", "reasoning", "content", "complete"]
        if reasoning_visible
        else ["status", "content", "complete"]
    )
    assert [event["type"] for event in decoded] == expected_types
    assert decoded[0]["reasoningVisible"] is reasoning_visible
    if reasoning_visible:
        assert decoded[1]["content"] == "内部思考"
    content_event = next(event for event in decoded if event["type"] == "content")
    assert content_event["content"] == "- **第一步**：停一下"
    assert decoded[-1]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"


def test_agent_debug_does_not_truncate_long_answer(monkeypatch):
    monkeypatch.setattr(
        "app.services.model_service._call_configured_model",
        lambda *_args, **_kwargs: "长" * 6000,
    )

    result = ModelClientService.debug_model(
        _model(),
        RagResult(question="测试", knowledge_scopes=[], chunks=[], prompt="测试", allow_general_knowledge=True),
        {"max_token": 8192},
    )

    assert len(result["answer"]) == 6000
