import { file, spawn } from "bun";
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
import { mkdir, writeFile } from "fs/promises";
import { bunExe, bunEnv as env, tmpdirSync } from "harness";
import { join, relative } from "path";
import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry";

beforeAll(dummyBeforeAll);
afterAll(dummyAfterAll);

let remove_dir: string;

beforeEach(async () => {
  remove_dir = tmpdirSync();
  await dummyBeforeEach();
});

afterEach(async () => {
  await dummyAfterEach();
});

it("should remove existing package", async () => {
  const pkg1_dir = join(remove_dir, "pkg1");
  const pkg1_path = relative(package_dir, pkg1_dir);
  await mkdir(pkg1_dir);
  const pkg2_dir = join(remove_dir, "pkg2");
  const pkg2_path = relative(package_dir, pkg2_dir);
  await mkdir(pkg2_dir);

  await writeFile(
    join(pkg1_dir, "package.json"),
    JSON.stringify({
      name: "pkg1",
      version: "0.0.1",
    }),
  );
  await writeFile(
    join(pkg2_dir, "package.json"),
    JSON.stringify({
      name: "pkg2",
      version: "0.0.1",
    }),
  );
  await writeFile(
    join(package_dir, "package.json"),
    JSON.stringify({
      name: "foo",
      version: "0.0.2",
    }),
  );
  const { exited: exited1 } = spawn({
    cmd: [bunExe(), "add", `file:${pkg1_path}`.replace(/\\/g, "\\\\")],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await exited1).toBe(0);
  const { exited: exited2 } = spawn({
    cmd: [bunExe(), "add", `file:${pkg2_path}`.replace(/\\/g, "\\\\")],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await exited2).toBe(0);
  expect(await file(join(package_dir, "package.json")).text()).toEqual(
    JSON.stringify(
      {
        name: "foo",
        version: "0.0.2",
        dependencies: {
          pkg1: `file:${pkg1_path.replace(/\\/g, "/")}`,
          pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
        },
      },
      null,
      2,
    ),
  );

  const {
    exited: removeExited1,
    stdout: stdout1,
    stderr: stderr1,
  } = spawn({
    cmd: [bunExe(), "remove", "pkg1"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await removeExited1).toBe(0);
  const out1 = await new Response(stdout1).text();
  const err1 = await new Response(stderr1).text();

  expect(out1.replace(/\s*\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
    expect.stringContaining("bun remove v1."),
    "",
    `+ pkg2@${pkg2_path.replace(/\\/g, "/")}`,
    "",
    "1 package installed",
    "Removed: 1",
    "",
  ]);
  expect(err1.split(/\r?\n/)).toEqual(["Saved lockfile", ""]);
  expect(await file(join(package_dir, "package.json")).text()).toEqual(
    JSON.stringify(
      {
        name: "foo",
        version: "0.0.2",
        dependencies: {
          pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`,
        },
      },
      null,
      2,
    ),
  );

  const {
    exited: removeExited2,
    stdout: stdout2,
    stderr: stderr2,
  } = spawn({
    cmd: [bunExe(), "remove", "pkg2"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await removeExited2).toBe(0);
  const out2 = await new Response(stdout2).text();
  const err2 = await new Response(stderr2).text();

  expect(out2.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
    expect.stringContaining("bun remove v1."),
    "",
    "- pkg2",
    "1 package removed",
    "",
  ]);
  expect(err2.split(/\r?\n/)).toEqual(["", "package.json has no dependencies! Deleted empty lockfile", ""]);
  expect(await file(join(package_dir, "package.json")).text()).toEqual(
    JSON.stringify(
      {
        name: "foo",
        version: "0.0.2",
      },
      null,
      2,
    ),
  );
});

it("should not reject missing package", async () => {
  await writeFile(
    join(package_dir, "package.json"),
    JSON.stringify({
      name: "foo",
      version: "0.0.1",
    }),
  );
  await writeFile(
    join(remove_dir, "package.json"),
    JSON.stringify({
      name: "pkg1",
      version: "0.0.2",
    }),
  );
  const pkg_path = relative(package_dir, remove_dir);
  const { exited: addExited } = spawn({
    cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await addExited).toBe(0);

  const { exited: rmExited } = spawn({
    cmd: [bunExe(), "remove", "pkg2"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await rmExited).toBe(0);
});

it("should not affect if package is not installed", async () => {
  await writeFile(
    join(package_dir, "package.json"),
    JSON.stringify({
      name: "foo",
      version: "0.0.1",
    }),
  );

  const { stdout, stderr, exited } = spawn({
    cmd: [bunExe(), "remove", "pkg"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await exited).toBe(0);
  const out = await new Response(stdout).text();
  expect(out.split("\n")).toEqual([expect.stringContaining("bun remove v1."), ""]);
  const err = await new Response(stderr).text();
  expect(err.replace(/ \[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
    "package.json doesn't have dependencies, there's nothing to remove!",
    "",
  ]);
});

it("should retain a new line in the end of package.json", async () => {
  await writeFile(
    join(package_dir, "package.json"),
    JSON.stringify({
      name: "foo",
      version: "0.0.1",
    }),
  );
  await writeFile(
    join(remove_dir, "package.json"),
    JSON.stringify({
      name: "pkg",
      version: "0.0.2",
    }),
  );
  const pkg_path = relative(package_dir, remove_dir);
  const { exited: addExited } = spawn({
    cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await addExited).toBe(0);
  const content_before_remove = await file(join(package_dir, "package.json")).text();
  expect(content_before_remove.endsWith("}")).toBe(true);
  expect(content_before_remove).toEqual(
    JSON.stringify(
      {
        name: "foo",
        version: "0.0.1",
        dependencies: {
          pkg: `file:${pkg_path.replace(/\\/g, "/")}`,
        },
      },
      null,
      2,
    ),
  );
  await writeFile(join(package_dir, "package.json"), content_before_remove + "\n");

  const { exited } = spawn({
    cmd: [bunExe(), "remove", "pkg"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  expect(await exited).toBe(0);
  const content_after_remove = await file(join(package_dir, "package.json")).text();
  expect(content_after_remove.endsWith("}\n")).toBe(true);
  expect(content_after_remove).toEqual(
    JSON.stringify(
      {
        name: "foo",
        version: "0.0.1",
      },
      null,
      2,
    ) + "\n",
  );
});

it("should remove peerDependencies", async () => {
  await writeFile(
    join(package_dir, "package.json"),
    JSON.stringify({
      name: "foo",
      peerDependencies: {
        bar: "~0.0.1",
      },
    }),
  );
  const { stdout, stderr, exited } = spawn({
    cmd: [bunExe(), "remove", "bar"],
    cwd: package_dir,
    stdout: "pipe",
    stdin: "pipe",
    stderr: "pipe",
    env,
  });
  const err = await new Response(stderr).text();
  expect(err).not.toContain("error:");
  const out = await new Response(stdout).text();
  expect(out.replace(/\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([
    expect.stringContaining("bun remove v1."),
    "",
    " done",
    "",
  ]);
  expect(await exited).toBe(0);
  expect(await file(join(package_dir, "package.json")).json()).toEqual({
    name: "foo",
  });
});
