#!/usr/bin/env node
/**
 * Freight e2e launcher — one command, no manual steps:
 *
 *   node e2e/freight/scripts/e2e.mjs <up|run|open|ci|down> [cypress args...]
 *
 * - run/open/ci auto-start the docker stack (build + wait healthy) if it
 *   isn't already running, then launch Cypress pointed at the right ports.
 * - Host ports default to 3101/5373/5383/5533/9310/9311; any default that is
 *   busy is replaced by the next free port. Chosen ports are written to
 *   e2e/freight/.e2e-ports.json (gitignored) and reused while the stack is
 *   up, so cypress and compose always agree.
 * - Extra args are forwarded to Cypress: `pnpm e2e:freight:run --spec ...`.
 *
 * No dependencies — plain Node, spawns `docker compose` and `pnpm`.
 */

import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const e2eDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(e2eDir, "..", "..");
const stateFile = join(e2eDir, ".e2e-ports.json");
const composeBase = ["compose", "-f", join(repoRoot, "docker-compose.e2e.yaml")];

const DEFAULT_PORTS = {
  E2E_API_PORT: 3101,
  E2E_PORTAL_PORT: 5373,
  E2E_BACKOFFICE_PORT: 5383,
  E2E_DB_PORT: 5533,
  E2E_MINIO_PORT: 9310,
  E2E_MINIO_CONSOLE_PORT: 9311,
};

// Long-running services that must be up before tests (minio-init exits).
const SERVICES = [
  "postgres-freight-e2e",
  "minio-e2e",
  "freight-api-e2e",
  "freight-portal-e2e",
  "freight-backoffice-e2e",
];

function fail(msg) {
  console.error(`\ne2e: ${msg}`);
  process.exit(1);
}

function preflight() {
  try {
    execFileSync("docker", ["info"], { stdio: "ignore" });
  } catch {
    fail("docker is not running (or not installed) — start Docker and retry.");
  }
  if (!existsSync(join(repoRoot, ".npmrc"))) {
    fail(
      ".npmrc missing at repo root — image builds need GitHub Packages auth " +
        "for @tria-plc (same file the main docker-compose.yaml uses).",
    );
  }
}

function isPortFree(port) {
  return new Promise((res) => {
    const srv = createServer();
    srv.once("error", () => res(false));
    srv.once("listening", () => srv.close(() => res(true)));
    srv.listen(port);
  });
}

function stackRunning(env) {
  try {
    const out = execFileSync(
      "docker",
      [...composeBase, "ps", "--services", "--status", "running"],
      { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] },
    );
    const running = new Set(out.split("\n").filter(Boolean));
    return SERVICES.every((s) => running.has(s));
  } catch {
    return false;
  }
}

/**
 * While the stack runs, ports are whatever it was started with (state file,
 * else the defaults — a hand-started stack used the compose defaults). Only a
 * fresh start gets to scan for free ports.
 */
async function resolvePorts() {
  if (stackRunning(process.env)) {
    return existsSync(stateFile)
      ? JSON.parse(readFileSync(stateFile, "utf8"))
      : { ...DEFAULT_PORTS };
  }
  const ports = {};
  const taken = new Set();
  for (const [name, preferred] of Object.entries(DEFAULT_PORTS)) {
    let port = preferred;
    while (taken.has(port) || !(await isPortFree(port))) port += 1;
    taken.add(port);
    ports[name] = port;
    if (port !== preferred)
      console.log(`e2e: port ${preferred} busy → ${name}=${port}`);
  }
  return ports;
}

function envFor(ports) {
  return {
    ...process.env,
    ...Object.fromEntries(
      Object.entries(ports).map(([k, v]) => [k, String(v)]),
    ),
    CYPRESS_BASE_URL: `http://localhost:${ports.E2E_BACKOFFICE_PORT}`,
    CYPRESS_API_URL: `http://localhost:${ports.E2E_API_PORT}`,
    CYPRESS_PORTAL_URL: `http://localhost:${ports.E2E_PORTAL_PORT}`,
    E2E_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${ports.E2E_DB_PORT}/edr_freight_e2e`,
  };
}

function compose(args, env) {
  const { status } = spawnSync("docker", [...composeBase, ...args], {
    stdio: "inherit",
    env,
  });
  return status ?? 1;
}

function up(ports, env) {
  preflight();
  console.log(
    `e2e: starting stack — api :${ports.E2E_API_PORT}  portal :${ports.E2E_PORTAL_PORT}  backoffice :${ports.E2E_BACKOFFICE_PORT}  db :${ports.E2E_DB_PORT}`,
  );
  const status = compose(["up", "-d", "--build", "--wait"], env);
  if (status !== 0)
    fail(
      "stack failed to become healthy. Inspect with:\n" +
        "  docker compose -f docker-compose.e2e.yaml logs freight-api-e2e",
    );
  writeFileSync(stateFile, JSON.stringify(ports, null, 2) + "\n");
}

function ensureUp(ports, env) {
  if (stackRunning(env)) return;
  up(ports, env);
}

function runPnpm(script, extra, env) {
  const { status } = spawnSync(
    "pnpm",
    ["--filter", "@edr/freight-e2e", "run", script, ...extra],
    { cwd: repoRoot, stdio: "inherit", env },
  );
  process.exit(status ?? 1);
}

const [cmd, ...extra] = process.argv.slice(2);
const ports = await resolvePorts();
const env = envFor(ports);

switch (cmd) {
  case "up":
    up(ports, env);
    break;
  case "run":
    ensureUp(ports, env);
    runPnpm("cy:run", extra, env);
    break;
  case "open":
    ensureUp(ports, env);
    runPnpm("cy:open", extra, env);
    break;
  case "ci":
    ensureUp(ports, env);
    process.exit(compose(["--profile", "cypress", "run", "--rm", "cypress", ...extra], env));
    break;
  case "down":
    process.exit(
      (() => {
        const status = compose(["down", "-v", "--remove-orphans"], env);
        rmSync(stateFile, { force: true });
        return status;
      })(),
    );
    break;
  default:
    fail(`unknown command "${cmd ?? ""}" — use up | run | open | ci | down`);
}
