// Unit tests for server/src/localdb/paths.ts (PG Task 1). Pure string work over
// a passed-in environment — no filesystem, no os.homedir(), nothing that varies
// with the machine running the suite.
//
// These assert the layout in docs/todo/2026-08-08-bundled-postgres-decisions.md
// literally, including the two properties that exist to protect the books:
//   - nothing lives inside the app folder (UPDATING.md is "unzip over the top")
//   - data/ is keyed by MAJOR version, so a PG17 cluster lands BESIDE a PG16 one

import { describe, it, expect } from "vitest";
import {
  resolveLayout,
  resolveAppDataRoot,
  runtimeDirName,
  dataDirName,
  binaryPath,
  APP_DATA_FOLDER,
  type PathEnv,
} from "../../server/src/localdb/paths.js";

const WIN_ENV: PathEnv = { LOCALAPPDATA: "C:\\Users\\owner\\AppData\\Local" };
const MAC_ENV: PathEnv = { HOME: "/Users/owner" };

/** Unwraps a layout or fails the test with the reason, so every assertion below
 *  reads as a fact about the layout rather than a chain of `if (result.ok)`. */
function layoutOrThrow(...args: Parameters<typeof resolveLayout>) {
  const result = resolveLayout(...args);
  if (!result.ok) throw new Error(`expected a layout, got ${result.reason}: ${result.message}`);
  return result.layout;
}

describe("resolveLayout — Windows", () => {
  const layout = layoutOrThrow("win32-x64", WIN_ENV, "16.10");

  it("roots everything at %LOCALAPPDATA%\\BalancedSum", () => {
    expect(layout.root).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum");
  });

  it("matches the documented layout, path for path", () => {
    expect(layout.runtimeDir).toBe(
      "C:\\Users\\owner\\AppData\\Local\\BalancedSum\\runtime\\postgresql-16.10-win32-x64");
    expect(layout.binDir).toBe(`${layout.runtimeDir}\\bin`);
    expect(layout.libDir).toBe(`${layout.runtimeDir}\\lib`);
    expect(layout.shareDir).toBe(`${layout.runtimeDir}\\share`);
    expect(layout.dataDir).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum\\data\\pg16");
    expect(layout.logFile).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum\\logs\\postgresql.log");
    expect(layout.instanceFile).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum\\instance.json");
    expect(layout.secretFile).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum\\secret");
    expect(layout.postmasterPidFile).toBe(`${layout.dataDir}\\postmaster.pid`);
  });

  it("records both the full version and the major", () => {
    expect(layout.postgresVersion).toBe("16.10");
    expect(layout.postgresMajor).toBe(16);
  });
});

describe("resolveLayout — macOS", () => {
  // Asserted from Windows, deliberately: this is the only way the Mac layout is
  // checkable at all until someone has one.
  const layout = layoutOrThrow("darwin-arm64", MAC_ENV, "16.10");

  it("roots everything at ~/Library/Application Support/BalancedSum, with POSIX separators", () => {
    expect(layout.root).toBe("/Users/owner/Library/Application Support/BalancedSum");
    expect(layout.root).not.toContain("\\");
  });

  it("matches the documented layout, path for path", () => {
    expect(layout.runtimeDir).toBe(
      "/Users/owner/Library/Application Support/BalancedSum/runtime/postgresql-16.10-darwin-arm64");
    expect(layout.dataDir).toBe("/Users/owner/Library/Application Support/BalancedSum/data/pg16");
    expect(layout.logFile).toBe("/Users/owner/Library/Application Support/BalancedSum/logs/postgresql.log");
    expect(layout.instanceFile).toBe("/Users/owner/Library/Application Support/BalancedSum/instance.json");
    expect(layout.secretFile).toBe("/Users/owner/Library/Application Support/BalancedSum/secret");
  });

  it("gives Intel and Apple Silicon separate runtime directories on the same root", () => {
    const intel = layoutOrThrow("darwin-x64", MAC_ENV, "16.10");
    expect(intel.root).toBe(layout.root);
    expect(intel.dataDir).toBe(layout.dataDir);
    expect(intel.runtimeDir).not.toBe(layout.runtimeDir);
  });
});

describe("resolveLayout — the properties that protect the books", () => {
  it("puts nothing under the app folder — every path is under the user's application data", () => {
    const layout = layoutOrThrow("win32-x64", WIN_ENV, "16.10");
    for (const p of [layout.runtimeDir, layout.dataDir, layout.logFile, layout.instanceFile, layout.secretFile]) {
      expect(p.startsWith(layout.root)).toBe(true);
      expect(p).toContain(APP_DATA_FOLDER);
    }
  });

  it("keys data/ by MAJOR only, so a patch bump reuses the SAME cluster", () => {
    const a = layoutOrThrow("win32-x64", WIN_ENV, "16.10");
    const b = layoutOrThrow("win32-x64", WIN_ENV, "16.11");
    expect(b.dataDir).toBe(a.dataDir);
    // ...but a different runtime directory, so both tarballs can sit on disk
    // while one replaces the other.
    expect(b.runtimeDir).not.toBe(a.runtimeDir);
  });

  it("keys data/ so a MAJOR bump lands BESIDE the old cluster, never on top of it", () => {
    const pg16 = layoutOrThrow("win32-x64", WIN_ENV, "16.10");
    const pg17 = layoutOrThrow("win32-x64", WIN_ENV, "17.2");
    expect(pg17.dataDir).not.toBe(pg16.dataDir);
    expect(pg17.dataDir.endsWith("pg17")).toBe(true);
    expect(pg16.dataDir.endsWith("pg16")).toBe(true);
    expect(pg17.dataRoot).toBe(pg16.dataRoot);
  });
});

describe("resolveLayout — refusals", () => {
  it("refuses rather than guessing when LOCALAPPDATA is missing or blank", () => {
    for (const env of [{}, { LOCALAPPDATA: "" }, { LOCALAPPDATA: "   " }]) {
      const result = resolveLayout("win32-x64", env, "16.10");
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.reason).toBe("missing-localappdata");
        expect(result.message.length).toBeGreaterThan(0);
      }
    }
  });

  it("refuses rather than guessing when HOME is missing or blank", () => {
    for (const env of [{}, { HOME: "" }]) {
      const result = resolveLayout("darwin-arm64", env, "16.10");
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.reason).toBe("missing-home");
    }
  });

  it("refuses a version string with no major in it, rather than naming a directory pgNaN", () => {
    for (const bad of ["", "sixteen", "v16", ".16"]) {
      const result = resolveLayout("win32-x64", WIN_ENV, bad);
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.reason).toBe("bad-version");
    }
  });

  it("ignores the OTHER platform's variable — a Windows env doesn't satisfy macOS", () => {
    const result = resolveLayout("darwin-arm64", WIN_ENV, "16.10");
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.reason).toBe("missing-home");
  });
});

describe("resolveAppDataRoot", () => {
  it("answers without needing a version", () => {
    const win = resolveAppDataRoot("win32-x64", WIN_ENV);
    expect(win.ok).toBe(true);
    if (win.ok) expect(win.root).toBe("C:\\Users\\owner\\AppData\\Local\\BalancedSum");

    const mac = resolveAppDataRoot("darwin-x64", MAC_ENV);
    expect(mac.ok).toBe(true);
    if (mac.ok) expect(mac.root).toBe("/Users/owner/Library/Application Support/BalancedSum");
  });
});

describe("runtimeDirName / dataDirName", () => {
  it("spell the names the release tooling has to produce", () => {
    expect(runtimeDirName("16.10", "win32-x64")).toBe("postgresql-16.10-win32-x64");
    expect(runtimeDirName("16.10", "darwin-arm64")).toBe("postgresql-16.10-darwin-arm64");
    expect(dataDirName(16)).toBe("pg16");
    expect(dataDirName(17)).toBe("pg17");
  });
});

describe("binaryPath", () => {
  it("appends .exe on Windows only", () => {
    const win = layoutOrThrow("win32-x64", WIN_ENV, "16.10");
    expect(binaryPath(win, "initdb")).toBe(`${win.binDir}\\initdb.exe`);
    expect(binaryPath(win, "pg_dump")).toBe(`${win.binDir}\\pg_dump.exe`);
    expect(binaryPath(win, "postgres")).toBe(`${win.binDir}\\postgres.exe`);

    const mac = layoutOrThrow("darwin-arm64", MAC_ENV, "16.10");
    expect(binaryPath(mac, "initdb")).toBe(`${mac.binDir}/initdb`);
    expect(binaryPath(mac, "pg_isready")).toBe(`${mac.binDir}/pg_isready`);
  });

  it("covers the full toolset the trimmed tarball is required to contain", () => {
    // This is the reason zonky.io's three-executable build was rejected:
    // server/src/backup.ts needs pg_dump and RESTORE.md needs pg_restore.
    const win = layoutOrThrow("win32-x64", WIN_ENV, "16.10");
    for (const name of [
      "initdb", "postgres", "pg_ctl", "pg_isready", "pg_dump", "pg_dumpall", "pg_restore", "pg_upgrade", "psql",
    ] as const) {
      expect(binaryPath(win, name)).toBe(`${win.binDir}\\${name}.exe`);
    }
  });
});
