import assert from "node:assert/strict";
import { test } from "bun:test";

import { createTaskTreeDispatcher } from "./dispatcher.ts";
import { createTaskTreeHttpApp } from "./http.ts";
import { numberTaskTree } from "./outline-ref.ts";
import { projectTimelineTasks, TaskTreeStore } from "./store.ts";

test("HTTP command endpoint returns the same dispatcher DTO", async () => {
  const group = {
    id: "tg_contract",
    name: "Contract",
    description: "",
    createdAt: "2026-07-16T00:00:00.000Z",
    updatedAt: "2026-07-16T00:00:00.000Z"
  };
  const store = {
    async listGroups() { return [group]; }
  };
  const dispatch = createTaskTreeDispatcher({ store: store as TaskTreeStore });
  const expected = await dispatch({ operation: "group.list" });
  const app = createTaskTreeHttpApp({ dispatch });
  const response = await app.fetch(new Request("http://tasktree/v1/tasktree/commands", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ operation: "group.list" })
  }));

  assert.equal(response.status, 200);
  assert.deepEqual(await response.json(), expected);
});

test("outline references follow the current sibling order and tree depth", () => {
  const task = (id: string, parentId: string | null, sortOrder: number): any => ({
    id, groupId: "tg_1", parentId, kind: parentId ? "subtask" : "task", title: id, description: "", status: "pending",
    startAt: null, dueAt: null, sortOrder, createdAt: `2026-07-21T00:00:0${sortOrder}.000Z`, updatedAt: "2026-07-21T00:00:00.000Z"
  });
  const numbered = numberTaskTree([
    task("root-2", null, 5), task("child-2", "root-1", 4), task("grandchild", "child-1", 3),
    task("root-1", null, 0), task("child-1", "root-1", 2)
  ]);
  assert.deepEqual(Object.fromEntries(numbered.map((item) => [item.id, item.outlineRef])), {
    "root-1": "R1", "child-1": "R1.1", grandchild: "R1.1.1", "child-2": "R1.2", "root-2": "R2"
  });
  const afterSiblingDeletion = numberTaskTree(numbered.filter((item) => item.id !== "child-2"));
  assert.equal(afterSiblingDeletion.find((item) => item.id === "child-1")?.outlineRef, "R1.1");
});

test("dispatcher keeps batch validation atomic and completion gated", async () => {
  let batchCalls = 0;
  let createCalls = 0;
  let updateCalls = 0;
  const store = {
    async createTask() { createCalls += 1; return null; },
    async createTasks() { batchCalls += 1; return []; },
    async updateTask() { updateCalls += 1; return null; },
    async deleteTask() { return false; }
  };
  const dispatch = createTaskTreeDispatcher({ store: store as TaskTreeStore });

  const batch = await dispatch({ operation: "task.create-batch", groupId: "tg_1", titles: ["valid", "invalid\nline"] });
  assert.equal(batch.ok, false);
  assert.equal(batch.error?.code, "invalid_title");
  assert.equal(batchCalls, 0);

  const invalidRange = await dispatch({
    operation: "task.create",
    groupId: "tg_1",
    title: "Invalid range",
    startAt: "2026-07-19T00:00:00.000Z",
    dueAt: "2026-07-18T00:00:00.000Z"
  });
  assert.equal(invalidRange.ok, false);
  assert.equal(invalidRange.error?.code, "invalid_time_range");
  assert.equal(createCalls, 0);

  const bypass = await dispatch({ operation: "task.update", taskId: "tt_1", status: "completed" });
  assert.equal(bypass.ok, false);
  assert.equal(bypass.error?.code, "completion_command_required");
  assert.equal(updateCalls, 0);

  const missing = await dispatch({ operation: "task.delete", taskId: "tt_missing" });
  assert.equal(missing.ok, false);
  assert.equal(missing.error?.code, "task_not_found");

  const workflow = await dispatch({ operation: "workflow.start", taskId: "tt_1" });
  assert.equal(workflow.ok, false);
  assert.equal(workflow.error?.code, "temporal_address_required");
});

test("timeline projects lifecycle timestamps and expands parents around descendants", () => {
  const tasks = projectTimelineTasks([
    {
      id: "tt_root", groupId: "tg_1", parentId: null, kind: "task", title: "Root", description: "", status: "in_progress",
      startAt: "2026-07-21T10:00:00.000Z", dueAt: "2026-07-21T11:00:00.000Z", sortOrder: 0,
      createdAt: "2026-07-21T10:00:00.000Z", updatedAt: "2026-07-21T11:00:00.000Z"
    },
    {
      id: "tt_child", groupId: "tg_1", parentId: "tt_root", kind: "subtask", title: "Child", description: "", status: "completed",
      startAt: null, dueAt: null, sortOrder: 1,
      createdAt: "2026-07-21T09:00:00.000Z", updatedAt: "2026-07-21T12:00:00.000Z"
    },
    {
      id: "tt_grandchild", groupId: "tg_1", parentId: "tt_child", kind: "subsubtask", title: "Grandchild", description: "", status: "pending",
      startAt: null, dueAt: "2026-07-21T15:00:00.000Z", sortOrder: 2,
      createdAt: "2026-07-21T13:00:00.000Z", updatedAt: "2026-07-21T13:00:00.000Z"
    }
  ], [{
    id: "tr_1", taskId: "tt_child", title: "Execution", body: "done", status: "succeeded",
    createdAt: "2026-07-21T14:00:00.000Z"
  }]);

  assert.deepEqual(tasks.map(({ id, startAt, dueAt }) => ({ id, startAt, dueAt })), [
    { id: "tt_root", startAt: "2026-07-21T09:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" },
    { id: "tt_child", startAt: "2026-07-21T09:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" },
    { id: "tt_grandchild", startAt: "2026-07-21T13:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" }
  ]);
});

test("native PostgreSQL store exposes overview and three task levels", async () => {
  const databaseUrl = process.env.TASKTREE_DATABASE_URL;
  assert.ok(databaseUrl, "TASKTREE_DATABASE_URL is required for the TaskTree store integration test");
  const store = new TaskTreeStore(databaseUrl);
  const group = await store.createGroup(`TaskTree test ${Date.now()}`);
  const otherGroup = await store.createGroup(`TaskTree cross-group test ${Date.now()}`);
  try {
    const task = await store.createTask({
      groupId: group.id,
      title: "Root task",
      startAt: "2026-07-16T00:00:00.000Z",
      dueAt: "2026-07-18T00:00:00.000Z"
    });
    const subtask = await store.createTask({ groupId: group.id, parentId: task.id, title: "Subtask" });
    const subsubtask = await store.createTask({ groupId: group.id, parentId: subtask.id, title: "Subsubtask" });
    const batch = await store.createTasks({ groupId: group.id, parentId: task.id, titles: ["Batch one", "Batch two"] });
    assert.deepEqual(batch.map((item) => item.title), ["Batch one", "Batch two"]);
    await store.createMilestone({ groupId: group.id, taskId: task.id, title: "Review", occursAt: "2026-07-17T00:00:00.000Z" });
    await assert.rejects(
      store.createMilestone({ groupId: otherGroup.id, taskId: task.id, title: "Wrong group", occursAt: "2026-07-17T00:00:00.000Z" }),
      (error: any) => error?.code === "task_group_mismatch"
    );
    await assert.rejects(
      store.createMilestone({ groupId: "tg_missing", title: "Missing group", occursAt: "2026-07-17T00:00:00.000Z" }),
      (error: any) => error?.code === "group_not_found"
    );
    await assert.rejects(
      store.createMilestone({ groupId: group.id, taskId: "tt_missing", title: "Missing task", occursAt: "2026-07-17T00:00:00.000Z" }),
      (error: any) => error?.code === "task_not_found"
    );
    await assert.rejects(
      store.createTask({
        groupId: group.id,
        title: "Invalid range",
        startAt: "2026-07-19T00:00:00.000Z",
        dueAt: "2026-07-18T00:00:00.000Z"
      }),
      (error: any) => error?.code === "invalid_time_range"
    );
    await assert.rejects(
      store.updateTask(task.id, { dueAt: "2026-07-15T00:00:00.000Z" }),
      (error: any) => error?.code === "invalid_time_range"
    );
    assert.equal((await store.getTask(task.id))?.dueAt, "2026-07-18T00:00:00.000Z");
    await assert.rejects(
      store.completeTask(task.id),
      (error: any) => error?.code === "execution_report_required"
    );
    const firstReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
    const repeatedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
    const changedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed with evidence" });
    assert.equal(firstReport.mutation, true);
    assert.equal(repeatedReport.mutation, false);
    assert.equal(changedReport.mutation, true);
    assert.equal(firstReport.report.id, repeatedReport.report.id);
    assert.equal(firstReport.report.id, changedReport.report.id);
    assert.equal((await store.completeTask(task.id))?.status, "completed");

    await assert.rejects(
      store.createTask({ groupId: group.id, parentId: subsubtask.id, title: "Unsupported fourth level" }),
      (error: any) => error?.code === "maximum_depth_exceeded"
    );
    const countBeforeRejectedBatch = (await store.listTasks(group.id)).length;
    await assert.rejects(
      store.createTasks({ groupId: group.id, parentId: subsubtask.id, titles: ["Unsupported one", "Unsupported two"] }),
      (error: any) => error?.code === "maximum_depth_exceeded"
    );
    assert.equal((await store.listTasks(group.id)).length, countBeforeRejectedBatch);
    const timeline = await store.timeline(group.id);
    assert.equal(timeline?.tasks.length, 5);
    assert.equal(timeline?.tasks[0]?.kind, "task");
    assert.equal(timeline?.tasks[1]?.kind, "subtask");
    assert.equal(timeline?.tasks[2]?.kind, "subsubtask");
    assert.equal(timeline?.milestones.length, 1);
    assert.equal(timeline?.reports.length, 1);
    assert.ok(timeline?.tasks.every((item) => item.startAt !== null && item.dueAt !== null));
    const timelineStart = Math.min(...timeline!.tasks.map((item) => new Date(item.startAt!).valueOf()));
    const timelineDue = Math.max(...timeline!.tasks.map((item) => new Date(item.dueAt!).valueOf()));
    assert.equal(timeline?.tasks[0]?.startAt, new Date(timelineStart).toISOString());
    assert.equal(timeline?.tasks[0]?.dueAt, new Date(timelineDue).toISOString());
    assert.equal((await store.getTask(subtask.id))?.title, "Subtask");
    assert.equal((await store.listReports(task.id)).length, 1);
    assert.equal((await store.getReport(firstReport.report.id))?.body, "passed with evidence");
    const stats = await store.groupStats(group.id);
    assert.equal(stats.total, 5);
    assert.equal(stats.completed, 1);
    assert.equal(stats.reports, 1);
    const overview = (await store.groupOverview()).find((item) => item.group.id === group.id);
    assert.equal(overview?.taskCount, 1);
    assert.equal(overview?.subtaskCount, 3);
    assert.equal(overview?.subsubtaskCount, 1);
    assert.equal(overview?.reportCount, 1);
    assert.equal(overview?.startAt, new Date(timelineStart).toISOString());
    assert.equal(overview?.dueAt, new Date(timelineDue).toISOString());
  } finally {
    await store.deleteGroup(otherGroup.id);
    await store.deleteGroup(group.id);
    await store.close();
  }
});
