/**
 * Session Output 服务单元测试
 */
import axios from "axios";
import { afterEach, describe, expect, it, vi } from "vitest";
import { logger } from "../utils/logger.js";
import { appendSessionOutput, sendOutput, sendStatus, sessions, } from "./session-output.js";
import { getRuntimeAccessToken } from "./runtime-binding.js";
vi.mock("axios", () => ({
    default: {
        post: vi.fn(),
    },
}));
vi.mock("./runtime-binding.js", () => ({
    getRuntimeAccessToken: vi.fn(() => "runtime-token"),
}));
vi.mock("../utils/logger.js", () => ({
    logger: {
        warn: vi.fn(),
    },
}));
const mockedAxiosPost = axios.post;
const mockedLogger = vi.mocked(logger);
const mockedGetRuntimeAccessToken = vi.mocked(getRuntimeAccessToken);
const createSession = (overrides = {}) => ({
    agentId: "agent-1",
    workspacePath: "D:/workspace",
    outputBuffer: "",
    flushTimer: null,
    flushInFlight: null,
    seq: 0,
    ...overrides,
});
afterEach(() => {
    sessions.clear();
    vi.clearAllMocks();
});
describe("session-output service", () => {
    it("manages sessions map correctly", () => {
        sessions.set("session-1", createSession());
        expect(sessions.size).toBe(1);
        expect(sessions.has("session-1")).toBe(true);
        const session = sessions.get("session-1");
        if (session)
            session.outputBuffer = "test output";
        expect(sessions.get("session-1")?.outputBuffer).toBe("test output");
        sessions.delete("session-1");
        expect(sessions.size).toBe(0);
    });
    it("finds session by agentId", () => {
        sessions.set("session-1", createSession({ agentId: "agent-1" }));
        sessions.set("session-2", createSession({ agentId: "agent-2" }));
        const findByAgentId = (agentId) => {
            for (const [id, session] of sessions) {
                if (session.agentId === agentId)
                    return id;
            }
            return null;
        };
        expect(findByAgentId("agent-1")).toBe("session-1");
        expect(findByAgentId("agent-3")).toBe(null);
    });
    it("accumulates output correctly", () => {
        sessions.set("session-1", createSession());
        appendSessionOutput("session-1", "line1\n");
        appendSessionOutput("session-1", "line2\n");
        expect(sessions.get("session-1")?.outputBuffer).toBe("line1\nline2\n");
    });
    it("bounds pending SDK output so callback failures cannot grow memory without limit", () => {
        sessions.set("session-bounded", createSession());
        appendSessionOutput("session-bounded", "a".repeat(70 * 1024));
        const output = sessions.get("session-bounded")?.outputBuffer ?? "";
        expect(output.length).toBeLessThanOrEqual(64 * 1024);
        expect(output).toContain("AgentsWorkStudio: omitted");
        expect(output.endsWith("a")).toBe(true);
    });
    it("increments sequence correctly", () => {
        const session = createSession();
        sessions.set("session-1", session);
        session.seq += 1;
        expect(session.seq).toBe(1);
        session.seq += 1;
        expect(session.seq).toBe(2);
    });
    it("builds correct status payload", () => {
        const buildStatusPayload = (agentId, sessionId, status) => ({
            agentId,
            sessionId,
            status,
        });
        expect(buildStatusPayload("agent-1", "session-1", "running")).toEqual({
            agentId: "agent-1",
            sessionId: "session-1",
            status: "running",
        });
        expect(buildStatusPayload("agent-1", null, "stopped")).toEqual({
            agentId: "agent-1",
            sessionId: null,
            status: "stopped",
        });
    });
    it("does not throw when runtime status callback fails", async () => {
        mockedAxiosPost.mockRejectedValueOnce(new Error("scheduler unavailable"));
        await expect(sendStatus("agent-1", "session-1", "waiting_input")).resolves.toBeUndefined();
        expect(mockedAxiosPost).toHaveBeenCalledWith(expect.stringContaining("/api/runtime/callback/status"), {
            agentId: "agent-1",
            sessionId: "session-1",
            status: "waiting_input",
        }, { headers: { "X-Runtime-Token": "runtime-token" } });
        expect(mockedLogger.warn).toHaveBeenCalledWith("Runtime status callback failed", expect.objectContaining({
            agentId: "agent-1",
            sessionId: "session-1",
            status: "waiting_input",
        }));
    });
    it("uses explicit runtime callback token before persisted binding token", async () => {
        mockedAxiosPost.mockResolvedValueOnce({ data: { ok: true } });
        await sendStatus("agent-1", "session-1", "waiting_input", undefined, undefined, {
            runtimeAccessToken: "session-runtime-token",
        });
        expect(mockedAxiosPost).toHaveBeenCalledWith(expect.stringContaining("/api/runtime/callback/status"), {
            agentId: "agent-1",
            sessionId: "session-1",
            status: "waiting_input",
        }, { headers: { "X-Runtime-Token": "session-runtime-token" } });
    });
    it("sends structured source event metadata with SDK output chunks", async () => {
        mockedAxiosPost.mockResolvedValueOnce({ data: { ok: true } });
        await sendOutput("agent-1", "answer chunk", "session-1", 7, { sourceEventType: "text_delta" }, { runtimeAccessToken: "session-runtime-token" });
        expect(mockedAxiosPost).toHaveBeenCalledWith(expect.stringContaining("/api/runtime/callback/output"), {
            agentId: "agent-1",
            output: "answer chunk",
            sessionId: "session-1",
            seq: 7,
            sourceEventType: "text_delta",
        }, { headers: { "X-Runtime-Token": "session-runtime-token" } });
    });
    it("uses launch scheduler URL for Docker-deployed scheduler callbacks", async () => {
        mockedAxiosPost.mockResolvedValue({ data: { ok: true } });
        await sendStatus("agent-1", "session-1", "thinking", undefined, undefined, {
            runtimeAccessToken: "session-runtime-token",
            schedulerBaseUrl: "http://127.0.0.1:7380",
        });
        await sendOutput("agent-1", "[ACode] 正在请求模型 gpt-test：hello", "session-1", 8, { sourceEventType: "ai_request", logCategory: "acode" }, {
            runtimeAccessToken: "session-runtime-token",
            schedulerBaseUrl: "http://127.0.0.1:7380",
        });
        expect(mockedAxiosPost).toHaveBeenNthCalledWith(1, "http://127.0.0.1:7380/api/runtime/callback/status", {
            agentId: "agent-1",
            sessionId: "session-1",
            status: "thinking",
        }, { headers: { "X-Runtime-Token": "session-runtime-token" } });
        expect(mockedAxiosPost).toHaveBeenNthCalledWith(2, "http://127.0.0.1:7380/api/runtime/callback/output", {
            agentId: "agent-1",
            output: "[ACode] 正在请求模型 gpt-test：hello",
            sessionId: "session-1",
            seq: 8,
            sourceEventType: "ai_request",
            logCategory: "acode",
        }, { headers: { "X-Runtime-Token": "session-runtime-token" } });
    });
    it("looks up persisted runtime token by launch scheduler URL when explicit token is missing", async () => {
        mockedAxiosPost.mockResolvedValueOnce({ data: { ok: true } });
        mockedGetRuntimeAccessToken.mockImplementationOnce((_, serverBaseUrl) => serverBaseUrl === "http://127.0.0.1:7380"
            ? "scoped-7380-token"
            : undefined);
        await sendStatus("agent-1", "session-1", "thinking", undefined, undefined, {
            schedulerBaseUrl: "http://127.0.0.1:7380",
        });
        expect(mockedGetRuntimeAccessToken).toHaveBeenNthCalledWith(1, undefined, "http://127.0.0.1:7380");
        expect(mockedAxiosPost).toHaveBeenCalledWith("http://127.0.0.1:7380/api/runtime/callback/status", {
            agentId: "agent-1",
            sessionId: "session-1",
            status: "thinking",
        }, { headers: { "X-Runtime-Token": "scoped-7380-token" } });
    });
});
