import { describe, expect, it } from "bun:test";
import { type OpencodeClient } from "@opencode-ai/sdk/v2";

import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";

const createRuntimeAdapter = (
  messages: unknown[],
  calls: {
    reverted: string[];
    removed: string[];
  } = { reverted: [], removed: [] },
) =>
  Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
    messages: async () => messages,
    revertMessage: async (_sessionId: string, messageId: string) => {
      calls.reverted.push(messageId);
    },
    removeMessage: async (_sessionId: string, messageId: string) => {
      calls.removed.push(messageId);
    },
  }) as OpencodeRuntimeAdapter;

describe("OpencodeRuntimeAdapter.revertToUserMessage", () => {
  it("skips reverting the first user message when the runtime session is empty", async () => {
    const calls = { reverted: [] as string[], removed: [] as string[] };
    const runtime = createRuntimeAdapter([], calls);

    await runtime.revertToUserMessage("session-1", { userOrdinal: 1 });

    expect(calls).toEqual({ reverted: [], removed: [] });
  });

  it("keeps ordinal mismatches visible when runtime messages exist", async () => {
    const runtime = createRuntimeAdapter([
      { info: { id: "user-1", role: "user" } },
      { info: { id: "assistant-1", role: "assistant" } },
    ]);

    await expect(
      runtime.revertToUserMessage("session-1", { userOrdinal: 2 }),
    ).rejects.toThrow("target user message not found to revert");
  });

  it("reverts and removes messages from the target user message onward", async () => {
    const calls = { reverted: [] as string[], removed: [] as string[] };
    const runtime = createRuntimeAdapter(
      [
        { info: { id: "user-1", role: "user" } },
        { info: { id: "assistant-1", role: "assistant" } },
        { info: { id: "user-2", role: "user" } },
        { info: { id: "assistant-2", role: "assistant" } },
      ],
      calls,
    );

    await runtime.revertToUserMessage("session-1", { userOrdinal: 2 });

    expect(calls).toEqual({
      reverted: ["user-2"],
      removed: ["assistant-2", "user-2"],
    });
  });
});

describe("OpencodeRuntimeAdapter.ensureClient", () => {
  it("retries bootstrap after a failed startup attempt", async () => {
    let attempts = 0;
    const client = {
      global: { health: async () => ({ data: { healthy: true } }) },
    } as unknown as OpencodeClient;
    const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
      clientPromise: null,
      closeServer: null,
      bootstrapClient: async () => {
        attempts += 1;
        if (attempts === 1) {
          throw new Error("startup failed");
        }
        return client;
      },
    }) as OpencodeRuntimeAdapter;

    await expect(runtime.ensureClient()).rejects.toThrow("startup failed");
    await expect(runtime.ensureClient()).resolves.toBe(client);
    expect(attempts).toBe(2);
  });
});
