import { describe, expect, it } from "bun:test";

import {
  streamPromptResponse,
  type PermissionRequestPayload,
} from "../../src/routes/chatStream.js";
import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";

const createEventStream = (events: unknown[]) => ({
  async *[Symbol.asyncIterator]() {
    for (const event of events) {
      yield event;
    }
  },
});

describe("streamPromptResponse", () => {
  it("forwards opencode permission requests as SSE payloads", async () => {
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "permission.asked",
            properties: {
              id: "perm-1",
              sessionID: "runtime-session-1",
              permission: "bash",
              patterns: ["rm *"],
              metadata: { command: "rm tmp.txt" },
              always: ["rm *"],
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "delete temp",
      write: (event, data) => events.push({ event, data }),
    });

    const permissionEvent = events.find((item) => item.event === "permission_request");
    expect(permissionEvent?.data).toMatchObject({
      session_id: "client-session-1",
      request_id: "perm-1",
      permission: "bash",
      patterns: ["rm *"],
      target: "rm tmp.txt",
      always: ["rm *"],
    } satisfies Partial<PermissionRequestPayload>);
  });

  it("auto replies always when approval mode is always", async () => {
    const replies: Array<Record<string, unknown>> = [];
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "permission.asked",
            properties: {
              id: "perm-1",
              sessionID: "runtime-session-1",
              permission: "bash",
              patterns: ["npm test"],
              metadata: { command: "npm test" },
              always: ["npm test"],
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
      replyPermission: async (options: Record<string, unknown>) => {
        replies.push(options);
      },
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "run tests",
      approvalMode: "always",
      write: (event, data) => events.push({ event, data }),
    });

    expect(replies).toEqual([
      {
        requestId: "perm-1",
        sessionId: "runtime-session-1",
        reply: "always",
      },
    ]);
    expect(events.some((item) => item.event === "permission_request")).toBe(false);
    expect(events.find((item) => item.event === "permission_response")?.data).toEqual({
      session_id: "client-session-1",
      request_id: "perm-1",
      reply: "always",
    });
  });

  it("forwards opencode v2 permission requests as SSE payloads", async () => {
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "permission.v2.asked",
            properties: {
              id: "perm-v2-1",
              sessionID: "runtime-session-1",
              action: "external_directory",
              resources: ["/tmp"],
              save: ["/tmp"],
              metadata: { path: "/tmp" },
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "read /tmp",
      write: (event, data) => events.push({ event, data }),
    });

    const permissionEvent = events.find((item) => item.event === "permission_request");
    expect(permissionEvent?.data).toMatchObject({
      session_id: "client-session-1",
      request_id: "perm-v2-1",
      permission: "external_directory",
      patterns: ["/tmp"],
      target: "/tmp",
      always: ["/tmp"],
    } satisfies Partial<PermissionRequestPayload>);
  });

  it("forwards opencode question requests and replies as SSE payloads", async () => {
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "question.asked",
            properties: {
              id: "question-1",
              sessionID: "runtime-session-1",
              questions: [
                {
                  header: "范围",
                  question: "选择分析范围",
                  options: [{ label: "城区", description: "中心城区" }],
                  multiple: false,
                  custom: true,
                },
              ],
            },
          },
          {
            type: "question.replied",
            properties: {
              sessionID: "runtime-session-1",
              requestID: "question-1",
              answers: [["城区", "补充说明"]],
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "ask",
      write: (event, data) => events.push({ event, data }),
    });

    expect(events.find((item) => item.event === "question_request")?.data).toMatchObject({
      session_id: "client-session-1",
      request_id: "question-1",
      questions: [
        {
          header: "范围",
          question: "选择分析范围",
          options: [{ label: "城区", description: "中心城区" }],
          multiple: false,
          custom: true,
        },
      ],
    });
    expect(events.find((item) => item.event === "question_response")?.data).toEqual({
      session_id: "client-session-1",
      request_id: "question-1",
      answers: [["城区", "补充说明"]],
    });
  });

  it("converts question tool parts into question request SSE payloads", async () => {
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "message.part.updated",
            properties: {
              sessionID: "runtime-session-1",
              part: {
                id: "tool-part-1",
                sessionID: "runtime-session-1",
                messageID: "message-1",
                type: "tool",
                callID: "call-1",
                tool: "question",
                state: {
                  status: "running",
                  input: {
                    questions: [
                      {
                        question: "你觉得这个 question 工具好用吗？",
                        header: "测试问题",
                        options: [
                          {
                            label: "非常好用",
                            description: "交互清晰，选项方便",
                          },
                        ],
                      },
                    ],
                  },
                  time: { start: Date.now() },
                },
              },
              time: Date.now(),
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "ask",
      write: (event, data) => events.push({ event, data }),
    });

    expect(events.find((item) => item.event === "question_request")?.data).toMatchObject({
      session_id: "client-session-1",
      request_id: "call-1",
      questions: [
        {
          header: "测试问题",
          question: "你觉得这个 question 工具好用吗？",
          options: [
            {
              label: "非常好用",
              description: "交互清晰，选项方便",
            },
          ],
        },
      ],
      tool: {
        messageID: "message-1",
        callID: "call-1",
      },
    });
    expect(
      events.some(
        (item) => item.event === "tool_call" && item.data.tool === "question",
      ),
    ).toBe(false);
  });

  it("forwards todo updates as structured SSE payloads and progress", async () => {
    const runtime = {
      subscribeEvents: async () =>
        createEventStream([
          {
            type: "todo.updated",
            properties: {
              sessionID: "runtime-session-1",
              todos: [
                { content: "分析水位", status: "completed", priority: "high" },
                { content: "生成建议", status: "in_progress", priority: "medium" },
              ],
            },
          },
          {
            type: "session.idle",
            properties: {
              sessionID: "runtime-session-1",
            },
          },
        ]),
      prompt: async () => undefined,
      messages: async () => [],
    } as unknown as OpencodeRuntimeAdapter;
    const events: Array<{ event: string; data: Record<string, unknown> }> = [];

    await streamPromptResponse({
      runtime,
      sessionId: "runtime-session-1",
      clientSessionId: "client-session-1",
      message: "plan",
      write: (event, data) => events.push({ event, data }),
    });

    expect(
      events.find(
        (item) => item.event === "progress" && item.data.id === "todo-progress",
      )?.data,
    ).toMatchObject({
      id: "todo-progress",
      phase: "planning",
      title: "计划进度 1/2",
    });
    expect(events.find((item) => item.event === "todo_update")?.data).toMatchObject({
      session_id: "client-session-1",
      todos: [
        expect.objectContaining({
          content: "分析水位",
          status: "completed",
          priority: "high",
        }),
        expect.objectContaining({
          content: "生成建议",
          status: "in_progress",
          priority: "medium",
        }),
      ],
    });
  });

});
