// The process wrapper against the real oxy binary on a scratch repository.
// Runs with `npm test`; needs oxy on PATH, at ~/.local/bin/oxy, or in OXY_BIN.

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Oxy, findBinary } from "../out/oxy.mjs";

const binary = process.env.OXY_BIN ?? findBinary(undefined);
const oxy = new Oxy(binary);

test("root, status, cat, log and commit round trip", async () => {
  const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-"));
  const home = join(dir, "home");
  mkdirSync(home);
  process.env.OXY_HOME = home;
  await oxy.check(dir, ["init", "repo"]);
  const root = join(dir, "repo");
  await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "t@example.org"]);

  const version = await oxy.version();
  assert.equal(version.protocol, 1);
  assert.equal(await oxy.root(dir), null, "outside a repository");
  const info = await oxy.root(root);
  assert.equal(info?.bookmark, "main");
  assert.ok(info?.root.endsWith("repo"));

  writeFileSync(join(root, "a.txt"), "one\n");
  let status = await oxy.status(root);
  assert.deepEqual(status.paths, [{ path: "a.txt", status: "Added" }]);
  assert.equal(status.clean, false);
  await oxy.commit(root, "first");
  status = await oxy.status(root);
  assert.equal(status.clean, true);

  writeFileSync(join(root, "a.txt"), "one\ntwo\n");
  assert.equal((await oxy.cat(root, "a.txt")).toString(), "one\n", "the base version");
  const log = await oxy.log(root, 10);
  assert.equal(log.changes.filter((c) => !c.working_copy).length, 1);
  assert.equal(log.changes.find((c) => !c.working_copy)?.signature?.status, "unsigned");
  assert.ok((await oxy.prompt(root)).includes("main"));
  const bookmarks = await oxy.bookmarks(root);
  assert.deepEqual(bookmarks.map((b) => [b.name, b.current]), [["main", true]]);
  await assert.rejects(oxy.cat(root, "missing.txt"), /not in that state/);

  // Amend folds the edit into the single head.
  await oxy.amend(root, "first, amended");
  assert.equal((await oxy.status(root)).clean, true);
  assert.ok((await oxy.log(root, 10)).changes.some((c) => c.message === "first, amended"));

  // Hunks: commit only b.txt's, leave a.txt's edit, then discard it.
  writeFileSync(join(root, "a.txt"), "one\ntwo\nthree\n");
  writeFileSync(join(root, "b.txt"), "b\n");
  const hunks = await oxy.hunks(root);
  assert.ok(hunks.length >= 2, JSON.stringify(hunks));
  const bHunk = hunks.find((h) => h.path === "b.txt");
  await oxy.commitHunks(root, "b only", [bHunk.index]);
  status = await oxy.status(root);
  assert.deepEqual(status.paths.map((p) => p.path), ["a.txt"], "a.txt stays uncommitted");
  assert.equal((await oxy.cat(root, "b.txt")).toString(), "b\n", "committed with its content");
  await oxy.discard(root, ["a.txt"]);
  assert.equal((await oxy.status(root)).clean, true);

  // The operation log, and undo of the last operation (the commit).
  const ops = await oxy.opLog(root);
  assert.ok(ops[0].current && ops.length > 3);
  assert.match(await oxy.undo(root), /Undone/);
  status = await oxy.status(root);
  assert.ok(status.paths.some((p) => p.path === "b.txt"), "the commit is undone; b.txt is uncommitted work again");
});

test("bookmarks, remotes and sync against a local origin", async () => {
  const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-sync-"));
  const home = join(dir, "home");
  mkdirSync(home);
  process.env.OXY_HOME = home;
  await oxy.check(dir, ["init", "origin"]);
  await oxy.clone(dir, join(dir, "origin"), join(dir, "work"));
  const root = join(dir, "work");
  writeFileSync(join(root, "a.txt"), "a\n");
  await oxy.commit(root, "a");
  const remotes = await oxy.remotes(root);
  assert.equal(remotes[0].name, "origin");
  let push = await oxy.push(root);
  assert.equal(push[0].ok, true);
  let bookmarks = await oxy.bookmarks(root);
  assert.deepEqual(bookmarks.map((b) => [b.name, b.current, b.ahead, b.behind]), [["main", true, 0, 0]]);

  await oxy.bookmarkCreate(root, "feature");
  await oxy.bookmarkSwitch(root, "feature");
  writeFileSync(join(root, "a.txt"), "a\nfeature\n");
  await oxy.commit(root, "feature work");
  bookmarks = await oxy.bookmarks(root);
  assert.equal(bookmarks.find((b) => b.current)?.name, "feature");
  await oxy.bookmarkSwitch(root, "main");
  assert.equal((await oxy.cat(root, "a.txt")).toString(), "a\n");
  writeFileSync(join(root, "b.txt"), "b\n");
  await assert.rejects(oxy.bookmarkSwitch(root, "feature"), /has changes/);
  await oxy.commit(root, "b on main");
  assert.match(await oxy.merge(root, "feature"), /Merged/);
  assert.equal((await oxy.cat(root, "a.txt")).toString(), "a\nfeature\n");
  push = await oxy.push(root, "origin", "feature");
  assert.equal(push[0].ok, true);
  push = await oxy.push(root);
  assert.equal(push[0].ok, true);
  const fetch = await oxy.fetch(root);
  assert.equal(fetch.remote, "origin");
  const pull = await oxy.pull(root);
  assert.equal(pull.changed, false);
  await oxy.bookmarkDelete(root, "feature");
  bookmarks = await oxy.bookmarks(root);
  assert.deepEqual(bookmarks.map((b) => b.name), ["main"]);

  // Blame, show, diff text, and file history.
  const blame = await oxy.blame(root, "a.txt");
  assert.deepEqual(blame.map((l) => l.line), [1, 2]);
  assert.notEqual(blame[0].hash, blame[1].hash, "two changes introduced the two lines");
  const show = await oxy.show(root, `#${blame[1].hash.slice(0, 12)}`);
  assert.equal(show.message, "feature work");
  assert.deepEqual(show.files.map((f) => f.path), ["a.txt"]);
  assert.match(await oxy.diffText(root, `#${blame[1].hash.slice(0, 12)}`), /\+feature/);
  const history = await oxy.log(root, 50, "a.txt");
  assert.equal(history.changes.filter((c) => !c.working_copy).length, 2);

  // Tags, workspaces, trust, bisect status, subrepos.
  await oxy.tagCreate(root, "v1", [], "first");
  const tags = await oxy.tags(root);
  assert.deepEqual(tags.map((t) => [t.name, t.message]), [["v1", "first"]]);
  const workspaces = await oxy.workspaces(root);
  assert.equal(workspaces.length, 1);
  assert.equal(workspaces[0].current, true);
  assert.equal(await oxy.policyShow(root), null, "no policy yet");
  const verify = await oxy.verify(root);
  assert.ok(verify.every((v) => v.status === "unsigned"));
  assert.equal(await oxy.bisectStatus(root), null);
  assert.deepEqual(await oxy.submodules(root), []);
  await oxy.bisectStart(root);
  assert.equal((await oxy.bisectStatus(root))?.state, "need-marks");
  await oxy.bisectReset(root);
});


test("split, squash and rebase through the history actions, each undoable", async () => {
  const dir = mkdtempSync(join(tmpdir(), "oxy-vscode-hist-"));
  const home = join(dir, "home");
  mkdirSync(home);
  process.env.OXY_HOME = home;
  await oxy.check(dir, ["init", "repo"]);
  const root = join(dir, "repo");
  await oxy.check(root, ["identity", "create", "--name", "Test", "--email", "t@example.org"]);
  writeFileSync(join(root, "a.txt"), "a\n");
  await oxy.commit(root, "one");
  writeFileSync(join(root, "b.txt"), "b\n");
  writeFileSync(join(root, "c.txt"), "c\n");
  await oxy.commit(root, "two files");
  const find = async (m) => (await oxy.log(root, 50)).changes.find((c) => !c.working_copy && c.message.startsWith(m));

  // Split: b.txt becomes its own change, c.txt stays.
  const two = await find("two files");
  const out = await oxy.split(root, `#${two.hash.slice(0, 12)}`, ["b.txt"]);
  assert.match(out, /Split into/);
  const part = await find("two files (part 1");
  assert.ok(part, "the first part exists");
  assert.deepEqual((await oxy.show(root, `#${part.hash.slice(0, 12)}`)).files.map((f) => f.path), ["b.txt"]);
  await oxy.undo(root);
  assert.equal(await find("two files (part 1"), undefined, "undo reverses the split");

  // Squash: an edit of a.txt folds into "one", which keeps its ChangeId.
  writeFileSync(join(root, "a.txt"), "a\na2\n");
  await oxy.commit(root, "edit a");
  const one = await find("one");
  const edit = await find("edit a");
  assert.deepEqual(edit.deps, [one.hash]);
  assert.match(await oxy.squash(root, `#${edit.hash.slice(0, 12)}`, `#${one.hash.slice(0, 12)}`), /Squashed into/);
  const after = await find("one");
  assert.equal(after.change_id, one.change_id);
  assert.equal(await find("edit a"), undefined);

  // Rebase onto a state that already has the change is refused, with oxy's reason.
  await assert.rejects(oxy.rebase(root, `#${after.hash.slice(0, 12)}`, ["main"]), /already part of the target state/);
});
