package com.agent.management.engine;

import com.agent.management.engine.PlannerService.PlannerDecision;
import com.agent.management.engine.hermes.ClarifyHooks;
import com.agent.management.external.SseEventBus;
import com.agent.management.model.entity.WorkflowRunVisit;
import com.agent.management.repository.WorkflowPauseRepository;
import com.agent.management.repository.WorkflowRunVisitRepository;
import com.agent.management.service.AiModelService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link AgenticExecutor} 关键路径单元测试。
 *
 * <p>设计原则：</p>
 * <ul>
 *   <li>仅覆盖核心行为：访问链顺序、Output 终止、Planner 失败兜底、节点失败 abort</li>
 *   <li>PlannerService 整体 mock（其内部 LLM 调用已在 {@link PlannerServiceTest} 覆盖），
 *       避免再串一遍 Spring AI fluent chain</li>
 *   <li>{@code visitRepo.save} 用 ArgumentCaptor 捕获，验证访问链结构</li>
 * </ul>
 */
class AgenticExecutorTest {

    private PlannerService plannerService;
    private WorkflowRunVisitRepository visitRepo;
    private SseEventBus eventBus;
    private WorkflowPauseRepository pauseRepository;
    private AiModelService aiModelService;
    private ChatClient.Builder fallbackBuilder;
    private SseEmitter emitter;

    private final List<WorkflowRunVisit> savedVisits = new ArrayList<>();

    @BeforeEach
    void setUp() {
        plannerService = mock(PlannerService.class);
        visitRepo = mock(WorkflowRunVisitRepository.class);
        // 捕获所有 save 调用，用于后续断言
        when(visitRepo.save(any(WorkflowRunVisit.class))).thenAnswer(inv -> {
            WorkflowRunVisit v = inv.getArgument(0);
            savedVisits.add(v);
            return v;
        });
        eventBus = mock(SseEventBus.class);
        pauseRepository = mock(WorkflowPauseRepository.class);
        aiModelService = mock(AiModelService.class);
        fallbackBuilder = mock(ChatClient.Builder.class);
        emitter = mock(SseEmitter.class);
    }

    private AgenticExecutor newExecutor(Map<String, NodeExecutor> executorMap) {
        return new AgenticExecutor(
                new LinkedHashMap<>(executorMap),
                plannerService, visitRepo, null, eventBus,
                pauseRepository, aiModelService, fallbackBuilder);
    }

    private static DagResolver.ResolvedDag dagOf(String nodesDecl, String edgesDecl) {
        return DagResolver.resolve("{\"nodes\":[" + nodesDecl + "],\"edges\":[" + edgesDecl + "]}");
    }

    /** 成功执行的 envelope 输出 */
    private static Map<String, Object> envelope(String outputValue) {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("output", outputValue);
        Map<String, Object> env = new LinkedHashMap<>();
        env.put("status", 200);
        env.put("message", "ok");
        env.put("data", data);
        return env;
    }

    private NodeExecutor mockExecutor(String type, Map<String, NodeExecutionResult> results) {
        NodeExecutor exec = mock(NodeExecutor.class);
        when(exec.getType()).thenReturn(type);
        when(exec.execute(anyString(), any(), any(), any())).thenAnswer(inv -> {
            String nodeId = inv.getArgument(0);
            return results.get(nodeId);
        });
        return exec;
    }

    // ============================ 测试用例 ============================

    /**
     * 场景 1：线性 A→B→Output。
     *
     * <p>预期：3 次 visit（A, B, Output），sortOrder 为 0/1/2，
     * Output 节点访问后 workflow complete，finalOutputs 包含 Output 节点的 envelope data。</p>
     */
    @Test
    void linearWorkflow_terminatesOnOutputNode() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"A\",\"type\":\"llm\",\"data\":{\"label\":\"A\"}},"
                + "{\"id\":\"B\",\"type\":\"llm\",\"data\":{\"label\":\"B\"}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"Output\"}}",
                "{\"source\":\"A\",\"target\":\"B\"},"
                + "{\"source\":\"B\",\"target\":\"O\"}"
        );
        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("llm", mockExecutor("llm", Map.of(
                "A", NodeExecutionResult.success("A", envelope("A-result")),
                "B", NodeExecutionResult.success("B", envelope("B-result")))));
        executors.put("output", mockExecutor("output", Map.of(
                "O", NodeExecutionResult.success("O", envelope("final-answer")))));

        // Planner 决策：A→B，B→O
        when(plannerService.decideNext(any(), eq("A"), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("B", "去 B"));
        when(plannerService.decideNext(any(), eq("B"), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("O", "去 Output"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        WorkflowLevelExecutor.ExecutionOutcome outcome = executor.execute(
                dag, context, "run-1", 1L, emitter);

        // 验证：成功完成，3 个 visit，最后节点为 Output
        assertThat(outcome.failed).isFalse();
        assertThat(outcome.errorMsg).isNull();
        assertThat(savedVisits).hasSize(3);
        assertThat(savedVisits.get(0).getNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(0).getSortOrder()).isEqualTo(0);
        assertThat(savedVisits.get(1).getNodeId()).isEqualTo("B");
        assertThat(savedVisits.get(1).getSortOrder()).isEqualTo(1);
        assertThat(savedVisits.get(2).getNodeId()).isEqualTo("O");
        assertThat(savedVisits.get(2).getSortOrder()).isEqualTo(2);

        // 验证：fromNodeId 链 A→B→O 中 B 的 fromNodeId 应为 A，O 的 fromNodeId 应为 B
        assertThat(savedVisits.get(0).getFromNodeId()).isNull();
        assertThat(savedVisits.get(1).getFromNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(2).getFromNodeId()).isEqualTo("B");

        // 验证：finalOutputs 应为 Output 节点的 envelope（与 DAG 模式 complete() 一致）
        assertThat(outcome.finalOutputs).containsEntry("status", 200);
        assertThat(outcome.finalOutputs).containsKey("data");
    }

    /**
     * 场景 2：工作区变量在 Visit 链上累积，同名变量最近覆盖。
     *
     * <p>A 输出 {x: 1, y: 1}，B 输出 {x: 2}（覆盖 x）。
     * 预期 B 的 variablesJson 含 x=2（B 的）和 y=1（A 的）。</p>
     */
    @Test
    void workspaceAccumulation_sameNameVariableOverriddenByMostRecent() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"A\",\"type\":\"llm\",\"data\":{\"label\":\"A\"}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"O\"}}",
                "{\"source\":\"A\",\"target\":\"O\"}"
        );
        // A 输出 x=1, y=1
        Map<String, Object> aData = new LinkedHashMap<>();
        aData.put("x", 1);
        aData.put("y", 1);
        Map<String, Object> aEnv = new LinkedHashMap<>();
        aEnv.put("status", 200);
        aEnv.put("message", "ok");
        aEnv.put("data", aData);
        // O 输出 x=2（同名覆盖）
        Map<String, Object> oData = new LinkedHashMap<>();
        oData.put("x", 2);
        Map<String, Object> oEnv = new LinkedHashMap<>();
        oEnv.put("status", 200);
        oEnv.put("message", "ok");
        oEnv.put("data", oData);

        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("llm", mockExecutor("llm", Map.of("A", NodeExecutionResult.success("A", aEnv))));
        executors.put("output", mockExecutor("output", Map.of("O", NodeExecutionResult.success("O", oEnv))));

        when(plannerService.decideNext(any(), eq("A"), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("O", "完成"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        executor.execute(dag, context, "run-1", 1L, emitter);

        // A 的 visit 变量应含 A__x=1, A__y=1
        WorkflowRunVisit aVisit = savedVisits.get(0);
        assertThat(aVisit.getNodeId()).isEqualTo("A");
        assertThat(aVisit.getVariablesJson()).contains("A__x").contains("A__y");

        // O 的 visit 变量应同时含 A__y=1（保留）和 O__x=2（新加）
        // 关键：visit 持久化时的 variablesJson 反映"上一个 Visit 工作区 + 本节点输出"
        WorkflowRunVisit oVisit = savedVisits.get(1);
        assertThat(oVisit.getNodeId()).isEqualTo("O");
        assertThat(oVisit.getVariablesJson()).contains("A__y").contains("O__x");
    }

    /**
     * 场景 3：Planner 抛异常时，AgenticExecutor 应捕获并调用 fallbackToNextEdge 走画布参考路径。
     *
     * <p>A→O 简单图，Planner.decideNext 永远抛异常，预期仍能通过 fallback 走到 O。</p>
     */
    @Test
    void plannerFailure_triggersCanvasFallback() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"A\",\"type\":\"llm\",\"data\":{\"label\":\"A\"}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"O\"}}",
                "{\"source\":\"A\",\"target\":\"O\"}"
        );
        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("llm", mockExecutor("llm", Map.of(
                "A", NodeExecutionResult.success("A", envelope("A-result")))));
        executors.put("output", mockExecutor("output", Map.of(
                "O", NodeExecutionResult.success("O", envelope("final")))));

        // Planner 总是抛异常
        when(plannerService.decideNext(any(), anyString(), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenThrow(new IllegalStateException("LLM 不可用"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        WorkflowLevelExecutor.ExecutionOutcome outcome = executor.execute(
                dag, context, "run-1", 1L, emitter);

        // 验证：fallback 走 A→O（A 的第一条出边），workflow 仍成功完成
        assertThat(outcome.failed).isFalse();
        assertThat(savedVisits).hasSize(2);
        assertThat(savedVisits.get(0).getNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(1).getNodeId()).isEqualTo("O");
    }

    /**
     * 场景 4：节点执行失败时按 maxRetries 重试，用尽后交 Planner 决策（lastFailed=true）。
     *
     * <p>A 配置 maxRetries=2，executor 始终失败。预期：
     * <ul>
     *   <li>A 被执行 2 次（外层重试）</li>
     *   <li>Planner 收到 lastFailed=true、lastError 非空</li>
     *   <li>Planner 选 O（绕过失败），最终 outcome.failed=false</li>
     * </ul>
     * </p>
     */
    @Test
    void nodeFailure_retriesThenDelegatesToPlanner() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"A\",\"type\":\"llm\",\"data\":{\"label\":\"A\",\"maxRetries\":2}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"O\"}}",
                "{\"source\":\"A\",\"target\":\"O\"}"
        );
        NodeExecutor llmExec = mock(NodeExecutor.class);
        when(llmExec.getType()).thenReturn("llm");
        when(llmExec.execute(anyString(), any(), any(), any()))
                .thenReturn(NodeExecutionResult.failed("A", "模型超时"));
        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("llm", llmExec);
        executors.put("output", mockExecutor("output", Map.of(
                "O", NodeExecutionResult.success("O", envelope("final")))));

        // Planner：A → O（绕过失败节点）
        when(plannerService.decideNext(any(), eq("A"), any(), any(), any(), any(),
                eq(true), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("O", "绕过失败"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        WorkflowLevelExecutor.ExecutionOutcome outcome = executor.execute(
                dag, context, "run-1", 1L, emitter);

        // A 被执行 2 次（maxRetries=2）
        verify(llmExec, times(2)).execute(eq("A"), any(), any(), any());
        // Planner 收到 lastFailed=true
        verify(plannerService).decideNext(any(), eq("A"), any(), any(), any(), any(),
                eq(true), nullable(String.class), any());
        // 最终到达 O，workflow 成功完成
        assertThat(outcome.failed).isFalse();
        assertThat(savedVisits).hasSize(2);
        assertThat(savedVisits.get(0).getNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(0).getStatus()).isEqualTo("FAILED");
        assertThat(savedVisits.get(1).getNodeId()).isEqualTo("O");
    }

    /**
     * 场景 5：userInput 节点优先作为起始节点（而非按拓扑序）。
     *
     * <p>画布上同时有 input 和 llm 节点（input 在拓扑上不是入度 0），仍应从 input 开始。</p>
     */
    @Test
    void userInputNode_preferredAsStart() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"U\",\"type\":\"userInput\",\"data\":{\"label\":\"入口\"}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"O\"}}",
                "{\"source\":\"U\",\"target\":\"O\"}"
        );
        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("userInput", mockExecutor("userInput", Map.of(
                "U", NodeExecutionResult.success("U", envelope("U-input")))));
        executors.put("output", mockExecutor("output", Map.of(
                "O", NodeExecutionResult.success("O", envelope("final")))));

        when(plannerService.decideNext(any(), eq("U"), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("O", "完成"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        executor.execute(dag, context, "run-1", 1L, emitter);

        // 第一个 visit 应为 U（userInput 优先）
        assertThat(savedVisits).hasSize(2);
        assertThat(savedVisits.get(0).getNodeId()).isEqualTo("U");
        assertThat(savedVisits.get(0).getNodeType()).isEqualTo("userInput");
    }

    /**
     * 场景 6：业务重走合法 —— 同节点连续访问不应触发死循环兜底。
     *
     * <p>构造 A→O 图，Planner 前 7 次都返回 A（业务重走，模拟 ReAct 模式 / 用户新需求场景），
     * 第 8 次返回 O 终结。预期运行正常完成，无 abort。</p>
     *
     * <p>背景：MAX_SAME_NODE_REVISITS=5 已移除（详见 docs/plans/agentic-revisit-decision-plan.md §4.3）。
     * 业务重走是 Planner 主动决策的合法行为，不应作为死循环指标。死循环防护仅靠 MAX_VISITS=50 总量兜底。</p>
     */
    @Test
    void sameNodeRevisitedMultipleTimes_isLegalBusinessRevisit() {
        DagResolver.ResolvedDag dag = dagOf(
                "{\"id\":\"A\",\"type\":\"llm\",\"data\":{\"label\":\"A\"}},"
                + "{\"id\":\"O\",\"type\":\"output\",\"data\":{\"label\":\"O\"}}",
                "{\"source\":\"A\",\"target\":\"O\"}"
        );
        Map<String, NodeExecutor> executors = new HashMap<>();
        executors.put("llm", mockExecutor("llm", Map.of(
                "A", NodeExecutionResult.success("A", envelope("A-result")))));
        executors.put("output", mockExecutor("output", Map.of(
                "O", NodeExecutionResult.success("O", envelope("final")))));

        // Planner：前 6 次返回 A（业务重走），第 7 次返回 O（终结）
        // 7 次决策对应 visit 序列：A×7 + O×1 = 8 个 visit
        when(plannerService.decideNext(any(), eq("A"), any(), any(), any(), any(),
                anyBoolean(), nullable(String.class), any()))
                .thenReturn(new PlannerDecision("A", "重走：处理用户新需求"),
                        new PlannerDecision("A", "重走：编译失败重做"),
                        new PlannerDecision("A", "重走：依赖补全"),
                        new PlannerDecision("A", "重走：第 4 次"),
                        new PlannerDecision("A", "重走：第 5 次"),
                        new PlannerDecision("A", "重走：第 6 次"),
                        new PlannerDecision("O", "完成"));

        AgenticExecutor executor = newExecutor(executors);
        WorkflowContext context = new WorkflowContext(1L, "run-1", null, new HashMap<>());

        WorkflowLevelExecutor.ExecutionOutcome outcome = executor.execute(
                dag, context, "run-1", 1L, emitter);

        // 验证：业务重走合法，不触发任何 abort
        assertThat(outcome.failed).isFalse();
        assertThat(outcome.errorMsg).isNull();
        // 共 8 个 visit：A×7 + O×1
        assertThat(savedVisits).hasSize(8);
        assertThat(savedVisits).allSatisfy(v -> {
            assertThat(v.getNodeId()).isIn("A", "O");
        });
        // 前 7 个 visit 是 A，最后一个是 O
        assertThat(savedVisits.get(0).getNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(6).getNodeId()).isEqualTo("A");
        assertThat(savedVisits.get(7).getNodeId()).isEqualTo("O");
        // iterSeq 应正确递增：A 的 7 次访问 iterSeq 分别为 1-7
        assertThat(savedVisits.get(0).getIterSeq()).isEqualTo(1);
        assertThat(savedVisits.get(6).getIterSeq()).isEqualTo(7);
        assertThat(savedVisits.get(7).getIterSeq()).isEqualTo(1); // O 的第 1 次
    }
}
