import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import Fastify from "fastify";
import sensible from "@fastify/sensible";

const serverRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const databaseDir = mkdtempSync(join(tmpdir(), "sybil-chat-forks-"));
process.env.DATABASE_URL = `file:${join(databaseDir, "test.db")}`;
process.env.OPENAI_API_KEY = "";
delete process.env.ADMIN_TOKEN;

execFileSync(process.execPath, [join(serverRoot, "node_modules/prisma/build/index.js"), "migrate", "deploy"], {
  cwd: serverRoot,
  env: { ...process.env, PRISMA_HIDE_UPDATE_MESSAGE: "1" },
  stdio: "pipe",
});

const [{ clearActiveChatStream, registerActiveChatStream, registerRoutes }, { prisma }] = await Promise.all([
  import("../src/routes.js"),
  import("../src/db.js"),
]);
const app = Fastify({ logger: false });
await app.register(sensible);
await registerRoutes(app);
await app.ready();

test.after(async () => {
  await app.close();
  await prisma.$disconnect();
  rmSync(databaseDir, { recursive: true, force: true });
});

test("forks copy bounded history and keep every child grouped under the root", async () => {
  const timestamp = new Date("2026-08-16T12:00:00.000Z");
  const source = await prisma.chat.create({
    data: {
      title: "Planning session",
      initiatedProvider: "openai",
      initiatedModel: "gpt-4.1-mini",
      lastUsedProvider: "anthropic",
      lastUsedModel: "claude-sonnet-4-20250514",
      additionalSystemPrompt: "Keep answers concise.",
      enabledTools: ["web_search"],
      messages: {
        create: [
          {
            id: "source-message-0001",
            createdAt: timestamp,
            role: "user",
            content: "Plan a trip",
            metadata: {
              clientRequestId: "source-request",
              attachments: [{ kind: "text", filename: "notes.txt" }],
            },
          },
          {
            id: "source-message-0002",
            createdAt: timestamp,
            role: "assistant",
            content: "First assistant response",
            metadata: { clientRequestId: "source-request", citations: ["https://example.com"] },
          },
          {
            id: "source-message-0003",
            createdAt: timestamp,
            role: "user",
            content: "Only visible in a whole-chat fork",
          },
        ],
      },
      calls: {
        create: {
          provider: "openai",
          model: "gpt-4.1-mini",
          request: { input: "Plan a trip" },
        },
      },
    },
    include: { messages: { orderBy: [{ createdAt: "asc" }, { id: "asc" }] } },
  });

  const forkResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${source.id}/fork`,
    payload: { messageId: source.messages[1].id },
  });
  assert.equal(forkResponse.statusCode, 200, forkResponse.body);
  const forkSummary = forkResponse.json().chat;
  assert.equal(forkSummary.parentChatId, source.id);
  assert.equal(forkSummary.title, "Fork of 'First assistant response'");
  assert.equal(forkSummary.titleGenerationPending, true);
  assert.equal(forkSummary.starred, false);

  const detailResponse = await app.inject({ method: "GET", url: `/v1/chats/${forkSummary.id}` });
  assert.equal(detailResponse.statusCode, 200, detailResponse.body);
  const fork = detailResponse.json().chat;
  assert.deepEqual(
    fork.messages.map((message: any) => [message.role, message.content]),
    [
      ["user", "Plan a trip"],
      ["assistant", "First assistant response"],
    ]
  );
  assert.notEqual(fork.messages[0].id, source.messages[0].id);
  assert.equal(fork.messages[0].createdAt, timestamp.toISOString());
  assert.deepEqual(fork.messages[0].metadata, {
    attachments: [{ kind: "text", filename: "notes.txt" }],
  });
  assert.deepEqual(fork.messages[1].metadata, { citations: ["https://example.com"] });
  assert.equal(fork.initiatedProvider, "openai");
  assert.equal(fork.initiatedModel, "gpt-4.1-mini");
  assert.equal(fork.lastUsedProvider, "anthropic");
  assert.equal(fork.lastUsedModel, "claude-sonnet-4-20250514");
  assert.equal(fork.additionalSystemPrompt, "Keep answers concise.");
  assert.deepEqual(fork.enabledTools, ["web_search"]);
  assert.deepEqual(fork.calls, []);

  const childForkResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${forkSummary.id}/fork`,
    payload: {},
  });
  assert.equal(childForkResponse.statusCode, 200, childForkResponse.body);
  const childFork = childForkResponse.json().chat;
  assert.equal(childFork.parentChatId, source.id);
  assert.equal(childFork.title, "Fork of Fork of 'First assistant response'");
  assert.equal(childFork.titleGenerationPending, true);

  const wholeForkResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${source.id}/fork`,
    payload: {},
  });
  assert.equal(wholeForkResponse.statusCode, 200, wholeForkResponse.body);
  const wholeFork = wholeForkResponse.json().chat;
  assert.equal(wholeFork.parentChatId, source.id);
  assert.equal(wholeFork.title, "Fork of Planning session");
  const wholeForkDetail = await app.inject({ method: "GET", url: `/v1/chats/${wholeFork.id}` });
  assert.equal(wholeForkDetail.statusCode, 200, wholeForkDetail.body);
  assert.deepEqual(
    wholeForkDetail.json().chat.messages.map((message: any) => message.content),
    ["Plan a trip", "First assistant response", "Only visible in a whole-chat fork"]
  );
  assert.equal(wholeForkDetail.json().chat.messages[2].metadata, null);

  const suggestedTitleResponse = await app.inject({
    method: "POST",
    url: "/v1/chats/title/suggest",
    payload: { chatId: wholeFork.id, content: "Compare rail and air options" },
  });
  assert.equal(suggestedTitleResponse.statusCode, 200, suggestedTitleResponse.body);
  assert.equal(suggestedTitleResponse.json().chat.title, "Compare rail and air");
  assert.equal(suggestedTitleResponse.json().chat.titleGenerationPending, false);

  const repeatedTitleResponse = await app.inject({
    method: "POST",
    url: "/v1/chats/title/suggest",
    payload: { chatId: wholeFork.id, content: "This must not overwrite the generated title" },
  });
  assert.equal(repeatedTitleResponse.statusCode, 200, repeatedTitleResponse.body);
  assert.equal(repeatedTitleResponse.json().chat.title, "Compare rail and air");

  const sourceAfterForks = await prisma.chat.findUniqueOrThrow({
    where: { id: source.id },
    include: { messages: true, calls: true },
  });
  assert.equal(sourceAfterForks.messages.length, 3);
  assert.equal(sourceAfterForks.calls.length, 1);

  const manualTitleResponse = await app.inject({
    method: "PATCH",
    url: `/v1/chats/${forkSummary.id}`,
    payload: { title: "Manual branch title" },
  });
  assert.equal(manualTitleResponse.statusCode, 200, manualTitleResponse.body);
  assert.equal(manualTitleResponse.json().chat.titleGenerationPending, false);

  const activeForkStream = registerActiveChatStream(wholeFork.id);
  try {
    const deleteActiveRootResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${source.id}` });
    assert.equal(deleteActiveRootResponse.statusCode, 409, deleteActiveRootResponse.body);
    assert.equal(deleteActiveRootResponse.json().message, "chat or fork has an active stream");

    const deleteActiveForkResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${wholeFork.id}` });
    assert.equal(deleteActiveForkResponse.statusCode, 409, deleteActiveForkResponse.body);
  } finally {
    clearActiveChatStream(wholeFork.id, activeForkStream);
  }

  const deleteChildResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${childFork.id}` });
  assert.equal(deleteChildResponse.statusCode, 200, deleteChildResponse.body);
  assert.equal(await prisma.chat.count({ where: { id: { in: [source.id, forkSummary.id, wholeFork.id] } } }), 3);

  const unrelated = await prisma.chat.create({
    data: {
      messages: { create: { role: "assistant", content: "Unrelated response" } },
    },
    include: { messages: true },
  });
  const countBeforeInvalidForks = await prisma.chat.count();

  const wrongChatResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${source.id}/fork`,
    payload: { messageId: unrelated.messages[0].id },
  });
  assert.equal(wrongChatResponse.statusCode, 404, wrongChatResponse.body);
  assert.equal(wrongChatResponse.json().message, "message not found in chat");

  const nonAssistantResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${source.id}/fork`,
    payload: { messageId: source.messages[0].id },
  });
  assert.equal(nonAssistantResponse.statusCode, 400, nonAssistantResponse.body);
  assert.equal(nonAssistantResponse.json().message, "fork message must be an assistant response");
  assert.equal(await prisma.chat.count(), countBeforeInvalidForks);

  const concurrentTarget = await prisma.chat.create({ data: { title: "Concurrent delete" } });
  const concurrentDeleteResponses = await Promise.all([
    app.inject({ method: "DELETE", url: `/v1/chats/${concurrentTarget.id}` }),
    app.inject({ method: "DELETE", url: `/v1/chats/${concurrentTarget.id}` }),
  ]);
  assert.equal(concurrentDeleteResponses.filter((response) => response.statusCode === 200).length, 1);
  assert.equal(concurrentDeleteResponses.some((response) => response.statusCode >= 500), false);

  const concurrentRoot = await prisma.chat.create({ data: { title: "Concurrent family delete" } });
  const concurrentChildResponse = await app.inject({
    method: "POST",
    url: `/v1/chats/${concurrentRoot.id}/fork`,
    payload: {},
  });
  assert.equal(concurrentChildResponse.statusCode, 200, concurrentChildResponse.body);
  const concurrentChild = concurrentChildResponse.json().chat;
  const concurrentFamilyDeleteResponses = await Promise.all([
    app.inject({ method: "DELETE", url: `/v1/chats/${concurrentRoot.id}` }),
    app.inject({ method: "DELETE", url: `/v1/chats/${concurrentChild.id}` }),
  ]);
  assert.equal(concurrentFamilyDeleteResponses.some((response) => response.statusCode >= 500), false);
  assert.equal(concurrentFamilyDeleteResponses.some((response) => response.statusCode === 200), true);
  await prisma.chat.deleteMany({ where: { id: concurrentRoot.id } });

  const deleteRootResponse = await app.inject({ method: "DELETE", url: `/v1/chats/${source.id}` });
  assert.equal(deleteRootResponse.statusCode, 200, deleteRootResponse.body);
  assert.equal(await prisma.chat.count({ where: { id: { in: [source.id, forkSummary.id, childFork.id, wholeFork.id] } } }), 0);
  assert.equal(await prisma.chat.count({ where: { id: unrelated.id } }), 1);
});
