from __future__ import annotations

import json
import re
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple

from app.services.testing_pipeline.base import TestingTool, ToolExecutionResult
from app.services.testing_pipeline.rules import (
    DECOMPOSE_FORCE_RULES,
    EXPECTED_RESULT_PLACEHOLDER_MAP,
    GENERIC_DECOMPOSITION_RULES,
    REQUIREMENT_RULES,
    REQUIREMENT_TYPES,
    TYPE_SIGNAL_RULES,
)


def _clean_text(value: str) -> str:
    return " ".join((value or "").replace("\n", " ").split())


def _truncate_text(value: str, max_len: int = 2000) -> str:
    text = _clean_text(value)
    if len(text) <= max_len:
        return text
    return f"{text[:max_len]}..."


def _safe_int(value: Any, default: int, low: int, high: int) -> int:
    try:
        parsed = int(value)
    except Exception:
        parsed = default
    return max(low, min(parsed, high))


def _strip_instruction_prefix(value: str) -> str:
    text = _clean_text(value)
    if not text:
        return text

    lowered = text.lower()
    if lowered.startswith("/testing"):
        text = _clean_text(text[len("/testing") :])

    prefixes = [
        "为以下需求生成测试用例",
        "根据以下需求生成测试用例",
        "请根据以下需求生成测试用例",
        "请根据需求生成测试用例",
        "请生成测试用例",
        "生成测试用例",
    ]
    for prefix in prefixes:
        if text.startswith(prefix):
            for sep in ("：", ":"):
                idx = text.find(sep)
                if idx != -1:
                    text = _clean_text(text[idx + 1 :])
                    break
            else:
                text = _clean_text(text[len(prefix) :])
            break

    pattern = re.compile(r"^(请)?(根据|按|基于).{0,40}(需求|场景).{0,30}(生成|输出).{0,20}(测试项|测试用例)[：:]")
    matched = pattern.match(text)
    if matched:
        text = _clean_text(text[matched.end() :])

    return text


def _extract_focus_points(value: str, max_points: int = 6) -> List[str]:
    text = _strip_instruction_prefix(value)
    if not text:
        return []

    parts = [_clean_text(part) for part in re.split(r"[，,。；;]", text)]
    parts = [part for part in parts if part]

    ignored_tokens = ["生成测试用例", "测试项分解", "测试用例生成", "以下需求"]
    filtered = [
        part
        for part in parts
        if len(part) >= 4 and not any(token in part for token in ignored_tokens)
    ]
    if not filtered:
        filtered = parts

    priority_keywords = [
        "启停",
        "开启",
        "关闭",
        "远程控制",
        "保护",
        "联动",
        "状态",
        "故障",
        "恢复",
        "切换",
        "告警",
        "模式",
        "边界",
        "时序",
    ]
    priority = [part for part in filtered if any(keyword in part for keyword in priority_keywords)]
    candidates = priority if priority else filtered

    unique: List[str] = []
    for part in candidates:
        if part not in unique:
            unique.append(part)

    return unique[:max_points]


def _build_type_scores(text: str) -> Dict[str, int]:
    scores: Dict[str, int] = {}
    lowered = text.lower()

    for req_type, rule in REQUIREMENT_RULES.items():
        score = 0
        if req_type in text:
            score += 5
        for keyword in rule.get("keywords", []):
            if keyword.lower() in lowered:
                score += 2
        scores[req_type] = score

    return scores


def _top_candidates(scores: Dict[str, int], top_n: int = 3) -> List[str]:
    sorted_pairs = sorted(scores.items(), key=lambda pair: pair[1], reverse=True)
    non_zero = [name for name, score in sorted_pairs if score > 0]
    if non_zero:
        return non_zero[:top_n]
    return ["功能测试", "边界测试", "性能测试"][:top_n]


def _message_to_text(value: Any) -> str:
    content = getattr(value, "content", value)
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        chunks: List[str] = []
        for item in content:
            if isinstance(item, str):
                chunks.append(item)
            elif isinstance(item, dict):
                text = item.get("text")
                if isinstance(text, str):
                    chunks.append(text)
            else:
                chunks.append(str(item))
        return "".join(chunks)
    return str(content)


def _extract_json_object(value: str) -> Optional[Dict[str, Any]]:
    text = (value or "").strip()
    if not text:
        return None

    if text.startswith("```"):
        text = re.sub(r"^```(?:json)?", "", text, flags=re.IGNORECASE).strip()
        if text.endswith("```"):
            text = text[:-3].strip()

    try:
        data = json.loads(text)
        if isinstance(data, dict):
            return data
    except Exception:
        pass

    start = text.find("{")
    if start == -1:
        return None

    depth = 0
    for idx in range(start, len(text)):
        ch = text[idx]
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                fragment = text[start : idx + 1]
                try:
                    data = json.loads(fragment)
                    if isinstance(data, dict):
                        return data
                except Exception:
                    return None
    return None


def _invoke_llm_json(context: Dict[str, Any], prompt: str) -> Optional[Dict[str, Any]]:
    model = context.get("llm_model")
    if model is None or not context.get("use_model_generation"):
        return None

    budget = context.get("llm_call_budget")
    if isinstance(budget, int):
        if budget <= 0:
            return None
        context["llm_call_budget"] = budget - 1

    try:
        response = model.invoke(prompt)
        text = _message_to_text(response)
        return _extract_json_object(text)
    except Exception:
        return None


def _invoke_llm_text(context: Dict[str, Any], prompt: str) -> str:
    model = context.get("llm_model")
    if model is None or not context.get("use_model_generation"):
        return ""

    budget = context.get("llm_call_budget")
    if isinstance(budget, int):
        if budget <= 0:
            return ""
        context["llm_call_budget"] = budget - 1

    try:
        response = model.invoke(prompt)
        return _clean_text(_message_to_text(response))
    except Exception:
        return ""


def _normalize_item_entry(item: Any) -> Optional[Dict[str, Any]]:
    if isinstance(item, str):
        content = _clean_text(item)
        if not content:
            return None
        return {"content": content, "coverage_tags": []}

    if isinstance(item, dict):
        content = _clean_text(str(item.get("content", "")))
        if not content:
            return None
        tags = item.get("coverage_tags") or item.get("covered_points") or []
        if not isinstance(tags, list):
            tags = [str(tags)]
        tags = [_clean_text(str(tag)) for tag in tags if _clean_text(str(tag))]
        return {"content": content, "coverage_tags": tags}

    return None


def _dedupe_items(items: List[Dict[str, Any]], max_items: int) -> List[Dict[str, Any]]:
    merged: Dict[str, Dict[str, Any]] = {}
    for item in items:
        content = _clean_text(item.get("content", ""))
        if not content:
            continue
        existing = merged.get(content)
        if existing is None:
            merged[content] = {
                "content": content,
                "coverage_tags": list(item.get("coverage_tags") or []),
            }
        else:
            existing_tags = set(existing.get("coverage_tags") or [])
            for tag in item.get("coverage_tags") or []:
                if tag and tag not in existing_tags:
                    existing_tags.add(tag)
            existing["coverage_tags"] = list(existing_tags)

    deduped = list(merged.values())
    return deduped[:max_items]


def _pick_expected_result_placeholder(content: str, abnormal: bool) -> str:
    text = content or ""

    if abnormal or any(token in text for token in ["非法", "异常", "错误", "拒绝", "越界", "失败"]):
        return "{{error_message}}"
    if any(token in text for token in ["状态", "切换", "转换", "恢复"]):
        return "{{state_change}}"
    if any(token in text for token in ["数据库", "存储", "落库", "持久化"]):
        return "{{data_persistence}}"
    if any(token in text for token in ["界面", "UI", "页面", "按钮", "提示"]):
        return "{{ui_display}}"
    return "{{return_value}}"


class IdentifyRequirementTypeTool(TestingTool):
    name = "identify-requirement-type"

    def execute(self, context: Dict[str, Any]) -> ToolExecutionResult:
        raw_text = _clean_text(context.get("user_requirement_text", ""))
        text = _strip_instruction_prefix(raw_text)
        if not text:
            text = raw_text

        max_focus_points = _safe_int(context.get("max_focus_points"), 6, 3, 12)
        provided_type = _clean_text(context.get("requirement_type_input", ""))
        focus_points = _extract_focus_points(text, max_points=max_focus_points)
        fallback_used = False

        if provided_type in REQUIREMENT_TYPES:
            result = {
                "requirement_type": provided_type,
                "reason": "用户已显式指定需求类型，系统按指定类型执行。",
                "candidates": [],
                "scores": {},
                "secondary_types": [],
            }
        else:
            scores = _build_type_scores(text)
            sorted_pairs = sorted(scores.items(), key=lambda pair: pair[1], reverse=True)
            best_type, best_score = sorted_pairs[0]
            secondary = [name for name, score in sorted_pairs[1:4] if score > 0]

            if best_score <= 0:
                fallback_used = True
                candidates = _top_candidates(scores)
                result = {
                    "requirement_type": "未知类型",
                    "reason": "未命中明确分类规则，已回退到未知类型并提供最接近候选。",
                    "candidates": candidates,
                    "scores": scores,
                    "secondary_types": [],
                }
            else:
                signal = TYPE_SIGNAL_RULES.get(best_type, "")
                result = {
                    "requirement_type": best_type,
                    "reason": f"命中{best_type}识别信号。{signal}",
                    "candidates": [],
                    "scores": scores,
                    "secondary_types": secondary,
                }

        context["requirement_type_result"] = result
        context["normalized_requirement_text"] = text
        context["requirement_focus_points"] = focus_points
        context["knowledge_used"] = bool(context.get("knowledge_context"))

        return ToolExecutionResult(
            context=context,
            output_summary=(
                f"type={result['requirement_type']}; candidates={len(result['candidates'])}; "
                f"secondary_types={len(result.get('secondary_types', []))}; focus_points={len(focus_points)}"
            ),
            fallback_used=fallback_used,
        )


class DecomposeTestItemsTool(TestingTool):
    name = "decompose-test-items"

    @staticmethod
    def _seed_items(
        req_type: str,
        req_text: str,
        focus_points: List[str],
        max_items: int,
    ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
        if req_type in REQUIREMENT_RULES:
            source_rules = REQUIREMENT_RULES[req_type]
            normal_templates = list(source_rules.get("normal", []))
            abnormal_templates = list(source_rules.get("abnormal", []))
        else:
            normal_templates = list(GENERIC_DECOMPOSITION_RULES["normal"])
            abnormal_templates = list(GENERIC_DECOMPOSITION_RULES["abnormal"])

        normal: List[Dict[str, Any]] = []
        abnormal: List[Dict[str, Any]] = []

        for template in normal_templates:
            normal.append({"content": template, "coverage_tags": [req_type]})
        for template in abnormal_templates:
            abnormal.append({"content": template, "coverage_tags": [req_type]})

        for point in focus_points:
            normal.extend(
                [
                    {
                        "content": f"验证{point}在标准作业流程下稳定执行且结果符合业务约束。",
                        "coverage_tags": [point, "正常流程"],
                    },
                    {
                        "content": f"验证{point}与相关联动控制、状态同步和回执反馈的一致性。",
                        "coverage_tags": [point, "联动一致性"],
                    },
                ]
            )
            abnormal.extend(
                [
                    {
                        "content": f"验证{point}在非法输入、错误指令或权限异常时的保护与拒绝机制。",
                        "coverage_tags": [point, "异常输入"],
                    },
                    {
                        "content": f"验证{point}在边界条件、时序冲突或设备故障下的告警和恢复行为。",
                        "coverage_tags": [point, "边界异常"],
                    },
                ]
            )

        if any(token in req_text for token in ["手册", "操作手册", "用户手册", "作业指导"]):
            normal.append(
                {
                    "content": "验证需求说明未显式给出但在用户手册或操作手册体现的功能流程。",
                    "coverage_tags": ["手册功能"],
                }
            )

        return _dedupe_items(normal, max_items), _dedupe_items(abnormal, max_items)

    @staticmethod
    def _generate_by_llm(context: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
        req_result = context.get("requirement_type_result", {})
        req_type = req_result.get("requirement_type", "未知类型")
        req_text = context.get("normalized_requirement_text", "")
        focus_points = context.get("requirement_focus_points", [])
        max_items = _safe_int(context.get("max_items_per_group"), 12, 4, 30)
        knowledge_context = _truncate_text(context.get("knowledge_context", ""), max_len=2500)

        prompt = f"""
你是资深测试分析师。请根据需求、分解规则和知识库片段，生成尽可能覆盖要点的测试项。

需求文本：{req_text}
需求类型：{req_type}
需求要点：{focus_points}
知识库片段：{knowledge_context or '无'}

分解约束：
1. 正常测试与异常测试必须分组输出。
2. 每条测试项必须可执行、可验证，避免模板化空话。
3. 尽可能覆盖全部需求要点；每组建议输出6-{max_items}条。
4. 优先生成与需求对象/控制逻辑/异常处理/边界条件强相关的测试项。

请仅输出 JSON 对象，结构如下：
{{
  "normal_test_items": [
    {{"content": "...", "coverage_tags": ["..."]}}
  ],
  "abnormal_test_items": [
    {{"content": "...", "coverage_tags": ["..."]}}
  ]
}}
""".strip()

        data = _invoke_llm_json(context, prompt)
        if not data:
            return [], []

        normal_raw = data.get("normal_test_items", [])
        abnormal_raw = data.get("abnormal_test_items", [])

        normal: List[Dict[str, Any]] = []
        abnormal: List[Dict[str, Any]] = []

        for item in normal_raw if isinstance(normal_raw, list) else []:
            normalized = _normalize_item_entry(item)
            if normalized:
                normal.append(normalized)

        for item in abnormal_raw if isinstance(abnormal_raw, list) else []:
            normalized = _normalize_item_entry(item)
            if normalized:
                abnormal.append(normalized)

        return _dedupe_items(normal, max_items), _dedupe_items(abnormal, max_items)

    def execute(self, context: Dict[str, Any]) -> ToolExecutionResult:
        req_result = context.get("requirement_type_result", {})
        req_type = req_result.get("requirement_type", "未知类型")
        req_text = context.get("normalized_requirement_text") or _strip_instruction_prefix(
            context.get("user_requirement_text", "")
        )
        focus_points = context.get("requirement_focus_points", [])
        max_items = _safe_int(context.get("max_items_per_group"), 12, 4, 30)

        seeded_normal, seeded_abnormal = self._seed_items(req_type, req_text, focus_points, max_items)
        llm_normal, llm_abnormal = self._generate_by_llm(context)

        merged_normal = _dedupe_items(llm_normal + seeded_normal, max_items)
        merged_abnormal = _dedupe_items(llm_abnormal + seeded_abnormal, max_items)

        fallback_used = not bool(llm_normal or llm_abnormal)

        normal_items: List[Dict[str, Any]] = []
        abnormal_items: List[Dict[str, Any]] = []

        for idx, item in enumerate(merged_normal, start=1):
            normal_items.append(
                {
                    "id": f"N{idx}",
                    "content": item["content"],
                    "coverage_tags": item.get("coverage_tags", []),
                }
            )

        for idx, item in enumerate(merged_abnormal, start=1):
            abnormal_items.append(
                {
                    "id": f"E{idx}",
                    "content": item["content"],
                    "coverage_tags": item.get("coverage_tags", []),
                }
            )

        context["test_items"] = {
            "normal": normal_items,
            "abnormal": abnormal_items,
        }
        context["decompose_force_rules"] = DECOMPOSE_FORCE_RULES

        return ToolExecutionResult(
            context=context,
            output_summary=(
                f"normal_items={len(normal_items)}; abnormal_items={len(abnormal_items)}; "
                f"llm_items={len(llm_normal) + len(llm_abnormal)}"
            ),
            fallback_used=fallback_used,
        )


class GenerateTestCasesTool(TestingTool):
    name = "generate-test-cases"

    @staticmethod
    def _build_fallback_steps(item_content: str, abnormal: bool, variant: str) -> List[str]:
        if abnormal:
            return [
                "确认测试前置环境、设备状态与日志采集开关已准备就绪。",
                f"准备异常场景“{variant}”所需的输入数据、操作账号和触发条件。",
                f"在目标对象执行异常触发操作，重点验证：{item_content}",
                "持续观察系统返回码、错误文案、告警信息与日志链路完整性。",
                "检查保护机制是否生效，包括拒绝策略、回滚行为和状态一致性。",
                "记录证据并复位环境，确认异常处理后系统可恢复到稳定状态。",
            ]

        return [
            "确认测试环境、设备连接状态和前置业务数据均已初始化。",
            f"准备“{variant}”所需输入参数、操作路径和判定阈值。",
            f"在目标对象执行业务控制流程，重点验证：{item_content}",
            "校验关键返回值、状态变化、控制回执及界面或接口反馈结果。",
            "检查联动模块、日志记录和数据落库是否满足一致性要求。",
            "沉淀测试证据并恢复环境，确保后续用例可重复执行。",
        ]

    def _generate_cases_by_llm(
        self,
        context: Dict[str, Any],
        item: Dict[str, Any],
        abnormal: bool,
        cases_per_item: int,
    ) -> List[Dict[str, Any]]:
        req_text = context.get("normalized_requirement_text", "")
        knowledge_context = _truncate_text(context.get("knowledge_context", ""), max_len=1800)

        prompt = f"""
你是资深测试工程师。请围绕给定测试项生成详细测试用例。

需求：{req_text}
测试项：{item.get('content', '')}
测试类型：{'异常测试' if abnormal else '正常测试'}
知识库片段：{knowledge_context or '无'}

要求：
1. 生成 {cases_per_item}-{max(cases_per_item + 1, cases_per_item)} 条测试用例。
2. 每条用例包含 test_content 与 operation_steps。
3. operation_steps 必须详细，至少5步，包含前置、执行、观察、校验与证据留存。
4. 内容必须围绕当前测试项，不要输出空洞模板。

仅输出 JSON：
{{
  "test_cases": [
    {{
      "title": "...",
      "test_content": "...",
      "operation_steps": ["...", "..."]
    }}
  ]
}}
""".strip()

        data = _invoke_llm_json(context, prompt)
        if not data:
            return []

        raw_cases = data.get("test_cases", [])
        if not isinstance(raw_cases, list):
            return []

        normalized_cases: List[Dict[str, Any]] = []
        for case in raw_cases:
            if not isinstance(case, dict):
                continue
            test_content = _clean_text(str(case.get("test_content", "")))
            if not test_content:
                continue
            steps = case.get("operation_steps", [])
            if not isinstance(steps, list):
                continue
            cleaned_steps = [_clean_text(str(step)) for step in steps if _clean_text(str(step))]
            if len(cleaned_steps) < 5:
                continue
            normalized_cases.append(
                {
                    "title": _clean_text(str(case.get("title", ""))),
                    "test_content": test_content,
                    "operation_steps": cleaned_steps,
                }
            )

        return normalized_cases[: max(1, cases_per_item)]

    def execute(self, context: Dict[str, Any]) -> ToolExecutionResult:
        test_items = context.get("test_items", {})
        cases_per_item = _safe_int(context.get("cases_per_item"), 2, 1, 5)

        normal_cases: List[Dict[str, Any]] = []
        abnormal_cases: List[Dict[str, Any]] = []
        llm_case_count = 0

        for item in test_items.get("normal", []):
            generated = self._generate_cases_by_llm(context, item, abnormal=False, cases_per_item=cases_per_item)
            if not generated:
                generated = [
                    {
                        "title": "标准流程验证",
                        "test_content": f"验证{item['content']}",
                        "operation_steps": self._build_fallback_steps(item["content"], False, "标准流程"),
                    },
                    {
                        "title": "边界与联动验证",
                        "test_content": f"验证{item['content']}在边界条件和联动场景下的稳定性",
                        "operation_steps": self._build_fallback_steps(item["content"], False, "边界与联动"),
                    },
                ][:cases_per_item]
            else:
                llm_case_count += len(generated)

            for idx, case in enumerate(generated, start=1):
                merged_content = _clean_text(case.get("test_content", item["content"]))
                placeholder = _pick_expected_result_placeholder(merged_content, abnormal=False)
                normal_cases.append(
                    {
                        "id": f"{item['id']}-C{idx}",
                        "item_id": item["id"],
                        "title": _clean_text(case.get("title", "")),
                        "operation_steps": case.get("operation_steps", []),
                        "test_content": merged_content,
                        "expected_result_placeholder": placeholder,
                    }
                )

        for item in test_items.get("abnormal", []):
            generated = self._generate_cases_by_llm(context, item, abnormal=True, cases_per_item=cases_per_item)
            if not generated:
                generated = [
                    {
                        "title": "非法输入与权限异常验证",
                        "test_content": f"验证{item['content']}在非法输入与权限异常下的处理表现",
                        "operation_steps": self._build_fallback_steps(item["content"], True, "非法输入与权限异常"),
                    },
                    {
                        "title": "故障与时序冲突验证",
                        "test_content": f"验证{item['content']}在故障和时序冲突场景下的保护行为",
                        "operation_steps": self._build_fallback_steps(item["content"], True, "故障与时序冲突"),
                    },
                ][:cases_per_item]
            else:
                llm_case_count += len(generated)

            for idx, case in enumerate(generated, start=1):
                merged_content = _clean_text(case.get("test_content", item["content"]))
                placeholder = _pick_expected_result_placeholder(merged_content, abnormal=True)
                abnormal_cases.append(
                    {
                        "id": f"{item['id']}-C{idx}",
                        "item_id": item["id"],
                        "title": _clean_text(case.get("title", "")),
                        "operation_steps": case.get("operation_steps", []),
                        "test_content": merged_content,
                        "expected_result_placeholder": placeholder,
                    }
                )

        context["test_cases"] = {
            "normal": normal_cases,
            "abnormal": abnormal_cases,
        }

        return ToolExecutionResult(
            context=context,
            output_summary=(
                f"normal_cases={len(normal_cases)}; abnormal_cases={len(abnormal_cases)}; llm_cases={llm_case_count}"
            ),
            fallback_used=llm_case_count == 0,
        )


class BuildExpectedResultsTool(TestingTool):
    name = "build_expected_results"

    def _expected_for_case(self, context: Dict[str, Any], case: Dict[str, Any], abnormal: bool) -> str:
        placeholder = case.get("expected_result_placeholder", "{{return_value}}")
        if placeholder not in EXPECTED_RESULT_PLACEHOLDER_MAP:
            placeholder = "{{return_value}}"

        req_text = context.get("normalized_requirement_text", "")
        knowledge_context = _truncate_text(context.get("knowledge_context", ""), max_len=1200)
        prompt = f"""
请基于以下信息生成一条可验证、可度量的测试预期结果，避免模板化空话。

需求：{req_text}
测试内容：{case.get('test_content', '')}
测试类型：{'异常测试' if abnormal else '正常测试'}
占位符语义：{placeholder} -> {EXPECTED_RESULT_PLACEHOLDER_MAP.get(placeholder, '')}
知识库片段：{knowledge_context or '无'}

输出要求：
1. 仅输出一句中文预期结果。
2. 结果必须可判定成功/失败。
3. 包含关键观测项（返回值、状态、告警、日志、数据一致性中的相关项）。
""".strip()

        llm_text = _invoke_llm_text(context, prompt)
        if llm_text:
            return _truncate_text(llm_text, max_len=220)

        test_content = _clean_text(case.get("test_content", ""))
        if placeholder == "{{error_message}}":
            return f"触发{test_content}后，系统应返回明确错误码与错误文案，拒绝非法请求且核心状态保持一致。"
        if placeholder == "{{state_change}}":
            return f"执行{test_content}后，系统状态转换应符合需求定义，状态变化可被日志与回执共同验证。"
        if placeholder == "{{data_persistence}}":
            return f"执行{test_content}后，数据库或存储层应产生符合约束的持久化结果且无脏数据。"
        if placeholder == "{{ui_display}}":
            return f"执行{test_content}后，界面应展示与控制结果一致的反馈信息且提示可被用户执行。"

        if abnormal:
            return f"执行异常场景“{test_content}”后，系统应触发保护策略并输出可追溯日志，业务状态保持可恢复。"

        return f"执行“{test_content}”后，返回值与状态变化应满足需求约束，关键结果可通过日志或回执验证。"

    def execute(self, context: Dict[str, Any]) -> ToolExecutionResult:
        test_cases = context.get("test_cases", {})

        normal_expected: List[Dict[str, str]] = []
        abnormal_expected: List[Dict[str, str]] = []

        for case in test_cases.get("normal", []):
            normal_expected.append(
                {
                    "id": case["id"],
                    "case_id": case["id"],
                    "result": self._expected_for_case(context, case, abnormal=False),
                }
            )

        for case in test_cases.get("abnormal", []):
            abnormal_expected.append(
                {
                    "id": case["id"],
                    "case_id": case["id"],
                    "result": self._expected_for_case(context, case, abnormal=True),
                }
            )

        context["expected_results"] = {
            "normal": normal_expected,
            "abnormal": abnormal_expected,
        }

        return ToolExecutionResult(
            context=context,
            output_summary=(
                f"normal_expected={len(normal_expected)}; abnormal_expected={len(abnormal_expected)}"
            ),
        )


class FormatOutputTool(TestingTool):
    name = "format_output"

    @staticmethod
    def _format_case_block(case: Dict[str, Any], index: int) -> List[str]:
        item_id = case.get("item_id", case.get("id", ""))
        title = _clean_text(case.get("title", ""))

        block: List[str] = []
        block.append(f"{index}. [用例 {case['id']}]（对应测试项 {item_id}）：{case.get('test_content', '')}")
        if title:
            block.append(f"   场景标题：{title}")
        block.append("   操作步骤：")
        for step_idx, step in enumerate(case.get("operation_steps", []), start=1):
            block.append(f"   {step_idx}) {step}")
        return block

    def execute(self, context: Dict[str, Any]) -> ToolExecutionResult:
        test_items = context.get("test_items", {"normal": [], "abnormal": []})
        test_cases = context.get("test_cases", {"normal": [], "abnormal": []})
        expected_results = context.get("expected_results", {"normal": [], "abnormal": []})

        lines: List[str] = []

        lines.append("**测试项**")
        lines.append("")
        lines.append("**正常测试**：")
        for index, item in enumerate(test_items.get("normal", []), start=1):
            lines.append(f"{index}. [测试项 {item['id']}]：{item['content']}")
        lines.append("")
        lines.append("**异常测试**：")
        for index, item in enumerate(test_items.get("abnormal", []), start=1):
            lines.append(f"{index}. [测试项 {item['id']}]：{item['content']}")

        lines.append("")
        lines.append("**测试用例**")
        lines.append("")
        lines.append("**正常测试**：")
        for index, case in enumerate(test_cases.get("normal", []), start=1):
            lines.extend(self._format_case_block(case, index))
        lines.append("")
        lines.append("**异常测试**：")
        for index, case in enumerate(test_cases.get("abnormal", []), start=1):
            lines.extend(self._format_case_block(case, index))

        lines.append("")
        lines.append("**预期成果**")
        lines.append("")
        lines.append("**正常测试**：")
        for index, expected in enumerate(expected_results.get("normal", []), start=1):
            lines.append(
                f"{index}. [预期 {expected['id']}]（对应用例 {expected['case_id']}）：{expected['result']}"
            )
        lines.append("")
        lines.append("**异常测试**：")
        for index, expected in enumerate(expected_results.get("abnormal", []), start=1):
            lines.append(
                f"{index}. [预期 {expected['id']}]（对应用例 {expected['case_id']}）：{expected['result']}"
            )

        context["formatted_output"] = "\n".join(lines)
        context["structured_output"] = {
            "test_items": test_items,
            "test_cases": test_cases,
            "expected_results": expected_results,
        }

        return ToolExecutionResult(
            context=context,
            output_summary="formatted_sections=3",
        )


def build_default_tool_chain() -> List[TestingTool]:
    return [
        IdentifyRequirementTypeTool(),
        DecomposeTestItemsTool(),
        GenerateTestCasesTool(),
        BuildExpectedResultsTool(),
        FormatOutputTool(),
    ]
