package tech.easyflow.agent.runtime.output;

import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.agent.runtime.agui.AgentAguiRunJournal;
import tech.easyflow.agent.runtime.agui.ResumableAguiSseEmitter;
import tech.easyflow.core.chat.protocol.ChatDomain;
import tech.easyflow.core.chat.protocol.ChatType;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;

import java.util.List;
import java.util.Map;

import static org.mockito.Mockito.*;

/**
 * {@link AguiAgentRunOutput} 的线级顺序、终态和脱敏测试。
 */
public class AguiAgentRunOutputTest {

    /**
     * 验证输入确认、正文、引用和完成终态顺序。
     */
    @Test
    public void shouldEmitOrderedAguiEventsAndSingleTerminal() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput(
                "123", "run-1", "user-message-1", emitter);

        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.SYSTEM, ChatType.INPUT_ACCEPTED, Map.of("messageId", "server-message-1")));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.LLM, ChatType.MESSAGE, Map.of("delta", "hello")));
        AgentRuntimeEvent completed = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
        Assert.assertTrue(output.emitRuntimeEvent(completed));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.BUSINESS, ChatType.CITATIONS, Map.of("items", List.of(Map.of("id", "c1")))));
        Assert.assertTrue(output.finish("hello"));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        List<String> json = frames.getAllValues();
        Assert.assertTrue(json.get(0).contains("\"type\":\"RUN_STARTED\""));
        Assert.assertTrue(json.stream().anyMatch(value -> value.contains("easyflow.input.accepted")));
        Assert.assertTrue(json.stream().anyMatch(value -> value.contains("\"type\":\"TEXT_MESSAGE_CONTENT\"")));
        Assert.assertTrue(json.stream().anyMatch(value -> value.contains("easyflow.knowledge.citations")));
        Assert.assertTrue(json.get(json.size() - 1).contains("\"type\":\"RUN_FINISHED\""));
        Assert.assertEquals(1, json.stream().filter(value -> value.contains("\"type\":\"RUN_FINISHED\"")).count());
        verify(emitter).complete();
    }

    /**
     * 验证可恢复日志终态刷盘失败会沿输出边界返回失败。
     */
    @Test
    public void shouldReportResumableJournalCompletionFailure() {
        AgentAguiRunJournal journal = mock(AgentAguiRunJournal.class);
        doThrow(new IllegalStateException("Redis unavailable"))
                .when(journal).complete("run-1");
        ResumableAguiSseEmitter emitter = new ResumableAguiSseEmitter(
                "123", "run-1", journal, new SseEmitter());
        AguiAgentRunOutput output = new AguiAgentRunOutput(
                "123", "run-1", "user-message-1", emitter);

        Assert.assertTrue(output.emitRuntimeEvent(
                AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED)));

        Assert.assertFalse(output.finish("final"));
        Assert.assertFalse(emitter.isJournalCompletionSuccessful());
    }

    /**
     * 验证审批 Custom Event 不泄漏恢复令牌。
     */
    @Test
    public void shouldHideResumeTokenInApprovalEvent() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput("123", "run-1", "user-1", emitter);
        AgentRuntimeEvent approval = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
        approval.setToolCallId("tool-1");
        approval.getPayload().put("resumeToken", "secret-resume-token");
        approval.getPayload().put("toolName", "dangerous-tool");
        approval.getPayload().put("input", Map.of(
                "authorization", "sentinel-secret-authorization",
                "callbackUrl", "https://example.test/callback?api_key=sentinel-secret-query",
                "value", 1));
        approval.getMetadata().put("approvalId", "approval-public");

        Assert.assertTrue(output.emitRuntimeEvent(approval));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("approval-public"));
        Assert.assertTrue(wire.contains("[已隐藏]"));
        Assert.assertFalse(wire.contains("secret-resume-token"));
        Assert.assertFalse(wire.contains("sentinel-secret"));
    }

    /**
     * 验证 EasyFlow 业务 CUSTOM 只接受服务层公开载荷，Runtime 原始检索数据不会透出。
     */
    @Test
    public void shouldNotExposeRawBusinessRuntimePayloads() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput("123", "run-1", "user-1", emitter);
        AgentRuntimeEvent knowledge = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
        knowledge.getPayload().put("documents", List.of(Map.of(
                "chunkContent", "private chunk",
                "sourceUri", "private://document")));

        Assert.assertTrue(output.emitRuntimeEvent(knowledge));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.BUSINESS,
                ChatType.STATUS,
                Map.of(
                        "documents", knowledge.getPayload().get("documents"),
                        "label", "已检索知识库",
                        "status", "done",
                        "statusKey", "knowledge-retrieval")));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("easyflow.knowledge.retrieval_status"));
        Assert.assertFalse(wire.contains("private chunk"));
        Assert.assertFalse(wire.contains("private://document"));
    }

    /**
     * 验证没有 Runtime 终态的正常 EOF 会转换为明确协议错误。
     */
    @Test
    public void shouldFailWhenStreamEndsWithoutTerminalEvent() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput("123", "run-1", "user-1", emitter);

        Assert.assertFalse(output.canFinishSuccessfully());
        Assert.assertTrue(output.finish(null));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("\"type\":\"RUN_ERROR\""));
        Assert.assertTrue(wire.contains("MISSING_TERMINAL_EVENT"));
    }

    /**
     * 验证最终权威文本发生修正时通过标准消息快照收敛，且协议保留字段不可被覆盖。
     */
    @Test
    public void shouldReconcileAuthoritativeTextAndProtectReservedFields() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput(
                "123", "run-1", "user-1", "question", emitter);
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.LLM, ChatType.MESSAGE, Map.of("delta", "draft")));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.SYSTEM, ChatType.INPUT_ACCEPTED,
                Map.of("messageId", "server-1", "runId", "untrusted-run")));
        Assert.assertTrue(output.emitRuntimeEvent(AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED)));
        Assert.assertTrue(output.finish("final"));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("\"type\":\"MESSAGES_SNAPSHOT\""));
        Assert.assertTrue(wire.contains("\"role\":\"user\""));
        Assert.assertTrue(wire.contains("\"content\":\"question\""));
        Assert.assertTrue(wire.contains("\"content\":\"final\""));
        Assert.assertTrue(wire.contains("\"runId\":\"run-1\""));
        Assert.assertFalse(wire.contains("untrusted-run"));
    }

    /**
     * 验证工具前后的多段正文使用不同消息 ID，推理开始事件使用标准 reasoning 角色。
     */
    @Test
    public void shouldUseUniqueSegmentMessageIdsAndReasoningRole() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput("123", "run-1", "user-1", emitter);
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.LLM, ChatType.MESSAGE, Map.of("delta", "before")));
        AgentRuntimeEvent toolCall = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_CALL);
        toolCall.setToolCallId("tool-1");
        toolCall.getPayload().put("toolName", "search");
        toolCall.getPayload().put("toolDisplayName", "联网搜索");
        Assert.assertTrue(output.emitRuntimeEvent(toolCall));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.LLM, ChatType.MESSAGE, Map.of("delta", "after")));
        Assert.assertTrue(output.emitViewEvent(
                ChatDomain.LLM, ChatType.THINKING, Map.of("delta", "reason")));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("run-1-assistant-1"));
        Assert.assertTrue(wire.contains("run-1-assistant-2"));
        Assert.assertTrue(wire.contains("\"role\":\"reasoning\""));
        Assert.assertTrue(wire.contains("easyflow.tool.metadata"));
        Assert.assertTrue(wire.contains("联网搜索"));
    }

    /**
     * 验证 Artifact 发布事件仅发送安全字段。
     */
    @Test
    public void shouldEmitSafeArtifactPublishedCustomEvent() {
        ChatSseEmitter emitter = emitter();
        AguiAgentRunOutput output = new AguiAgentRunOutput("123", "run-1", "user-1", emitter);

        Assert.assertTrue(output.emitViewEvent(ChatDomain.BUSINESS, ChatType.STATUS, Map.of(
                "statusKey", "artifact-published",
                "schemaVersion", 1,
                "artifactId", "a1",
                "fileName", "report.csv",
                "mimeType", "text/csv",
                "size", 12,
                "sha256", "abc",
                "downloadUrl", "/api/v1/agent/artifacts/a1/content",
                "status", "AVAILABLE",
                "objectKey", "private/object/key")));

        ArgumentCaptor<String> frames = ArgumentCaptor.forClass(String.class);
        verify(emitter, atLeastOnce()).sendData(frames.capture());
        String wire = String.join("\n", frames.getAllValues());
        Assert.assertTrue(wire.contains("easyflow.artifact.published"));
        Assert.assertTrue(wire.contains("report.csv"));
        Assert.assertFalse(wire.contains("private/object/key"));
    }

    private static ChatSseEmitter emitter() {
        ChatSseEmitter emitter = mock(ChatSseEmitter.class);
        when(emitter.getEmitter()).thenReturn(new SseEmitter());
        when(emitter.sendData(anyString())).thenReturn(true);
        when(emitter.isClosed()).thenReturn(false);
        return emitter;
    }
}
