const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const {
  classifyProcessError,
  createProcessErrorController,
  installProcessErrorHandlers,
  isNonFatalNetworkError,
} = require("./processErrorGuards.cjs");

test("treats Chromium ERR_NETWORK_CHANGED as non-fatal", () => {
  assert.equal(
    isNonFatalNetworkError(new Error("net::ERR_NETWORK_CHANGED")),
    true,
  );
});

test("treats other Chromium net::ERR_* failures as non-fatal network errors", () => {
  assert.equal(
    isNonFatalNetworkError(new Error("net::ERR_INTERNET_DISCONNECTED")),
    true,
  );
  assert.equal(
    isNonFatalNetworkError(new Error("net::ERR_NAME_NOT_RESOLVED")),
    true,
  );
});

test("treats Node socket error codes as non-fatal network errors", () => {
  const err = new Error("socket reset");
  err.code = "ECONNRESET";
  assert.equal(isNonFatalNetworkError(err), true);

  const dnsErr = new Error("dns failed");
  dnsErr.code = "ENOTFOUND";
  assert.equal(isNonFatalNetworkError(dnsErr), true);
});

test("keeps non-network errors fatal", () => {
  assert.equal(
    isNonFatalNetworkError(new Error("Something else broke")),
    false,
  );
});

test("generic startup exceptions stay fatal before the app is up", () => {
  const result = classifyProcessError(new Error("boom"), {
    runtimeStarted: false,
  });

  assert.equal(result.action, "fatal");
});

test("generic runtime exceptions are suppressed after startup", () => {
  const result = classifyProcessError(new Error("boom"), {
    runtimeStarted: true,
  });

  assert.equal(result.action, "suppress");
  assert.match(result.reason, /runtime/i);
});

test("generic runtime promise rejections are also suppressed after startup", () => {
  const result = classifyProcessError(new Error("promise boom"), {
    runtimeStarted: true,
    origin: "unhandledRejection",
  });

  assert.equal(result.action, "suppress");
  assert.match(result.reason, /runtime/i);
});

test("controller keeps startup strict until the main window is actually shown", () => {
  const controller = createProcessErrorController();

  controller.beginMainWindowStartup();
  assert.equal(controller.isRuntimeProtectionActive(), false);

  controller.completeMainWindowStartup({ windowShown: true });
  assert.equal(controller.isRuntimeProtectionActive(), true);
});

test("controller becomes strict again while recreating a missing main window", () => {
  const controller = createProcessErrorController();

  controller.beginMainWindowStartup();
  controller.completeMainWindowStartup({ windowShown: true });
  assert.equal(controller.isRuntimeProtectionActive(), true);

  controller.beginMainWindowStartup();
  assert.equal(controller.isRuntimeProtectionActive(), false);

  controller.completeMainWindowStartup({ windowShown: false });
  assert.equal(controller.isRuntimeProtectionActive(), true);
});

test("startup-period errors stay fatal while recreating the main window", () => {
  const fakeProcess = new EventEmitter();
  const fatals = [];
  const controller = createProcessErrorController({
    captureError() {},
    onFatalError(err) {
      fatals.push(err.message);
      throw err;
    },
    logError() {},
    logWarn() {},
  });

  installProcessErrorHandlers(fakeProcess, controller);
  controller.completeMainWindowStartup({ windowShown: true });
  controller.beginMainWindowStartup();

  assert.throws(() => {
    fakeProcess.emit("uncaughtException", new Error("recreate boom"));
  }, /recreate boom/);
  assert.deepEqual(fatals, ["recreate boom"]);
});

test("fatal startup failures uninstall listeners and keep throwing", () => {
  const fakeProcess = new EventEmitter();
  const captured = [];
  const fatals = [];
  let uninstall = null;
  const controller = createProcessErrorController({
    captureError(source, err) {
      captured.push([source, err.message]);
    },
    onFatalError(err) {
      fatals.push(err.message);
      uninstall?.();
      throw err;
    },
    logError() {},
    logWarn() {},
  });

  uninstall = installProcessErrorHandlers(fakeProcess, controller);

  assert.throws(() => {
    fakeProcess.emit("uncaughtException", new Error("startup boom"));
  }, /startup boom/);
  assert.deepEqual(fatals, ["startup boom"]);
  assert.deepEqual(captured, [["uncaughtException", "startup boom"]]);
  assert.equal(fakeProcess.listenerCount("uncaughtException"), 0);
  assert.equal(fakeProcess.listenerCount("unhandledRejection"), 0);
});

test("installed handlers suppress runtime failures after startup", () => {
  const fakeProcess = new EventEmitter();
  const captured = [];
  const errors = [];
  const warnings = [];
  const controller = createProcessErrorController({
    captureError(source, err) {
      captured.push([source, err.message]);
    },
    onFatalError(err) {
      throw err;
    },
    logError(...args) {
      errors.push(args.map(String).join(" "));
    },
    logWarn(...args) {
      warnings.push(args.map(String).join(" "));
    },
  });

  installProcessErrorHandlers(fakeProcess, controller);

  controller.beginMainWindowStartup();
  controller.completeMainWindowStartup({ windowShown: true });

  fakeProcess.emit("uncaughtException", new Error("runtime boom"));
  fakeProcess.emit("unhandledRejection", new Error("runtime rejection"));
  assert.deepEqual(captured, [
    ["uncaughtException", "runtime boom"],
    ["unhandledRejection", "runtime rejection"],
  ]);
  assert.equal(errors.some((line) => line.includes("runtime error after startup")), true);
  assert.equal(warnings.length, 0);
});

test("unhandled rejection marks the forwarded error so uncaught follow-up is not double-captured", () => {
  const captured = [];
  const fatals = [];
  const controller = createProcessErrorController({
    captureError(source, err) {
      captured.push([source, err.message]);
    },
    onFatalError(err) {
      fatals.push(err);
    },
    logError() {},
    logWarn() {},
  });

  controller.handleUnhandledRejection(new Error("startup rejection"));
  assert.equal(fatals.length, 1);
  assert.equal(fatals[0].__fromUnhandledRejection, true);
  assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);

  controller.handleUncaughtException(fatals[0]);
  assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);
});

test("benign stream teardown errors are ignored by the installed handlers", () => {
  const fakeProcess = new EventEmitter();
  let captureCount = 0;
  let fatalCount = 0;
  const controller = createProcessErrorController({
    captureError() {
      captureCount += 1;
    },
    onFatalError() {
      fatalCount += 1;
    },
    logError() {},
    logWarn() {},
  });

  installProcessErrorHandlers(fakeProcess, controller);
  const err = new Error("broken pipe");
  err.code = "EPIPE";
  fakeProcess.emit("uncaughtException", err);

  assert.equal(captureCount, 0);
  assert.equal(fatalCount, 0);
});

test("controller suppresses wrapped network errors from err.cause", () => {
  const err = new Error("request failed");
  err.cause = new Error("net::ERR_NETWORK_CHANGED");

  const result = classifyProcessError(err, {
    runtimeStarted: false,
  });

  assert.equal(isNonFatalNetworkError(err), true);
  assert.equal(result.action, "suppress");
});

test("controller suppresses ssh-style errors with a level property", () => {
  const err = new Error("connection lost before handshake");
  err.level = "client-socket";

  const result = classifyProcessError(err, {
    runtimeStarted: false,
  });

  assert.equal(isNonFatalNetworkError(err), true);
  assert.equal(result.action, "suppress");
});
