import { describe, expect, it, vi } from "vitest";

import type { AppServerRequestClient } from "../../src/app-server/services/request-client";
import {
  forkEphemeralThread,
  listThreads,
  startEphemeralThread,
  startThread,
  threadFromAppServerRecord,
} from "../../src/app-server/services/threads";

describe("app-server thread response adapters", () => {
  it("preserves spawned subagent provenance in the domain thread", () => {
    const thread = threadFromAppServerRecord({
      id: "child",
      preview: "Inspect",
      name: null,
      createdAt: 1,
      updatedAt: 2,
      sessionId: "session",
      parentThreadId: "parent",
      source: {
        subAgent: {
          thread_spawn: {
            parent_thread_id: "parent",
            depth: 2,
            agent_path: null,
            agent_nickname: "Scout",
            agent_role: "explorer",
          },
        },
      },
      agentNickname: "Scout",
      agentRole: "explorer",
    });

    expect(thread.provenance).toEqual({
      kind: "subagent",
      subagentKind: "thread-spawn",
      parentThreadId: "parent",
      sessionId: "session",
      depth: 2,
      agentNickname: "Scout",
      agentRole: "explorer",
    });
  });

  it("forks read-only ephemeral side-chat threads", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({
        thread: { id: "side", preview: "", name: null, createdAt: 1, updatedAt: 1 },
        cwd: "/vault",
        model: "gpt-5.5",
        serviceTier: null,
        approvalsReviewer: null,
        reasoningEffort: null,
        approvalPolicy: "never",
        sandbox: { type: "readOnly", networkAccess: false },
        activePermissionProfile: null,
      }),
    } as unknown as AppServerRequestClient;

    const result = await forkEphemeralThread(client, "source", "/vault");

    expect(client.request).toHaveBeenCalledWith("thread/fork", {
      threadId: "source",
      cwd: "/vault",
      ephemeral: true,
      sandbox: "read-only",
      approvalPolicy: "never",
      excludeTurns: true,
    });
    expect(result).toMatchObject({ sourceThreadId: "source", activation: { thread: { id: "side" }, cwd: "/vault" } });
  });

  it("starts panel-owned threads with the codex-panel service name", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
    } as unknown as AppServerRequestClient;

    await startThread(client, { cwd: "/vault" });

    expect(client.request).toHaveBeenCalledWith("thread/start", {
      cwd: "/vault",
      serviceName: "codex-panel",
    });
  });

  it("passes explicit service tier requests when starting panel-owned threads", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
    } as unknown as AppServerRequestClient;

    await startThread(client, { cwd: "/vault", serviceTier: "priority" });

    expect(client.request).toHaveBeenCalledWith("thread/start", {
      cwd: "/vault",
      serviceName: "codex-panel",
      serviceTier: "priority",
    });
  });

  it("starts ephemeral helper threads without deprecated multi-agent mode params", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({ thread: { id: "thread-new" } }),
    } as unknown as AppServerRequestClient;

    await startEphemeralThread(client, {
      cwd: "/vault",
      serviceName: "codex-panel-selection-rewrite",
      developerInstructions: "Return structured output.",
    });

    expect(client.request).toHaveBeenCalledWith("thread/start", {
      cwd: "/vault",
      serviceName: "codex-panel-selection-rewrite",
      developerInstructions: "Return structured output.",
      ephemeral: true,
      sandbox: "read-only",
      approvalPolicy: "never",
      environments: [],
    });
  });

  it("maps listed threads to domain threads with archive state", async () => {
    const clientListThreads = vi.fn().mockResolvedValue({
      data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20 }],
    });
    const client = {
      request: clientListThreads,
    } as unknown as AppServerRequestClient;

    await expect(listThreads(client, "/vault", { archived: true })).resolves.toEqual([
      {
        id: "thread-1",
        preview: "Preview",
        name: null,
        archived: true,
        createdAt: 10,
        updatedAt: 20,
        provenance: { kind: "interactive" },
      },
    ]);
    expect(clientListThreads).toHaveBeenCalledWith("thread/list", {
      cwd: "/vault",
      archived: true,
      limit: 100,
      sortKey: "recency_at",
      sortDirection: "desc",
    });
  });

  it("preserves app-server recency timestamps when available", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({
        data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20, recencyAt: 30 }],
      }),
    } as unknown as AppServerRequestClient;

    await expect(listThreads(client, "/vault")).resolves.toEqual([
      {
        id: "thread-1",
        preview: "Preview",
        name: null,
        archived: false,
        createdAt: 10,
        updatedAt: 20,
        recencyAt: 30,
        provenance: { kind: "interactive" },
      },
    ]);
  });

  it("preserves nullable app-server recency timestamps", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({
        data: [{ id: "thread-1", preview: "Preview", name: null, createdAt: 10, updatedAt: 20, recencyAt: null }],
      }),
    } as unknown as AppServerRequestClient;

    await expect(listThreads(client, "/vault")).resolves.toEqual([
      {
        id: "thread-1",
        preview: "Preview",
        name: null,
        archived: false,
        createdAt: 10,
        updatedAt: 20,
        recencyAt: null,
        provenance: { kind: "interactive" },
      },
    ]);
  });

  it("follows thread list pagination until the final page", async () => {
    const clientListThreads = vi
      .fn()
      .mockResolvedValueOnce({
        data: [{ id: "thread-1", preview: "First", name: null, createdAt: 10, updatedAt: 20 }],
        nextCursor: "next",
      })
      .mockResolvedValueOnce({
        data: [{ id: "thread-2", preview: "Second", name: null, createdAt: 30, updatedAt: 40 }],
        nextCursor: null,
      });
    const client = {
      request: clientListThreads,
    } as unknown as AppServerRequestClient;

    await expect(listThreads(client, "/vault")).resolves.toEqual([
      {
        id: "thread-1",
        preview: "First",
        name: null,
        archived: false,
        createdAt: 10,
        updatedAt: 20,
        provenance: { kind: "interactive" },
      },
      {
        id: "thread-2",
        preview: "Second",
        name: null,
        archived: false,
        createdAt: 30,
        updatedAt: 40,
        provenance: { kind: "interactive" },
      },
    ]);
    expect(clientListThreads).toHaveBeenNthCalledWith(1, "thread/list", {
      cwd: "/vault",
      archived: false,
      limit: 100,
      sortKey: "recency_at",
      sortDirection: "desc",
    });
    expect(clientListThreads).toHaveBeenNthCalledWith(2, "thread/list", {
      cwd: "/vault",
      cursor: "next",
      archived: false,
      limit: 100,
      sortKey: "recency_at",
      sortDirection: "desc",
    });
  });

  it("rejects repeated thread list cursors", async () => {
    const client = {
      request: vi.fn().mockResolvedValue({
        data: [],
        nextCursor: "same",
      }),
    } as unknown as AppServerRequestClient;

    await expect(listThreads(client, "/vault")).rejects.toThrow("repeated thread list cursor");
  });
});
