import asyncio
from datetime import datetime
from types import SimpleNamespace

import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

import app.models  # noqa: F401
from app.database import Base
from app.models.learning import LearningArtifactRecord, SessionRetrospectiveRecord
from app.models.skill import Skill
from app.models.user import User
from app.services.runtime_observability import build_runtime_observability_report
from app.services.agent_service import AgentService
from app.services.skill_service import SkillService
from app.services.auth_service import get_password_hash
from app.agents.learning.jobs import schedule_retrospective_job, schedule_retrospective_learning_event
from app.agents.learning.bridge import build_learning_bridge_summary, route_learning_signal
from app.agents.learning.retrospector import build_session_retrospective
from app.agents.learning.store import LearningArtifactStore, SessionRetrospectiveStore
from app.agents.learning.pattern_miner import LearningPatternMiner
from app.agents.learning.session_search import SessionRetrospectiveSearch, summarize_retrospective
from app.agents.learning.signal_extractor import RetrospectiveSignalExtractor
from app.agents.learning.skill_candidate_builder import SkillCandidateBuilder
from app.agents.skills.policy import render_skill_shortlist_context
from app.agents.schemas.orchestration import (
    ExecutionDecision,
    RuntimeRequestContext,
    assess_parallel_worthiness,
)
from app.agents.schemas.skills import SkillShortlistEntry
from app.agents.skills.retriever import build_shortlisted_skill_context, shortlist_skills_for_request


def test_runtime_request_context_accepts_request_time_fields():
    context = RuntimeRequestContext(
        request_id="req-1",
        session_id="conv-1",
        user_id="user-1",
        raw_user_query="帮我安排下周计划",
        recalled_memories=["prefers concise updates"],
        recalled_retrospectives=[{"summary": "mode=collaboration", "task_type": "analysis"}],
        shortlisted_skills=["weekly-planning"],
        current_agent_role="master",
        execution_mode="direct",
        conversation_state_ref="conv-1",
    )

    assert context.session_id == "conv-1"
    assert context.raw_user_query == "帮我安排下周计划"
    assert context.shortlisted_skills == ["weekly-planning"]


def test_execution_decision_captures_reason_and_selected_roles():
    decision = ExecutionDecision(
        request_id="req-2",
        mode="collaboration",
        reason="complex_multi_role_request",
        complexity_score=0.78,
        parallel_worthiness_score=0.42,
        selected_roles=["librarian", "analyst"],
    )

    assert decision.mode == "collaboration"
    assert decision.reason == "complex_multi_role_request"
    assert decision.selected_roles == ["librarian", "analyst"]


def test_retrospective_builder_and_summary_support_runtime_context_compatibility():
    retrospective = build_session_retrospective(
        request_id="assistant-msg-1",
        session_id="conv-1",
        user_query="帮我安排下周计划，并把重点列出来",
        runtime_context={"user_id": "user-1", "recalled_retrospectives": [{"summary": "older run"}]},
        state={
            "user_id": "user-1",
            "execution_mode": "collaboration",
            "current_agent": "schedule_planner",
            "verification_status": "passed",
            "verification_summary": "all checks passed",
            "skill_shortlist": [{"skill_name": "weekly-planning"}],
            "completed_tasks": [{"task_id": "task-1", "title": "draft schedule", "status": "completed"}],
        },
    )

    summary = summarize_retrospective(retrospective)

    assert retrospective.conversation_id == "conv-1"
    assert retrospective.response_message_id == "assistant-msg-1"
    assert retrospective.used_skill_names == ["weekly-planning"]
    assert retrospective.outcome == "completed"
    assert summary["execution_mode"] == "collaboration"
    assert "skill_shortlist_hit" in summary["reusable_patterns"]


def test_build_shortlisted_skill_context_filters_entries_by_agent_scope():
    shortlist = [
        SkillShortlistEntry(
            skill_name="weekly-planning",
            scope=["schedule_planner"],
            score=0.9,
            summary="Turn weekly intent into concrete schedule blocks.",
            injection_mode="summary",
        ),
        SkillShortlistEntry(
            skill_name="forum-posting",
            scope=["executor"],
            score=0.7,
            summary="Post updates to forum threads.",
            injection_mode="metadata_only",
        ),
    ]

    rendered = build_shortlisted_skill_context(shortlist, agent_type="schedule_planner")

    assert "weekly-planning" in rendered
    assert "forum-posting" not in rendered


def test_render_skill_shortlist_context_truncates_summary_and_avoids_full_injection():
    rendered = render_skill_shortlist_context(
        [
            SkillShortlistEntry(
                skill_name="long-skill",
                score=0.9,
                summary="x" * 300,
                injection_mode="summary",
            )
        ]
    )

    assert "mode=summary" in rendered
    assert "x" * 150 not in rendered


def test_assess_parallel_worthiness_prefers_collaboration_for_multi_source_request():
    worthiness = assess_parallel_worthiness(
        "帮我对比论坛、知识库和代码里的信息，然后给我一个分析结论",
        retrospective_count=2,
        skill_count=1,
    )

    assert worthiness.score > 0
    assert worthiness.preferred_mode in {"collaboration", "parallel"}
    assert worthiness.estimated_subtasks >= 2


def test_signal_pattern_and_skill_candidate_pipeline_builds_candidates():
    retrospective = build_session_retrospective(
        request_id="assistant-msg-2",
        session_id="conv-2",
        user_query="先收集资料，再总结结论，再给我计划",
        runtime_context={"user_id": "user-2"},
        state={
            "execution_mode": "collaboration",
            "current_agent": "analyst",
            "verification_status": "passed",
            "final_response": "done",
            "skill_shortlist": [{"skill_name": "research-synthesis"}],
            "completed_tasks": [
                {"task_id": "task-1", "title": "collect", "status": "completed"},
                {"task_id": "task-2", "title": "summarize", "status": "completed"},
            ],
            "verification_evidence": [{"type": "verification"}],
        },
    )

    signals = RetrospectiveSignalExtractor().extract(retrospective)
    patterns = LearningPatternMiner().mine(signals)
    candidates = SkillCandidateBuilder().build(patterns)

    assert signals
    assert any(signal.signal_type == "workflow" for signal in signals)
    assert patterns
    assert candidates
    assert candidates[0].recommended_status == "candidate"


def test_decomposition_signal_carries_effectiveness_from_merge_context():
    retrospective = build_session_retrospective(
        request_id="assistant-msg-merge",
        session_id="conv-merge",
        user_query="先收集资料，再总结结论",
        runtime_context={"user_id": "user-2"},
        state={
            "execution_mode": "collaboration",
            "current_agent": "analyst",
            "verification_status": "failed",
            "final_response": "done",
            "completed_tasks": [
                {"task_id": "task-1", "title": "collect", "status": "completed"},
                {"task_id": "task-2", "title": "summarize", "status": "completed"},
            ],
            "scheduled_subtasks": [{"subtask_id": "task-1"}, {"subtask_id": "task-2"}],
            "merge_report": {"status": "fallback", "conflict_flags": ["failed_or_blocked_tasks:task-2"]},
            "verification_report": {"status": "failed"},
        },
    )

    signals = RetrospectiveSignalExtractor().extract(retrospective)
    decomposition_signal = next(signal for signal in signals if signal.signal_type == "decomposition")

    assert decomposition_signal.payload["merge_status"] == "fallback"
    assert decomposition_signal.payload["effectiveness_score"] == 0.25


def test_learning_bridge_routes_workflow_to_skill_and_preference_to_memory():
    signals = [
        RetrospectiveSignalExtractor().extract(
            build_session_retrospective(
                request_id="bridge-r1",
                session_id="bridge-c1",
                user_query="先查资料再总结",
                runtime_context={"user_id": "u1"},
                state={
                    "execution_mode": "collaboration",
                    "current_agent": "analyst",
                    "verification_status": "passed",
                    "final_response": "done",
                    "completed_tasks": [
                        {"task_id": "t1", "title": "collect", "status": "completed"},
                        {"task_id": "t2", "title": "summarize", "status": "completed"},
                    ],
                },
            )
        )[0]
    ]

    summary = build_learning_bridge_summary(signals)

    assert route_learning_signal(signals[0]) == "skill"
    assert summary["skill_signal_count"] == 1


def test_learning_bridge_summary_is_written_into_learning_decision_metadata():
    retrospective = build_session_retrospective(
        request_id="bridge-meta-r1",
        session_id="bridge-meta-c1",
        user_query="先查资料再总结",
        runtime_context={"user_id": "u1"},
        state={
            "execution_mode": "collaboration",
            "current_agent": "analyst",
            "verification_status": "passed",
            "final_response": "done",
            "completed_tasks": [
                {"task_id": "t1", "title": "collect", "status": "completed"},
                {"task_id": "t2", "title": "summarize", "status": "completed"},
            ],
        },
    )
    from app.agents.learning.jobs import _enrich_retrospective

    enriched = _enrich_retrospective(retrospective)

    assert enriched.learning_decision is not None
    bridge = enriched.learning_decision.metadata["bridge"]
    assert bridge["skill_signal_count"] >= 1


@pytest.mark.asyncio
async def test_schedule_retrospective_job_returns_without_waiting_for_persistence(monkeypatch):
    started = asyncio.Event()
    release = asyncio.Event()

    async def fake_persist_retrospective(**_kwargs):
        started.set()
        await release.wait()

    monkeypatch.setattr("app.agents.learning.jobs.persist_retrospective", fake_persist_retrospective)

    task = schedule_retrospective_job(
        user_id="u1",
        conversation_id="c1",
        request_message_id="m1",
        response_message_id="m2",
        query_text="hello",
        final_response="world",
        state={},
    )

    assert task is not None
    await asyncio.wait_for(started.wait(), timeout=1)
    assert task.done() is False
    release.set()
    await asyncio.wait_for(task, timeout=1)


@pytest.mark.asyncio
async def test_runtime_skill_shortlist_keeps_shadow_skills_metadata_only_and_skips_candidates(tmp_path):
    db_path = tmp_path / "skill_lifecycle.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="skill-user",
            email="skill@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Skill User",
        )
        session.add(user)
        await session.flush()

        session.add(
            Skill(
                owner_id=user.id,
                name="weekly-planning-shadow",
                description="用于安排下周计划的 shadow skill",
                instructions="把下周计划拆成具体时间块。",
                agent_type="schedule_planner",
                status="shadow",
                visibility="private",
                scope=["schedule_planner"],
            )
        )
        session.add(
            Skill(
                owner_id=user.id,
                name="weekly-planning-candidate",
                description="Candidate skill should not enter runtime shortlist",
                instructions="Candidate",
                agent_type="schedule_planner",
                status="candidate",
                visibility="private",
                scope=["schedule_planner"],
            )
        )
        await session.commit()

        shortlist = await shortlist_skills_for_request(
            session,
            user_id=user.id,
            user_query="帮我安排下周计划并拆成时间块",
            limit=5,
        )

    await engine.dispose()

    names = [item.skill_name for item in shortlist]
    assert "weekly-planning-shadow" in names
    assert "weekly-planning-candidate" not in names
    shadow_entry = next(item for item in shortlist if item.skill_name == "weekly-planning-shadow")
    assert shadow_entry.status == "shadow"
    assert shadow_entry.injection_mode == "metadata_only"


@pytest.mark.asyncio
async def test_schedule_retrospective_learning_event_persists_learning_artifacts(tmp_path, monkeypatch):
    db_path = tmp_path / "learning_artifacts.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    import app.agents.learning.jobs as jobs_module

    monkeypatch.setattr(jobs_module, "async_session", session_factory)

    async with session_factory() as session:
        user = User(
            username="learning-user",
            email="learning@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Learning User",
        )
        session.add(user)
        await session.flush()
        await session.commit()

        retrospective = build_session_retrospective(
            request_id="assistant-msg-3",
            session_id="conv-3",
            user_query="先查资料，再总结，再给计划",
            runtime_context={"user_id": user.id},
            state={
                "execution_mode": "collaboration",
                "current_agent": "analyst",
                "verification_status": "passed",
                "final_response": "done",
                "skill_shortlist": [{"skill_name": "research-synthesis"}],
                "completed_tasks": [
                    {"task_id": "task-1", "title": "collect", "status": "completed"},
                    {"task_id": "task-2", "title": "summarize", "status": "completed"},
                ],
                "verification_evidence": [{"type": "verification"}],
            },
        )

        task = schedule_retrospective_learning_event(
            user_id=user.id,
            conversation_id="conv-3",
            retrospective=retrospective,
            session_factory=session_factory,
        )
        assert task is not None
        await task

    async with session_factory() as session:
        retrospective_count = await session.scalar(
            text("SELECT COUNT(*) FROM session_retrospectives")
        )
        artifact_count = await session.scalar(
            text("SELECT COUNT(*) FROM learning_artifacts")
        )
        result = await session.execute(text("SELECT artifact_type FROM learning_artifacts"))
        artifact_types = {row[0] for row in result.fetchall()}

    await engine.dispose()

    assert retrospective_count == 1
    assert artifact_count >= 2
    assert "signal" in artifact_types
    assert "learning_decision" in artifact_types
    assert "skill_lifecycle_decision" in artifact_types


@pytest.mark.asyncio
async def test_learned_skill_candidate_promotes_to_shadow_after_repeated_candidates(tmp_path):
    db_path = tmp_path / "skill_candidate_promotion.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="candidate-user",
            email="candidate@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Candidate User",
        )
        session.add(user)
        await session.flush()

        service = SkillService(session)
        retrospective = build_session_retrospective(
            request_id="r1",
            session_id="c1",
            user_query="先查资料再总结",
            runtime_context={"user_id": user.id},
            state={
                "execution_mode": "collaboration",
                "current_agent": "analyst",
                "verification_status": "passed",
                "final_response": "done",
                "completed_tasks": [
                    {"task_id": "t1", "title": "collect", "status": "completed"},
                    {"task_id": "t2", "title": "summarize", "status": "completed"},
                ],
            },
        )
        signals = RetrospectiveSignalExtractor().extract(retrospective)
        patterns = LearningPatternMiner().mine(signals)
        candidate = SkillCandidateBuilder().build(patterns)[0]

        candidate_2 = candidate.model_copy(
            update={
                "candidate_id": "candidate-alt",
                "source_pattern_ids": ["pattern-alt-1"],
                "evidence_refs": [{"type": "analysis", "detail": "second run"}],
            }
        )

        first = await service.upsert_learned_candidate(
            user_id=user.id,
            candidate=candidate,
            primary_agent="analyst",
        )
        second = await service.upsert_learned_candidate(
            user_id=user.id,
            candidate=candidate_2,
            primary_agent="analyst",
        )
        skill = await service.get_by_name_for_user(user.id, candidate.name)

    await engine.dispose()

    assert first.new_status == "candidate"
    assert second.new_status == "shadow"
    assert skill is not None
    assert skill.status == "shadow"
    assert skill.candidate_count == 2


@pytest.mark.asyncio
async def test_shadow_skill_promotes_to_active_after_positive_feedback(tmp_path):
    db_path = tmp_path / "shadow_to_active.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="shadow-user",
            email="shadow@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Shadow User",
        )
        session.add(user)
        await session.flush()

        skill = Skill(
            owner_id=user.id,
            name="workflow-shadow-skill",
            description="shadow skill",
            instructions="shadow skill",
            agent_type="analyst",
            visibility="private",
            is_active=True,
            status="shadow",
            scope=["analyst", "learned"],
            effectiveness=0.72,
            activation_count=1,
        )
        session.add(skill)
        await session.commit()

        decision = await SkillService(session).record_activation_feedback(
            user_id=user.id,
            skill_name="workflow-shadow-skill",
            outcome_score=0.9,
        )
        refreshed = await session.get(Skill, skill.id)

    await engine.dispose()

    assert decision is not None
    assert decision.action == "promoted_to_active"
    assert refreshed is not None
    assert refreshed.status == "active"
    assert refreshed.activation_count == 2


@pytest.mark.asyncio
async def test_runtime_skill_shortlist_excludes_retired_skills(tmp_path):
    db_path = tmp_path / "retired_skill_filter.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="retired-user",
            email="retired@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Retired User",
        )
        session.add(user)
        await session.flush()

        session.add(
            Skill(
                owner_id=user.id,
                name="retired-skill",
                description="retired skill",
                instructions="retired skill",
                agent_type="analyst",
                visibility="private",
                is_active=True,
                status="retired",
                scope=["analyst", "learned"],
            )
        )
        await session.commit()

        shortlist = await shortlist_skills_for_request(
            session,
            user_id=user.id,
            user_query="帮我分析一下资料",
            limit=5,
        )

    await engine.dispose()

    assert all(item.skill_name != "retired-skill" for item in shortlist)


@pytest.mark.asyncio
async def test_activation_feedback_updates_effectiveness_score(tmp_path):
    db_path = tmp_path / "skill_effectiveness.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="effect-user",
            email="effect@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Effect User",
        )
        session.add(user)
        await session.flush()

        skill = Skill(
            owner_id=user.id,
            name="active-skill",
            description="active skill",
            instructions="active skill",
            agent_type="analyst",
            visibility="private",
            is_active=True,
            status="active",
            scope=["analyst", "learned"],
            effectiveness=0.4,
            activation_count=1,
        )
        session.add(skill)
        await session.commit()

        await SkillService(session).record_activation_feedback(
            user_id=user.id,
            skill_name="active-skill",
            outcome_score=0.8,
        )
        refreshed = await session.get(Skill, skill.id)

    await engine.dispose()

    assert refreshed is not None
    assert refreshed.effectiveness > 0.4


@pytest.mark.asyncio
async def test_duplicate_candidate_does_not_increment_promotion_count_twice(tmp_path):
    db_path = tmp_path / "duplicate_candidate.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="dup-user",
            email="dup@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Dup User",
        )
        session.add(user)
        await session.flush()

        retrospective = build_session_retrospective(
            request_id="dup-r1",
            session_id="dup-c1",
            user_query="先查资料再总结",
            runtime_context={"user_id": user.id},
            state={
                "execution_mode": "collaboration",
                "current_agent": "analyst",
                "verification_status": "passed",
                "final_response": "done",
                "completed_tasks": [
                    {"task_id": "t1", "title": "collect", "status": "completed"},
                    {"task_id": "t2", "title": "summarize", "status": "completed"},
                ],
            },
        )
        candidate = SkillCandidateBuilder().build(
            LearningPatternMiner().mine(RetrospectiveSignalExtractor().extract(retrospective))
        )[0]

        service = SkillService(session)
        await service.upsert_learned_candidate(
            user_id=user.id,
            candidate=candidate,
            primary_agent="analyst",
        )
        await service.upsert_learned_candidate(
            user_id=user.id,
            candidate=candidate,
            primary_agent="analyst",
        )
        skill = await service.get_by_name_for_user(user.id, candidate.name)

    await engine.dispose()

    assert skill is not None
    assert skill.candidate_count == 1
    assert skill.status == "candidate"


@pytest.mark.asyncio
async def test_low_effect_active_skill_degrades_to_deprecated(tmp_path):
    db_path = tmp_path / "degrade_skill.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="degrade-user",
            email="degrade@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Degrade User",
        )
        session.add(user)
        await session.flush()

        skill = Skill(
            owner_id=user.id,
            name="active-low-skill",
            description="active low skill",
            instructions="active low skill",
            agent_type="analyst",
            visibility="private",
            is_active=True,
            status="active",
            scope=["analyst"],
            effectiveness=0.2,
            activation_count=2,
        )
        session.add(skill)
        await session.commit()

        decision = await SkillService(session).record_activation_feedback(
            user_id=user.id,
            skill_name="active-low-skill",
            outcome_score=0.1,
        )
        refreshed = await session.get(Skill, skill.id)

    await engine.dispose()

    assert decision is not None
    assert decision.action == "degraded_to_deprecated"
    assert refreshed is not None
    assert refreshed.status == "deprecated"


@pytest.mark.asyncio
async def test_runtime_skill_shortlist_can_disable_learned_skill_loading(tmp_path):
    db_path = tmp_path / "disable_learned_loading.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="builtin-user",
            email="builtin@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Builtin User",
        )
        session.add(user)
        await session.flush()

        session.add(
            Skill(
                owner_id=user.id,
                name="builtin-active-skill",
                description="用于分析资料的 builtin active skill",
                instructions="帮我分析资料并总结重点",
                agent_type="analyst",
                visibility="market",
                is_builtin=True,
                is_active=True,
                status="active",
                scope=["analyst"],
            )
        )
        session.add(
            Skill(
                owner_id=user.id,
                name="learned-active-skill",
                description="用于分析资料的 learned active skill",
                instructions="帮我分析资料并总结重点",
                agent_type="analyst",
                visibility="private",
                is_active=True,
                status="active",
                scope=["analyst", "learned"],
            )
        )
        await session.commit()

        shortlist = await shortlist_skills_for_request(
            session,
            user_id=user.id,
            user_query="帮我分析一下资料",
            include_learned=False,
            limit=5,
        )

    await engine.dispose()

    names = [item.skill_name for item in shortlist]
    assert "builtin-active-skill" in names
    assert "learned-active-skill" not in names


@pytest.mark.asyncio
async def test_runtime_request_context_collects_assembly_metrics_with_small_latency(monkeypatch):
    service = AgentService(None)

    async def fake_shortlist_retrospectives(self, *, user_id, query_text, conversation_id=None, limit=3):
        await asyncio.sleep(0.01)
        return []

    async def fake_shortlist_skills(db, *, user_id, user_query, memory_context=None, retrospectives=None, include_learned=True, limit=4):
        await asyncio.sleep(0.01)
        return []

    monkeypatch.setattr(
        "app.services.agent_service.SessionRetrospectiveSearch.shortlist",
        fake_shortlist_retrospectives,
    )
    monkeypatch.setattr(
        "app.services.agent_service.shortlist_skills_for_request",
        fake_shortlist_skills,
    )

    runtime_request_context, _retros, _skills = await service._build_runtime_request_context(
        request_id="req-latency",
        user_id="u1",
        conversation=SimpleNamespace(id="c1"),
        user_query="帮我分析一下资料",
        memory_context="【用户记忆】\n- 喜欢结构化输出",
    )

    assert runtime_request_context.assembly_metrics["retrospective_ms"] >= 0
    assert runtime_request_context.assembly_metrics["skill_shortlist_ms"] >= 0
    assert runtime_request_context.assembly_metrics["total_ms"] < 500


def test_runtime_observability_report_includes_parallel_metrics_and_flags():
    report = build_runtime_observability_report(
        state={
            "execution_mode": "collaboration",
            "verification_status": "passed",
            "skill_shortlist": [{"skill_name": "weekly-planning"}],
            "retrospective_shortlist": [{"summary": "older"}],
            "task_graph": {"nodes": [{"node_id": "t1"}]},
            "scheduled_subtasks": [{"subtask_id": "t1"}],
            "task_results": [{"task_id": "t1", "status": "completed"}],
            "merge_report": {"status": "merged", "conflict_flags": [], "fallback_used": False},
        },
        feature_flags={"ENABLE_PARALLEL_TASK_GRAPH": True},
    )

    assert report["execution_mode"] == "collaboration"
    assert report["parallel_metrics"]["task_graph_node_count"] == 1
    assert report["parallel_metrics"]["completed_subtask_count"] == 1
    assert report["feature_flags"]["ENABLE_PARALLEL_TASK_GRAPH"] is True


@pytest.mark.asyncio
async def test_session_retrospective_search_supports_task_type_and_skill_filters(tmp_path):
    db_path = tmp_path / "retrospective_filters.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        store = SessionRetrospectiveStore(session)
        await store.save(
            build_session_retrospective(
                request_id="r1",
                session_id="c1",
                user_query="帮我安排下周计划",
                runtime_context={"user_id": "u1"},
                state={
                    "execution_mode": "collaboration",
                    "current_agent": "schedule_planner",
                    "verification_status": "passed",
                    "final_response": "done",
                    "skill_shortlist": [{"skill_name": "weekly-planning"}],
                    "completed_tasks": [{"task_id": "t1", "title": "plan", "status": "completed"}],
                },
            )
        )
        await store.save(
            build_session_retrospective(
                request_id="r2",
                session_id="c2",
                user_query="帮我分析风险",
                runtime_context={"user_id": "u1"},
                state={
                    "execution_mode": "collaboration",
                    "current_agent": "analyst",
                    "verification_status": "passed",
                    "final_response": "done",
                    "skill_shortlist": [{"skill_name": "risk-analysis"}],
                    "completed_tasks": [{"task_id": "t2", "title": "analysis", "status": "completed"}],
                },
            )
        )

        results = await SessionRetrospectiveSearch(session).shortlist(
            user_id="u1",
            query_text="帮我安排计划",
            task_type="planning_or_execution",
            skill_name="weekly-planning",
            limit=5,
        )

    await engine.dispose()

    assert len(results) == 1
    assert results[0].used_skill_names == ["weekly-planning"]


@pytest.mark.asyncio
async def test_learning_artifact_store_aggregates_skill_candidate_counts(tmp_path):
    db_path = tmp_path / "artifact_aggregate.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        store = LearningArtifactStore(session)
        await store.save_batch(
            user_id="u1",
            conversation_id="c1",
            retrospective_id="r1",
            artifacts=[
                {"artifact_type": "skill_candidate", "artifact_key": "workflow-a", "summary_text": "a", "payload": {}},
                {"artifact_type": "skill_candidate", "artifact_key": "workflow-a", "summary_text": "a2", "payload": {}},
                {"artifact_type": "skill_candidate", "artifact_key": "workflow-b", "summary_text": "b", "payload": {}},
            ],
        )
        counts = await store.aggregate_counts_by_key(
            user_id="u1",
            artifact_type="skill_candidate",
        )

    await engine.dispose()

    assert counts["workflow-a"] == 2
    assert counts["workflow-b"] == 1


@pytest.mark.asyncio
async def test_run_decay_review_deprecates_shadow_and_retires_deprecated_skills(tmp_path):
    db_path = tmp_path / "decay_review.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username="review-user",
            email="review@example.com",
            hashed_password=get_password_hash("secret123"),
            full_name="Review User",
        )
        session.add(user)
        await session.flush()

        session.add(
            Skill(
                owner_id=user.id,
                name="shadow-low-skill",
                description="shadow low skill",
                instructions="shadow low skill",
                agent_type="analyst",
                visibility="private",
                is_active=True,
                status="shadow",
                effectiveness=0.3,
                review_after=datetime.utcnow(),
            )
        )
        session.add(
            Skill(
                owner_id=user.id,
                name="deprecated-low-skill",
                description="deprecated low skill",
                instructions="deprecated low skill",
                agent_type="analyst",
                visibility="private",
                is_active=True,
                status="deprecated",
                effectiveness=0.1,
                review_after=datetime.utcnow(),
            )
        )
        await session.commit()

        decisions = await SkillService(session).run_decay_review(user_id=user.id)
        shadow_skill = await SkillService(session).get_by_name_for_user(user.id, "shadow-low-skill")
        deprecated_skill = await SkillService(session).get_by_name_for_user(user.id, "deprecated-low-skill")

    await engine.dispose()

    actions = {decision.skill_name: decision.action for decision in decisions}
    assert actions["shadow-low-skill"] == "degraded_to_deprecated"
    assert actions["deprecated-low-skill"] == "retired"
    assert shadow_skill is not None and shadow_skill.status == "deprecated"
    assert deprecated_skill is not None and deprecated_skill.status == "retired"
