from __future__ import annotations

import asyncio
from datetime import datetime
from unittest.mock import AsyncMock, patch

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

from app.api.admin_knowledge import delete_knowledge
from app.api.admin_knowledge_lifecycle import batch_sync, batch_update_metadata, content_search, update_metadata
from app.models import Base
from app.models.admin import Admin, Role
from app.models.knowledge import (
    Knowledge,
    KnowledgeCard,
    KnowledgeChunk,
    KnowledgeManifest,
    KnowledgePublishLog,
    KnowledgeSection,
    KnowledgeSourceSnapshot,
    KnowledgeSyncJob,
    KnowledgeVersion,
)
from app.schemas.knowledge import (
    KnowledgeBatchMetadataUpdateRequest,
    KnowledgeBatchSyncRequest,
    KnowledgeMetadataUpdateRequest,
)
from app.services.knowledge_agent_service import KnowledgeAgentService
from app.services.knowledge_pipeline_service import KnowledgePipelineService


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(role_code: str | None = None) -> Admin:
    role = Role(id=10, code=role_code, name=role_code) if role_code else None
    return Admin(id=1, username="admin", password="hash", name="管理员", status=1, role=role)


def _knowledge(db: Session, knowledge_id: int = 1) -> Knowledge:
    item = Knowledge(
        id=knowledge_id,
        name="原名称",
        feishu_space_id=f"space-{knowledge_id}",
        feishu_node_id=f"node-{knowledge_id}",
        status=1,
        knowledge_type="general",
        review_mode="manual",
        source_status="normal",
        manifest_confirmed=1,
    )
    db.add(item)
    db.flush()
    snapshot = KnowledgeSourceSnapshot(
        knowledge_id=item.id,
        source_title="源标题",
        source_content="源内容",
        content_hash=f"hash-{knowledge_id}",
        source_identifier=f"space-{knowledge_id}/node-{knowledge_id}",
    )
    db.add(snapshot)
    db.flush()
    version = KnowledgeVersion(
        knowledge_id=item.id,
        snapshot_id=snapshot.id,
        version_no=1,
        status="published",
        quality_status="passed",
        processing_rule_version="test-v1",
    )
    db.add(version)
    db.flush()
    item.current_version_id = version.id
    db.add(
        KnowledgeManifest(
            knowledge_id=item.id,
            version_id=version.id,
            purpose="原用途",
            applicable_questions="课程问题",
            inapplicable_questions="天气",
            core_topics="课程",
            content_hash=f"manifest-{knowledge_id}",
            confirmed=1,
        )
    )
    db.commit()
    return item


def test_metadata_update_preserves_source_version_and_open_state():
    with _database() as db:
        item = _knowledge(db)
        original = (item.feishu_space_id, item.feishu_node_id, item.current_version_id, item.status)
        update_metadata(
            item.id,
            KnowledgeMetadataUpdateRequest(name="新后台名称", knowledgeType="course"),
            db,
            _admin(),
        )
        db.refresh(item)
        assert (item.name, item.knowledge_type, item.review_mode) == ("新后台名称", "course", "automatic")
        assert (item.feishu_space_id, item.feishu_node_id, item.current_version_id, item.status) == original
        assert KnowledgeAgentService.get_knowledge_catalog(db)[0]["name"] == "新后台名称"


def test_metadata_update_requires_super_admin():
    with _database() as db:
        item = _knowledge(db)
        with pytest.raises(HTTPException) as exc:
            update_metadata(
                item.id,
                KnowledgeMetadataUpdateRequest(name="新名称", knowledgeType="qa"),
                db,
                _admin("operator"),
            )
        assert exc.value.status_code == 403


def test_batch_metadata_update_reports_missing_items():
    with _database() as db:
        item = _knowledge(db)
        response = batch_update_metadata(
            KnowledgeBatchMetadataUpdateRequest(knowledgeIds=[item.id, 999], knowledgeType="qa"),
            db,
            _admin(),
        )
        assert response["data"]["success"] == 1
        assert response["data"]["failed"] == 1
        assert db.scalar(select(Knowledge).where(Knowledge.id == item.id)).knowledge_type == "qa"


def test_active_knowledge_can_be_deleted_without_manual_archive_steps():
    with _database() as db:
        item = _knowledge(db)
        knowledge_id = item.id

        response = delete_knowledge(knowledge_id, db, _admin())

        assert response["code"] == 0
        assert db.get(Knowledge, knowledge_id) is None
        assert db.scalars(select(KnowledgeVersion).where(KnowledgeVersion.knowledge_id == knowledge_id)).all() == []
        assert db.scalars(select(KnowledgeManifest).where(KnowledgeManifest.knowledge_id == knowledge_id)).all() == []
        assert db.scalars(select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == knowledge_id)).all() == []


def test_batch_sync_continues_after_an_item_fails_and_reports_each_result():
    with _database() as db:
        first = _knowledge(db, 1)
        second = _knowledge(db, 2)
        successful_job = KnowledgeSyncJob(
            id=101,
            knowledge_id=first.id,
            mode="replace",
            status="published",
            stage="completed",
            progress=100,
            requested_by=1,
            created_at=datetime.now(),
        )

        async def synchronize(_db, knowledge, *, admin_id):
            assert admin_id == 1
            if knowledge.id == second.id:
                raise RuntimeError("飞书接口暂时不可用")
            return successful_job

        with patch.object(KnowledgePipelineService, "synchronize", side_effect=synchronize) as mocked:
            response = asyncio.run(
                batch_sync(
                    KnowledgeBatchSyncRequest(knowledgeIds=[first.id, first.id, second.id, 999]),
                    db,
                    _admin(),
                )
            )

        result = response["data"]
        assert result["total"] == 3
        assert result["success"] == 1
        assert result["failed"] == 2
        assert [item["knowledgeId"] for item in result["results"]] == [first.id, second.id, 999]
        assert result["results"][0]["status"] == "published"
        assert result["results"][1]["error"] == "飞书接口暂时不可用"
        assert result["results"][2]["error"] == "知识库不存在"
        assert mocked.call_count == 2


def test_sync_automatically_replaces_current_version_and_removes_history():
    source_content = (
        "# 一、课程说明\n这是更新后的课程内容，同步成功后应当立即生效。\n\n"
        "# 二、练习一：静水流深静心\n选择舒适安全的姿势，觉察呼吸和身体，不适时立即停止。"
    )
    with _database() as db:
        item = _knowledge(db)
        previous_version_id = item.current_version_id
        loader = AsyncMock(return_value=[("新源标题", source_content, None)])

        with patch(
            "app.services.knowledge_pipeline_service.AsyncFeishuKnowledgeService._load_documents",
            loader,
        ):
            job = asyncio.run(
                KnowledgePipelineService.synchronize(
                    db,
                    item,
                    admin_id=1,
                )
            )

        db.refresh(item)
        current = db.get(KnowledgeVersion, item.current_version_id)
        versions = db.scalars(
            select(KnowledgeVersion).where(KnowledgeVersion.knowledge_id == item.id)
        ).all()
        snapshots = db.scalars(
            select(KnowledgeSourceSnapshot).where(KnowledgeSourceSnapshot.knowledge_id == item.id)
        ).all()
        manifest = db.scalar(select(KnowledgeManifest).where(KnowledgeManifest.version_id == current.id))
        cards = db.scalars(select(KnowledgeCard).where(KnowledgeCard.version_id == current.id)).all()
        publish_log = db.scalar(
            select(KnowledgePublishLog).where(KnowledgePublishLog.to_version_id == current.id)
        )

        assert job.status == "published"
        assert job.mode == "replace"
        assert current.status == "published"
        assert current.quality_status == "passed"
        assert db.get(KnowledgeVersion, previous_version_id) is None
        assert [version.id for version in versions] == [current.id]
        assert [snapshot.id for snapshot in snapshots] == [current.snapshot_id]
        assert item.pending_version_id is None
        assert item.status == 1
        assert item.migration_status == "completed"
        assert manifest.confirmed == 1
        assert cards and all(card.review_status == "approved" for card in cards)
        assert publish_log.action == "sync_publish"


def test_sync_validation_failure_keeps_current_version_active():
    with _database() as db:
        item = _knowledge(db)
        previous_version_id = item.current_version_id
        loader = AsyncMock(return_value=[("无效源内容", "内容过短", None)])

        with patch(
            "app.services.knowledge_pipeline_service.AsyncFeishuKnowledgeService._load_documents",
            loader,
        ):
            with pytest.raises(ValueError, match="质检未通过"):
                asyncio.run(
                    KnowledgePipelineService.synchronize(
                        db,
                        item,
                        admin_id=1,
                    )
                )

        db.refresh(item)
        current = db.get(KnowledgeVersion, previous_version_id)
        versions = db.scalars(
            select(KnowledgeVersion).where(KnowledgeVersion.knowledge_id == item.id)
        ).all()
        assert item.current_version_id == previous_version_id
        assert current.status == "published"
        assert len(versions) == 1


def test_content_search_finds_current_sections_cards_and_chunks():
    with _database() as db:
        item = _knowledge(db)
        version_id = item.current_version_id
        section = KnowledgeSection(
            knowledge_id=item.id,
            version_id=version_id,
            section_key="S0098",
            title="三十、练习一：静水流深静心",
            content="选择舒适安全的姿势，觉察呼吸和身体，不适时立即停止。",
            source_start=0,
            source_end=40,
            sort_order=1,
            content_hash="section-hash",
        )
        db.add(section)
        db.flush()
        db.add_all([
            KnowledgeChunk(
                knowledge_id=item.id,
                version_id=version_id,
                section_id=section.id,
                title=section.title,
                content="静水流深静心需要选择舒适安全的姿势。",
                normalized_text="静水流深静心 选择舒适安全的姿势",
                keywords='["静水流深静心"]',
                source_start=0,
                source_end=20,
                sort_order=1,
                content_hash="chunk-hash",
            ),
            KnowledgeCard(
                knowledge_id=item.id,
                version_id=version_id,
                section_id=section.id,
                title=section.title,
                summary="静水流深静心的摘要",
                core_conclusion="回到身体觉察",
                applicable_questions="静水流深静心怎么做",
                inapplicable_questions="天气",
                keywords='["静水流深静心"]',
                risk_level="normal",
                source_range="0-40",
                content_hash="card-hash",
                generation_rule_version="test-v1",
                review_status="approved",
            ),
        ])
        db.commit()

        response = content_search("静水流深", includeClosed=True, page=1, pageSize=10, db=db, current_admin=_admin())

        data = response["data"]
        assert data["total"] == 1
        assert len(data["items"]) == 1
        assert data["items"][0]["itemType"] == "section"
        assert data["items"][0]["itemTypes"] == ["section", "card", "chunk"]
        assert data["items"][0]["sectionKey"] == "S0098"
        assert data["items"][0]["hasChunks"] is True
        assert data["items"][0]["canAgentUse"] is True
        assert "静水流深" in data["items"][0]["snippet"]


def test_content_search_can_exclude_closed_knowledge():
    with _database() as db:
        item = _knowledge(db)
        item.status = 0
        db.add(item)
        db.add(KnowledgeSection(
            knowledge_id=item.id,
            version_id=item.current_version_id,
            section_key="S0001",
            title="合一作业",
            content="合一作业内容",
            source_start=0,
            source_end=10,
            sort_order=1,
            content_hash="closed-section",
        ))
        db.commit()

        included = content_search("合一", includeClosed=True, page=1, pageSize=10, db=db, current_admin=_admin())
        excluded = content_search("合一", includeClosed=False, page=1, pageSize=10, db=db, current_admin=_admin())

        assert included["data"]["total"] == 1
        assert included["data"]["items"][0]["canAgentUse"] is False
        assert excluded["data"]["total"] == 0


def test_content_search_highlight_terms_keep_full_keyword():
    with _database() as db:
        item = _knowledge(db)
        db.add(KnowledgeSection(
            knowledge_id=item.id,
            version_id=item.current_version_id,
            section_key="S0002",
            title="能力心密钥",
            content="能力心钥用于说明能力心的核心方向。",
            source_start=0,
            source_end=10,
            sort_order=1,
            content_hash="highlight-section",
        ))
        db.commit()

        response = content_search("心钥", includeClosed=True, page=1, pageSize=10, db=db, current_admin=_admin())

        assert response["data"]["total"] == 1
        assert response["data"]["items"][0]["highlightTerms"][0] == "心钥"
