import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, relative } from "node:path";
import { test } from "bun:test";

import {
  canonicalSnapshot,
  parseManifest,
  parseSnapshot,
  restoreDiff,
  snapshotManifest,
  snapshotSha256,
  verifySnapshotManifest,
  type TaskTreeSnapshot,
} from "./backup-contracts.ts";
import { TaskTreeGitBackup } from "./backup-git.ts";
import { startTaskTreeBackupScheduler, type TaskTreeBackupService } from "./backup.ts";

const snapshot: TaskTreeSnapshot = {
  schemaVersion: 1,
  groups: [
    {
      id: "tg_1",
      name: "Backup",
      description: "",
      createdAt: "2026-07-18T00:00:00.000Z",
      updatedAt: "2026-07-18T00:00:00.000Z",
    },
  ],
  tasks: [
    {
      id: "tt_2",
      groupId: "tg_1",
      parentId: "tt_1",
      kind: "subtask",
      title: "Child",
      description: "",
      status: "pending",
      startAt: null,
      dueAt: null,
      sortOrder: 1,
      createdAt: "2026-07-18T00:00:00.000Z",
      updatedAt: "2026-07-18T00:00:00.000Z",
    },
    {
      id: "tt_1",
      groupId: "tg_1",
      parentId: null,
      kind: "task",
      title: "Root",
      description: "",
      status: "in_progress",
      startAt: "2026-07-18T00:00:00.000Z",
      dueAt: "2026-07-19T00:00:00.000Z",
      sortOrder: 0,
      createdAt: "2026-07-18T00:00:00.000Z",
      updatedAt: "2026-07-18T00:00:00.000Z",
    },
  ],
  milestones: [],
  reports: [
    {
      id: "tr_1",
      taskId: "tt_1",
      title: "Execution",
      body: "Passed.",
      status: "succeeded",
      createdAt: "2026-07-18T00:00:00.000Z",
    },
  ],
};

test("TaskTree backup canonical JSON and SHA ignore entity input order", () => {
  const reversed = { ...snapshot, tasks: [...snapshot.tasks].reverse() };
  assert.equal(canonicalSnapshot(snapshot), canonicalSnapshot(reversed));
  assert.equal(snapshotSha256(snapshot), snapshotSha256(reversed));
  assert.deepEqual(parseSnapshot(canonicalSnapshot(snapshot)), reversed);
});

test("TaskTree backup manifest rejects changed snapshot content", () => {
  const manifest = snapshotManifest(snapshot, {
    target: "development",
    generatedAt: "2026-07-18T01:00:00.000Z",
  });
  assert.doesNotThrow(() => verifySnapshotManifest(snapshot, parseManifest(JSON.stringify(manifest))));
  const changed = {
    ...snapshot,
    tasks: snapshot.tasks.map((task) => task.id === "tt_1" ? { ...task, title: "Changed" } : task),
  };
  assert.throws(
    () => verifySnapshotManifest(changed, manifest),
    (error: any) => error?.code === "backup_content_sha_mismatch",
  );
});

test("TaskTree restore diff reports create, update, and delete independently", () => {
  const incoming: TaskTreeSnapshot = {
    ...snapshot,
    tasks: [
      { ...snapshot.tasks[0], title: "Updated child" },
      {
        ...snapshot.tasks[1],
        id: "tt_3",
        parentId: null,
        title: "New root",
      },
    ],
  };
  assert.deepEqual(restoreDiff(snapshot, incoming).tasks, { create: 1, update: 1, delete: 1 });
});

test("TaskTree Git backup accepts a relative worktree path and deduplicates identical content", async () => {
  const root = mkdtempSync(join(tmpdir(), "tasktree-backup-test-"));
  const remote = join(root, "remote.git");
  const worktree = join(root, "worktree");
  try {
    const init = Bun.spawnSync(["git", "init", "--bare", remote], { stdout: "pipe", stderr: "pipe" });
    assert.equal(init.exitCode, 0);
    const backup = new TaskTreeGitBackup({
      repoSshUrl: remote,
      branch: "main",
      basePath: "tasktree/test",
      worktreePath: relative(process.cwd(), worktree),
      target: "test",
      sshCommand: "",
      authorName: "TaskTree Test",
      authorEmail: "tasktree-test@example.invalid",
      commitMessagePrefix: "tasktree:",
    });
    const first = await backup.publish(snapshot, "test snapshot");
    const second = await backup.publish(snapshot, "duplicate snapshot");
    assert.equal(first.changed, true);
    assert.equal(second.changed, false);
    assert.equal(second.gitCommit, first.gitCommit);
    assert.equal(backup.readCurrent()?.manifest.contentSha256, snapshotSha256(snapshot));
    assert.equal(JSON.parse(readFileSync(join(worktree, "tasktree/test/manifest.json"), "utf8")).target, "test");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("TaskTree backup scheduler waits for an in-flight snapshot before stopping", async () => {
  let finish!: () => void;
  const createFinished = new Promise<void>((resolve) => {
    finish = resolve;
  });
  const service = {
    config: { enabled: true, intervalMs: 60_000 },
    async create() {
      await createFinished;
      return { changed: false };
    },
  } as unknown as TaskTreeBackupService;
  const scheduler = startTaskTreeBackupScheduler(service);
  await Bun.sleep(10);
  let stopped = false;
  const stopping = scheduler.stop().then(() => {
    stopped = true;
  });
  await Bun.sleep(10);
  assert.equal(stopped, false);
  finish();
  await stopping;
  assert.equal(stopped, true);
});
