from __future__ import annotations

import asyncio
from decimal import Decimal

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

from app.models import Base
from app.api.admin_settings import enable_model, set_default_model
from app.models.admin import Admin
from app.models.ai_config import ModelConfig, SystemConfig
from app.models.logs import AiRequestLog
from app.schemas.admin import DefaultModelRequest, EnableModelRequest
from app.services.model_routing_service import ModelRoutingService
from app.services.model_service import ModelClientService
from app.services.model_stream_service import ModelStreamService
from app.services.external_errors import ExternalServiceError
from app.services.rag_service import RagResult, RetrievedChunk
from app.services.tracked_generation_service import TrackedGenerationService


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


def _model(
    model_id: int,
    name: str,
    *,
    is_default: int,
    allow_report: int,
    allow_summary: int,
    allow_fixed_info: int = 1,
    allow_simple_knowledge: int = 1,
) -> ModelConfig:
    return ModelConfig(
        id=model_id,
        provider="test",
        api_type="openai_compatible",
        model_name=name,
        api_url="https://example.com",
        api_key="secret",
        enabled=1,
        is_default=is_default,
        allow_report=allow_report,
        allow_summary=allow_summary,
        allow_fixed_info=allow_fixed_info,
        allow_simple_knowledge=allow_simple_knowledge,
        allow_deep_chat=1,
        input_price_per_1k=Decimal("0.002"),
        output_price_per_1k=Decimal("0.006"),
        currency="CNY",
        timeout_second=30,
    )


def test_background_scenario_routes_without_changing_live_default_model():
    with _db() as db:
        main = _model(1, "main-model", is_default=1, allow_report=0, allow_summary=1)
        report = _model(2, "report-model", is_default=0, allow_report=1, allow_summary=0)
        db.add_all([main, report])
        db.commit()

        report_route = ModelRoutingService.resolve(db, "report")
        summary_route = ModelRoutingService.resolve(db, "summary")

        assert report_route.model is report
        assert report_route.fallback_used is False
        assert summary_route.model is main
        assert ModelClientService._get_enabled_model(db) is main


def test_background_scenario_falls_back_to_default_model():
    with _db() as db:
        main = _model(1, "main-model", is_default=1, allow_report=0, allow_summary=0)
        db.add(main)
        db.commit()

        route = ModelRoutingService.resolve(db, "report")

        assert route.model is main
        assert route.fallback_used is True
        assert "回退默认主模型" in route.reason


def test_fixed_info_chat_routes_only_when_all_recalled_knowledge_is_fixed():
    with _db() as db:
        main = _model(
            1,
            "main-model",
            is_default=1,
            allow_report=1,
            allow_summary=1,
            allow_fixed_info=0,
        )
        fixed = _model(
            2,
            "fixed-model",
            is_default=0,
            allow_report=0,
            allow_summary=0,
            allow_fixed_info=1,
        )
        db.add_all([main, fixed])
        db.commit()

        fixed_route = ModelRoutingService.resolve_chat(db, ["fixed", "fixed"])
        mixed_route = ModelRoutingService.resolve_chat(db, ["fixed", "course"])
        no_hit_route = ModelRoutingService.resolve_chat(db, [])

        assert fixed_route.model is fixed
        assert fixed_route.question_type == "fixed_info"
        assert mixed_route.model is main
        assert mixed_route.question_type == "knowledge_grounded"
        assert "混合类型" in mixed_route.reason
        assert no_hit_route.model is main
        assert no_hit_route.question_type == "general_chat"

        db.add(SystemConfig(config_key="fixed_info_model_routing_enabled", config_value="false"))
        db.commit()
        disabled_route = ModelRoutingService.resolve_chat(db, ["fixed"])
        assert disabled_route.model is main
        assert disabled_route.question_type == "fixed_info"
        assert "分流开关已关闭" in disabled_route.reason


def test_simple_knowledge_routing_defaults_to_fixed_only_for_safe_rollout():
    with _db() as db:
        main = _model(
            1,
            "main-model",
            is_default=1,
            allow_report=1,
            allow_summary=1,
            allow_simple_knowledge=0,
        )
        simple = _model(
            2,
            "simple-model",
            is_default=0,
            allow_report=0,
            allow_summary=0,
            allow_simple_knowledge=1,
        )
        db.add_all([main, simple])
        db.commit()

        route = ModelRoutingService.resolve_chat(db, ["course"], "原生里的作业都有哪些")

        assert ModelRoutingService.chat_routing_mode(db) == "fixed_only"
        assert route.model is main
        assert route.question_type == "knowledge_grounded"


def test_conservative_mode_routes_only_clear_simple_knowledge_questions():
    with _db() as db:
        main = _model(
            1,
            "main-model",
            is_default=1,
            allow_report=1,
            allow_summary=1,
            allow_fixed_info=0,
            allow_simple_knowledge=0,
        )
        simple = _model(
            2,
            "simple-model",
            is_default=0,
            allow_report=0,
            allow_summary=0,
            allow_fixed_info=0,
            allow_simple_knowledge=1,
        )
        db.add_all(
            [
                main,
                simple,
                SystemConfig(config_key="chat_model_routing_mode", config_value="conservative"),
            ]
        )
        db.commit()

        simple_route = ModelRoutingService.resolve_chat(db, ["course", "qa"], "原生里的作业都有哪些")
        personal_route = ModelRoutingService.resolve_chat(
            db,
            ["course"],
            "我做阴影人格练习时身体很难受怎么办",
        )
        mixed_route = ModelRoutingService.resolve_chat(db, ["fixed", "course"], "课程作业有哪些")

        assert simple_route.model is simple
        assert simple_route.question_type == "simple_knowledge"
        assert "保守简单知识规则" in simple_route.reason
        assert personal_route.model is main
        assert personal_route.question_type == "knowledge_grounded"
        assert mixed_route.model is main
        assert mixed_route.question_type == "knowledge_grounded"


def test_chat_routing_off_keeps_fixed_information_on_main_model():
    with _db() as db:
        main = _model(
            1,
            "main-model",
            is_default=1,
            allow_report=1,
            allow_summary=1,
            allow_fixed_info=0,
        )
        fixed = _model(
            2,
            "fixed-model",
            is_default=0,
            allow_report=0,
            allow_summary=0,
            allow_fixed_info=1,
        )
        db.add_all(
            [
                main,
                fixed,
                SystemConfig(config_key="chat_model_routing_mode", config_value="off"),
            ]
        )
        db.commit()

        route = ModelRoutingService.resolve_chat(db, ["fixed"], "本周上课时间是什么")

        assert route.model is main
        assert route.question_type == "fixed_info"
        assert "分流开关已关闭" in route.reason


def test_user_stream_uses_conservative_simple_knowledge_route(monkeypatch):
    async def collect(response):
        return [chunk async for chunk in response.chunks]

    with _db() as db:
        main = _model(
            1,
            "main-model",
            is_default=1,
            allow_report=1,
            allow_summary=1,
            allow_simple_knowledge=0,
        )
        simple = _model(
            2,
            "simple-model",
            is_default=0,
            allow_report=0,
            allow_summary=0,
            allow_simple_knowledge=1,
        )
        db.add_all(
            [
                main,
                simple,
                SystemConfig(config_key="chat_model_routing_mode", config_value="conservative"),
                SystemConfig(config_key="mock_model_enabled", config_value="false"),
            ]
        )
        db.commit()

        async def stream(model, _rag_result):
            yield f"由{model.model_name}回答"

        rag_result = RagResult(
            question="原生里的作业都有哪些",
            knowledge_scopes=[],
            chunks=[
                RetrievedChunk(
                    knowledge_id=1,
                    knowledge_name="原生课程",
                    title="课程作业",
                    content="静水流深静心。",
                    knowledge_type="course",
                )
            ],
            prompt="原生里的作业都有哪些",
        )
        monkeypatch.setattr("app.services.model_stream_service._stream_configured_model_async", stream)
        response = ModelStreamService.stream_async(db, rag_result)
        chunks = asyncio.run(collect(response))

        assert chunks == ["由simple-model回答"]
        assert response.model_id == simple.id
        assert response.question_type == "simple_knowledge"


def test_fixed_info_non_stream_call_falls_back_to_main_model_on_provider_failure(monkeypatch):
    with _db() as db:
        main = _model(1, "main-model", is_default=1, allow_report=1, allow_summary=1, allow_fixed_info=0)
        fixed = _model(2, "fixed-model", is_default=0, allow_report=0, allow_summary=0, allow_fixed_info=1)
        db.add_all([main, fixed, SystemConfig(config_key="mock_model_enabled", config_value="false")])
        db.commit()
        calls: list[str] = []

        def call(model, _rag_result, *, allow_no_hit=False):
            calls.append(model.model_name)
            if model.id == fixed.id:
                raise ExternalServiceError("固定信息模型暂时不可用", provider="model")
            return "主模型回退回答"

        monkeypatch.setattr("app.services.model_service._call_configured_model", call)
        completion = ModelClientService.complete(db, _rag_result("fixed"))

        assert calls == ["fixed-model", "main-model"]
        assert completion.model_id == main.id
        assert completion.answer == "主模型回退回答"
        assert completion.question_type == "fixed_info"
        assert "运行时回退默认主模型" in (completion.route_reason or "")


def test_fixed_info_async_stream_falls_back_before_first_output(monkeypatch):
    async def collect(response):
        return [chunk async for chunk in response.chunks]

    with _db() as db:
        main = _model(1, "main-model", is_default=1, allow_report=1, allow_summary=1, allow_fixed_info=0)
        fixed = _model(2, "fixed-model", is_default=0, allow_report=0, allow_summary=0, allow_fixed_info=1)
        db.add_all([main, fixed, SystemConfig(config_key="mock_model_enabled", config_value="false")])
        db.commit()

        async def stream(model, _rag_result):
            if model.id == fixed.id:
                raise ExternalServiceError("固定信息模型暂时不可用", provider="model")
            yield "主模型流式回退回答"

        monkeypatch.setattr("app.services.model_stream_service._stream_configured_model_async", stream)
        response = ModelStreamService.stream_async(db, _rag_result("fixed"))
        chunks = asyncio.run(collect(response))

        assert chunks == ["主模型流式回退回答"]
        assert response.model_id == main.id
        assert response.question_type == "fixed_info"
        assert "运行时回退默认主模型" in (response.route_reason or "")


def test_fixed_info_stream_does_not_restart_after_partial_output(monkeypatch):
    with _db() as db:
        main = _model(1, "main-model", is_default=1, allow_report=1, allow_summary=1, allow_fixed_info=0)
        fixed = _model(2, "fixed-model", is_default=0, allow_report=0, allow_summary=0, allow_fixed_info=1)
        db.add_all([main, fixed, SystemConfig(config_key="mock_model_enabled", config_value="false")])
        db.commit()
        calls: list[str] = []

        async def stream(model, _rag_result):
            calls.append(model.model_name)
            if model.id == fixed.id:
                yield "已经输出的部分"
                raise ExternalServiceError("输出中断", provider="model")
            yield "不应重新生成"

        async def collect(response):
            chunks = []
            with pytest.raises(ExternalServiceError):
                async for chunk in response.chunks:
                    chunks.append(chunk)
            return chunks

        monkeypatch.setattr("app.services.model_stream_service._stream_configured_model_async", stream)
        response = ModelStreamService.stream_async(db, _rag_result("fixed"))
        chunks = asyncio.run(collect(response))

        assert chunks == ["已经输出的部分"]
        assert calls == ["fixed-model"]
        assert response.model_id == fixed.id


def test_tracked_background_generation_records_actual_route_tokens_and_cost():
    with _db() as db:
        db.add(SystemConfig(config_key="mock_model_enabled", config_value="true"))
        report = _model(2, "report-model", is_default=0, allow_report=1, allow_summary=0)
        db.add(report)
        db.commit()

        completion = TrackedGenerationService.generate(
            db,
            prompt="生成本周报告",
            scenario="report",
            user_id=7,
        )
        db.commit()

        log = db.query(AiRequestLog).one()
        assert completion.model_name == "report-model"
        assert log.model_id == report.id
        assert log.user_id == 7
        assert log.question_type == "background_report"
        assert log.total_token == completion.input_token + completion.output_token
        assert log.estimated_cost is not None
        assert "周期报告" in (log.route_reason or "")


def test_model_pool_keeps_one_default_and_rejects_disabling_last_default():
    with _db() as db:
        admin = Admin(id=1, username="admin", password="hash", name="管理员", status=1)
        main = _model(1, "main-model", is_default=1, allow_report=1, allow_summary=1)
        alternate = _model(2, "alternate-model", is_default=0, allow_report=1, allow_summary=1)
        alternate.enabled = 0
        db.add_all([admin, main, alternate])
        db.commit()

        enable_model(
            EnableModelRequest(modelId=alternate.id, enabled=1),
            db=db,
            current_admin=admin,
        )
        db.refresh(main)
        db.refresh(alternate)
        assert main.is_default == 1
        assert alternate.enabled == 1

        set_default_model(
            DefaultModelRequest(modelId=alternate.id),
            db=db,
            current_admin=admin,
        )
        db.refresh(main)
        db.refresh(alternate)
        assert main.is_default == 0
        assert alternate.is_default == 1

        enable_model(
            EnableModelRequest(modelId=main.id, enabled=0),
            db=db,
            current_admin=admin,
        )
        with pytest.raises(HTTPException) as exc_info:
            enable_model(
                EnableModelRequest(modelId=alternate.id, enabled=0),
                db=db,
                current_admin=admin,
            )
        assert exc_info.value.status_code == 400


def _rag_result(knowledge_type: str) -> RagResult:
    return RagResult(
        question="本周上课时间是什么？",
        knowledge_scopes=[],
        chunks=[
            RetrievedChunk(
                knowledge_id=1,
                knowledge_name="当前安排",
                title="本周安排",
                content="本周三晚八点上课。",
                knowledge_type=knowledge_type,
            )
        ],
        prompt="本周上课时间是什么？",
    )
