import { describe, expect, test } from "bun:test"
import type { SessionConfigOption } from "@agentclientprotocol/sdk"
import { makeACPFixture, makeSession, secondModel } from "./service-fixture"

describe("acp service lifecycle", () => {
  test("loads and forks with paginated replay while resume does not replay", async () => {
    await using fixture = makeACPFixture({
      fetch(request) {
        if (request.method === "GET" && request.path === "/api/session/ses_loaded") {
          return Response.json({
            data: makeSession("ses_loaded", {
              cwd: "/workspace",
              agent: "plan",
              model: { providerID: "test", id: secondModel.id, variant: "medium" },
            }),
          })
        }
        if (request.method === "GET" && request.path === "/api/session/ses_resume") {
          return Response.json({
            data: makeSession("ses_resume", {
              cwd: "/workspace",
              agent: "plan",
              model: { providerID: "test", id: secondModel.id, variant: "low" },
            }),
          })
        }
        if (request.method === "POST" && request.path === "/api/session/ses_loaded/fork") {
          return Response.json({
            data: makeSession("ses_fork", {
              cwd: "/workspace",
              agent: "plan",
              model: { providerID: "test", id: secondModel.id, variant: "medium" },
            }),
          })
        }
        if (request.method === "GET" && request.path === "/api/session/ses_loaded/message") {
          if (request.query.cursor === "messages-2") {
            return Response.json({
              data: [
                {
                  id: "msg_assistant",
                  type: "assistant",
                  content: [{ type: "text", text: "hi there" }],
                },
              ],
              cursor: {},
            })
          }
          return Response.json({
            data: [{ id: "msg_user", type: "user", text: "hello", time: { created: 1 } }],
            cursor: { next: "messages-2" },
          })
        }
        if (request.method === "GET" && request.path === "/api/session/ses_fork/message") {
          return Response.json({
            data: [{ id: "msg_fork", type: "user", text: "forked", time: { created: 2 } }],
            cursor: {},
          })
        }
        return undefined
      },
    })

    const loaded = await fixture.service.loadSession({
      cwd: "/ignored",
      sessionId: "ses_loaded",
      mcpServers: [],
    })
    const resumed = await fixture.service.resumeSession({
      cwd: "/ignored",
      sessionId: "ses_resume",
      mcpServers: [],
    })
    const forked = await fixture.service.forkSession({
      cwd: "/ignored",
      sessionId: "ses_loaded",
      mcpServers: [],
    })

    expect(currentValue(loaded, "model")).toBe("test/second-model")
    expect(currentValue(loaded, "effort")).toBe("medium")
    expect(currentValue(loaded, "mode")).toBe("plan")
    expect(currentValue(resumed, "effort")).toBe("low")
    expect(forked.sessionId).toBe("ses_fork")
    expect(currentValue(forked, "effort")).toBe("medium")
    expect(
      fixture.updates.filter(
        (item) =>
          item.update.sessionUpdate === "user_message_chunk" || item.update.sessionUpdate === "agent_message_chunk",
      ),
    ).toEqual([
      {
        sessionId: "ses_loaded",
        update: {
          sessionUpdate: "user_message_chunk",
          messageId: "msg_user",
          content: { type: "text", text: "hello" },
        },
      },
      {
        sessionId: "ses_loaded",
        update: {
          sessionUpdate: "agent_message_chunk",
          messageId: "msg_assistant",
          content: { type: "text", text: "hi there" },
        },
      },
      {
        sessionId: "ses_fork",
        update: {
          sessionUpdate: "user_message_chunk",
          messageId: "msg_fork",
          content: { type: "text", text: "forked" },
        },
      },
    ])
    expect(
      fixture.requests
        .filter((request) => request.path.endsWith("/message"))
        .map((request) => ({ path: request.path, query: request.query })),
    ).toEqual([
      {
        path: "/api/session/ses_loaded/message",
        query: { limit: "200", order: "asc" },
      },
      {
        path: "/api/session/ses_loaded/message",
        query: { limit: "200", cursor: "messages-2" },
      },
      {
        path: "/api/session/ses_fork/message",
        query: { limit: "200", order: "asc" },
      },
    ])
    expect(fixture.requests).toContainEqual({
      method: "POST",
      path: "/api/session/ses_loaded/fork",
      query: {},
      body: { boundary: { type: "through" } },
    })
  })

  test("lists server-backed pages and forwards cwd and cursor", async () => {
    const firstPage = Array.from({ length: 100 }, (_, index) =>
      makeSession(`ses_${100 - index}`, {
        cwd: "/workspace",
        time: { created: index, updated: 100_000 - index },
        title: `Session ${100 - index}`,
      }),
    )
    await using fixture = makeACPFixture({
      fetch(request) {
        if (request.method !== "GET" || request.path !== "/api/session") return undefined
        if (request.query.cursor === "page-2") {
          return Response.json({
            data: [makeSession("ses_0", { cwd: "/workspace", time: { created: 0, updated: 1 } })],
            cursor: {},
          })
        }
        return Response.json({ data: firstPage, cursor: { next: "page-2" } })
      },
    })

    const first = await fixture.service.listSessions({ cwd: "/workspace" })
    const second = await fixture.service.listSessions({ cwd: "/workspace", cursor: first.nextCursor })

    expect(first.sessions).toHaveLength(100)
    expect(first.sessions[0]).toEqual({
      sessionId: "ses_100",
      cwd: "/workspace",
      title: "Session 100",
      updatedAt: new Date(100_000).toISOString(),
    })
    expect(first.nextCursor).toBe("page-2")
    expect(second.sessions.map((session) => session.sessionId)).toEqual(["ses_0"])
    expect(second.nextCursor).toBeUndefined()
    expect(
      fixture.requests.filter((request) => request.path === "/api/session").map((request) => request.query),
    ).toEqual([
      { limit: "100", order: "desc", directory: "/workspace" },
      { limit: "100", order: "desc", directory: "/workspace", cursor: "page-2" },
    ])
  })

  test("cancel preserves the attachment while close removes it and interrupts best-effort", async () => {
    await using fixture = makeACPFixture({
      fetch(request) {
        if (request.method === "POST" && request.path === "/api/session") {
          return Response.json({ data: makeSession("ses_lifecycle") })
        }
        if (request.method === "POST" && request.path === "/api/session/ses_lifecycle/model") {
          return new Response(null, { status: 204 })
        }
        if (request.method === "POST" && request.path.endsWith("/interrupt")) {
          return new Response(null, { status: 500 })
        }
        return undefined
      },
    })
    const created = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })

    await fixture.service.cancel({ sessionId: created.sessionId })
    const updated = await fixture.service.setSessionConfigOption({
      sessionId: created.sessionId,
      configId: "effort",
      value: "high",
    })

    expect(currentValue(updated, "effort")).toBe("high")
    expect(await fixture.service.closeSession({ sessionId: created.sessionId })).toEqual({})
    const missing = await fixture.service
      .setSessionConfigOption({
        sessionId: created.sessionId,
        configId: "effort",
        value: "default",
      })
      .catch((error: unknown) => error)
    expect(missing).toMatchObject({ _tag: "ACPSessionNotFoundError", sessionId: created.sessionId })
    expect(await fixture.service.closeSession({ sessionId: "missing" })).toEqual({})
    expect(
      fixture.requests.filter((request) => request.path.endsWith("/interrupt")).map((request) => request.path),
    ).toEqual([
      "/api/session/ses_lifecycle/interrupt",
      "/api/session/ses_lifecycle/interrupt",
      "/api/session/missing/interrupt",
    ])
  })

  test("deletes sessions from backing and local storage", async () => {
    await using fixture = makeACPFixture({
      fetch(request) {
        if (request.method === "POST" && request.path === "/api/session") {
          return Response.json({ data: makeSession("ses_delete") })
        }
        if (request.method === "DELETE" && request.path === "/api/session/ses_delete") {
          return new Response(null, { status: 204 })
        }
        return undefined
      },
    })
    const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })

    expect(await fixture.service.deleteSession({ sessionId: session.sessionId })).toEqual({})
    expect(fixture.requests).toContainEqual({
      method: "DELETE",
      path: "/api/session/ses_delete",
      query: {},
      body: undefined,
    })
    const missing = await fixture.service
      .setSessionConfigOption({ sessionId: session.sessionId, configId: "effort", value: "high" })
      .catch((error: unknown) => error)
    expect(missing).toMatchObject({ _tag: "ACPSessionNotFoundError", sessionId: session.sessionId })
  })
})

function currentValue(result: { readonly configOptions?: readonly SessionConfigOption[] | null }, id: string) {
  return result.configOptions?.find((option) => option.id === id)?.currentValue
}
