from __future__ import annotations

import asyncio
import json
from datetime import datetime
from types import SimpleNamespace

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

from app.models import Base
from app.models.knowledge import (
    Knowledge,
    KnowledgeChunk,
    KnowledgeManifest,
    KnowledgeRetrievalLog,
    KnowledgeSection,
    KnowledgeSourceSnapshot,
    KnowledgeVersion,
)
from app.models.chat import ChatMessage
from app.services.knowledge_agent_service import Candidate, KnowledgeAgentService
from app.services.knowledge_pipeline_service import parse_sections


@pytest.fixture(autouse=True)
def _disable_shared_redis_cache(monkeypatch: pytest.MonkeyPatch) -> None:
    """Keep unit-test catalogs isolated from the running development database cache."""
    monkeypatch.setattr(
        "app.services.knowledge_catalog_cache_service.get_sync_redis_client",
        lambda: None,
    )


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 _add_published_knowledge(
    db: Session,
    *,
    knowledge_id: int,
    name: str,
    open_status: int = 1,
    knowledge_type: str = "course",
    purpose: str = "解答家长与孩子沟通、学习动力相关课程问题",
    applicable_questions: str = "孩子学习动力、亲子沟通",
    core_topics: str = "家长情绪、倾听孩子、学习动力",
    section_title: str = "家长沟通的第一步",
    section_content: str = "家长和学习动力不足的孩子沟通时，第一步是先稳定自己的焦虑，再倾听孩子遇到的具体困难。",
) -> Knowledge:
    knowledge = Knowledge(
        id=knowledge_id,
        name=name,
        feishu_space_id="space",
        feishu_node_id=f"node-{knowledge_id}",
        status=open_status,
        source_status="normal",
        manifest_confirmed=1,
        knowledge_type=knowledge_type,
        review_mode="manual",
    )
    db.add(knowledge)
    db.flush()
    snapshot = KnowledgeSourceSnapshot(
        knowledge_id=knowledge.id,
        source_title=name,
        source_content="家长沟通需要先稳定自己的情绪，再倾听孩子的真实困难。",
        content_hash=f"hash-{knowledge_id}",
        source_identifier=f"space/node-{knowledge_id}",
    )
    db.add(snapshot)
    db.flush()
    version = KnowledgeVersion(
        knowledge_id=knowledge.id,
        snapshot_id=snapshot.id,
        version_no=1,
        status="published",
        quality_status="passed",
        processing_rule_version="test-v1",
    )
    db.add(version)
    db.flush()
    knowledge.current_version_id = version.id
    db.add(
        KnowledgeManifest(
            knowledge_id=knowledge.id,
            version_id=version.id,
            purpose=purpose,
            applicable_questions=applicable_questions,
            inapplicable_questions="天气和交通",
            core_topics=core_topics,
            boundaries="不输出完整课程资料",
            content_hash=f"manifest-{knowledge_id}",
            confirmed=1,
        )
    )
    section = KnowledgeSection(
        knowledge_id=knowledge.id,
        version_id=version.id,
        section_key="S0001",
        title=section_title,
        content=section_content,
        source_start=0,
        source_end=44,
        sort_order=1,
        content_hash=f"section-{knowledge_id}",
    )
    db.add(section)
    db.flush()
    db.add(
        KnowledgeChunk(
            knowledge_id=knowledge.id,
            version_id=version.id,
            section_id=section.id,
            title=section.title,
            content=section.content,
            normalized_text=section_content,
            keywords='["家长","沟通","学习动力"]',
            synonyms='["父母","交流"]',
            source_start=0,
            source_end=44,
            sort_order=1,
            content_hash=f"chunk-{knowledge_id}",
        )
    )
    db.commit()
    return knowledge


def test_catalog_only_contains_open_published_knowledge():
    with _database() as db:
        _add_published_knowledge(db, knowledge_id=1, name="开放课程", open_status=1)
        _add_published_knowledge(db, knowledge_id=2, name="关闭课程", open_status=0)
        catalog = KnowledgeAgentService.get_knowledge_catalog(db)
        assert [item["knowledgeId"] for item in catalog] == [1]


def test_common_question_skips_knowledge_tools():
    with _database() as db:
        _add_published_knowledge(db, knowledge_id=1, name="亲子课程")
        result = asyncio.run(KnowledgeAgentService.build_result(db, question="今天北京天气怎么样？"))
        assert result.allow_general_knowledge is True
        assert result.chunks == []
        decision = next(item for item in result.tool_trace if item["tool"] == "agent_decision")
        assert decision["needKnowledge"] is False


def test_course_question_without_catalog_does_not_fall_back_to_general_knowledge():
    with _database() as db:
        result = asyncio.run(KnowledgeAgentService.build_result(db, question="卢慧老师课程里怎么解释亲子沟通？"))
        assert result.allow_general_knowledge is False
        assert result.chunks == []


def test_fixed_information_question_without_catalog_does_not_fall_back_to_general_knowledge():
    with _database() as db:
        result = asyncio.run(KnowledgeAgentService.build_result(db, question="这周的会议链接在哪里？"))
        assert result.allow_general_knowledge is False
        assert result.chunks == []


def test_course_type_is_preferred_over_qa_for_course_source_questions():
    catalog = [
        {
            "knowledgeId": 2,
            "name": "课程常见答疑",
            "type": "qa",
            "purpose": "回答课程练习问题",
            "applicableQuestions": "课程练习",
            "coreTopics": "静心练习",
            "publishedAt": "2026-07-17T10:00:00",
        },
        {
            "knowledgeId": 1,
            "name": "课程原文",
            "type": "course",
            "purpose": "说明课程练习方法",
            "applicableQuestions": "课程练习",
            "coreTopics": "静心练习",
            "publishedAt": "2026-07-16T10:00:00",
        },
    ]

    need_knowledge, selected_ids, _ = KnowledgeAgentService._decide("课程里的静心练习怎么做？", catalog)

    assert need_knowledge is True
    assert selected_ids[:2] == [1, 2]


def test_fixed_information_is_retrieved_first_and_overrides_other_types():
    with _database() as db:
        _add_published_knowledge(
            db,
            knowledge_id=1,
            name="课程历史安排",
            section_title="练习一：课程会议安排",
            section_content="课程作业沿用旧安排，会议链接为旧链接，周二晚上上课。",
            purpose="课程上课时间和会议安排",
            applicable_questions="上课时间、会议链接",
            core_topics="课程安排",
        )
        _add_published_knowledge(
            db,
            knowledge_id=2,
            name="当前固定信息",
            knowledge_type="fixed",
            section_title="本周上课安排",
            section_content="本周上课时间为周三晚上，会议链接为最新链接。",
            purpose="保存当前上课时间、会议链接和服务安排",
            applicable_questions="上课时间、会议链接、带练安排",
            core_topics="当前安排",
        )

        result = asyncio.run(KnowledgeAgentService.build_result(db, question="课程作业和会议链接都有什么？"))

        assert result.allow_general_knowledge is False
        assert result.chunks[0].knowledge_type == "fixed"
        assert result.chunks[0].knowledge_name == "当前固定信息"
        assert "固定信息类·最高优先级" in result.prompt
        assert "如与课程、答疑或通用知识库冲突，忽略其他库的冲突内容" in result.prompt
        decision = next(item for item in result.tool_trace if item["tool"] == "agent_decision")
        assert decision["selectedKnowledgeIds"][0] == 2


def test_course_question_searches_chunk_and_reads_parent_section():
    with _database() as db:
        _add_published_knowledge(db, knowledge_id=1, name="亲子课程")
        result = asyncio.run(KnowledgeAgentService.build_result(db, question="课程里家长怎么和学习动力不足的孩子沟通？"))
        assert result.allow_general_knowledge is False
        assert len(result.chunks) == 1
        assert "先稳定自己的焦虑" in result.chunks[0].content
        assert any(item["tool"] == "read_knowledge_sections" for item in result.tool_trace)
        knowledge_context = next(
            message["content"] for message in result.messages if message["content"].startswith("[本轮可靠知识上下文]")
        )
        assert "来源知识库：亲子课程" in knowledge_context


def test_tool_trace_serializes_published_datetime():
    with _database() as db:
        knowledge = _add_published_knowledge(db, knowledge_id=1, name="亲子课程")
        version = db.get(KnowledgeVersion, knowledge.current_version_id)
        assert version is not None
        version.published_at = datetime(2026, 7, 13, 10, 30, 0)
        db.commit()

        result = asyncio.run(KnowledgeAgentService.build_result(db, question="课程里如何进行亲子沟通？"))
        log = db.get(KnowledgeRetrievalLog, result.retrieval_log_id)
        trace = json.loads(log.tool_trace)

        catalog_call = next(item for item in trace if item["tool"] == "get_knowledge_catalog")
        assert catalog_call["items"][0]["publishedAt"] == "2026-07-13T10:30:00"


def test_contextual_follow_up_is_rewritten_before_agent_decision():
    with _database() as db:
        history = [
            ChatMessage(id=1, session_id=1, user_id=1, role="user", content="课程退款条件有哪些？"),
            ChatMessage(id=2, session_id=1, user_id=1, role="assistant", content="第一种和第二种情况不同。"),
        ]
        result = asyncio.run(
            KnowledgeAgentService.build_result(db, question="那第二种情况呢？", history=history)
        )
        rewrite = next(item for item in result.tool_trace if item["tool"] == "rewrite_contextual_question")
        decision = next(item for item in result.tool_trace if item["tool"] == "agent_decision")

        assert rewrite["rewrittenQuestion"] == (
            "结合以下历史问题回答当前追问：\n"
            "历史问题1：课程退款条件有哪些？\n"
            "当前追问：那第二种情况呢？"
        )
        assert decision["request"]["question"] == rewrite["rewrittenQuestion"]


def test_long_follow_up_reference_is_detected_and_keeps_multiple_user_questions():
    history = [
        ChatMessage(id=1, session_id=1, user_id=1, role="user", content="觉式练习和慧氏练习分别是什么，区别是什么？"),
        ChatMessage(id=2, session_id=1, user_id=1, role="assistant", content="暂时无法确认。"),
        ChatMessage(id=3, session_id=1, user_id=1, role="user", content="阴影人格练习步骤是什么？"),
        ChatMessage(id=4, session_id=1, user_id=1, role="assistant", content="先稳定状态，再进行觉察。"),
    ]
    question = "你能根据这个练习的基本方向再来回答一下我上面的问题吗？"

    assert KnowledgeAgentService._needs_contextual_rewrite(question) is True
    rewritten = KnowledgeAgentService._fallback_rewrite(question, history)
    assert "觉式练习和慧氏练习" in rewritten
    assert "阴影人格练习步骤" in rewritten
    assert question in rewritten


def test_homework_overview_expands_practice_terms_and_section_limit():
    terms = KnowledgeAgentService._query_terms("合一的作业是什么？")

    assert "功课" in terms
    assert "练习" in terms
    assert KnowledgeAgentService._candidate_limit("合一的作业是什么？") == 48
    assert KnowledgeAgentService._selection_limit("合一的作业是什么？") == 24
    assert KnowledgeAgentService._candidate_limit("这个练习怎么做？") == 12
    assert KnowledgeAgentService._selection_limit("这个练习怎么做？") == 4
    assert KnowledgeAgentService._title_intent_boost("十七、练习一：风铃式静心", terms) == 20.0
    assert KnowledgeAgentService._title_intent_boost("完整练习的方向", terms) == 3.0
    assert KnowledgeAgentService._title_intent_boost("课程定位", terms) == 0.0
    assert KnowledgeAgentService._is_practice_overview("原生里的作业内容都有什么") is True
    assert KnowledgeAgentService._is_practice_overview(
        "关于“心光里都有什么功课”的追问：金色光练习具体内容是什么"
    ) is False


def test_homework_overview_keeps_all_numbered_practices_before_selection():
    candidates = [
        Candidate(
            chunk=SimpleNamespace(title=f"练习{index}：课程作业", id=index),
            section=SimpleNamespace(id=index),
            knowledge=SimpleNamespace(id=1),
            version=SimpleNamespace(id=1),
            lexical_score=float(30 - index),
        )
        for index in range(1, 15)
    ]

    unrelated = Candidate(
        chunk=SimpleNamespace(title="课程介绍", id=99),
        section=SimpleNamespace(id=99),
        knowledge=SimpleNamespace(id=1),
        version=SimpleNamespace(id=1),
        lexical_score=100.0,
    )

    selected = KnowledgeAgentService._select_sections(
        [unrelated, *candidates],
        limit=KnowledgeAgentService._selection_limit("原生里的作业内容都有什么"),
        prefer_numbered_practice=True,
    )

    assert len(selected) == 14
    assert all(item.chunk.title.startswith("练习") for item in selected)


def test_complete_section_includes_child_headings_but_stops_at_next_peer():
    with _database() as db:
        knowledge = _add_published_knowledge(db, knowledge_id=1, name="合一课程")
        version_id = knowledge.current_version_id
        parent = db.scalar(select(KnowledgeSection).where(KnowledgeSection.version_id == version_id))
        assert parent is not None
        parent.title = "练习一：风铃式静心"
        parent.content = "# 练习一：风铃式静心"
        parent.sort_order = 1
        db.add_all([
            parent,
            KnowledgeSection(
                knowledge_id=knowledge.id,
                version_id=version_id,
                section_key="S0002",
                title="基本操作",
                content="## 基本操作\n鼻吸鼻呼，呼气时发出连续蜂鸣声。",
                source_start=10,
                source_end=40,
                sort_order=2,
                content_hash="child",
            ),
            KnowledgeSection(
                knowledge_id=knowledge.id,
                version_id=version_id,
                section_key="S0003",
                title="练习二：内感知建模",
                content="# 练习二：内感知建模\n这是下一项作业。",
                source_start=41,
                source_end=70,
                sort_order=3,
                content_hash="next",
            ),
        ])
        db.commit()

        content = KnowledgeAgentService._read_complete_section(db, parent)

        assert "鼻吸鼻呼" in content
        assert "下一项作业" not in content


def test_same_level_practice_headings_are_read_as_one_semantic_unit():
    with _database() as db:
        knowledge = _add_published_knowledge(db, knowledge_id=1, name="心光课程")
        version_id = knowledge.current_version_id
        parent = db.scalar(select(KnowledgeSection).where(KnowledgeSection.version_id == version_id))
        assert parent is not None
        parent.title = "二十九、练习一：金色光欧姆静心"
        parent.content = "# 二十九、练习一：金色光欧姆静心"
        parent.sort_order = 1
        parent_chunk = db.scalar(select(KnowledgeChunk).where(KnowledgeChunk.section_id == parent.id))
        assert parent_chunk is not None
        parent_chunk.title = parent.title
        parent_chunk.content = parent.content
        parent_chunk.normalized_text = "金色光 欧姆 静心 练习"
        detail = KnowledgeSection(
            knowledge_id=knowledge.id,
            version_id=version_id,
            section_key="S0002",
            title="基础原则",
            content="# 基础原则\n练习时保持自然呼吸，不追求必须看见金色光。",
            source_start=10,
            source_end=50,
            sort_order=2,
            content_hash="detail",
        )
        db.add_all([
            parent,
            parent_chunk,
            detail,
            KnowledgeSection(
                knowledge_id=knowledge.id,
                version_id=version_id,
                section_key="S0003",
                title="三十、金色光练习的现实边界",
                content="# 三十、金色光练习的现实边界\n不能替代医疗诊断。",
                source_start=51,
                source_end=80,
                sort_order=3,
                content_hash="boundary",
            ),
            KnowledgeSection(
                knowledge_id=knowledge.id,
                version_id=version_id,
                section_key="S0004",
                title="三十一、练习二：纠缠之心静心",
                content="# 三十一、练习二：纠缠之心静心\n下一项练习内容。",
                source_start=81,
                source_end=120,
                sort_order=4,
                content_hash="next",
            ),
        ])
        db.flush()
        db.add(
            KnowledgeChunk(
                knowledge_id=knowledge.id,
                version_id=version_id,
                section_id=detail.id,
                title=detail.title,
                content=detail.content,
                normalized_text="金色光 基础原则 自然呼吸 画面",
                keywords='["金色光","自然呼吸"]',
                synonyms="[]",
                source_start=detail.source_start,
                source_end=detail.source_end,
                sort_order=1,
                content_hash="detail-chunk",
            )
        )
        db.commit()
        for knowledge_id in range(2, 6):
            _add_published_knowledge(db, knowledge_id=knowledge_id, name=f"其他课程{knowledge_id}")

        content = KnowledgeAgentService._read_complete_section(db, detail)
        result = asyncio.run(
            KnowledgeAgentService.build_result(db, question="金色光欧姆是什么？")
        )
        title_route = next(item for item in result.tool_trace if item["tool"] == "route_knowledge_by_titles")

        assert "金色光欧姆静心" in content
        assert "保持自然呼吸" in content
        assert "不能替代医疗诊断" in content
        assert "下一项练习内容" not in content
        assert result.chunks
        assert "保持自然呼吸" in result.chunks[0].content
        assert title_route["matches"][0]["knowledgeId"] == knowledge.id


def test_parser_merges_practice_title_and_same_level_detail_headings():
    source = """# 二十九、练习一：金色光欧姆静心
# 练习定位
帮助学员稳定注意力。
# 基础原则
保持自然呼吸，不追求画面。
# 三十、金色光练习的现实边界
不能替代医疗诊断。
# 三十一、练习二：纠缠之心静心
这是下一项练习。
"""

    sections = parse_sections(source)

    assert [section.title for section in sections] == [
        "二十九、练习一：金色光欧姆静心",
        "三十一、练习二：纠缠之心静心",
    ]
    assert "基础原则" in sections[0].content
    assert "现实边界" in sections[0].content
    assert "下一项练习" not in sections[0].content
