import assert from "node:assert/strict";
import { afterEach, before, beforeEach, describe, it } from "node:test";

// diagnostics.ts reads localStorage at import time, so the window stub must
// be installed before the module is loaded; hence the dynamic import below.
const storage = new Map<string, string>();
(globalThis as { window?: unknown }).window = {
  localStorage: {
    getItem: (key: string) => storage.get(key) ?? null,
    setItem: (key: string, value: string) => {
      storage.set(key, value);
    },
    removeItem: (key: string) => {
      storage.delete(key);
    },
  },
  // Stubs so installDiagnosticsCapture (which patches window.fetch and adds
  // window error listeners) can run under node --test.
  fetch: (() => Promise.resolve(new Response())) as typeof fetch,
  addEventListener: () => {},
  removeEventListener: () => {},
};

type DiagnosticsModule = typeof import("../apps/geolibre-desktop/src/lib/diagnostics");
let appendDiagnostic: DiagnosticsModule["appendDiagnostic"];
let clearDiagnostics: DiagnosticsModule["clearDiagnostics"];
let getDiagnosticsSnapshot: DiagnosticsModule["getDiagnosticsSnapshot"];
let setCaptureNetworkInfo: DiagnosticsModule["setCaptureNetworkInfo"];
let installDiagnosticsCapture: DiagnosticsModule["installDiagnosticsCapture"];
let OPTIONAL_RESOURCE_HEADER: DiagnosticsModule["OPTIONAL_RESOURCE_HEADER"];

before(async () => {
  ({
    appendDiagnostic,
    clearDiagnostics,
    getDiagnosticsSnapshot,
    setCaptureNetworkInfo,
    installDiagnosticsCapture,
    OPTIONAL_RESOURCE_HEADER,
  } = await import("../apps/geolibre-desktop/src/lib/diagnostics"));
});

// Intentionally duplicated from diagnostics.ts: the key is a persistence
// contract with users' localStorage, so an accidental rename in the source
// should fail this test rather than be silently mirrored by an import.
const CAPTURE_NETWORK_INFO_STORAGE_KEY = "geolibre.diagnostics.captureNetworkInfo";

describe("diagnostics network info capture", () => {
  beforeEach(() => {
    setCaptureNetworkInfo(false);
    clearDiagnostics();
    storage.clear();
  });

  it("drops info-level network entries by default", () => {
    appendDiagnostic({
      category: "network",
      level: "info",
      message: "GET 200 OK",
    });
    assert.equal(getDiagnosticsSnapshot().totalCount, 0);
    assert.equal(getDiagnosticsSnapshot().networkCount, 0);
  });

  it("keeps error-level network entries by default", () => {
    appendDiagnostic({
      category: "network",
      level: "error",
      message: "GET 500 Internal Server Error",
    });
    const snapshot = getDiagnosticsSnapshot();
    assert.equal(snapshot.totalCount, 1);
    assert.equal(snapshot.networkCount, 1);
    assert.equal(snapshot.errorCount, 1);
  });

  it("keeps warning-level network entries even when capture is off", () => {
    appendDiagnostic({
      category: "network",
      level: "warning",
      message: "GET 301 Moved Permanently",
    });
    const snapshot = getDiagnosticsSnapshot();
    assert.equal(snapshot.totalCount, 1);
    assert.equal(snapshot.networkCount, 1);
    assert.equal(snapshot.warningCount, 1);
  });

  it("redacts sensitive query params from a URL embedded in the detail", () => {
    appendDiagnostic({
      category: "network",
      level: "error",
      message: "GET fetch_url_bytes failed (network)",
      // A native reqwest error embeds the full request URL, including secrets.
      detail:
        "error sending request for url (https://tiles.example.com/1/2/3.png?api_key=SECRET123): connection refused",
    });
    const [record] = getDiagnosticsSnapshot().records;
    // The param value is replaced with the REDACTED marker (URL-encoded in the
    // query string); the important guarantee is the secret no longer appears.
    assert.ok(record.detail?.includes("REDACTED"));
    assert.ok(!record.detail?.includes("SECRET123"));
  });

  it("does not filter info-level entries from other categories", () => {
    appendDiagnostic({
      category: "console",
      level: "info",
      message: "informational",
    });
    assert.equal(getDiagnosticsSnapshot().totalCount, 1);
  });

  it("records info-level network entries once enabled", () => {
    setCaptureNetworkInfo(true);
    appendDiagnostic({
      category: "network",
      level: "info",
      message: "GET 200 OK",
    });
    const snapshot = getDiagnosticsSnapshot();
    assert.equal(snapshot.totalCount, 1);
    assert.equal(snapshot.networkCount, 1);
    assert.equal(snapshot.captureNetworkInfo, true);
  });

  it("persists opt-in to localStorage and clears the key on opt-out", () => {
    setCaptureNetworkInfo(true);
    assert.equal(storage.get(CAPTURE_NETWORK_INFO_STORAGE_KEY), "true");
    setCaptureNetworkInfo(false);
    assert.equal(storage.has(CAPTURE_NETWORK_INFO_STORAGE_KEY), false);
  });

  it("exposes the capture flag through the snapshot", () => {
    assert.equal(getDiagnosticsSnapshot().captureNetworkInfo, false);
    setCaptureNetworkInfo(true);
    assert.equal(getDiagnosticsSnapshot().captureNetworkInfo, true);
  });
});

describe("diagnostics startup transient suppression", () => {
  type Listener = (event: unknown) => void;
  const listeners = new Map<string, Listener>();
  const win = (globalThis as { window?: Record<string, unknown> }).window!;
  let installCapture: DiagnosticsModule["installDiagnosticsCapture"];
  let realWarn: typeof console.warn;
  let realError: typeof console.error;
  const realDateNow = Date.now;
  // Tracked so afterEach can tear down the interceptors even if an assertion
  // throws mid-test, keeping the module-level capture ref-count clean.
  let activeCleanup: (() => void) | null = null;

  function install(): void {
    activeCleanup = installCapture();
  }

  before(async () => {
    ({ installDiagnosticsCapture: installCapture } =
      await import("../apps/geolibre-desktop/src/lib/diagnostics"));
  });

  beforeEach(() => {
    listeners.clear();
    clearDiagnostics();
    realWarn = console.warn;
    realError = console.error;
    win.fetch = (() => Promise.resolve()) as unknown as typeof fetch;
    win.addEventListener = (type: string, listener: Listener) => {
      listeners.set(type, listener);
    };
    win.removeEventListener = (type: string) => {
      listeners.delete(type);
    };
    delete win.__TAURI_INTERNALS__;
  });

  afterEach(() => {
    activeCleanup?.();
    activeCleanup = null;
    console.warn = realWarn;
    console.error = realError;
    Date.now = realDateNow;
  });

  function rejectionEvent(reason: unknown) {
    let prevented = false;
    return {
      event: {
        reason,
        preventDefault: () => {
          prevented = true;
        },
      },
      wasPrevented: () => prevented,
    };
  }

  function windowErrorEvent(message: string) {
    let prevented = false;
    return {
      event: {
        message,
        filename: "https://web.geolibre.app/",
        lineno: 0,
        colno: 0,
        preventDefault: () => {
          prevented = true;
        },
      },
      wasPrevented: () => prevented,
    };
  }

  it("suppresses benign ResizeObserver delivery-loop errors", () => {
    install();
    for (const message of [
      "ResizeObserver loop completed with undelivered notifications.",
      "ResizeObserver loop limit exceeded",
    ]) {
      const { event, wasPrevented } = windowErrorEvent(message);
      listeners.get("error")?.(event);
      assert.equal(wasPrevented(), true);
    }
    assert.equal(getDiagnosticsSnapshot().totalCount, 0);
  });

  it("still records genuine window errors", () => {
    install();
    const { event, wasPrevented } = windowErrorEvent("ResizeObserver callback failed");
    listeners.get("error")?.(event);
    assert.equal(wasPrevented(), false);
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.message, "ResizeObserver callback failed");
    assert.equal(record.level, "error");
  });

  it("swallows a benign startup fetch rejection under Tauri", () => {
    win.__TAURI_INTERNALS__ = {};
    install();
    const { event, wasPrevented } = rejectionEvent(new TypeError("Failed to fetch"));
    listeners.get("unhandledrejection")?.(event);
    assert.equal(wasPrevented(), true);
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.level, "warning");
    assert.equal(record.category, "network");
  });

  it("leaves a fetch rejection alone outside the Tauri runtime", () => {
    install();
    const { event, wasPrevented } = rejectionEvent(new TypeError("Failed to fetch"));
    listeners.get("unhandledrejection")?.(event);
    assert.equal(wasPrevented(), false);
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.level, "error");
    assert.equal(record.category, "runtime");
  });

  it("does not swallow a fetch rejection after the startup window", () => {
    win.__TAURI_INTERNALS__ = {};
    install();
    // installedAt is captured at install; jump past the grace window so the
    // rejection no longer counts as a startup transient.
    Date.now = () => realDateNow() + 60_000;
    const { event, wasPrevented } = rejectionEvent(new TypeError("Failed to fetch"));
    listeners.get("unhandledrejection")?.(event);
    assert.equal(wasPrevented(), false);
    assert.equal(getDiagnosticsSnapshot().records[0]?.level, "error");
  });

  it("does not swallow a non-fetch rejection under Tauri", () => {
    win.__TAURI_INTERNALS__ = {};
    install();
    const { event, wasPrevented } = rejectionEvent(new Error("boom"));
    listeners.get("unhandledrejection")?.(event);
    assert.equal(wasPrevented(), false);
    assert.equal(getDiagnosticsSnapshot().records[0]?.level, "error");
  });

  it("records but does not echo Tauri's IPC fallback warning", () => {
    win.__TAURI_INTERNALS__ = {};
    let echoed: unknown[] | null = null;
    console.warn = (...args: unknown[]) => {
      echoed = args;
    };
    install();
    console.warn(
      "IPC custom protocol failed, Tauri will now use the postMessage interface instead",
      new TypeError("Failed to fetch"),
    );
    assert.equal(echoed, null);
    assert.equal(getDiagnosticsSnapshot().records[0]?.level, "warning");
  });

  it("echoes the IPC fallback warning after the startup window", () => {
    win.__TAURI_INTERNALS__ = {};
    let echoed: unknown[] | null = null;
    console.warn = (...args: unknown[]) => {
      echoed = args;
    };
    install();
    Date.now = () => realDateNow() + 60_000;
    const message =
      "IPC custom protocol failed, Tauri will now use the postMessage interface instead";
    console.warn(message);
    assert.deepEqual(echoed, [message]);
  });

  it("still echoes ordinary warnings under Tauri", () => {
    win.__TAURI_INTERNALS__ = {};
    let echoed: unknown[] | null = null;
    console.warn = (...args: unknown[]) => {
      echoed = args;
    };
    install();
    console.warn("a normal warning");
    assert.deepEqual(echoed, ["a normal warning"]);
  });

  it("keeps the benign globe easing warning out of diagnostics but echoes it", () => {
    let echoed: unknown[] | null = null;
    console.warn = (...args: unknown[]) => {
      echoed = args;
    };
    install();
    const message = "Easing around a point is not supported under globe projection.";
    console.warn(message);
    // Echoed to the console for contributors, but not recorded in the panel.
    assert.deepEqual(echoed, [message]);
    assert.equal(getDiagnosticsSnapshot().totalCount, 0);
  });

  it("keeps the benign three.js multiple-instances warning out of diagnostics but echoes it", () => {
    let echoed: unknown[] | null = null;
    console.warn = (...args: unknown[]) => {
      echoed = args;
    };
    install();
    const message = "WARNING: Multiple instances of Three.js being imported.";
    console.warn(message);
    // Echoed to the console for contributors, but not recorded in the panel.
    assert.deepEqual(echoed, [message]);
    assert.equal(getDiagnosticsSnapshot().totalCount, 0);
  });

  it("flags an unmarked non-ok response as an error", async () => {
    win.fetch = (() =>
      Promise.resolve(
        new Response(null, { status: 404, statusText: "Not Found" }),
      )) as unknown as typeof fetch;
    install();
    await (win.fetch as typeof fetch)("/missing.json");
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.category, "network");
    assert.equal(record.level, "error");
    assert.equal(getDiagnosticsSnapshot().errorCount, 1);
  });

  it("downgrades a non-ok response on an optional-resource request", async () => {
    setCaptureNetworkInfo(true);
    try {
      win.fetch = (() =>
        Promise.resolve(
          new Response(null, { status: 404, statusText: "Not Found" }),
        )) as unknown as typeof fetch;
      install();
      await (win.fetch as typeof fetch)("/admin-profile.json", {
        headers: { [OPTIONAL_RESOURCE_HEADER]: "1" },
      });
      const [record] = getDiagnosticsSnapshot().records;
      assert.equal(record.category, "network");
      // Marked optional, so the 404 is informational rather than an error.
      assert.equal(record.level, "info");
      assert.equal(getDiagnosticsSnapshot().errorCount, 0);
    } finally {
      setCaptureNetworkInfo(false);
    }
  });

  it("treats init.headers as replacing a Request's optional marker", async () => {
    win.fetch = (() =>
      Promise.resolve(
        new Response(null, { status: 404, statusText: "Not Found" }),
      )) as unknown as typeof fetch;
    install();
    // The Request carries the optional marker, but init.headers replaces the
    // Request's headers entirely (fetch spec), dropping it — so the 404 is a
    // real error.
    await (win.fetch as typeof fetch)(
      new Request("http://localhost/admin-profile.json", {
        headers: { [OPTIONAL_RESOURCE_HEADER]: "1" },
      }),
      { headers: {} },
    );
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.category, "network");
    assert.equal(record.level, "error");
    assert.equal(getDiagnosticsSnapshot().errorCount, 1);
  });

  it("downgrades a thrown network error on an optional-resource request", async () => {
    setCaptureNetworkInfo(true);
    try {
      win.fetch = (() =>
        Promise.reject(new TypeError("Failed to fetch"))) as unknown as typeof fetch;
      install();
      await (win.fetch as typeof fetch)("/admin-profile.json", {
        headers: { [OPTIONAL_RESOURCE_HEADER]: "1" },
      }).catch(() => {});
      const [record] = getDiagnosticsSnapshot().records;
      assert.equal(record.category, "network");
      // Optional, so even a thrown failure is informational rather than an error.
      assert.equal(record.level, "info");
      assert.equal(getDiagnosticsSnapshot().errorCount, 0);
    } finally {
      setCaptureNetworkInfo(false);
    }
  });

  it("downgrades a benign startup fetch failure under Tauri to a warning", async () => {
    win.__TAURI_INTERNALS__ = {};
    win.fetch = (() => Promise.reject(new TypeError("Load failed"))) as unknown as typeof fetch;
    install();
    // The URL is illustrative of the real warm-up failure (a call to the Tauri
    // IPC endpoint); the benign-startup downgrade keys off the error message,
    // not the URL, so any URL would produce the same result here.
    await (win.fetch as typeof fetch)("http://ipc.localhost/main").catch(() => {});
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.category, "network");
    // The Tauri custom-protocol warm-up at launch retries over postMessage, so
    // it is a benign transient rather than a critical error (issue #657).
    assert.equal(record.level, "warning");
    assert.equal(getDiagnosticsSnapshot().errorCount, 0);
    // The panel badge reads warningCount, so assert it tracks the downgrade.
    assert.equal(getDiagnosticsSnapshot().warningCount, 1);
  });

  it("keeps a startup fetch failure outside the Tauri runtime as an error", async () => {
    win.fetch = (() => Promise.reject(new TypeError("Failed to fetch"))) as unknown as typeof fetch;
    install();
    await (win.fetch as typeof fetch)("https://example.com/data").catch(() => {});
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.category, "network");
    assert.equal(record.level, "error");
    assert.equal(getDiagnosticsSnapshot().errorCount, 1);
  });

  it("flags a fetch failure after the startup window as an error under Tauri", async () => {
    win.__TAURI_INTERNALS__ = {};
    win.fetch = (() => Promise.reject(new TypeError("Load failed"))) as unknown as typeof fetch;
    install();
    // Jump past the grace window so the failure is no longer a startup transient.
    Date.now = () => realDateNow() + 60_000;
    await (win.fetch as typeof fetch)("http://ipc.localhost/main").catch(() => {});
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.level, "error");
    assert.equal(getDiagnosticsSnapshot().errorCount, 1);
  });

  it("classifies a genuine fetch network failure with an actionable hint", async () => {
    win.fetch = (() => Promise.reject(new TypeError("Failed to fetch"))) as unknown as typeof fetch;
    install();
    await (win.fetch as typeof fetch)("https://example.com/data").catch(() => {});
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.level, "error");
    // The opaque browser error is interpreted rather than left as a raw stack.
    assert.match(record.message, /network\/TLS\/CORS/);
    assert.ok(record.detail?.includes("CORS"));
    // The raw error is still preserved after the hint.
    assert.ok(record.detail?.includes("Failed to fetch"));
  });

  it("does not append a redundant label for an unclassified failure", async () => {
    win.fetch = (() => Promise.reject(new Error("some opaque failure"))) as unknown as typeof fetch;
    install();
    await (win.fetch as typeof fetch)("https://example.com/data").catch(() => {});
    const [record] = getDiagnosticsSnapshot().records;
    assert.equal(record.level, "error");
    // An "unknown" classification must not render "request failed (request failed)".
    assert.equal(record.message, "GET request failed");
    assert.ok(!/\(request failed\)/.test(record.message));
  });
});
