"""#11 Bootstrap 四段式拼装单元测试（v2.1）

覆盖：
- T1: build(task, role) 4 段结构
- T2: token 估算 + 预算告警
- T3: 缺失组件降级
- T4: build_for_task 便捷方法
- T5: _read_skill fallback
- T6: ROLE_SKILL_MAP 覆盖
"""

import pytest

pytestmark = pytest.mark.unit
from unittest.mock import patch
from pathlib import Path

from src.daemon.bootstrap import BootstrapBuilder, estimate_tokens


@pytest.fixture
def builder():
    return BootstrapBuilder(max_tokens=4096)


# ---------------------------------------------------------------------------
# T1: build(task, role) 4 段结构
# ---------------------------------------------------------------------------

class TestFourSectionBuild:
    def test_basic_executor_build(self, builder):
        b = builder.build(
            task={"task_id": "t1", "title": "Write tests", "description": "Write unit tests",
                  "must_haves": "100% coverage", "status": "claimed"},
            role="executor",
        )
        # 段 1: 任务上下文
        assert "Write tests" in b
        assert "t1" in b
        assert "100% coverage" in b
        # 段 4: 硬约束
        assert "review" in b
        assert "handoff" in b

    def test_basic_reviewer_build(self, builder):
        b = builder.build(
            task={"task_id": "t2", "title": "Review PR"},
            role="reviewer",
        )
        assert "Review PR" in b
        # 段 4: reviewer 硬约束
        assert "pass/fail" in b or "pass" in b

    def test_planner_build(self, builder):
        b = builder.build(
            task={"task_id": "t3", "title": "Plan sprint"},
            role="planner",
        )
        assert "Plan sprint" in b

    def test_depends_on_outputs_injected(self, builder):
        b = builder.build(
            task={
                "title": "T",
                "depends_on_outputs": [
                    {"task_id": "t0", "summary": "Data downloaded"},
                ],
            },
            role="executor",
        )
        assert "前序产出" in b
        assert "Data downloaded" in b

    def test_no_depends_on_omitted(self, builder):
        b = builder.build(
            task={"title": "T"},
            role="executor",
        )
        assert "前序产出" not in b


# ---------------------------------------------------------------------------
# T2: token 估算 + 预算告警
# ---------------------------------------------------------------------------

class TestTokenEstimation:
    def test_estimate_tokens_basic(self):
        assert estimate_tokens("hello") > 0

    def test_estimate_tokens_long_text(self):
        text = "a" * 1000
        tokens = estimate_tokens(text)
        assert 200 < tokens < 400


# ---------------------------------------------------------------------------
# T3: 缺失组件降级
# ---------------------------------------------------------------------------

class TestGracefulDegradation:
    def test_empty_task(self, builder):
        b = builder.build(task={}, role="executor")
        assert b  # 不为空
        assert "硬约束" in b

    def test_partial_task(self, builder):
        b = builder.build(task={"title": "Only title"}, role="executor")
        assert "Only title" in b


# ---------------------------------------------------------------------------
# T4: build_for_task 便捷方法
# ---------------------------------------------------------------------------

class TestBuildForTask:
    def test_build_for_task_object(self, builder):
        """用 mock task 对象测试 build_for_task"""
        class MockTask:
            id = "t1"
            title = "Build Feature"
            description = "Implement X with tests"
            must_haves = "Unit tests, Documentation"
            status = "claimed"
        task = MockTask()
        b = builder.build_for_task(task, role="executor")
        assert "Build Feature" in b
        assert "Implement X" in b

    def test_build_for_task_ignores_kwargs(self, builder):
        """build_for_task 忽略旧参数"""
        class MockTask:
            id = "t1"
            title = "T"
            description = ""
            must_haves = ""
            status = ""
        task = MockTask()
        # 旧参数 project_config/experiences 不应报错
        b = builder.build_for_task(
            task, role="executor",
            project_config={"name": "Old"},
            experiences=[{"x": 1}],
        )
        assert "T" in b
        # 不应出现旧参数内容
        assert "Old" not in b


# ---------------------------------------------------------------------------
# T5: _read_skill fallback
# ---------------------------------------------------------------------------

class TestReadSkillFallback:
    def test_missing_skill_file_returns_empty(self, builder):
        """Skill 文件不存在时返回空字符串，不抛异常"""
        result = builder._read_skill("nonexistent-skill-xyz")
        assert result == ""

    def test_existing_skill_file_read(self, builder):
        """能读取实际存在的 Skill 文件"""
        # blackboard-executor 应该存在（P1 已创建）
        result = builder._read_skill("blackboard-executor")
        assert "执行" in result or "executor" in result.lower()


# ---------------------------------------------------------------------------
# T6: ROLE_SKILL_MAP 覆盖
# ---------------------------------------------------------------------------

class TestRoleSkillMap:
    def test_all_roles_mapped(self):
        assert set(BootstrapBuilder.ROLE_SKILL_MAP.keys()) == {
            "executor", "reviewer", "reviewer-simayi",
            "reviewer-pangtong", "planner", "claim",
        }

    def test_unknown_role_warns(self, builder):
        """未映射的 role 输出 warning"""
        import logging
        with patch("src.daemon.bootstrap.logger") as mock_logger:
            builder.build(task={"title": "T"}, role="unknown_role")
            mock_logger.warning.assert_called_with(
                "No skill mapping for role: %s", "unknown_role"
            )

    def test_discussion_role_no_warning(self, builder, caplog):
        """discussion 角色不应触发 warning"""
        import logging
        with caplog.at_level(logging.WARNING, logger="moziplus-v2.bootstrap"):
            builder.build(task={"title": "T"}, role="discussion")
        assert "No skill mapping" not in caplog.text
