import { describe, it, expect, vi, beforeEach, afterEach } from "vite-plus/test";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { PAGES_FIXTURE_DIR } from "./helpers.js";
import { isExternalUrl, isHashOnlyChange } from "../packages/vinext/src/shims/router.js";
import { extractVinextNextDataJson } from "../packages/vinext/src/client/vinext-next-data.js";
import { isValidModulePath } from "../packages/vinext/src/client/validate-module-path.js";
import vinext from "../packages/vinext/src/index.js";
import { safeJsonStringify } from "../packages/vinext/src/server/html.js";
import { buildPagesNextDataScript } from "../packages/vinext/src/server/pages-page-response.js";
import type { Plugin } from "vite-plus";
import type { NextRouter } from "../packages/vinext/src/shims/router.js";
import type {
  CacheHandler,
  CacheHandlerValue,
  IncrementalCacheValue,
} from "../packages/vinext/src/shims/cache.js";
import { normalizePathSeparators } from "../packages/vinext/src/utils/path.ts";

const FIXTURE_DIR = PAGES_FIXTURE_DIR;

function commitRenderedPagesRouterElement(element: unknown): void {
  if (typeof document === "undefined" || document.documentElement === undefined) return;

  const props = (element as { props?: unknown } | null)?.props;
  const onCommit =
    props && typeof props === "object" ? (props as { onCommit?: unknown }).onCommit : undefined;
  if (typeof onCommit === "function") queueMicrotask(onCommit as () => void);
}

describe("vinext next data client helpers", () => {
  it("extracts Next.js-compatible application/json data scripts", () => {
    const json = '{"props":{"pageProps":{"nested":{"value":"}"}}},"page":"/","query":{}}';

    expect(
      extractVinextNextDataJson(
        `<script id="__NEXT_DATA__" type="application/json">${json}</script>`,
      ),
    ).toBe(json);
  });

  it("extracts __NEXT_DATA__ after carriage-return whitespace", () => {
    const json = '{"props":{},"page":"/","query":{}}';

    expect(extractVinextNextDataJson(`<script>window.__NEXT_DATA__ = \r\n\t${json}</script>`)).toBe(
      json,
    );
  });
});

describe("slot BFCache entry ordering", () => {
  it("keeps the three most recent state-key entries", async () => {
    const { updateBfcacheSlotEntryOrder } = await import("../packages/vinext/src/shims/slot.js");

    let order: readonly string[] = [];
    order = updateBfcacheSlotEntryOrder(order, "one", 3);
    order = updateBfcacheSlotEntryOrder(order, "two", 3);
    order = updateBfcacheSlotEntryOrder(order, "three", 3);
    order = updateBfcacheSlotEntryOrder(order, "four", 3);

    expect(order).toEqual(["four", "three", "two"]);
  });

  it("moves an existing entry to the front without evicting another entry", async () => {
    const { updateBfcacheSlotEntryOrder } = await import("../packages/vinext/src/shims/slot.js");

    let order: readonly string[] = [];
    order = updateBfcacheSlotEntryOrder(order, "one", 3);
    order = updateBfcacheSlotEntryOrder(order, "two", 3);
    order = updateBfcacheSlotEntryOrder(order, "three", 3);
    order = updateBfcacheSlotEntryOrder(order, "one", 3);

    expect(order).toEqual(["one", "three", "two"]);
  });

  it("keeps a single retained entry when the limit is one", async () => {
    const { updateBfcacheSlotEntryOrder } = await import("../packages/vinext/src/shims/slot.js");

    expect(updateBfcacheSlotEntryOrder(["one", "two", "three"], "two", 1)).toEqual(["two"]);
  });

  it("normalizes non-finite or non-positive limits to a single entry", async () => {
    const { updateBfcacheSlotEntryOrder } = await import("../packages/vinext/src/shims/slot.js");

    expect(updateBfcacheSlotEntryOrder(["one", "two"], "two", Number.NaN)).toEqual(["two"]);
    expect(updateBfcacheSlotEntryOrder(["one", "two"], "two", 0)).toEqual(["two"]);
    expect(updateBfcacheSlotEntryOrder(["one", "two"], "two", 1.9)).toEqual(["two"]);
  });

  it("ignores the active key when scanning the previous order", async () => {
    const { updateBfcacheSlotEntryOrder } = await import("../packages/vinext/src/shims/slot.js");

    expect(updateBfcacheSlotEntryOrder(["one", "two", "three"], "two", 3)).toEqual([
      "two",
      "one",
      "three",
    ]);
  });
});

describe("next/navigation shim", () => {
  it("exports usePathname, useSearchParams, useParams, useRouter", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof nav.usePathname).toBe("function");
    expect(typeof nav.useSearchParams).toBe("function");
    expect(typeof nav.useParams).toBe("function");
    expect(typeof nav.useRouter).toBe("function");
  });

  // Next.js parity: next/navigation's useRouter reads AppRouterContext and
  // throws when it is rendered outside the App Router provider.
  // Ported from Next.js:
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/navigation.ts
  it("useRouter() throws when AppRouterContext is not mounted", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");

    function Probe() {
      useRouter();
      return React.createElement("span", null, "unreachable");
    }

    expect(() => renderToStaticMarkup(React.createElement(Probe))).toThrow(
      "invariant expected app router to be mounted",
    );
  });

  // Regression test: within the App Router provider, useRouter() must preserve
  // the mounted router instance as the source of method behavior and only layer
  // contextual bfcacheId on top.
  // Ported from: https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/navigation.ts
  it("useRouter() preserves methods from the mounted AppRouterContext router", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const navigation = await import("../packages/vinext/src/shims/navigation.js");
    const { AppRouterContext } =
      await import("../packages/vinext/src/shims/internal/app-router-context.js");
    const customRefresh = vi.fn();
    const customMethod = vi.fn(() => "custom");
    const mountedRouter = {
      ...navigation.appRouterInstance,
      refresh: customRefresh,
      customMethod,
    };
    const captured: any[] = [];

    function Probe() {
      const router = navigation.useRouter() as typeof mountedRouter;
      captured.push(router);
      router.refresh();
      router.customMethod();
      return React.createElement("span", null, "ok");
    }

    if (!AppRouterContext) {
      throw new Error("Expected AppRouterContext to be available in the test renderer");
    }

    renderToStaticMarkup(
      React.createElement(
        AppRouterContext.Provider,
        { value: mountedRouter },
        React.createElement(Probe),
      ),
    );

    expect(captured).toHaveLength(1);
    expect(captured[0]).not.toBe(mountedRouter);
    expect(captured[0].refresh).toBe(customRefresh);
    expect(captured[0].customMethod).toBe(customMethod);
    expect(captured[0].bfcacheId).toBe("_b_0_");
    expect(customRefresh).toHaveBeenCalledTimes(1);
    expect(customMethod).toHaveBeenCalledTimes(1);
  });

  it("appRouterInstance singleton exposes the expected navigation methods", async () => {
    const { appRouterInstance: router } =
      await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof router.push).toBe("function");
    expect(typeof router.replace).toBe("function");
    expect(typeof router.back).toBe("function");
    expect(typeof router.forward).toBe("function");
    expect(typeof router.refresh).toBe("function");
    expect(typeof router.prefetch).toBe("function");
  });

  it("appRouterInstance exposes the stable bfcacheId placeholder", async () => {
    const { appRouterInstance } = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof appRouterInstance.bfcacheId).toBe("string");
    expect(appRouterInstance.bfcacheId).toBe("0");
  });

  it("useRouter() reads bfcacheId from the nearest segment context", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const navigation = await import("../packages/vinext/src/shims/navigation.js");
    const { AppRouterContext } =
      await import("../packages/vinext/src/shims/internal/app-router-context.js");
    const BfcacheIdMapContext = navigation.getBfcacheIdMapContext();
    const BfcacheSegmentIdContext = navigation.getBfcacheSegmentIdContext();
    if (!AppRouterContext || !BfcacheIdMapContext || !BfcacheSegmentIdContext) {
      throw new Error("Expected bfcache contexts");
    }

    function Probe() {
      return React.createElement("span", null, navigation.useRouter().bfcacheId);
    }

    expect(
      renderToStaticMarkup(
        React.createElement(
          AppRouterContext.Provider,
          { value: navigation.appRouterInstance },
          React.createElement(
            BfcacheIdMapContext.Provider,
            { value: { "page:/x/1": "_b_7_" } },
            React.createElement(
              BfcacheSegmentIdContext.Provider,
              { value: "page:/x/1" },
              React.createElement(Probe),
            ),
          ),
        ),
      ),
    ).toBe("<span>_b_7_</span>");
  });

  it("useRouter() treats an empty string segment id as a valid context key", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const navigation = await import("../packages/vinext/src/shims/navigation.js");
    const { AppRouterContext } =
      await import("../packages/vinext/src/shims/internal/app-router-context.js");
    const BfcacheIdMapContext = navigation.getBfcacheIdMapContext();
    const BfcacheSegmentIdContext = navigation.getBfcacheSegmentIdContext();
    if (!AppRouterContext || !BfcacheIdMapContext || !BfcacheSegmentIdContext) {
      throw new Error("Expected bfcache contexts");
    }

    function Probe() {
      return React.createElement("span", null, navigation.useRouter().bfcacheId);
    }

    expect(
      renderToStaticMarkup(
        React.createElement(
          AppRouterContext.Provider,
          { value: navigation.appRouterInstance },
          React.createElement(
            BfcacheIdMapContext.Provider,
            { value: { "": "_b_9_" } },
            React.createElement(
              BfcacheSegmentIdContext.Provider,
              { value: "" },
              React.createElement(Probe),
            ),
          ),
        ),
      ),
    ).toBe("<span>_b_9_</span>");
  });

  it("useRouter() materializes the initial bfcacheId in Next-compatible format", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const navigation = await import("../packages/vinext/src/shims/navigation.js");
    const { AppRouterContext } =
      await import("../packages/vinext/src/shims/internal/app-router-context.js");
    const BfcacheIdMapContext = navigation.getBfcacheIdMapContext();
    const BfcacheSegmentIdContext = navigation.getBfcacheSegmentIdContext();
    if (!AppRouterContext || !BfcacheIdMapContext || !BfcacheSegmentIdContext) {
      throw new Error("Expected bfcache contexts");
    }

    function Probe() {
      return React.createElement("span", null, navigation.useRouter().bfcacheId);
    }

    expect(
      renderToStaticMarkup(
        React.createElement(
          AppRouterContext.Provider,
          { value: navigation.appRouterInstance },
          React.createElement(
            BfcacheIdMapContext.Provider,
            { value: { "page:/x/1": "0" } },
            React.createElement(
              BfcacheSegmentIdContext.Provider,
              { value: "page:/x/1" },
              React.createElement(Probe),
            ),
          ),
        ),
      ),
    ).toBe("<span>_b_0_</span>");
  });

  // Next.js parity: refresh-reducer.ts invalidates the entire segment cache.
  // Our equivalent is clearClientNavigationCaches(), which router.refresh()
  // must call before re-fetching, or stale cached RSC payloads for sibling
  // routes will still satisfy a subsequent client navigation. The clear must
  // happen before the rscNavigate dispatch so it cannot race with prefetches
  // kicked off during the transition's renders.
  // Ported from: https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts
  it("router.refresh() clears nav caches before dispatching the RSC re-fetch", async () => {
    const previousWindow = (globalThis as any).window;
    const calls: string[] = [];
    const win = {
      location: { href: "http://localhost/current" },
      history: {
        state: null,
        pushState: () => {},
        replaceState: () => {},
      },
      addEventListener: () => {},
      [Symbol.for("vinext.navigationRuntime")]: {
        bootstrap: {
          routeManifest: null,
          rsc: undefined,
        },
        functions: {
          clearNavigationCaches: () => {
            calls.push("clear");
          },
          navigate: async (_href: string, _depth: number, kind: string) => {
            calls.push(`navigate:${kind}`);
          },
        },
      },
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { appRouterInstance } = await import("../packages/vinext/src/shims/navigation.js");
      appRouterInstance.refresh();
      // refresh() schedules the rscNavigate inside React.startTransition, so
      // the navigate call lands after the synchronous clear but is dispatched
      // in the same tick — yield once to let it flush.
      await Promise.resolve();
      await Promise.resolve();

      expect(calls[0]).toBe("clear");
      expect(calls).toContain("navigate:refresh");
      expect(calls.indexOf("clear")).toBeLessThan(calls.indexOf("navigate:refresh"));
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("attaches experimental_gesturePush only when the gesture transition define is set", async () => {
    // In real builds `process.env.__NEXT_GESTURE_TRANSITION` is replaced with a
    // boolean literal by the define in packages/vinext/src/index.ts; in vitest
    // it reads the live env, so stub it and re-import the shim to exercise both
    // sides of the gate.
    try {
      vi.stubEnv("__NEXT_GESTURE_TRANSITION", "");
      vi.resetModules();
      const { appRouterInstance: withoutDefine } =
        await import("../packages/vinext/src/shims/navigation.js");
      expect(typeof withoutDefine.experimental_gesturePush).toBe("undefined");

      vi.stubEnv("__NEXT_GESTURE_TRANSITION", "true");
      vi.resetModules();
      const { appRouterInstance: withDefine } =
        await import("../packages/vinext/src/shims/navigation.js");
      expect(typeof withDefine.experimental_gesturePush).toBe("function");
    } finally {
      vi.unstubAllEnvs();
      vi.resetModules();
    }
  });

  // Next.js parity: upstream's gesturePush early-returns when
  // `getCurrentAppRouterState() === null` — a gesture dispatched before
  // hydration is a no-op. Without the guard, navigateClientSide's non-runtime
  // fallback would perform a real history push instead.
  it("experimental_gesturePush is a no-op before the navigation runtime is ready", async () => {
    const previousWindow = (globalThis as any).window;
    const historyCalls: string[] = [];
    const win = {
      location: { href: "http://localhost/current", origin: "http://localhost" },
      history: {
        state: null,
        pushState: () => {
          historyCalls.push("push");
        },
        replaceState: () => {
          historyCalls.push("replace");
        },
      },
      addEventListener: () => {},
      // No Symbol.for("vinext.navigationRuntime") entry: pre-hydration state.
    };
    (globalThis as any).window = win;

    try {
      vi.stubEnv("__NEXT_GESTURE_TRANSITION", "true");
      vi.resetModules();
      const { appRouterInstance } = await import("../packages/vinext/src/shims/navigation.js");
      if (!appRouterInstance.experimental_gesturePush) {
        throw new Error("Expected experimental_gesturePush to be attached");
      }
      appRouterInstance.experimental_gesturePush("/next-page");
      // navigateClientSide is async; flush microtasks so a (buggy) fallthrough
      // into its history-push branch would be observable.
      await Promise.resolve();
      await Promise.resolve();

      expect(historyCalls).toEqual([]);
      expect(win.location.href).toBe("http://localhost/current");
    } finally {
      (globalThis as any).window = previousWindow;
      vi.unstubAllEnvs();
      vi.resetModules();
    }
  });

  it("hash-only app router navigation preserves bfcache metadata without copying scroll restoration", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    let historyState: unknown = {
      __vinext_bfcacheIds: { "page:/current": "_b_1_" },
      __vinext_bfcacheVersion: 2,
      __vinext_previousNextUrl: "/feed",
      customState: "drop-me",
    };
    const location = {
      href: "http://localhost/current",
      origin: "http://localhost",
      pathname: "/current",
      search: "",
      hash: "",
      assign: vi.fn(),
      replace: vi.fn(),
    };
    const applyUrl = (url: string | URL | null | undefined) => {
      if (url == null) return;
      const next = new URL(String(url), location.href);
      location.href = next.href;
      location.pathname = next.pathname;
      location.search = next.search;
      location.hash = next.hash;
    };
    const pushState = vi.fn((data: unknown, _unused: string, url?: string | URL | null) => {
      historyState = data;
      applyUrl(url);
    });
    const replaceState = vi.fn((data: unknown, _unused: string, url?: string | URL | null) => {
      historyState = data;
      applyUrl(url);
    });

    const win = {
      location,
      history: {
        get state() {
          return historyState;
        },
        pushState,
        replaceState,
      },
      scrollX: 12,
      scrollY: 345,
      scrollTo: vi.fn(),
      addEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
      __VINEXT_RSC_NAVIGATE__: vi.fn(),
    };
    const scrollIntoView = vi.fn();

    (globalThis as any).window = win;
    (globalThis as any).document = {
      getElementById: vi.fn(() => ({ scrollIntoView })),
      getElementsByName: vi.fn(() => []),
    };

    try {
      vi.resetModules();
      const { navigateClientSide } = await import("../packages/vinext/src/shims/navigation.js");

      await navigateClientSide("#content", "push", true, true);

      expect(replaceState).toHaveBeenLastCalledWith(
        expect.objectContaining({ __vinext_scrollX: 12, __vinext_scrollY: 345 }),
        "",
        undefined,
      );
      expect(pushState).toHaveBeenCalledWith(
        {
          __vinext_bfcacheIds: { "page:/current": "_b_1_" },
          __vinext_bfcacheVersion: 2,
          __vinext_previousNextUrl: "/feed",
        },
        "",
        "/current#content",
      );
      expect(scrollIntoView).toHaveBeenCalled();
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      vi.resetModules();
    }
  });

  it("treats streamed App Router redirect meta as an MPA navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const location = {
      href: "http://localhost/redirect/redirect-with-loading",
      origin: "http://localhost",
      pathname: "/redirect/redirect-with-loading",
      search: "",
      hash: "",
      assign: vi.fn(),
      replace: vi.fn(),
    };
    const win = {
      location,
      history: {
        state: null,
        pushState: vi.fn(),
        replaceState: vi.fn(),
      },
      scrollX: 0,
      scrollY: 0,
      scrollTo: vi.fn(),
      addEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    };
    const getElementById = vi.fn((id: string) => (id === "__next-page-redirect" ? { id } : null));
    const navigate = vi.fn(async () => {});
    const navigateExternal = vi.fn(async () => {});

    (globalThis as any).window = win;
    (globalThis as any).document = {
      getElementById,
      getElementsByName: vi.fn(() => []),
    };

    try {
      vi.resetModules();
      const { NAVIGATION_RUNTIME_KEY } =
        await import("../packages/vinext/src/client/navigation-runtime.js");
      (win as any)[NAVIGATION_RUNTIME_KEY] = {
        bootstrap: {
          routeManifest: null,
          rsc: undefined,
        },
        functions: {
          navigate,
          navigateExternal,
        },
      };
      const { navigateClientSide } = await import("../packages/vinext/src/shims/navigation.js");

      await navigateClientSide("/redirect/result", "replace", true, true);

      expect(getElementById).toHaveBeenCalledWith("__next-page-redirect");
      expect(navigateExternal).toHaveBeenCalledWith("/redirect/result", "replace");
      expect(navigate).not.toHaveBeenCalled();
      expect(location.replace).not.toHaveBeenCalled();
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      vi.resetModules();
    }
  });

  it("keeps pending render snapshot active when external history.pushState syncs the URL", async () => {
    const previousWindow = (globalThis as any).window;
    const win = {
      location: {
        pathname: "/current",
        search: "?from=committed",
        hash: "",
        href: "http://localhost/current?from=committed",
        origin: "http://localhost",
      },
      history: {
        state: null,
        pushState(_data: unknown, _unused: string, url?: string | URL | null) {
          if (!url) return;
          const parsed = new URL(url, win.location.href);
          win.location.pathname = parsed.pathname;
          win.location.search = parsed.search;
          win.location.hash = parsed.hash;
          win.location.href = parsed.href;
        },
        replaceState(_data: unknown, _unused: string, url?: string | URL | null) {
          if (!url) return;
          const parsed = new URL(url, win.location.href);
          win.location.pathname = parsed.pathname;
          win.location.search = parsed.search;
          win.location.hash = parsed.hash;
          win.location.href = parsed.href;
        },
      },
      addEventListener: vi.fn(),
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const React = await import("react");
      const { renderToStaticMarkup } = await import("react-dom/server");
      const navigation = await import("../packages/vinext/src/shims/navigation.js");
      const Context = navigation.getClientNavigationRenderContext();
      if (!Context) {
        throw new Error("Expected client navigation render context");
      }

      navigation.activateNavigationSnapshot();
      const snapshot = navigation.createClientNavigationRenderSnapshot(
        "http://localhost/pending?from=snapshot",
        {},
      );

      const readHookValues = () => {
        let pathname: string | null = "";
        let search = "";
        function Probe() {
          pathname = navigation.usePathname();
          search = navigation.useSearchParams().toString();
          return React.createElement("span", null, pathname);
        }

        renderToStaticMarkup(
          React.createElement(Context.Provider, { value: snapshot }, React.createElement(Probe)),
        );

        return { pathname, search };
      };

      expect(readHookValues()).toEqual({
        pathname: "/pending",
        search: "from=snapshot",
      });

      win.history.pushState(null, "", "/ownerless?from=history");

      expect(readHookValues()).toEqual({
        pathname: "/pending",
        search: "from=snapshot",
      });
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("preserves App Router history metadata when external history calls provide caller state", async () => {
    // Matches Next.js' external History API wrapper behavior:
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L114-L127
    // Covered by Next.js shallow-routing tests for object, null, and undefined state:
    // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/shallow-routing/shallow-routing.test.ts
    const previousWindow = (globalThis as any).window;
    const historyPreviousNextUrlKey = "__vinext_previousNextUrl";
    const historyTraversalIndexKey = "__vinext_historyIndex";
    const win = {
      location: {
        pathname: "/photo/1",
        search: "",
        hash: "",
        href: "http://localhost/photo/1",
        origin: "http://localhost",
      },
      history: {
        state: {
          [historyPreviousNextUrlKey]: "/feed",
          [historyTraversalIndexKey]: 4,
        } as unknown,
        pushState(data: unknown, _unused: string, url?: string | URL | null) {
          this.state = data;
          if (!url) return;
          const parsed = new URL(url, win.location.href);
          win.location.pathname = parsed.pathname;
          win.location.search = parsed.search;
          win.location.hash = parsed.hash;
          win.location.href = parsed.href;
        },
        replaceState(data: unknown, _unused: string, url?: string | URL | null) {
          this.state = data;
          if (!url) return;
          const parsed = new URL(url, win.location.href);
          win.location.pathname = parsed.pathname;
          win.location.search = parsed.search;
          win.location.hash = parsed.hash;
          win.location.href = parsed.href;
        },
      },
      addEventListener: vi.fn(),
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/navigation.js");

      win.history.pushState({ myData: { foo: "bar" } }, "", "/photo/1?filter=active");
      expect(win.history.state).toEqual({
        [historyPreviousNextUrlKey]: "/feed",
        [historyTraversalIndexKey]: 4,
        myData: { foo: "bar" },
      });

      win.history.pushState(null, "", "/photo/1?filter=pending");
      expect(win.history.state).toEqual({
        [historyPreviousNextUrlKey]: "/feed",
        [historyTraversalIndexKey]: 4,
      });

      win.history.replaceState(null, "", "/photo/1?filter=archived");
      expect(win.history.state).toEqual({
        [historyPreviousNextUrlKey]: "/feed",
        [historyTraversalIndexKey]: 4,
      });

      win.history.replaceState(undefined, "", "/photo/1?filter=all");
      expect(win.history.state).toEqual({
        [historyPreviousNextUrlKey]: "/feed",
        [historyTraversalIndexKey]: 4,
      });

      win.history.state = { [historyTraversalIndexKey]: 7 };
      win.history.pushState({ next: true }, "", "/photo/1?filter=done");
      expect(win.history.state).toEqual({
        [historyTraversalIndexKey]: 7,
        next: true,
      });
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("exports redirect, notFound, permanentRedirect", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof nav.redirect).toBe("function");
    expect(typeof nav.notFound).toBe("function");
    expect(typeof nav.permanentRedirect).toBe("function");
  });

  it("redirect() throws with correct digest", async () => {
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      redirect("/login");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      expect(e.digest).toContain("NEXT_REDIRECT");
      // URL is encodeURIComponent-encoded in the digest to prevent delimiter injection
      expect(e.digest).toContain(encodeURIComponent("/login"));
    }
  });

  // Ported from Next.js: packages/next/src/client/components/redirect.ts
  // In Next.js, redirect() without an explicit type uses an empty sentinel so
  // the context (action vs render) can resolve the default at the catch site.
  it("redirect() without explicit type uses empty sentinel (context-dependent default)", async () => {
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      redirect("/dashboard");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      expect(parts[0]).toBe("NEXT_REDIRECT");
      // Empty string sentinel — the catch site determines push vs replace
      expect(parts[1]).toBe("");
      expect(decodeURIComponent(parts[2])).toBe("/dashboard");
    }
  });

  it("redirect() with explicit 'push' type preserves it in digest", async () => {
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      redirect("/dashboard", "push");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      expect(parts[1]).toBe("push");
    }
  });

  it("redirect() with explicit 'replace' type preserves it in digest", async () => {
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      redirect("/dashboard", "replace");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      expect(parts[1]).toBe("replace");
    }
  });

  it("redirect() encodes semicolons in URL to prevent digest injection", async () => {
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      redirect("http://example.com;301");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      // The URL field must not leak into the status code position
      expect(parts).toHaveLength(3); // NEXT_REDIRECT, type, encoded-url
      expect(parts[0]).toBe("NEXT_REDIRECT");
      // Empty sentinel — no explicit type passed
      expect(parts[1]).toBe("");
      expect(decodeURIComponent(parts[2])).toBe("http://example.com;301");
    }
  });

  it("permanentRedirect() accepts an optional type parameter", async () => {
    const { permanentRedirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      permanentRedirect("/new-page", "push");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      expect(parts[0]).toBe("NEXT_REDIRECT");
      expect(parts[1]).toBe("push");
      expect(decodeURIComponent(parts[2])).toBe("/new-page");
      expect(parts[3]).toBe("308");
    }
  });

  it("permanentRedirect() defaults to 'replace' when no type given", async () => {
    const { permanentRedirect } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      permanentRedirect("/new-page");
      expect.unreachable("should have thrown");
    } catch (e: any) {
      const parts = e.digest.split(";");
      expect(parts[1]).toBe("replace");
    }
  });

  it("notFound() throws with correct digest", async () => {
    const { notFound } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      notFound();
      expect.unreachable("should have thrown");
    } catch (e: any) {
      expect(e.digest).toBe("NEXT_HTTP_ERROR_FALLBACK;404");
    }
  });

  it("forbidden() throws with correct digest", async () => {
    const { forbidden } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      forbidden();
      expect.unreachable("should have thrown");
    } catch (e: any) {
      expect(e.digest).toBe("NEXT_HTTP_ERROR_FALLBACK;403");
    }
  });

  it("unauthorized() throws with correct digest", async () => {
    const { unauthorized } = await import("../packages/vinext/src/shims/navigation.js");
    try {
      unauthorized();
      expect.unreachable("should have thrown");
    } catch (e: any) {
      expect(e.digest).toBe("NEXT_HTTP_ERROR_FALLBACK;401");
    }
  });

  it("isHTTPAccessFallbackError detects all HTTP access fallback errors", async () => {
    const {
      notFound,
      forbidden,
      unauthorized,
      isHTTPAccessFallbackError,
      getAccessFallbackHTTPStatus,
    } = await import("../packages/vinext/src/shims/navigation.js");

    // Test notFound
    try {
      notFound();
    } catch (e) {
      expect(isHTTPAccessFallbackError(e)).toBe(true);
      expect(getAccessFallbackHTTPStatus(e)).toBe(404);
    }

    // Test forbidden
    try {
      forbidden();
    } catch (e) {
      expect(isHTTPAccessFallbackError(e)).toBe(true);
      expect(getAccessFallbackHTTPStatus(e)).toBe(403);
    }

    // Test unauthorized
    try {
      unauthorized();
    } catch (e) {
      expect(isHTTPAccessFallbackError(e)).toBe(true);
      expect(getAccessFallbackHTTPStatus(e)).toBe(401);
    }

    // Test non-access error
    expect(isHTTPAccessFallbackError(new Error("random"))).toBe(false);
    expect(isHTTPAccessFallbackError(null)).toBe(false);

    // Test legacy NEXT_NOT_FOUND format
    const legacyErr = new Error("old");
    (legacyErr as any).digest = "NEXT_NOT_FOUND";
    expect(isHTTPAccessFallbackError(legacyErr)).toBe(true);
    expect(getAccessFallbackHTTPStatus(legacyErr)).toBe(404);
  });

  it("setNavigationContext / useParams works on server side", async () => {
    const { setNavigationContext, useParams } =
      await import("../packages/vinext/src/shims/navigation.js");
    setNavigationContext({
      pathname: "/blog/test",
      searchParams: new URLSearchParams(""),
      params: { slug: "test" },
    });
    const params = useParams();
    expect(params).toEqual({ slug: "test" });
    setNavigationContext(null);
  });

  it("shares the hydrated navigation snapshot across browser module instances", async () => {
    // Next.js derives usePathname/useSearchParams from PathnameContext in
    // packages/next/src/client/components/app-router.tsx, so hydration does
    // not have a per-module fallback to "/" for the first client snapshot.
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const previousWindow = globalThis.window;
    const accessorsKey = Symbol.for("vinext.navigation.globalAccessors");
    const hydrationKey = Symbol.for("vinext.navigation.clientHydrationContext");
    const globalRecord = globalThis as Record<PropertyKey, unknown>;
    const previousAccessors = globalRecord[accessorsKey];
    const previousHydration = globalRecord[hydrationKey];

    try {
      delete globalRecord[accessorsKey];
      delete globalRecord[hydrationKey];
      (globalThis as any).window = {
        addEventListener() {},
        dispatchEvent() {
          return true;
        },
        history: {
          pushState() {},
          replaceState() {},
        },
        location: {
          href: "http://localhost/split-hydration?q=hello",
          origin: "http://localhost",
          pathname: "/split-hydration",
          search: "?q=hello",
        },
        removeEventListener() {},
      };

      const setterPath = "../packages/vinext/src/shims/navigation.js?hydration-setter=issue-871";
      const hookPath = "../packages/vinext/src/shims/navigation.js?hydration-hook=issue-871";
      const setterMod = (await import(
        setterPath
      )) as typeof import("../packages/vinext/src/shims/navigation.js");
      const hookMod = (await import(
        hookPath
      )) as typeof import("../packages/vinext/src/shims/navigation.js");

      setterMod.setNavigationContext({
        pathname: "/split-hydration",
        searchParams: new URLSearchParams("q=hello"),
        params: { slug: "hello" },
      });

      function Probe() {
        const pathname = hookMod.usePathname();
        const searchParams = hookMod.useSearchParams();
        const params = hookMod.useParams<{ slug: string }>();
        if (!params) throw new Error("expected app navigation params");
        return React.createElement(
          "span",
          null,
          `${pathname}|${searchParams.get("q") ?? ""}|${params.slug ?? ""}`,
        );
      }

      expect(renderToStaticMarkup(React.createElement(Probe))).toBe(
        "<span>/split-hydration|hello|hello</span>",
      );

      setterMod.setNavigationContext(null);
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousAccessors === undefined) {
        delete globalRecord[accessorsKey];
      } else {
        globalRecord[accessorsKey] = previousAccessors;
      }
      if (previousHydration === undefined) {
        delete globalRecord[hydrationKey];
      } else {
        globalRecord[hydrationKey] = previousHydration;
      }
    }
  });

  it("keeps explicit app navigation context ahead of globally installed Pages compat state", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const previousWindow = globalThis.window;
    const accessorKey = Symbol.for("vinext.navigation.pagesNavigationContextAccessor");
    const hydrationKey = Symbol.for("vinext.navigation.clientHydrationContext");
    const globalRecord = globalThis as Record<PropertyKey, unknown>;
    const previousAccessor = globalRecord[accessorKey];
    const previousHydration = globalRecord[hydrationKey];

    try {
      delete globalRecord[hydrationKey];
      globalRecord[accessorKey] = () => ({
        pathname: "/pages/[slug]",
        searchParams: new URLSearchParams("from=pages"),
        params: { slug: "pages" },
      });
      (globalThis as any).window = {
        addEventListener() {},
        dispatchEvent() {
          return true;
        },
        history: {
          pushState() {},
          replaceState() {},
        },
        location: {
          href: "http://localhost/app/alpha?from=app",
          origin: "http://localhost",
          pathname: "/app/alpha",
          search: "?from=app",
        },
        removeEventListener() {},
      };

      const setterPath =
        "../packages/vinext/src/shims/navigation.js?app-context-setter=pages-fallback";
      const hookPath = "../packages/vinext/src/shims/navigation.js?app-context-hook=pages-fallback";
      const setterMod = (await import(
        setterPath
      )) as typeof import("../packages/vinext/src/shims/navigation.js");
      const hookMod = (await import(
        hookPath
      )) as typeof import("../packages/vinext/src/shims/navigation.js");

      setterMod.setNavigationContext({
        pathname: "/app/alpha",
        searchParams: new URLSearchParams("from=app"),
        params: { slug: "app" },
      });

      function Probe() {
        const searchParams = hookMod.useSearchParams();
        const params = hookMod.useParams<{ slug: string }>();
        if (!params) throw new Error("expected app navigation params");
        return React.createElement(
          "span",
          null,
          `${searchParams.get("from") ?? ""}|${params.slug ?? ""}`,
        );
      }

      expect(renderToStaticMarkup(React.createElement(Probe))).toBe("<span>app|app</span>");

      setterMod.setNavigationContext(null);
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousAccessor === undefined) {
        delete globalRecord[accessorKey];
      } else {
        globalRecord[accessorKey] = previousAccessor;
      }
      if (previousHydration === undefined) {
        delete globalRecord[hydrationKey];
      } else {
        globalRecord[hydrationKey] = previousHydration;
      }
    }
  });

  it("setClientParams provides referential stability for identical params", async () => {
    const { setClientParams, getClientParams } =
      await import("../packages/vinext/src/shims/navigation.js");
    // Set params initially
    setClientParams({ slug: "hello" });
    const first = getClientParams();
    // Set params with same values — should return same object reference
    setClientParams({ slug: "hello" });
    const second = getClientParams();
    expect(first).toBe(second); // referential equality

    // Set params with different values — should return new object
    setClientParams({ slug: "world" });
    const third = getClientParams();
    expect(third).not.toBe(first);
    expect(third).toEqual({ slug: "world" });

    // Clean up
    setClientParams({});
  });

  it("exports useSelectedLayoutSegment and useSelectedLayoutSegments", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof nav.useSelectedLayoutSegment).toBe("function");
    expect(typeof nav.useSelectedLayoutSegments).toBe("function");
  });

  it("marks navigation hooks dynamic during fallback-shell prerendering", async () => {
    const { createPprFallbackShellState, runWithPprFallbackShellState } =
      await import("../packages/vinext/src/shims/ppr-fallback-shell.js");
    const navigation = await import("../packages/vinext/src/shims/navigation.js");

    const readHooks: Array<() => unknown> = [
      () => navigation.usePathname(),
      () => navigation.useSearchParams(),
      () => navigation.useParams(),
      () => navigation.useSelectedLayoutSegment(),
      () => navigation.useSelectedLayoutSegments(),
    ];

    for (const readHook of readHooks) {
      const state = createPprFallbackShellState({
        fallbackParamNames: ["slug"],
        routePattern: "/blog/:slug",
      });

      runWithPprFallbackShellState(state, readHook);

      expect(state.hasDynamicBoundary).toBe(true);
    }
  });

  it("useSelectedLayoutSegment still works when provider and hook are loaded from different module instances", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const hookPath = "../packages/vinext/src/shims/navigation.js?hook-instance=a";
    const providerPath =
      "../packages/vinext/src/shims/layout-segment-context.tsx?provider-instance=b";
    const hookMod: typeof import("../packages/vinext/src/shims/navigation.js") = await import(
      hookPath
    );
    const providerMod: typeof import("../packages/vinext/src/shims/layout-segment-context.tsx") =
      await import(providerPath);

    function Probe() {
      const segment = hookMod.useSelectedLayoutSegment();
      return React.createElement("span", { "data-testid": "segment" }, segment ?? "null");
    }

    const html = renderToStaticMarkup(
      React.createElement(providerMod.LayoutSegmentProvider, {
        segmentMap: { children: ["explore"] },
        // oxlint-disable-next-line react/no-children-prop
        children: React.createElement(Probe),
      }),
    );

    expect(html).toContain(">explore<");
  });

  it("ServerInsertedHTMLContext stays shared across multiple module instances", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const providerPath = "../packages/vinext/src/shims/navigation.js?inserted-html-provider=a";
    const consumerPath = "../packages/vinext/src/shims/navigation.js?inserted-html-consumer=b";
    const [providerMod, consumerMod]: [
      typeof import("../packages/vinext/src/shims/navigation.js"),
      typeof import("../packages/vinext/src/shims/navigation.js"),
    ] = await Promise.all([import(providerPath), import(consumerPath)]);
    const providerCtx = providerMod.ServerInsertedHTMLContext;
    const consumerCtx = consumerMod.ServerInsertedHTMLContext;
    expect(providerCtx).toBeTruthy();
    expect(consumerCtx).toBeTruthy();

    const register = () => {};
    let received: unknown = undefined;

    function Probe() {
      received = React.useContext(consumerCtx!);
      return null;
    }

    renderToStaticMarkup(
      React.createElement(providerCtx!.Provider, { value: register }, React.createElement(Probe)),
    );

    expect(received).toBe(register);
  });

  it("useSelectedLayoutSegments returns empty array outside React context", async () => {
    const { useSelectedLayoutSegments } =
      await import("../packages/vinext/src/shims/navigation.js");
    // Outside a React tree, no LayoutSegmentProvider wraps us,
    // so the context defaults to [].
    const segments = useSelectedLayoutSegments();
    expect(segments).toEqual([]);
  });

  it("useSelectedLayoutSegment returns null outside React context", async () => {
    const { useSelectedLayoutSegment } = await import("../packages/vinext/src/shims/navigation.js");
    // Outside a React tree, no LayoutSegmentProvider wraps us,
    // so there are no child segments → null.
    expect(useSelectedLayoutSegment()).toBeNull();
  });

  it("useSelectedLayoutSegments() accepts parallelRoutesKey and returns matching segments", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    // Outside a React tree, returns [] regardless of key.
    const result = nav.useSelectedLayoutSegments("team");
    expect(result).toEqual([]);
  });

  it("useSelectedLayoutSegment() accepts parallelRoutesKey and returns null outside React tree", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    const result = nav.useSelectedLayoutSegment("team");
    expect(result).toBeNull();
  });

  it("useSelectedLayoutSegments(parallelRoutesKey) returns per-slot segments via segmentMap", async () => {
    const React = await import("react");
    const ReactDOMServer = await import("react-dom/server");
    const { createElement } = React;
    const { LayoutSegmentProvider } =
      await import("../packages/vinext/src/shims/layout-segment-context.js");
    const { useSelectedLayoutSegments, useSelectedLayoutSegment } =
      await import("../packages/vinext/src/shims/navigation.js");

    function TestComponent() {
      const childrenSegs = useSelectedLayoutSegments();
      const teamSegs = useSelectedLayoutSegments("team");
      const teamSeg = useSelectedLayoutSegment("team");
      return createElement(
        "div",
        null,
        createElement("span", { id: "children" }, JSON.stringify(childrenSegs)),
        createElement("span", { id: "team" }, JSON.stringify(teamSegs)),
        createElement("span", { id: "team-singular" }, teamSeg ?? "null"),
      );
    }

    const html = ReactDOMServer.renderToStaticMarkup(
      createElement(LayoutSegmentProvider, {
        segmentMap: { children: ["blog", "hello"], team: ["settings"] },
        // oxlint-disable-next-line react/no-children-prop
        children: createElement(TestComponent),
      }),
    );

    // React SSR HTML-encodes " as &quot; in text content
    const Q = "&quot;";
    expect(html).toContain(`<span id="children">[${Q}blog${Q},${Q}hello${Q}]</span>`);
    expect(html).toContain(`<span id="team">[${Q}settings${Q}]</span>`);
    expect(html).toContain('<span id="team-singular">settings</span>');
  });

  it("useSelectedLayoutSegments(unknownKey) returns [] for missing slot", async () => {
    const React = await import("react");
    const ReactDOMServer = await import("react-dom/server");
    const { createElement } = React;
    const { LayoutSegmentProvider } =
      await import("../packages/vinext/src/shims/layout-segment-context.js");
    const { useSelectedLayoutSegments } =
      await import("../packages/vinext/src/shims/navigation.js");

    function TestComponent() {
      const segs = useSelectedLayoutSegments("nonexistent");
      return createElement("span", null, JSON.stringify(segs));
    }

    const html = ReactDOMServer.renderToStaticMarkup(
      createElement(LayoutSegmentProvider, {
        segmentMap: { children: ["blog"] },
        // oxlint-disable-next-line react/no-children-prop
        children: createElement(TestComponent),
      }),
    );

    expect(html).toContain("[]");
  });

  // -------------------------------------------------------------------------
  // unstable_rethrow + unstable_isUnrecognizedActionError
  //
  // Ported from Next.js:
  //   https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/unstable-rethrow.ts
  //   https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/unrecognized-action-error.ts
  // -------------------------------------------------------------------------
  it("exports unstable_rethrow and unstable_isUnrecognizedActionError", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof nav.unstable_rethrow).toBe("function");
    expect(typeof nav.unstable_isUnrecognizedActionError).toBe("function");
    expect(typeof nav.isRedirectError).toBe("function");
    expect(typeof nav.isNextRouterError).toBe("function");
    expect(typeof nav.UnrecognizedActionError).toBe("function");
  });

  it("unstable_rethrow re-throws redirect errors", async () => {
    const { redirect, unstable_rethrow } =
      await import("../packages/vinext/src/shims/navigation.js");
    let captured: unknown = null;
    try {
      redirect("/login");
    } catch (e) {
      captured = e;
    }
    expect(captured).not.toBeNull();

    expect(() => unstable_rethrow(captured)).toThrow();
    try {
      unstable_rethrow(captured);
    } catch (rethrown) {
      // Identity-preserving rethrow — must be the same error reference
      expect(rethrown).toBe(captured);
    }
  });

  it("unstable_rethrow re-throws notFound/forbidden/unauthorized errors", async () => {
    const { notFound, forbidden, unauthorized, unstable_rethrow } =
      await import("../packages/vinext/src/shims/navigation.js");
    for (const trigger of [notFound, forbidden, unauthorized]) {
      let captured: unknown = null;
      try {
        trigger();
      } catch (e) {
        captured = e;
      }
      expect(captured).not.toBeNull();
      expect(() => unstable_rethrow(captured)).toThrow();
    }
  });

  it("unstable_rethrow is a no-op for unrelated errors", async () => {
    const { unstable_rethrow } = await import("../packages/vinext/src/shims/navigation.js");
    // Plain Error: no digest, no cause
    expect(() => unstable_rethrow(new Error("plain"))).not.toThrow();
    expect(() => unstable_rethrow("string error")).not.toThrow();
    expect(() => unstable_rethrow(null)).not.toThrow();
    expect(() => unstable_rethrow(undefined)).not.toThrow();
    expect(() => unstable_rethrow({ digest: "not-a-next-error" })).not.toThrow();
  });

  // -------------------------------------------------------------------------
  // BailoutToCSRError + DynamicServerError parity
  //
  // Ported from Next.js:
  //   https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts
  //   https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/hooks-server-context.ts
  // -------------------------------------------------------------------------
  it("exports BailoutToCSRError + isBailoutToCSRError with the canonical digest", async () => {
    const { BailoutToCSRError, isBailoutToCSRError } =
      await import("../packages/vinext/src/shims/navigation.js");
    const err = new BailoutToCSRError("test-reason");
    expect(err.digest).toBe("BAILOUT_TO_CLIENT_SIDE_RENDERING");
    expect(err.reason).toBe("test-reason");
    expect(err.message).toBe("Bail out to client-side rendering: test-reason");
    expect(isBailoutToCSRError(err)).toBe(true);
    // Predicate matches by digest, not by instanceof — a foreign object with
    // the canonical digest should also be detected (Next.js parity).
    expect(isBailoutToCSRError({ digest: "BAILOUT_TO_CLIENT_SIDE_RENDERING" })).toBe(true);
    // Negative cases.
    expect(isBailoutToCSRError(new Error("plain"))).toBe(false);
    expect(isBailoutToCSRError({ digest: "OTHER" })).toBe(false);
    expect(isBailoutToCSRError(null)).toBe(false);
    expect(isBailoutToCSRError(undefined)).toBe(false);
  });

  it("exports DynamicServerError + isDynamicServerError with the canonical digest", async () => {
    const { DynamicServerError, isDynamicServerError } =
      await import("../packages/vinext/src/shims/navigation.js");
    const err = new DynamicServerError("cookies()");
    expect(err.digest).toBe("DYNAMIC_SERVER_USAGE");
    expect(err.description).toBe("cookies()");
    expect(err.message).toBe("Dynamic server usage: cookies()");
    expect(isDynamicServerError(err)).toBe(true);
    // Predicate matches by digest — Next.js parity.
    expect(isDynamicServerError({ digest: "DYNAMIC_SERVER_USAGE" })).toBe(true);
    // Negative cases.
    expect(isDynamicServerError(new Error("plain"))).toBe(false);
    expect(isDynamicServerError({ digest: "OTHER" })).toBe(false);
    expect(isDynamicServerError(null)).toBe(false);
    expect(isDynamicServerError(undefined)).toBe(false);
  });

  // Mirrors the Next.js fixture
  //   .nextjs-ref/test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx
  // where `cookies()` throws a DynamicServerError inside a try/catch and
  // unstable_rethrow must propagate it.
  it("unstable_rethrow re-throws BailoutToCSRError (next/dynamic ssr:false bailout)", async () => {
    const { BailoutToCSRError, unstable_rethrow } =
      await import("../packages/vinext/src/shims/navigation.js");
    const err = new BailoutToCSRError("Lazy(): No ssr");
    expect(() => unstable_rethrow(err)).toThrow();
    try {
      unstable_rethrow(err);
    } catch (rethrown) {
      expect(rethrown).toBe(err);
    }
  });

  it("unstable_rethrow re-throws DynamicServerError (cookies()/headers() in static render)", async () => {
    const { DynamicServerError, unstable_rethrow } =
      await import("../packages/vinext/src/shims/navigation.js");
    const err = new DynamicServerError("Route used cookies()");
    expect(() => unstable_rethrow(err)).toThrow();
    try {
      unstable_rethrow(err);
    } catch (rethrown) {
      expect(rethrown).toBe(err);
    }
  });

  it("unstable_rethrow does NOT match the four server-only categories vinext does not implement", async () => {
    // These categories (isDynamicPostpone, isPostpone,
    // isHangingPromiseRejectionError, isPrerenderInterruptedError) are
    // server-only Next.js internals tied to PPR / prerender-controller
    // machinery vinext does not implement. They are deferred as follow-ups
    // (see the JSDoc on unstable_rethrow). This test pins that intentional
    // gap so the omission is visible to future maintainers.
    const { unstable_rethrow } = await import("../packages/vinext/src/shims/navigation.js");

    const postpone = { $$typeof: Symbol.for("react.postpone") };
    expect(() => unstable_rethrow(postpone)).not.toThrow();

    const hanging = Object.assign(new Error("hanging"), { digest: "HANGING_PROMISE_REJECTION" });
    expect(() => unstable_rethrow(hanging)).not.toThrow();

    const interrupted = Object.assign(new Error("interrupt"), {
      digest: "NEXT_PRERENDER_INTERRUPTED",
    });
    expect(() => unstable_rethrow(interrupted)).not.toThrow();

    // No fixed digest — message-shape based detection. We don't try to
    // construct a faithful payload here; just confirm an arbitrary message
    // doesn't match.
    const dynamicPostpone = new Error("some random message");
    expect(() => unstable_rethrow(dynamicPostpone)).not.toThrow();
  });

  it("unstable_rethrow recurses through error.cause to rethrow wrapped Next.js errors", async () => {
    const { redirect, unstable_rethrow } =
      await import("../packages/vinext/src/shims/navigation.js");
    let inner: unknown = null;
    try {
      redirect("/login");
    } catch (e) {
      inner = e;
    }
    const wrapped = new Error("user wrapped this", { cause: inner });

    expect(() => unstable_rethrow(wrapped)).toThrow();
    try {
      unstable_rethrow(wrapped);
    } catch (rethrown) {
      // The recursion should rethrow the original Next.js error, not the wrapper
      expect(rethrown).toBe(inner);
    }
  });

  it("unstable_isUnrecognizedActionError returns true for UnrecognizedActionError instances", async () => {
    const { UnrecognizedActionError, unstable_isUnrecognizedActionError } =
      await import("../packages/vinext/src/shims/navigation.js");
    const err = new UnrecognizedActionError("missing action 'abc'");
    expect(err.name).toBe("UnrecognizedActionError");
    expect(unstable_isUnrecognizedActionError(err)).toBe(true);
  });

  it("unstable_isUnrecognizedActionError returns false for other errors", async () => {
    const { unstable_isUnrecognizedActionError } =
      await import("../packages/vinext/src/shims/navigation.js");
    expect(unstable_isUnrecognizedActionError(new Error("other"))).toBe(false);
    expect(unstable_isUnrecognizedActionError(null)).toBe(false);
    expect(unstable_isUnrecognizedActionError(undefined)).toBe(false);
    expect(unstable_isUnrecognizedActionError("string")).toBe(false);
    expect(unstable_isUnrecognizedActionError({ name: "UnrecognizedActionError" })).toBe(false);
  });

  // -------------------------------------------------------------------------
  // useUntrackedPathname
  //
  // Ported from Next.js:
  //   https://github.com/vercel/next.js/blob/v16.2.6/packages/next/src/client/components/navigation-untracked.ts
  // -------------------------------------------------------------------------
  it("useUntrackedPathname returns '/' on server when no navigation context is set", async () => {
    const { useUntrackedPathname } =
      await import("../packages/vinext/src/shims/internal/navigation-untracked.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    let captured: string | null = "";
    function Probe() {
      captured = useUntrackedPathname();
      return React.createElement("span", null, captured ?? "null");
    }

    renderToStaticMarkup(React.createElement(Probe));
    expect(captured).toBe("/");
  });

  it("useUntrackedPathname returns pathname from server context", async () => {
    const { useUntrackedPathname } =
      await import("../packages/vinext/src/shims/internal/navigation-untracked.js");
    const { setNavigationContext } = await import("../packages/vinext/src/shims/navigation.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    setNavigationContext({
      pathname: "/untracked/test",
      searchParams: new URLSearchParams(""),
      params: {},
    });

    let captured: string | null = "";
    function Probe() {
      captured = useUntrackedPathname();
      return React.createElement("span", null, captured ?? "null");
    }

    try {
      renderToStaticMarkup(React.createElement(Probe));
      expect(captured).toBe("/untracked/test");
    } finally {
      setNavigationContext(null);
    }
  });

  it("useUntrackedPathname prefers render snapshot during active navigation", async () => {
    const previousWindow = globalThis.window;
    const win = {
      addEventListener() {},
      dispatchEvent() {
        return true;
      },
      history: {
        pushState() {},
        replaceState() {},
      },
      location: {
        href: "http://localhost/committed",
        origin: "http://localhost",
        pathname: "/committed",
        search: "",
      },
      removeEventListener() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const React = await import("react");
      const { renderToStaticMarkup } = await import("react-dom/server");
      const { useUntrackedPathname } =
        await import("../packages/vinext/src/shims/internal/navigation-untracked.js");
      const navigation = await import("../packages/vinext/src/shims/navigation.js");
      const Context = navigation.getClientNavigationRenderContext();
      if (!Context) {
        throw new Error("Expected client navigation render context");
      }

      navigation.activateNavigationSnapshot();
      const snapshot = navigation.createClientNavigationRenderSnapshot(
        "http://localhost/pending-untracked?from=snapshot",
        {},
      );

      let pathname: string | null = "";
      function Probe() {
        pathname = useUntrackedPathname();
        return React.createElement("span", null, pathname ?? "null");
      }

      renderToStaticMarkup(
        React.createElement(Context.Provider, { value: snapshot }, React.createElement(Probe)),
      );
      expect(pathname).toBe("/pending-untracked");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("useUntrackedPathname ignores stale render snapshot after navigation commits", async () => {
    const previousWindow = globalThis.window;
    const win = {
      addEventListener() {},
      dispatchEvent() {
        return true;
      },
      history: {
        pushState() {},
        replaceState() {},
      },
      location: {
        href: "http://localhost/committed",
        origin: "http://localhost",
        pathname: "/committed",
        search: "",
      },
      removeEventListener() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const React = await import("react");
      const { renderToStaticMarkup } = await import("react-dom/server");
      const { useUntrackedPathname } =
        await import("../packages/vinext/src/shims/internal/navigation-untracked.js");
      const navigation = await import("../packages/vinext/src/shims/navigation.js");
      const Context = navigation.getClientNavigationRenderContext();
      if (!Context) {
        throw new Error("Expected client navigation render context");
      }

      // Snapshot is present but NOT active (navigationSnapshotActiveCount === 0).
      // This simulates a post-commit re-render where the provider still wraps
      // the tree but the URL has since changed via user pushState/replaceState.
      const snapshot = navigation.createClientNavigationRenderSnapshot(
        "http://localhost/stale-provider?from=snapshot",
        {},
      );

      let pathname: string | null = "";
      function Probe() {
        pathname = useUntrackedPathname();
        return React.createElement("span", null, pathname ?? "null");
      }

      renderToStaticMarkup(
        React.createElement(Context.Provider, { value: snapshot }, React.createElement(Probe)),
      );
      // Must return the committed pathname, not the stale provider snapshot.
      expect(pathname).toBe("/committed");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("useUntrackedPathname returns Pages Router pathname when no App context is set", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const accessorKey = Symbol.for("vinext.navigation.pagesNavigationContextAccessor");
    const globalRecord = globalThis as Record<PropertyKey, unknown>;
    const previousAccessor = globalRecord[accessorKey];

    try {
      globalRecord[accessorKey] = () => ({
        pathname: "/pages/untracked",
        searchParams: new URLSearchParams(""),
        params: {},
      });

      const hookPath =
        "../packages/vinext/src/shims/internal/navigation-untracked.js?pages-untracked=1";
      const { useUntrackedPathname } = (await import(
        hookPath
      )) as typeof import("../packages/vinext/src/shims/internal/navigation-untracked.js");

      let captured: string | null = "";
      function Probe() {
        captured = useUntrackedPathname();
        return React.createElement("span", null, captured ?? "null");
      }

      renderToStaticMarkup(React.createElement(Probe));
      expect(captured).toBe("/pages/untracked");
    } finally {
      if (previousAccessor === undefined) {
        delete globalRecord[accessorKey];
      } else {
        globalRecord[accessorKey] = previousAccessor;
      }
    }
  });

  // -------------------------------------------------------------------------
  // react-server re-exports
  // -------------------------------------------------------------------------
  it("navigation.react-server re-exports unstable_rethrow and stubs unstable_isUnrecognizedActionError", async () => {
    const rsc = await import("../packages/vinext/src/shims/navigation.react-server.js");
    expect(typeof rsc.unstable_rethrow).toBe("function");
    expect(typeof rsc.unstable_isUnrecognizedActionError).toBe("function");

    // The RSC stub should throw a clear "client-only" error, matching Next.js.
    expect(() => rsc.unstable_isUnrecognizedActionError()).toThrow(/client/i);

    // unstable_rethrow itself is environment-agnostic — re-exported from
    // ./navigation.js — and should behave identically to the client export.
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");
    let captured: unknown = null;
    try {
      redirect("/login");
    } catch (e) {
      captured = e;
    }
    expect(() => rsc.unstable_rethrow(captured)).toThrow();
  });
});

// ---------------------------------------------------------------------------
// next/error shim — unstable_catchError
//
// Ported from Next.js:
//   https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/catch-error.tsx
//   https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/catch-error/
// ---------------------------------------------------------------------------
describe("next/error shim — unstable_catchError", () => {
  // Ported from Next.js:
  // packages/next/src/api/error.react-server.ts
  // https://github.com/vercel/next.js/blob/v16.2.6/packages/next/src/api/error.react-server.ts
  it("error.react-server throws the exact Client Components diagnostic", async () => {
    const { unstable_catchError } =
      await import("../packages/vinext/src/shims/error.react-server.js");

    expect(() => unstable_catchError()).toThrow(
      "`unstable_catchError` can only be used in Client Components.",
    );
  });

  it("exports unstable_catchError as a function", async () => {
    const mod = await import("../packages/vinext/src/shims/error.js");
    expect(typeof mod.unstable_catchError).toBe("function");
  });

  it("returns a Component that renders children when no error occurs", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

    function Fallback(_props: { title: string }) {
      return React.createElement("p", null, "should not render");
    }

    const Boundary = unstable_catchError<{ title: string }>(Fallback);
    const html = renderToStaticMarkup(
      React.createElement(
        Boundary,
        { title: "ignored" },
        React.createElement("span", { id: "ok" }, "hello"),
      ),
    );

    expect(html).toContain('id="ok"');
    expect(html).toContain("hello");
  });

  it("rethrows Next.js navigation signals (redirect, notFound) instead of catching them", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");

    function RedirectThrower(): React.ReactElement {
      redirect("/login");
      // Unreachable: `redirect()` throws. Annotated explicitly to satisfy
      // the `ReactElement` return-type contract.
      return React.createElement("span", null);
    }

    function Fallback() {
      return React.createElement("p", null, "should not be reached");
    }

    const Boundary = unstable_catchError(Fallback);

    const spy = vi.spyOn(console, "error").mockImplementation(() => {});
    try {
      // The boundary must let the redirect propagate up — renderToStaticMarkup
      // should throw with the redirect digest.
      let captured: unknown = null;
      try {
        renderToStaticMarkup(
          React.createElement(Boundary, null, React.createElement(RedirectThrower)),
        );
      } catch (e) {
        captured = e;
      }
      expect(captured).not.toBeNull();
      expect((captured as { digest?: string }).digest).toContain("NEXT_REDIRECT");
    } finally {
      spy.mockRestore();
    }
  });

  it("exposes the displayName matching Next.js (`unstable_catchError(...)`)", async () => {
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");
    const Fallback = function MyFallback() {
      return null;
    };
    const Boundary = unstable_catchError(Fallback);
    // Wrapper component carries the user fallback name for DevTools.
    expect(Boundary.displayName).toBe("unstable_catchError(MyFallback)");
  });

  // Ported from Next.js:
  //   .nextjs-ref/test/e2e/app-dir/catch-error/catch-error.test.ts
  //   "should render fallback when null is thrown from a Client Component"
  //   "should render fallback when undefined is thrown from a Client Component"
  //
  // The boundary must accept null/undefined as `thrownValue` (not just Error
  // instances) and route them to the fallback. Crucially, the rethrow guard
  // (`isNextRouterError`) must not crash on null/undefined inputs.
  it("class-component getDerivedStateFromError accepts null thrown values", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

    function Fallback() {
      return null;
    }
    const Boundary = unstable_catchError(Fallback);

    // The Boundary wrapper is a function component that calls hooks
    // (usePathname, useContext). We must call it inside a React render
    // so React's dispatcher is active.
    let wrapperResult: React.ReactElement | null = null;
    function Capture() {
      wrapperResult = (Boundary as unknown as (p: Record<string, never>) => React.ReactElement)({});
      return React.createElement("span");
    }
    renderToStaticMarkup(React.createElement(Capture));

    const InnerCatchError = wrapperResult!.type as unknown as {
      getDerivedStateFromError(e: unknown): { error: { thrownValue: unknown } | null };
    };

    const derivedNull = InnerCatchError.getDerivedStateFromError(null);
    expect(derivedNull).toEqual({ error: { thrownValue: null } });

    const derivedUndefined = InnerCatchError.getDerivedStateFromError(undefined);
    expect(derivedUndefined).toEqual({ error: { thrownValue: undefined } });

    // Exhaustive non-error primitives (strings, numbers, booleans) should
    // also flow through to the fallback without throwing.
    void React; // keep import for module side-effects parity with other tests
    expect(InnerCatchError.getDerivedStateFromError("string")).toEqual({
      error: { thrownValue: "string" },
    });
    expect(InnerCatchError.getDerivedStateFromError(0)).toEqual({
      error: { thrownValue: 0 },
    });
  });

  // class-component lifecycle catches non-router errors and renders the fallback
  it("class-component lifecycle catches non-router errors and renders the fallback", async () => {
    // React 19's renderToStaticMarkup does NOT invoke error boundaries during
    // SSR — errors propagate up by design (boundaries only run during client
    // commit). To validate behavior without spinning up a real browser, we
    // exercise the lifecycle hooks directly: `getDerivedStateFromError` is
    // the canonical predicate driving the class component's behavior, and
    // its return value is the only thing the React runtime feeds into the
    // next render.
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

    const seenErrors: unknown[] = [];
    function Fallback({
      props,
      errorInfo,
    }: {
      props: { title: string };
      errorInfo: { error: unknown; reset: () => void; unstable_retry: () => void };
    }) {
      seenErrors.push(errorInfo.error);
      const message =
        errorInfo.error instanceof Error ? errorInfo.error.message : String(errorInfo.error);
      return React.createElement(
        "div",
        null,
        React.createElement("p", { id: "title" }, props.title),
        React.createElement("p", { id: "msg" }, message),
      );
    }

    // `as any` is necessary here because this test probes the inner
    // `_CatchError` class component directly, where `Fallback` is used
    // as a React component type (receives `{ props, errorInfo }` as a
    // single props object per React.createElement semantics), matching
    // the internal wrapper shape rather than the public API signature
    // `(props: P, errorInfo: ErrorInfo) => React.ReactNode`.
    const Boundary = unstable_catchError<{ title: string }>(Fallback as any);

    // The Boundary wrapper is a function component that calls hooks.
    // We must call it inside a React render so React's dispatcher is active.
    let wrapperResult: React.ReactElement | null = null;
    function Capture() {
      wrapperResult = (
        Boundary as unknown as (props: {
          title: string;
          children?: React.ReactNode;
        }) => React.ReactElement
      )({
        title: "hello-title",
        children: React.createElement("span", null, "child"),
      });
      return React.createElement("span");
    }
    renderToStaticMarkup(React.createElement(Capture));

    const InnerCatchError = wrapperResult!.type as unknown as React.ComponentClass<{
      fallback: typeof Fallback;
      props: { title: string };
      children?: React.ReactNode;
    }> & {
      getDerivedStateFromError(thrownValue: unknown): { error: { thrownValue: unknown } | null };
    };

    const props = {
      fallback: Fallback,
      props: { title: "hello-title" },
      children: React.createElement("span", null, "child"),
    };
    // Cast through unknown to instantiate without engaging React's renderer.
    const instance = new (InnerCatchError as unknown as new (p: typeof props) => InstanceType<
      typeof InnerCatchError
    > & {
      state: { error: { thrownValue: unknown } | null };
      render(): React.ReactNode;
    })(props);

    // Before an error: children render untouched.
    expect(renderToStaticMarkup(instance.render() as React.ReactElement)).toContain("child");

    // Simulate React calling getDerivedStateFromError with a thrown Error.
    const thrown = new Error("boom");
    const derived = InnerCatchError.getDerivedStateFromError(thrown);
    expect(derived).toEqual({ error: { thrownValue: thrown } });

    // Feed the derived state into the instance and render the fallback.
    instance.state = derived as { error: { thrownValue: unknown } | null };
    const fallbackOutput = renderToStaticMarkup(instance.render() as React.ReactElement);
    expect(fallbackOutput).toContain("boom");
    expect(fallbackOutput).toContain("hello-title");
    expect(seenErrors[seenErrors.length - 1]).toBe(thrown);
  });

  it("class-component getDerivedStateFromError re-throws Next.js router errors", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");
    const { redirect } = await import("../packages/vinext/src/shims/navigation.js");

    function Fallback() {
      return null;
    }
    const Boundary = unstable_catchError(Fallback);

    // The Boundary wrapper is a function component that calls hooks.
    // We must call it inside a React render so React's dispatcher is active.
    let wrapperResult: React.ReactElement | null = null;
    function Capture() {
      wrapperResult = (Boundary as unknown as (p: Record<string, never>) => React.ReactElement)({});
      return React.createElement("span");
    }
    renderToStaticMarkup(React.createElement(Capture));

    const InnerCatchError = wrapperResult!.type as unknown as {
      getDerivedStateFromError(e: unknown): unknown;
    };

    let captured: unknown = null;
    try {
      redirect("/login");
    } catch (e) {
      captured = e;
    }
    expect(() => InnerCatchError.getDerivedStateFromError(captured)).toThrow();
    try {
      InnerCatchError.getDerivedStateFromError(captured);
    } catch (rethrown) {
      // Identity-preserving rethrow.
      expect(rethrown).toBe(captured);
    }
  });

  it("unstable_retry on the client calls appRouterInstance.refresh and resets the boundary", async () => {
    // Stub `window` with the minimum surface navigation.ts needs at
    // module-load time (location, history, addEventListener). This must be
    // installed BEFORE re-importing the shims, otherwise navigation.ts will
    // initialize its client navigation state against a bare `{}` and crash.
    // Mutable view over globalThis that allows assigning/deleting `window`.
    const globalAny = globalThis as unknown as { window?: unknown };
    const previousWindow = globalAny.window;
    const win = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        origin: "http://localhost",
      },
      history: {
        state: null,
        pushState() {},
        replaceState() {},
      },
      addEventListener() {},
      dispatchEvent() {
        return true;
      },
      removeEventListener() {},
      scrollTo() {},
      scrollX: 0,
      scrollY: 0,
    };
    globalAny.window = win;

    try {
      vi.resetModules();

      const React = (await import("react")).default;
      const { renderToStaticMarkup } = await import("react-dom/server");
      const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

      const refreshSpy = vi.fn();

      function Fallback() {
        return null;
      }
      const Boundary = unstable_catchError(Fallback);
      // The Boundary wrapper is a function component that calls hooks.
      // We must call it inside a React render so React's dispatcher is active.
      let wrapperResult: React.ReactElement | null = null;
      function Capture() {
        wrapperResult = (Boundary as unknown as (p: Record<string, never>) => React.ReactElement)(
          {},
        );
        return React.createElement("span");
      }
      renderToStaticMarkup(React.createElement(Capture));

      const InnerCatchError = wrapperResult!.type as unknown as new (props: object) => {
        state: { error: { thrownValue: unknown } | null };
        unstable_retry: () => void;
      };
      const instance = new InnerCatchError({
        fallback: Fallback,
        props: {},
      });
      // Manually instantiating the class skips React's context machinery.
      // Seed `this.context` with a mock App Router instance so the App Router
      // branch in `unstable_retry` fires and calls `context.refresh()`.
      (instance as unknown as { context: { refresh: typeof refreshSpy } }).context = {
        refresh: refreshSpy,
      };

      // Seed an error so reset has something to clear, and replace setState
      // with a spy so we can confirm the boundary self-resets.
      instance.state = { error: { thrownValue: new Error("boom") } };
      const setStateCalls: Array<{ error: { thrownValue: unknown } | null }> = [];
      (instance as unknown as { setState: (partial: object) => void }).setState = (partial) => {
        setStateCalls.push(partial as { error: { thrownValue: unknown } | null });
        instance.state = { ...instance.state, ...(partial as object) } as typeof instance.state;
      };

      // startTransition runs synchronously here because there's no
      // concurrent renderer in the test environment.
      void React.startTransition;
      instance.unstable_retry();

      expect(refreshSpy).toHaveBeenCalledTimes(1);
      expect(setStateCalls).toHaveLength(1);
      expect(setStateCalls[0]).toEqual({ error: null });
    } finally {
      globalAny.window = previousWindow;
      vi.resetModules();
    }
  });

  // class component's `contextType`). When a non-null Pages Router instance
  // is in context, `unstable_retry()` must throw the verbatim Next.js
  // message instead of calling App Router's `refresh()`.
  it("unstable_retry under Pages Router throws Next.js parity error message", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

    function Fallback() {
      return null;
    }
    const Boundary = unstable_catchError(Fallback);
    // The Boundary wrapper is a function component that calls hooks.
    // We must call it inside a React render so React's dispatcher is active.
    let wrapperResult: React.ReactElement | null = null;
    function Capture() {
      wrapperResult = (Boundary as unknown as (p: Record<string, never>) => React.ReactElement)({});
      return React.createElement("span");
    }
    renderToStaticMarkup(React.createElement(Capture));

    const InnerCatchError = wrapperResult!.type as unknown as new (props: object) => {
      state: { error: { thrownValue: unknown } | null };
      unstable_retry: () => void;
    };
    const instance = new InnerCatchError({
      fallback: Fallback,
      isPagesRouter: true,
      props: {},
    });
    instance.state = { error: { thrownValue: new Error("boom") } };

    void React; // keep React import for parity with sibling tests

    expect(() => instance.unstable_retry()).toThrow(
      "`unstable_retry()` can only be used in the App Router. Use `reset()` in the Pages Router.",
    );
  });

  // Integration test: the boundary contract that useUntrackedPathname protects.
  // The error boundary must clear its captured error when the pathname changes
  // (navigation) and retain it when the pathname stays the same. This is the
  // reason useUntrackedPathname exists: it provides a pathname that changes on
  // navigation without subscribing the component to every URL change.
  it("getDerivedStateFromProps clears the error when pathname changes and retains it when pathname stays the same", async () => {
    const React = (await import("react")).default;
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { unstable_catchError } = await import("../packages/vinext/src/shims/error.js");

    function Fallback() {
      return null;
    }
    const Boundary = unstable_catchError(Fallback);

    // The Boundary wrapper is a function component that calls hooks.
    // We must call it inside a React render so React's dispatcher is active.
    let wrapperResult: React.ReactElement | null = null;
    function Capture() {
      wrapperResult = (Boundary as unknown as (p: Record<string, never>) => React.ReactElement)({});
      return React.createElement("span");
    }
    renderToStaticMarkup(React.createElement(Capture));

    // Structural type for the internal class component so we can call
    // getDerivedStateFromProps without importing non-public exports.
    const InnerCatchError = wrapperResult!.type as unknown as React.ComponentClass<{
      fallback: typeof Fallback;
      pathname: string | null;
      props: Record<string, never>;
    }> & {
      getDerivedStateFromProps(
        props: Pick<{ pathname: string | null }, "pathname">,
        state: { error: { thrownValue: unknown } | null; previousPathname: string | null },
      ): { error: { thrownValue: unknown } | null; previousPathname: string | null };
    };

    const thrown = new Error("boom");
    const initialState = {
      error: { thrownValue: thrown },
      previousPathname: "/initial",
    };

    // Same pathname: error must be retained
    const samePath = InnerCatchError.getDerivedStateFromProps(
      { pathname: "/initial" },
      initialState,
    );
    expect(samePath).toEqual({
      error: { thrownValue: thrown },
      previousPathname: "/initial",
    });

    // Different pathname: error must be cleared
    const changedPath = InnerCatchError.getDerivedStateFromProps(
      { pathname: "/new" },
      initialState,
    );
    expect(changedPath).toEqual({
      error: null,
      previousPathname: "/new",
    });

    // Null pathname to real pathname: error must be cleared.
    // This matches the missing-params shell → real render transition.
    const nullToReal = InnerCatchError.getDerivedStateFromProps(
      { pathname: "/real" },
      { error: { thrownValue: thrown }, previousPathname: null },
    );
    expect(nullToReal).toEqual({
      error: null,
      previousPathname: "/real",
    });
  });
});

// ---------------------------------------------------------------------------
// window.next debug/diagnostic global
//
// Next.js exposes a `window.next` object from both the Pages Router client
// bootstrap (packages/next/src/client/next.ts) and the App Router bootstrap
// (packages/next/src/client/app-bootstrap.ts). Pages Router test suites,
// userland code, and third-party libraries reach into it directly — most
// commonly `window.next.router.push(...)` and
// `window.next.router.events.on(...)`. Without this global, the Next.js
// deploy test suite reports ~422 console errors and 30+ runtime failures
// against vinext, with `TypeError: Cannot read properties of undefined
// (reading 'router')` the most cited.
//
// The installer helper lives in packages/vinext/src/client/window-next.ts
// and is invoked from both router bootstraps. The Pages Router shim
// (shims/router.ts) installs it as a top-level side effect, and the App
// Router browser entry (server/app-browser-entry.ts) installs it before
// hydration starts.
// ---------------------------------------------------------------------------
describe("window.next debug global", () => {
  it("installWindowNext sets version, router, and appDir fields on window.next", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {};
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { installWindowNext } = await import("../packages/vinext/src/client/window-next.js");

      const routerStub = {
        push() {},
        replace() {},
        back() {},
        forward() {},
        refresh() {},
        prefetch() {},
      };
      installWindowNext({ version: "test-version", appDir: true, router: routerStub });

      expect(win.next).toBeDefined();
      expect(win.next.version).toBe("test-version");
      expect(win.next.appDir).toBe(true);
      expect(win.next.router).toBe(routerStub);
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("installWindowNext merges subsequent calls without clobbering existing fields", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {};
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { installWindowNext } = await import("../packages/vinext/src/client/window-next.js");

      const pagesRouter = { kind: "pages" } as any;
      const appRouter = { kind: "app" } as any;

      installWindowNext({ version: "v1", router: pagesRouter });
      installWindowNext({ appDir: true, router: appRouter });

      // Whichever installer ran last (in real-world hybrid setups, App
      // Router) wins for `router` — mirrors Next.js's load order where
      // app-bootstrap.ts runs after next.ts.
      expect(win.next.router).toBe(appRouter);
      expect(win.next.appDir).toBe(true);
      // Fields not overridden are preserved.
      expect(win.next.version).toBe("v1");
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("setWindowNextInternalSourcePage mirrors the App Router source-page field", async () => {
    // Ported from Next.js: test/e2e/app-dir/app/index.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app/index.test.ts
    const previousWindow = (globalThis as any).window;
    const win: any = {};
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { setWindowNextInternalSourcePage } =
        await import("../packages/vinext/src/client/window-next.js");

      setWindowNextInternalSourcePage("/dashboard/page");
      expect(win.next.__internal_src_page).toBe("/dashboard/page");

      setWindowNextInternalSourcePage("/dynamic/[category]/[id]/page");
      expect(win.next.__internal_src_page).toBe("/dynamic/[category]/[id]/page");

      setWindowNextInternalSourcePage(null);
      expect(win.next.__internal_src_page).toBeUndefined();
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("Pages Router shim installs window.next.router with the expected NextRouter surface", async () => {
    // Build a minimal fake window so importing shims/router.ts (which
    // touches window at module load to attach popstate) does not crash.
    const previousWindow = (globalThis as any).window;
    const win: any = {
      location: { pathname: "/", search: "", hash: "", href: "http://localhost/" },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      // Side-effecting import: installs window.next.router at module load.
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      expect(win.next).toBeDefined();
      expect(win.next.router).toBe(Router);

      // Ported from Next.js: NextRouter type in
      // packages/next/src/shared/lib/router/router.ts (line 372).
      const router = win.next.router;
      expect(typeof router.push).toBe("function");
      expect(typeof router.replace).toBe("function");
      expect(typeof router.back).toBe("function");
      expect(typeof router.reload).toBe("function");
      expect(typeof router.prefetch).toBe("function");
      expect(typeof router.beforePopState).toBe("function");
      expect(router.events).toBeDefined();
      expect(typeof router.events.on).toBe("function");
      expect(typeof router.events.off).toBe("function");
      expect(typeof router.events.emit).toBe("function");

      // State fields — Next.js's singletonRouter exposes pathname/route/query/
      // asPath/basePath/locale*/isReady/isPreview/isFallback as live getters
      // off `window.next.router` (urlPropertyFields in
      // .nextjs-ref/packages/next/src/client/router.ts lines 32-47). The
      // Next.js deploy suite drives navigations via
      // `browser.eval('next.router.push(...)')` and then asserts on
      // `browser.eval('next.router.pathname')`, so these must read like data
      // properties, not raise.
      expect(typeof router.pathname).toBe("string");
      expect(typeof router.asPath).toBe("string");
      expect(typeof router.query).toBe("object");
      expect(typeof router.basePath).toBe("string");
      expect(typeof router.isReady).toBe("boolean");
      expect(typeof router.isPreview).toBe("boolean");
      expect(typeof router.isFallback).toBe("boolean");
      expect(typeof router.route).toBe("string");
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  // Ported from Next.js: tests that rely on `window.next.router.events.on(...)`
  // — e.g. test/development/pages-dir/client-navigation/index.test.ts:457
  // (`window.next.router.events.on('routeChangeError', ...)`).
  it("window.next.router.events forwards Pages Router events", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {
      location: { pathname: "/", search: "", hash: "", href: "http://localhost/" },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");

      const fired: unknown[] = [];
      win.next.router.events.on("routeChangeStart", (url: unknown) => {
        fired.push(url);
      });
      win.next.router.events.emit("routeChangeStart", "/next-page");
      expect(fired).toEqual(["/next-page"]);
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  // Issue #1467 — `window.next.router.push(...)` must resolve to a boolean
  // (true on success, false if blocked), matching Next.js's contract in
  // `packages/next/src/shared/lib/router/router.ts:1025-1048` where push/replace
  // delegate to `change()` which returns `Promise<boolean>`. The Next.js deploy
  // test suite (prerender, use-router-with-rewrites, etc.) reads the resolved
  // value via `browser.eval('await window.next.router.push("/foo")')` and
  // asserts truthiness; resolving to `undefined` is observable as a regression
  // even when the navigation otherwise succeeds.
  it("window.next.router.push resolves to true on shallow success", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        origin: "http://localhost",
      },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");

      // Shallow push avoids the HTML fetch + render path so this unit test
      // does not need a navigateClient stub. The boolean-return contract
      // applies equally to shallow and non-shallow navigations.
      const result = await win.next.router.push("/foo", undefined, { shallow: true });
      expect(result).toBe(true);
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("window.next.router.replace resolves to true on shallow success", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        origin: "http://localhost",
      },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");

      const result = await win.next.router.replace("/foo", undefined, { shallow: true });
      expect(result).toBe(true);
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  // Shallow navigations skip the render-commit path, so the scroll reset is
  // applied synchronously in performNavigation — before routeChangeComplete.
  // This matches the non-shallow path, where the x/y reset runs inside the
  // render-commit callback (also ahead of routeChangeComplete). Pins the
  // ordering so listeners observe the final scroll position.
  it("shallow push applies the scroll reset before routeChangeComplete", async () => {
    const previousWindow = (globalThis as any).window;
    const order: string[] = [];
    const win: any = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        origin: "http://localhost",
      },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo(x: number, y: number) {
        order.push(`scrollTo:${x},${y}`);
      },
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");

      win.next.router.events.on("routeChangeComplete", () => {
        order.push("routeChangeComplete");
      });
      const result = await win.next.router.push("/foo", undefined, { shallow: true });
      expect(result).toBe(true);
      expect(order).toEqual(["scrollTo:0,0", "routeChangeComplete"]);
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("query-only UrlObjects preserve the current visible pathname", async () => {
    const previousWindow = (globalThis as any).window;
    const pushState = vi.fn();
    const win: any = {
      location: {
        pathname: "/rewrite-navigation/0",
        search: "",
        hash: "",
        href: "http://localhost/rewrite-navigation/0",
        origin: "http://localhost",
      },
      history: { state: null, pushState, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");

      const result = await routerModule.default.push({ query: { id: "1" } }, undefined, {
        shallow: true,
      });

      expect(result).toBe(true);
      expect(pushState).toHaveBeenCalledWith(
        expect.objectContaining({
          url: "/rewrite-navigation/0?id=1",
          as: "/rewrite-navigation/0?id=1",
        }),
        "",
        "/rewrite-navigation/0?id=1",
      );
    } finally {
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it.each([
    [{}, "/rewrite-navigation/[id]/destination", "/rewrite-navigation/[id]/destination"],
    [{ query: {} }, "/rewrite-navigation/[id]/destination", "/rewrite-navigation/[id]/destination"],
    [{ search: "flag" }, "/rewrite-navigation/0?flag", "/rewrite-navigation/0?flag"],
    [{ search: "q=a%20b" }, "/rewrite-navigation/0?q=a%20b", "/rewrite-navigation/0?q=a%20b"],
    [{ search: "q=a#b" }, "/rewrite-navigation/0?q=a%23b", "/rewrite-navigation/0?q=a%23b"],
    [{ search: "q=a#b#c" }, "/rewrite-navigation/0?q=a%23b#c", "/rewrite-navigation/0?q=a%23b"],
    [{ search: "?" }, "/rewrite-navigation/0?", "/rewrite-navigation/0?"],
    [
      { query: { id: "ignored" }, search: "id=3" },
      "/rewrite-navigation/0?id=3",
      "/rewrite-navigation/0?id=3",
    ],
    [
      { search: "q=a%20b", hash: "result" },
      "/rewrite-navigation/0?q=a%20b#result",
      "/rewrite-navigation/0?q=a%20b",
    ],
    [
      { search: "q=a#b", hash: "result" },
      "/rewrite-navigation/0?q=a%23b#result",
      "/rewrite-navigation/0?q=a%23b",
    ],
    [
      { query: { id: "ignored" }, search: "?id=3", hash: "#result" },
      "/rewrite-navigation/0?id=3#result",
      "/rewrite-navigation/0?id=3",
    ],
    [
      { hash: "section" },
      "/rewrite-navigation/0?existing=1#section",
      "/rewrite-navigation/0?existing=1",
    ],
  ])(
    "formats Pages Router UrlObjects with Next.js semantics: %j",
    async (url, expected, stateAs) => {
      const previousWindow = (globalThis as any).window;
      const previousDocument = (globalThis as any).document;
      const pushState = vi.fn();
      const win: any = {
        location: {
          pathname: "/rewrite-navigation/0",
          search: "?existing=1",
          hash: "",
          href: "http://localhost/rewrite-navigation/0?existing=1",
          origin: "http://localhost",
        },
        history: { state: null, pushState, replaceState() {} },
        addEventListener() {},
        dispatchEvent() {},
        scrollTo() {},
        __NEXT_DATA__: {
          page: "/rewrite-navigation/[id]/destination",
          query: { id: "0" },
          isFallback: false,
        },
      };
      (globalThis as any).window = win;
      (globalThis as any).document = {
        getElementById: vi.fn(() => null),
        getElementsByName: vi.fn(() => []),
      };

      try {
        vi.resetModules();
        const routerModule = await import("../packages/vinext/src/shims/router.js");
        await routerModule.default.push(url, undefined, { shallow: true });

        expect(pushState).toHaveBeenCalledWith(
          expect.objectContaining({ as: stateAs }),
          "",
          expected,
        );
      } finally {
        (globalThis as any).window = previousWindow;
        if (previousDocument === undefined) delete (globalThis as any).document;
        else (globalThis as any).document = previousDocument;
        vi.resetModules();
      }
    },
  );

  it.each(["push", "replace"] as const)(
    "preserves a bare trailing question mark in router.asPath after %s",
    async (method) => {
      const previousWindow = (globalThis as any).window;
      const win: any = {
        location: {
          pathname: "/rewrite-navigation/0",
          search: "",
          hash: "",
          href: "http://localhost/rewrite-navigation/0",
          origin: "http://localhost",
        },
        history: {
          state: null as unknown,
          pushState(state: unknown, _title: string, url: string) {
            this.state = state;
            const nextUrl = new URL(url, win.location.href);
            win.location.pathname = nextUrl.pathname;
            win.location.search = nextUrl.search;
            win.location.hash = nextUrl.hash;
            win.location.href = nextUrl.href;
          },
          replaceState(state: unknown, _title: string, url?: string) {
            this.state = state;
            if (url === undefined) return;
            const nextUrl = new URL(url, win.location.href);
            win.location.pathname = nextUrl.pathname;
            win.location.search = nextUrl.search;
            win.location.hash = nextUrl.hash;
            win.location.href = nextUrl.href;
          },
        },
        addEventListener() {},
        dispatchEvent() {},
        scrollTo() {},
        scrollX: 0,
        scrollY: 0,
        __NEXT_DATA__: {
          page: "/rewrite-navigation/[id]/destination",
          query: { id: "0" },
          isFallback: false,
        },
      };
      (globalThis as any).window = win;

      try {
        vi.resetModules();
        const routerModule = await import("../packages/vinext/src/shims/router.js");
        await routerModule.default[method]({ search: "?" }, undefined, { shallow: true });

        expect(win.location.search).toBe("");
        expect(win.history.state.as).toBe("/rewrite-navigation/0?");
        expect(routerModule.default.asPath).toBe("/rewrite-navigation/0?");
      } finally {
        if (previousWindow === undefined) delete (globalThis as any).window;
        else (globalThis as any).window = previousWindow;
        vi.resetModules();
      }
    },
  );

  it.each([
    ["/", "", "/#section"],
    ["/posts", "?tab=1", "/posts?tab=1#section"],
  ])(
    "preserves the visible path and search in router.asPath after hash-only navigation from %s%s",
    async (pathname, search, expectedAsPath) => {
      const previousWindow = (globalThis as any).window;
      const win: any = {
        location: {
          pathname,
          search,
          hash: "",
          href: `http://localhost${pathname}${search}`,
          origin: "http://localhost",
        },
        history: {
          state: null as unknown,
          pushState(state: unknown, _title: string, url: string) {
            this.state = state;
            const nextUrl = new URL(url, win.location.href);
            win.location.pathname = nextUrl.pathname;
            win.location.search = nextUrl.search;
            win.location.hash = nextUrl.hash;
            win.location.href = nextUrl.href;
          },
          replaceState() {},
        },
        addEventListener() {},
        dispatchEvent() {},
        scrollTo() {},
        scrollX: 0,
        scrollY: 0,
        __NEXT_DATA__: { page: pathname, query: {}, isFallback: false },
      };
      (globalThis as any).window = win;
      (globalThis as any).document = {
        getElementById: vi.fn(() => null),
        getElementsByName: vi.fn(() => []),
      };

      try {
        vi.resetModules();
        const routerModule = await import("../packages/vinext/src/shims/router.js");
        await routerModule.default.push("#section");

        expect(win.history.state.as).toBe("");
        expect(routerModule.default.asPath).toBe(expectedAsPath);
      } finally {
        if (previousWindow === undefined) delete (globalThis as any).window;
        else (globalThis as any).window = previousWindow;
        delete (globalThis as any).document;
        vi.resetModules();
      }
    },
  );

  it("query-only push replaces the current locale under basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const pushState = vi.fn();
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";
    const win: any = {
      location: {
        pathname: "/docs/fr/rewrite-navigation/0",
        search: "",
        hash: "",
        href: "http://localhost/docs/fr/rewrite-navigation/0",
        origin: "http://localhost",
        hostname: "localhost",
      },
      history: { state: null, pushState, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
      __VINEXT_LOCALE__: "fr",
      __VINEXT_LOCALES__: ["en", "fr", "nl"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      await routerModule.default.push({ query: { id: "1" } }, undefined, {
        locale: "nl",
        shallow: true,
      });

      expect(pushState).toHaveBeenCalledWith(
        expect.objectContaining({ as: "/nl/rewrite-navigation/0?id=1" }),
        "",
        "/docs/nl/rewrite-navigation/0?id=1",
      );
    } finally {
      if (previousBasePath === undefined) delete process.env.__NEXT_ROUTER_BASEPATH;
      else process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  it("query-only replace with locale false removes the current locale under basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const replaceState = vi.fn();
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";
    const win: any = {
      location: {
        pathname: "/docs/fr/rewrite-navigation/0",
        search: "",
        hash: "",
        href: "http://localhost/docs/fr/rewrite-navigation/0",
        origin: "http://localhost",
        hostname: "localhost",
      },
      history: { state: null, pushState() {}, replaceState },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
      __VINEXT_LOCALE__: "fr",
      __VINEXT_LOCALES__: ["en", "fr"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      await routerModule.default.replace({ search: "id=2" }, undefined, {
        locale: false,
        shallow: true,
      });

      expect(replaceState).toHaveBeenCalledWith(
        expect.objectContaining({ as: "/rewrite-navigation/0?id=2" }),
        "",
        "/docs/rewrite-navigation/0?id=2",
      );
    } finally {
      if (previousBasePath === undefined) delete process.env.__NEXT_ROUTER_BASEPATH;
      else process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      (globalThis as any).window = previousWindow;
      vi.resetModules();
    }
  });

  // Issue #1522 — shallow `Router.push(url, as, { shallow: true })` must
  // update history to `as` (the visible URL) without fetching the server,
  // even when `as` matches a server-side redirect rule (e.g. `/redirect-1`
  // configured in `next.config.js#redirects`). This mirrors the Next.js
  // bloom-filter "client router filter" bypass for shallow updates.
  //
  // Ported from Next.js: test/e2e/app-dir/app/index.test.ts
  // "should not apply client router filter on shallow"
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app/index.test.ts
  it("Router.push with shallow=true updates URL to `as` and skips fetch for redirect-matching paths", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const pushState = vi.fn();
    const fetchSpy = vi.fn(async () => {
      throw new Error("shallow Router.push must not fetch the page HTML");
    });
    const win: any = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        origin: "http://localhost",
        hostname: "localhost",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: { state: null, pushState, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
      __NEXT_DATA__: { page: "/", query: {}, isFallback: false },
    };
    (globalThis as any).window = win;
    globalThis.fetch = fetchSpy as any;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");

      // Mirror the failing Next.js call exactly:
      //   window.next.router.push('/', '/redirect-1', { shallow: true })
      const result = await routerModule.default.push("/", "/redirect-1", { shallow: true });

      expect(result).toBe(true);
      expect(fetchSpy).not.toHaveBeenCalled();
      // The visible URL is `as` (/redirect-1), even though the underlying
      // route is `url` (/). The server's redirect rule for /redirect-1
      // must not fire on a shallow update. History state now follows the
      // Next.js shape (`{ url, as, options, __N, key }`) instead of `{}`.
      expect(pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/redirect-1",
      );
    } finally {
      (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("appRouterInstance exported from the navigation shim has the public router surface", async () => {
    vi.resetModules();
    const { appRouterInstance } = await import("../packages/vinext/src/shims/navigation.js");

    // Ported from Next.js: publicAppRouterInstance shape in
    // packages/next/src/client/components/app-router-instance.ts (line 392).
    expect(typeof appRouterInstance.push).toBe("function");
    expect(typeof appRouterInstance.replace).toBe("function");
    expect(typeof appRouterInstance.back).toBe("function");
    expect(typeof appRouterInstance.forward).toBe("function");
    expect(typeof appRouterInstance.refresh).toBe("function");
    expect(typeof appRouterInstance.prefetch).toBe("function");
    expect(appRouterInstance.bfcacheId).toBe("0");
  });

  it("installWindowNext is a no-op on the server (typeof window === 'undefined')", async () => {
    const previousWindow = (globalThis as any).window;
    delete (globalThis as any).window;

    try {
      vi.resetModules();
      const { installWindowNext } = await import("../packages/vinext/src/client/window-next.js");

      // Does not throw and does not attempt to create a global. We cannot
      // observe a non-existent window, so the assertion is structural: the
      // call returns without error and there is no global to inspect.
      expect(() => installWindowNext({ version: "x", appDir: true })).not.toThrow();
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      vi.resetModules();
    }
  });
});

// ---------------------------------------------------------------------------
// next/router withRouter HOC
//
// withRouter is a higher-order component that injects the Pages Router
// `router` instance as a prop into class components (which cannot call
// hooks). Real Next.js apps and the Next.js deploy test suite depend on
// this export — without it, bundlers fail with
// `[MISSING_EXPORT] "withRouter" is not exported by ".../shims/router.js"`.
//
// Ported from Next.js: packages/next/src/client/with-router.tsx
// https://github.com/vercel/next.js/blob/canary/packages/next/src/client/with-router.tsx
//
// Reference Next.js e2e fixture (class component using withRouter):
// .nextjs-ref/test/e2e/with-router/pages/a.js
// ---------------------------------------------------------------------------
describe("next/router withRouter HOC", () => {
  let previousWindow: unknown;

  function createTestRouter(overrides: Partial<NextRouter> = {}): NextRouter {
    const router: NextRouter = {
      pathname: "/provided",
      route: "/provided",
      query: {},
      asPath: "/provided",
      basePath: "",
      isReady: true,
      isPreview: false,
      isFallback: false,
      push: vi.fn(async () => true),
      replace: vi.fn(async () => true),
      back: vi.fn(),
      reload: vi.fn(),
      prefetch: vi.fn(async () => {}),
      beforePopState: vi.fn(),
      events: {
        on: vi.fn(),
        off: vi.fn(),
        emit: vi.fn(),
      },
    };
    return { ...router, ...overrides };
  }

  beforeEach(() => {
    previousWindow = (globalThis as any).window;
    (globalThis as any).window = {
      location: { pathname: "/", search: "", hash: "", href: "http://localhost/" },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
    };
    vi.resetModules();
  });

  afterEach(() => {
    (globalThis as any).window = previousWindow;
    vi.resetModules();
  });

  it("exports withRouter as a named function", async () => {
    const { withRouter } = await import("../packages/vinext/src/shims/router.js");
    expect(typeof withRouter).toBe("function");
  });

  it("next/router useRouter reads the mounted RouterContext value", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter } = await import("../packages/vinext/src/shims/router.js");
    const { RouterContext } =
      await import("../packages/vinext/src/shims/internal/router-context.js");

    const providedRouter = createTestRouter({ pathname: "/from-context" });
    let captured: NextRouter | null = null;

    function Probe() {
      captured = useRouter();
      return React.createElement("span", null, "ok");
    }

    renderToStaticMarkup(
      React.createElement(
        RouterContext.Provider,
        { value: providedRouter },
        React.createElement(Probe),
      ),
    );

    expect(captured).toBe(providedRouter);
  });

  it("next/router useRouter throws when the Pages Router context is not mounted", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter } = await import("../packages/vinext/src/shims/router.js");

    function Probe() {
      useRouter();
      return React.createElement("span", null, "ok");
    }

    expect(() => renderToStaticMarkup(React.createElement(Probe))).toThrow(
      "NextRouter was not mounted",
    );
  });

  it("next/router useRouter falls back to the singleton in a Pages Router browser document", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter } = await import("../packages/vinext/src/shims/router.js");
    const win = (globalThis as { window: Window }).window;
    const previousPageLoaders = win.__VINEXT_PAGE_LOADERS__;

    try {
      win.__VINEXT_PAGE_LOADERS__ = {};

      const captured: NextRouter[] = [];
      function Probe() {
        captured.push(useRouter());
        return React.createElement("span", null, "ok");
      }

      renderToStaticMarkup(React.createElement(Probe));

      const router = captured[0];
      expect(router).toBeDefined();
      expect(router?.events).toBeDefined();
      expect(typeof router?.push).toBe("function");
    } finally {
      win.__VINEXT_PAGE_LOADERS__ = previousPageLoaders;
    }
  });

  it("next/router useRouter does not subscribe once per hook call", async () => {
    const previousWindowForMock = (globalThis as any).window;
    const addEventListener = vi.fn();
    const providedRouter = createTestRouter();

    (globalThis as any).window = {
      location: { pathname: "/", search: "", hash: "", href: "http://localhost/" },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener,
      removeEventListener: vi.fn(),
      __NEXT_DATA__: { page: "/", query: {}, isFallback: false },
    };

    vi.resetModules();
    vi.doMock("react", () => {
      const react = {
        Component: class {},
        createContext(defaultValue: unknown) {
          return { Provider: "Provider", Consumer: "Consumer", defaultValue };
        },
        createElement(type: unknown, props: unknown, ...children: unknown[]) {
          return { type, props, children };
        },
        useContext() {
          return providedRouter;
        },
        useState(initialValue: unknown) {
          return [typeof initialValue === "function" ? initialValue() : initialValue, vi.fn()];
        },
        useEffect(effect: () => void | (() => void)) {
          effect();
        },
        useLayoutEffect(effect: () => void | (() => void)) {
          effect();
        },
        useMemo(factory: () => unknown) {
          return factory();
        },
      };
      return { ...react, default: react };
    });

    try {
      const { useRouter } = await import("../packages/vinext/src/shims/router.js");

      expect(useRouter()).toBe(providedRouter);
      expect(useRouter()).toBe(providedRouter);
      expect(useRouter()).toBe(providedRouter);

      const navigateListenerCalls = addEventListener.mock.calls.filter(
        (call) => call[0] === "vinext:navigate",
      );
      expect(navigateListenerCalls).toHaveLength(0);
    } finally {
      vi.doUnmock("react");
      if (previousWindowForMock === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindowForMock;
      }
      vi.resetModules();
    }
  });

  it("withRouter wraps a component and forwards static props", async () => {
    const { withRouter } = await import("../packages/vinext/src/shims/router.js");
    const React = await import("react");

    const Inner: React.ComponentType<{ router: any; label: string }> = ({ label }) =>
      React.createElement("span", null, label);

    const Wrapped = withRouter(Inner);
    expect(typeof Wrapped).toBe("function");
    // displayName is set in dev to `withRouter(<Inner>)`.
    if (process.env.NODE_ENV !== "production") {
      expect(Wrapped.displayName).toContain("withRouter");
    }
  });

  it("withRouter injects a router prop into the wrapped component", async () => {
    const { withRouter, wrapWithRouterContext } =
      await import("../packages/vinext/src/shims/router.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    let receivedRouter: any = null;
    let receivedLabel: string | undefined;
    const Inner: React.ComponentType<{ router: any; label: string }> = (props) => {
      receivedRouter = props.router;
      receivedLabel = props.label;
      return React.createElement("span", null, "ok");
    };

    const Wrapped = withRouter(Inner);
    const html = renderToStaticMarkup(
      wrapWithRouterContext(React.createElement(Wrapped, { label: "hi" })),
    );
    expect(html).toBe("<span>ok</span>");
    expect(receivedLabel).toBe("hi");
    // router must be the NextRouter shape (push/replace/back/...).
    expect(receivedRouter).toBeTruthy();
    expect(typeof receivedRouter.push).toBe("function");
    expect(typeof receivedRouter.replace).toBe("function");
    expect(typeof receivedRouter.back).toBe("function");
    expect(typeof receivedRouter.reload).toBe("function");
    expect(typeof receivedRouter.prefetch).toBe("function");
    expect(receivedRouter.events).toBeDefined();
  });

  // Regression guard for Next.js spread order:
  // packages/next/src/client/with-router.tsx renders
  //   `<ComposedComponent router={useRouter()} {...props} />`
  // — the injected `router` is placed FIRST and `{...props}` spread after,
  // so a user-passed `router` prop overrides the HOC-injected one. If the
  // spread order is ever inverted in the shim, this test fails.
  it("user-passed router prop overrides the HOC-injected router (Next.js spread order)", async () => {
    const { withRouter, wrapWithRouterContext } =
      await import("../packages/vinext/src/shims/router.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    let receivedRouter: any = null;
    const Inner: React.ComponentType<{ router: any }> = (props) => {
      receivedRouter = props.router;
      return React.createElement("span", null, "ok");
    };

    const Wrapped = withRouter(Inner);
    const userRouter = { sentinel: "user-provided" };
    const WrappedWithOverride = Wrapped as React.ComponentType<{ router: unknown }>;
    renderToStaticMarkup(
      wrapWithRouterContext(React.createElement(WrappedWithOverride, { router: userRouter })),
    );
    // Last spread wins: the user-passed router survives.
    expect(receivedRouter).toBe(userRouter);
  });

  it("withRouter forwards getInitialProps from the composed component", async () => {
    const { withRouter } = await import("../packages/vinext/src/shims/router.js");

    type InnerType = ((props: { router: any }) => null) & {
      getInitialProps?: () => unknown;
      origGetInitialProps?: () => unknown;
    };
    const Inner: InnerType = (() => null) as InnerType;
    const gip = () => ({ foo: "bar" });
    Inner.getInitialProps = gip;
    Inner.origGetInitialProps = gip;

    const Wrapped = withRouter(Inner) as typeof Inner;
    expect(Wrapped.getInitialProps).toBe(gip);
    expect(Wrapped.origGetInitialProps).toBe(gip);
  });
});

describe("next/headers shim", () => {
  it("exports cookies, headers, draftMode", async () => {
    const mod = await import("../packages/vinext/src/shims/headers.js");
    expect(typeof mod.cookies).toBe("function");
    expect(typeof mod.headers).toBe("function");
    expect(typeof mod.draftMode).toBe("function");
  });

  it("headers() returns request headers from context", async () => {
    const { setHeadersContext, headers } = await import("../packages/vinext/src/shims/headers.js");
    const reqHeaders = new Headers({ "x-custom": "test-value" });
    setHeadersContext({
      headers: reqHeaders,
      cookies: new Map(),
    });

    const h = await headers();
    expect(h.get("x-custom")).toBe("test-value");
    setHeadersContext(null);
  });

  it("headers() supports the legacy sync access pattern", async () => {
    // Next.js docs: headers() temporarily supports sync property access in v15.
    const { setHeadersContext, headers } = await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers({ "x-sync-header": "sync-value" }),
      cookies: new Map(),
    });

    const headerStore = headers();
    expect(typeof headerStore.get).toBe("function");
    expect(headerStore.get("x-sync-header")).toBe("sync-value");

    const awaited = await headerStore;
    expect(awaited.get("x-sync-header")).toBe("sync-value");
    setHeadersContext(null);
  });

  it("headers() and cookies() reuse request API promise identity within a request context", async () => {
    // Next.js caches request API promises by the underlying request object:
    // packages/next/src/server/request/headers.ts
    // packages/next/src/server/request/cookies.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/request/headers.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/request/cookies.ts
    const { setHeadersAccessPhase, setHeadersContext, headers, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers({ "x-custom": "test-value" }),
      cookies: new Map([["session", "abc123"]]),
    });

    try {
      const firstHeaders = headers();
      const secondHeaders = headers();
      expect(secondHeaders).toBe(firstHeaders);
      expect(await secondHeaders).toBe(await firstHeaders);

      const firstReadonlyCookies = cookies();
      const secondReadonlyCookies = cookies();
      expect(secondReadonlyCookies).toBe(firstReadonlyCookies);
      expect(await secondReadonlyCookies).toBe(await firstReadonlyCookies);

      setHeadersAccessPhase("route-handler");

      const firstMutableCookies = cookies();
      const secondMutableCookies = cookies();
      expect(secondMutableCookies).toBe(firstMutableCookies);
      expect(await secondMutableCookies).toBe(await firstMutableCookies);
      expect(firstMutableCookies).not.toBe(firstReadonlyCookies);
    } finally {
      setHeadersContext(null);
    }
  });

  it("headers() is read-only for both sync and awaited access", async () => {
    // Ported from Next.js:
    // packages/next/src/server/web/spec-extension/adapters/headers.test.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/adapters/headers.test.ts
    const { setHeadersContext, headers } = await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers({ foo: "original" }),
      cookies: new Map(),
    });

    const syncHeaders = headers();
    expect(() => Reflect.get(syncHeaders, "set")).toThrow(/Headers cannot be modified/);
    expect(() => Reflect.get(syncHeaders, "append")).toThrow(/Headers cannot be modified/);
    expect(() => Reflect.get(syncHeaders, "delete")).toThrow(/Headers cannot be modified/);
    expect(() => syncHeaders.set("foo", "mutated")).toThrow(/Headers cannot be modified/);
    expect(() => syncHeaders.append("foo", "mutated")).toThrow(/Headers cannot be modified/);
    expect(() => syncHeaders.delete("foo")).toThrow(/Headers cannot be modified/);
    expect(syncHeaders.get("foo")).toBe("original");

    const awaitedHeaders = await headers();
    expect(() => Reflect.get(awaitedHeaders, "set")).toThrow(/Headers cannot be modified/);
    expect(() => awaitedHeaders.set("foo", "mutated")).toThrow(/Headers cannot be modified/);
    expect(awaitedHeaders.get("foo")).toBe("original");
    expect((await headers()).get("foo")).toBe("original");
    setHeadersContext(null);
  });

  it("cookies() returns parsed cookies from context", async () => {
    const { setHeadersContext, cookies } = await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([
        ["session", "abc123"],
        ["theme", "dark"],
      ]),
    });

    const c = await cookies();
    expect(c.get("session")).toEqual({ name: "session", value: "abc123" });
    expect(c.get("theme")).toEqual({ name: "theme", value: "dark" });
    expect(c.has("session")).toBe(true);
    expect(c.has("missing")).toBe(false);
    expect(c.size).toBe(2);
    setHeadersContext(null);
  });

  it("cookies() supports the legacy sync access pattern", async () => {
    // Next.js docs: cookies() temporarily supports sync property access in v15.
    const { setHeadersContext, cookies } = await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "sync-cookie"]]),
    });

    const cookieStore = cookies();
    expect(typeof cookieStore.get).toBe("function");
    expect(cookieStore.get("session")).toEqual({ name: "session", value: "sync-cookie" });

    const awaited = await cookieStore;
    expect(awaited.get("session")).toEqual({ name: "session", value: "sync-cookie" });
    setHeadersContext(null);
  });

  it("cookies() is read-only during render for both sync and awaited access", async () => {
    // Ported from Next.js:
    // packages/next/src/server/web/spec-extension/adapters/request-cookies.test.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/adapters/request-cookies.test.ts
    const { setHeadersContext, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "abc123"]]),
    });

    const syncCookies = cookies();
    expect(() => Reflect.get(syncCookies, "set")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(() => Reflect.get(syncCookies, "delete")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(() => syncCookies.set("session", "mutated")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(() => syncCookies.delete("session")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(syncCookies.get("session")).toEqual({ name: "session", value: "abc123" });

    const awaitedCookies = await cookies();
    expect(() => Reflect.get(awaitedCookies, "set")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(() => awaitedCookies.set("session", "mutated")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );
    expect(() => awaitedCookies.delete("session")).toThrow(
      /Cookies can only be modified in a Server Action or Route Handler/,
    );

    expect(getAndClearPendingCookies()).toEqual([]);
    expect((await cookies()).get("session")).toEqual({ name: "session", value: "abc123" });
    setHeadersContext(null);
  });

  it("headersContextFromRequest parses cookies from Request", async () => {
    const { headersContextFromRequest } = await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { cookie: "a=1; b=2" },
    });
    const ctx = headersContextFromRequest(req);

    expect(ctx.cookies.get("a")).toBe("1");
    expect(ctx.cookies.get("b")).toBe("2");
    expect(ctx.headers.get("cookie")).toBe("a=1; b=2");
  });

  it("cookies().getAll(name) filters by name and matches upstream duplicate semantics", async () => {
    const { headersContextFromRequest, runWithHeadersContext, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    // Ported from the current @edge-runtime/cookies RequestCookies behavior:
    // duplicate names are collapsed to the last value, and getAll(name) filters.
    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "a=1; a=2; b=3" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const jar = await cookies();
      expect(jar.get("a")).toEqual({ name: "a", value: "2" });
      expect(jar.getAll("a")).toEqual([{ name: "a", value: "2" }]);
      expect(jar.getAll()).toEqual([
        { name: "a", value: "2" },
        { name: "b", value: "3" },
      ]);
    });
  });

  it("cookies().getAll({ name }) supports the RequestCookie overload and missing names", async () => {
    const { headersContextFromRequest, runWithHeadersContext, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "a=1; a=2; token=abc%3D123" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const jar = await cookies();
      expect(jar.getAll({ name: "a" })).toEqual([{ name: "a", value: "2" }]);
      expect(jar.getAll("missing")).toEqual([]);
      expect(jar.getAll({ name: "missing" })).toEqual([]);
      expect(jar.get("token")).toEqual({ name: "token", value: "abc=123" });
    });
  });

  it("cookies() ignores malformed cookie values and treats bare tokens as true", async () => {
    const { headersContextFromRequest, runWithHeadersContext, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "bad=%E0%A4%A; good=ok; flag" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const jar = await cookies();
      expect(jar.get("bad")).toBeUndefined();
      expect(jar.get("good")).toEqual({ name: "good", value: "ok" });
      expect(jar.get("flag")).toEqual({ name: "flag", value: "true" });
      expect(jar.getAll()).toEqual([
        { name: "good", value: "ok" },
        { name: "flag", value: "true" },
      ]);
    });
  });

  it("cookies() preserves explicit empty values", async () => {
    const { headersContextFromRequest, runWithHeadersContext, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "empty=; flag" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const jar = await cookies();
      expect(jar.get("empty")).toEqual({ name: "empty", value: "" });
      expect(jar.get("flag")).toEqual({ name: "flag", value: "true" });
      expect(jar.getAll()).toEqual([
        { name: "empty", value: "" },
        { name: "flag", value: "true" },
      ]);
    });
  });

  it("cookies() preserves whitespace exactly like the Next.js parser", async () => {
    const { headersContextFromRequest, runWithHeadersContext, cookies } =
      await import("../packages/vinext/src/shims/headers.js");

    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "a= 1 ; a =2" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const jar = await cookies();
      expect(jar.get("a")).toEqual({ name: "a", value: " 1 " });
      expect(jar.get("a ")).toEqual({ name: "a ", value: "2" });
      expect(jar.getAll()).toEqual([
        { name: "a", value: " 1 " },
        { name: "a ", value: "2" },
      ]);
    });
  });

  it("headersContextFromRequest returns mutable headers (not the immutable Request.headers)", async () => {
    // In Cloudflare Workers, Request.headers is immutable. applyMiddlewareRequestHeaders
    // needs ctx.headers.set() after middleware runs, so the context must hold a mutable
    // copy, not the original Headers reference.
    const { headersContextFromRequest } = await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { "x-custom": "original" },
    });
    const ctx = headersContextFromRequest(req);

    // Must be a separate, mutable copy — not the same reference
    expect(ctx.headers).not.toBe(req.headers);

    // Must be writable without throwing
    expect(() => ctx.headers.set("x-custom", "modified")).not.toThrow();
    expect(ctx.headers.get("x-custom")).toBe("modified");

    // Original request headers must be unaffected
    expect(req.headers.get("x-custom")).toBe("original");
  });

  it("headersContextFromRequest defers new Headers() copy until first write", async () => {
    // Performance regression guard: the expensive cross-boundary copy in Workerd
    // (new Headers(request.headers)) must NOT happen on reads — only on the
    // first mutating call (.set/.delete/.append).
    const { headersContextFromRequest } = await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { "x-foo": "bar", cookie: "a=1" },
    });
    const ctx = headersContextFromRequest(req);

    // Reads must work before any write (no copy yet)
    expect(ctx.headers.get("x-foo")).toBe("bar");
    expect(ctx.headers.has("x-foo")).toBe(true);

    // After a write, the copy is materialised and the new value is visible
    ctx.headers.set("x-foo", "baz");
    expect(ctx.headers.get("x-foo")).toBe("baz");

    // Original request is untouched
    expect(req.headers.get("x-foo")).toBe("bar");
  });

  it("headersContextFromRequest preserves iterator-based reads before copy-on-write", async () => {
    // Ported from Next.js:
    // packages/next/src/server/web/spec-extension/adapters/headers.test.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/adapters/headers.test.ts
    const { headersContextFromRequest } = await import("../packages/vinext/src/shims/headers.js");
    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: {
          "x-iter-a": "alpha",
          "x-iter-b": "beta",
        },
      }),
    );

    expect(Array.from(ctx.headers)).toEqual([
      ["x-iter-a", "alpha"],
      ["x-iter-b", "beta"],
    ]);
    expect(Array.from(ctx.headers.entries())).toEqual([
      ["x-iter-a", "alpha"],
      ["x-iter-b", "beta"],
    ]);
    expect(Array.from(ctx.headers.keys())).toEqual(["x-iter-a", "x-iter-b"]);
    expect(Array.from(ctx.headers.values())).toEqual(["alpha", "beta"]);
    expect(Object.fromEntries(ctx.headers)).toEqual({
      "x-iter-a": "alpha",
      "x-iter-b": "beta",
    });
  });

  it("headers() preserves iterator-based reads for sync and awaited access", async () => {
    const { headersContextFromRequest, runWithHeadersContext, headers } =
      await import("../packages/vinext/src/shims/headers.js");
    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: {
          "x-iter-a": "alpha",
          "x-iter-b": "beta",
        },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      const syncHeaders = headers();
      expect(Array.from(syncHeaders)).toEqual([
        ["x-iter-a", "alpha"],
        ["x-iter-b", "beta"],
      ]);
      expect(Array.from(syncHeaders.keys())).toEqual(["x-iter-a", "x-iter-b"]);

      const awaitedHeaders = await headers();
      expect(Array.from(awaitedHeaders.entries())).toEqual([
        ["x-iter-a", "alpha"],
        ["x-iter-b", "beta"],
      ]);
      expect(Array.from(awaitedHeaders.values())).toEqual(["alpha", "beta"]);
      expect(Object.fromEntries(awaitedHeaders)).toEqual({
        "x-iter-a": "alpha",
        "x-iter-b": "beta",
      });
    });
  });

  it("headersContextFromRequest defers cookie parsing until first access", async () => {
    // Cookie parsing should be deferred: accessing ctx.cookies triggers parsing,
    // but merely calling headersContextFromRequest must not.
    const { headersContextFromRequest } = await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { cookie: "session=xyz; theme=dark" },
    });
    const ctx = headersContextFromRequest(req);

    // First access parses cookies
    expect(ctx.cookies.get("session")).toBe("xyz");
    expect(ctx.cookies.get("theme")).toBe("dark");

    // Subsequent access returns the same map (no re-parse)
    const map1 = ctx.cookies;
    const map2 = ctx.cookies;
    expect(map1).toBe(map2);
  });

  it("headersContextFromRequest cookie getter reflects middleware-modified cookie header", async () => {
    // When middleware calls ctx.headers.set("cookie", ...) the lazy cookie
    // map must reflect the new value on next access.
    const { headersContextFromRequest, applyMiddlewareRequestHeaders, runWithHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { cookie: "a=1" },
    });
    const ctx = headersContextFromRequest(req);

    // Simulate middleware updating the cookie header
    const middlewareResponseHeaders = new Headers({
      "x-middleware-request-cookie": "a=2; b=3",
    });

    await runWithHeadersContext(ctx, async () => {
      applyMiddlewareRequestHeaders(middlewareResponseHeaders);
      // Cookies map should be rebuilt with the new values
      expect(ctx.cookies.get("a")).toBe("2");
      expect(ctx.cookies.get("b")).toBe("3");
    });
  });

  it("cookies().getAll(name) reflects middleware cookie rewrites with duplicate names", async () => {
    const {
      headersContextFromRequest,
      applyMiddlewareRequestHeaders,
      runWithHeadersContext,
      cookies,
    } = await import("../packages/vinext/src/shims/headers.js");
    const req = new Request("https://example.com", {
      headers: { cookie: "a=1; b=2" },
    });
    const ctx = headersContextFromRequest(req);

    await runWithHeadersContext(ctx, async () => {
      applyMiddlewareRequestHeaders(
        new Headers({
          "x-middleware-request-cookie": "a=1; a=2; b=4",
        }),
      );

      const jar = await cookies();
      expect(jar.get("a")).toEqual({ name: "a", value: "2" });
      expect(jar.getAll("a")).toEqual([{ name: "a", value: "2" }]);
      expect(jar.getAll({ name: "a" })).toEqual([{ name: "a", value: "2" }]);
      expect(jar.getAll()).toEqual([
        { name: "a", value: "2" },
        { name: "b", value: "4" },
      ]);
    });
  });

  it("cookies() preserves explicit empty values after middleware cookie rewrites", async () => {
    const {
      headersContextFromRequest,
      applyMiddlewareRequestHeaders,
      runWithHeadersContext,
      cookies,
    } = await import("../packages/vinext/src/shims/headers.js");
    const ctx = headersContextFromRequest(
      new Request("https://example.com", {
        headers: { cookie: "start=1" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      applyMiddlewareRequestHeaders(
        new Headers({
          "x-middleware-request-cookie": "empty=; flag",
        }),
      );

      const jar = await cookies();
      expect(jar.get("empty")).toEqual({ name: "empty", value: "" });
      expect(jar.get("flag")).toEqual({ name: "flag", value: "true" });
      expect(jar.getAll()).toEqual([
        { name: "empty", value: "" },
        { name: "flag", value: "true" },
      ]);
    });
  });

  // Ported from Next.js:
  // - packages/next/src/server/async-storage/request-store.ts
  // - test/e2e/app-dir/app-middleware/app-middleware.test.ts
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/async-storage/request-store.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-middleware/app-middleware.test.ts
  it("middleware-set cookies are visible to cookies() in the same render", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const {
      headersContextFromRequest,
      applyMiddlewareRequestHeaders,
      runWithHeadersContext,
      cookies,
    } = await import("../packages/vinext/src/shims/headers.js");

    const middlewareResponse = NextResponse.next();
    middlewareResponse.cookies.set("rsc-cookie-value-1", "123", { path: "/" });
    middlewareResponse.cookies.set("rsc-cookie-value-2", "456", { path: "/", secure: true });

    const ctx = headersContextFromRequest(
      new Request("https://example.com/rsc-cookies", {
        headers: { cookie: "existing=kept" },
      }),
    );

    await runWithHeadersContext(ctx, async () => {
      applyMiddlewareRequestHeaders(middlewareResponse.headers);

      const jar = await cookies();
      expect(jar.get("existing")).toEqual({ name: "existing", value: "kept" });
      expect(jar.get("rsc-cookie-value-1")).toEqual({
        name: "rsc-cookie-value-1",
        value: "123",
      });
      expect(jar.get("rsc-cookie-value-2")).toEqual({
        name: "rsc-cookie-value-2",
        value: "456",
      });
      expect(jar.getAll()).toEqual([
        { name: "existing", value: "kept" },
        { name: "rsc-cookie-value-1", value: "123" },
        { name: "rsc-cookie-value-2", value: "456" },
      ]);
    });
  });

  it("throws when called outside request context", async () => {
    const { headers, cookies, draftMode } = await import("../packages/vinext/src/shims/headers.js");
    // Ensure context is cleared
    const { setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext(null);

    await expect(headers()).rejects.toThrow("Server Component");
    await expect(cookies()).rejects.toThrow("Server Component");
    await expect(draftMode()).rejects.toThrow("draftMode() can only be called");
  });

  it("legacy sync access still throws the request-context error outside request context", async () => {
    const { headers, cookies, setHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext(null);

    expect(() => headers().get("x-test")).toThrow("Server Component");
    expect(() => cookies().get("session")).toThrow("Server Component");
  });

  it("draftMode() returns isEnabled=false when no bypass cookie", async () => {
    const { setHeadersContext, draftMode } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });
    const dm = await draftMode();
    expect(dm.isEnabled).toBe(false);
    setHeadersContext(null);
  });

  it("draftMode() returns isEnabled=false for arbitrary cookie values (not signed)", async () => {
    const { setHeadersContext, draftMode } =
      await import("../packages/vinext/src/shims/headers.js");
    // An arbitrary cookie value should NOT enable draft mode — only the
    // server-generated secret is valid.
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["__prerender_bypass", "1"]]),
    });
    const dm = await draftMode();
    expect(dm.isEnabled).toBe(false);
    setHeadersContext(null);
  });

  it("draft mode validation is scoped to the request context secret", async () => {
    const { draftMode, headersContextFromRequest, isDraftModeRequest, runWithHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");

    const firstContext = headersContextFromRequest(new Request("https://example.test/one"), {
      draftModeSecret: "first-secret",
    });
    const secondContext = headersContextFromRequest(new Request("https://example.test/two"), {
      draftModeSecret: "second-secret",
    });

    await runWithHeadersContext(firstContext, async () => {
      const dm = await draftMode();
      dm.enable();
      expect(dm.isEnabled).toBe(true);
    });

    await runWithHeadersContext(secondContext, async () => {
      const dm = await draftMode();
      dm.enable();
      expect(dm.isEnabled).toBe(true);
    });

    const firstCookieRequest = new Request("https://example.test/one", {
      headers: { Cookie: "__prerender_bypass=first-secret" },
    });
    expect(isDraftModeRequest(firstCookieRequest, "first-secret")).toBe(true);
    expect(isDraftModeRequest(firstCookieRequest, "second-secret")).toBe(false);
  });

  it("draftMode().enable() sets the bypass cookie in context", async () => {
    const { setHeadersContext, draftMode, getDraftModeCookieHeader } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });
    const dm = await draftMode();
    expect(dm.isEnabled).toBe(false);

    dm.enable();
    // isEnabled should reflect the change on the same object (live getter)
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/request/draft-mode.ts
    expect(dm.isEnabled).toBe(true);
    // A fresh draftMode() call should also see the change
    const dm2 = await draftMode();
    expect(dm2.isEnabled).toBe(true);

    // The Set-Cookie header should be generated with a non-predictable secret (UUID)
    const cookieHeader = getDraftModeCookieHeader();
    expect(cookieHeader).toContain("__prerender_bypass=");
    const bypassMatch = cookieHeader!.match(/__prerender_bypass=([^;]+)/);
    expect(bypassMatch).not.toBeNull();
    expect(bypassMatch![1]).toMatch(
      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
    );
    expect(cookieHeader).toContain("HttpOnly");
    setHeadersContext(null);
  });

  it("draftMode().disable() clears the bypass cookie", async () => {
    const { setHeadersContext, draftMode, getDraftModeCookieHeader } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });
    // Enable first so the cookie is set to the server secret
    const dm = await draftMode();
    dm.enable();
    // Consume the enable Set-Cookie header
    getDraftModeCookieHeader();

    const dm1 = await draftMode();
    expect(dm1.isEnabled).toBe(true);

    dm1.disable();
    // isEnabled should reflect the change on the same object (live getter)
    expect(dm1.isEnabled).toBe(false);
    // A fresh draftMode() call should also see the change
    const dm2 = await draftMode();
    expect(dm2.isEnabled).toBe(false);

    const cookieHeader = getDraftModeCookieHeader();
    expect(cookieHeader).toContain("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
    expect(cookieHeader).not.toContain("Max-Age=0");
    setHeadersContext(null);
  });

  it("draftMode().disable() throws after its request context has been cleared", async () => {
    const { setHeadersContext, draftMode, getDraftModeCookieHeader } =
      await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["__prerender_bypass", "draft-secret"]]),
      draftModeSecret: "draft-secret",
    });
    const dm = await draftMode();
    expect(dm.isEnabled).toBe(true);

    setHeadersContext(null);

    expect(() => dm.disable()).toThrow("draftMode().disable() can only be called");
    expect(getDraftModeCookieHeader()).toBeNull();
  });

  it('draftMode() throws the dynamic = "error" access error before exposing draft controls', async () => {
    const { setHeadersContext, draftMode, getDraftModeCookieHeader, consumeDynamicUsage } =
      await import("../packages/vinext/src/shims/headers.js");
    const accessError = new Error(
      'Page with `dynamic = "error"` used a dynamic API. This page was expected to be fully static.',
    );

    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
      accessError,
    });

    await expect(draftMode()).rejects.toThrow(accessError);
    expect(consumeDynamicUsage()).toBe(false);
    expect(getDraftModeCookieHeader()).toBeNull();

    setHeadersContext(null);
  });
});

describe("next/headers phase-aware cookies", () => {
  it("cookies().set() works in the route-handler phase and accumulates Set-Cookie headers", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      c.set("token", "xyz", { path: "/", httpOnly: true, secure: true });

      expect(c.get("token")).toEqual({ name: "token", value: "xyz" });
      expect(c.has("token")).toBe(true);

      const pending = getAndClearPendingCookies();
      expect(pending.length).toBe(1);
      expect(pending[0]).toContain("token=xyz");
      expect(pending[0]).toContain("Path=/");
      expect(pending[0]).toContain("HttpOnly");
      expect(pending[0]).toContain("Secure");
      expect(getAndClearPendingCookies().length).toBe(0);
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().set() supports legacy sync access in the route-handler phase", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const cookieStore = cookies();
      void cookieStore.set("token", "sync-token", { httpOnly: true });

      expect(cookieStore.get("token")).toEqual({ name: "token", value: "sync-token" });
      expect(getAndClearPendingCookies()).toEqual([expect.stringContaining("token=sync-token")]);
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().set() emits Path=/ when no path option is provided", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      c.set("token", "implicit-path");

      const pending = getAndClearPendingCookies();
      expect(pending).toHaveLength(1);
      expect(pending[0]).toContain("token=implicit-path");
      expect(pending[0]).toContain("Path=/");
      expect(getAndClearPendingCookies()).toHaveLength(0);
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().set() preserves an explicit path option", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      c.set("token", "scoped-path", { path: "/api" });

      const pending = getAndClearPendingCookies();
      expect(pending).toHaveLength(1);
      expect(pending[0]).toContain("token=scoped-path");
      expect(pending[0]).toContain("Path=/api");
      expect(pending[0]).not.toContain("Path=/;");
      expect(getAndClearPendingCookies()).toHaveLength(0);
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().delete() works in the route-handler phase", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "abc"]]),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      expect(c.has("session")).toBe(true);
      c.delete("session");
      expect(c.has("session")).toBe(false);

      const pending = getAndClearPendingCookies();
      expect(pending.length).toBe(1);
      expect(pending[0]).toContain("session=");
      expect(pending[0]).toContain("Expires=");
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().delete() accepts options with path and domain", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "abc"]]),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      c.delete({ name: "session", path: "/account", domain: ".example.com" });

      const pending = getAndClearPendingCookies();
      expect(pending).toHaveLength(1);
      expect(pending[0]).toContain("session=");
      expect(pending[0]).toContain("Path=/account");
      expect(pending[0]).toContain("Domain=.example.com");
      expect(pending[0]).toContain("Expires=");
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().delete() defaults Path=/ for object syntax without path", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "abc"]]),
    });

    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      const c = await cookies();
      c.delete({ name: "session" });

      const pending = getAndClearPendingCookies();
      expect(pending).toHaveLength(1);
      expect(pending[0]).toContain("session=");
      expect(pending[0]).toContain("Path=/");
      expect(pending[0]).toContain("Expires=");
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("cookies().set() with object syntax works in the action phase", async () => {
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("action");
    try {
      const c = await cookies();
      c.set({ name: "pref", value: "dark", sameSite: "Lax" });
      expect(c.get("pref")?.value).toBe("dark");

      const pending = getAndClearPendingCookies();
      expect(pending[0]).toContain("pref=dark");
      expect(pending[0]).toContain("SameSite=Lax");
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });

  it("mutable cookie references stop accepting writes after the phase returns to render", async () => {
    // Ported from Next.js:
    // packages/next/src/server/web/spec-extension/adapters/request-cookies.test.ts
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/adapters/request-cookies.test.ts
    const { setHeadersContext, setHeadersAccessPhase, cookies, getAndClearPendingCookies } =
      await import("../packages/vinext/src/shims/headers.js");
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map(),
    });

    const previousPhase = setHeadersAccessPhase("action");
    try {
      const c = await cookies();
      c.set("session", "abc123");
      expect(c.get("session")?.value).toBe("abc123");

      setHeadersAccessPhase("render");
      expect(() => c.set("session", "mutated")).toThrow(
        /Cookies can only be modified in a Server Action or Route Handler/,
      );
      expect(() => c.delete("session")).toThrow(
        /Cookies can only be modified in a Server Action or Route Handler/,
      );
      expect(c.get("session")?.value).toBe("abc123");
      expect(getAndClearPendingCookies()).toEqual([expect.stringContaining("session=abc123")]);
    } finally {
      setHeadersAccessPhase(previousPhase);
      setHeadersContext(null);
    }
  });
});

describe("next/server shim", () => {
  it("NextRequest wraps a standard Request with nextUrl and cookies", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("https://example.com/blog?page=2", {
      headers: { cookie: "session=abc123; theme=dark" },
    });

    expect(req.nextUrl.pathname).toBe("/blog");
    expect(req.nextUrl.searchParams.get("page")).toBe("2");
    expect(req.cookies.get("session")).toEqual({ name: "session", value: "abc123" });
    expect(req.cookies.get("theme")).toEqual({ name: "theme", value: "dark" });
    expect(req.cookies.has("session")).toBe(true);
    expect(req.cookies.has("missing")).toBe(false);
  });

  it("NextRequest copies a Request body without disturbing the source request", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const source = new Request("https://example.com/api/echo", {
      body: JSON.stringify({ ok: true }),
      headers: { "content-type": "application/json" },
      method: "POST",
    });

    const req = new NextRequest(source);

    await expect(req.json()).resolves.toEqual({ ok: true });
    expect(source.bodyUsed).toBe(false);
    await expect(source.json()).resolves.toEqual({ ok: true });
  });

  it("NextResponse.json() creates a JSON response", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.json({ message: "hello" }, { status: 201 });

    expect(res.status).toBe(201);
    expect(res.headers.get("content-type")).toBe("application/json");
    const body = await res.json();
    expect(body).toEqual({ message: "hello" });
  });

  it("NextResponse.redirect() creates a redirect response", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com/new", 308);

    expect(res.status).toBe(308);
    expect(res.headers.get("Location")).toBe("https://example.com/new");
  });

  // Ported from Next.js: test/e2e/middleware-general/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-general/test/index.test.ts
  it("NextResponse.redirect() throws when using a relative URL", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    expect(() => NextResponse.redirect("/urls-b")).toThrow("URL is malformed");
  });

  it("NextResponse.rewrite() sets x-middleware-rewrite header", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.rewrite("https://example.com/internal");

    expect(res.headers.get("x-middleware-rewrite")).toBe("https://example.com/internal");
  });

  // Ported from Next.js: test/e2e/middleware-general/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-general/test/index.test.ts
  it("NextResponse.rewrite() throws when using a relative URL", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    expect(() => NextResponse.rewrite("/urls-b")).toThrow("URL is malformed");
  });

  it("NextResponse.rewrite() forwards request header overrides", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const forwardedHeaders = new Headers({
      cookie: "a=1",
      "x-added": "1",
    });

    const res = NextResponse.rewrite("https://example.com/internal", {
      request: {
        headers: forwardedHeaders,
      },
    });

    expect(res.headers.get("x-middleware-rewrite")).toBe("https://example.com/internal");
    expect(res.headers.get("x-middleware-override-headers")).toBe("cookie,x-added");
    expect(res.headers.get("x-middleware-request-cookie")).toBe("a=1");
    expect(res.headers.get("x-middleware-request-x-added")).toBe("1");
  });

  // Ported from Next.js:
  // - packages/next/src/server/web/spec-extension/response.ts
  // - test/e2e/app-dir/app-middleware/app-middleware.test.ts
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/response.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-middleware/app-middleware.test.ts
  it("NextResponse.cookies.set() emits x-middleware-set-cookie for same-render reads", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.next();

    res.cookies.set("rsc-cookie-value-1", "123", { path: "/" });
    res.cookies.set("rsc-cookie-value-2", "456", { path: "/", httpOnly: true });

    expect(res.headers.getSetCookie()).toEqual([
      "rsc-cookie-value-1=123; Path=/",
      "rsc-cookie-value-2=456; Path=/; HttpOnly",
    ]);
    const internalCookieHeader = res.headers.get("x-middleware-set-cookie");
    expect(internalCookieHeader).not.toBeNull();
    expect(internalCookieHeader).toContain("rsc-cookie-value-1=123; Path=/");
    expect(internalCookieHeader).toContain("rsc-cookie-value-2=456; Path=/; HttpOnly");
  });

  it("NextResponse.next() sets x-middleware-next header", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.next();

    expect(res.headers.get("x-middleware-next")).toBe("1");
  });

  it("ResponseCookies set/get/delete work", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = new NextResponse();
    res.cookies.set("token", "xyz", { path: "/", httpOnly: true });

    const cookie = res.cookies.get("token");
    expect(cookie).toBeTruthy();
    expect(cookie!.value).toBe("xyz");

    // Verify the Set-Cookie header was set
    const setCookie = res.headers.getSetCookie();
    expect(setCookie.length).toBeGreaterThan(0);
    expect(setCookie[0]).toContain("token=xyz");
    expect(setCookie[0]).toContain("HttpOnly");
  });

  it("userAgentFromString detects bots", async () => {
    const { userAgentFromString } = await import("../packages/vinext/src/shims/server.js");
    const bot = userAgentFromString("Googlebot/2.1");
    expect(bot.isBot).toBe(true);

    const human = userAgentFromString("Mozilla/5.0");
    expect(human.isBot).toBe(false);
  });

  it("after() runs a callback asynchronously without throwing", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    let called = false;
    after(() => {
      called = true;
    });
    // after() schedules as a microtask, so await a tick
    await new Promise((r) => setTimeout(r, 10));
    expect(called).toBe(true);
  });

  it("after() handles a promise argument", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    let resolved = false;
    const p = new Promise<void>((resolve) => {
      setTimeout(() => {
        resolved = true;
        resolve();
      }, 5);
    });
    after(p);
    await new Promise((r) => setTimeout(r, 20));
    expect(resolved).toBe(true);
  });

  it("after() swallows errors from failing tasks", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
    after(() => {
      throw new Error("task failed");
    });
    // after() wraps function tasks in Promise.resolve().then(task) — two microtask
    // ticks are sufficient and more deterministic than a setTimeout.
    await Promise.resolve();
    await Promise.resolve();
    expect(consoleError).toHaveBeenCalledWith("[vinext] after() task failed:", expect.any(Error));
    consoleError.mockRestore();
  });

  it("after() calls waitUntil on the execution context when one exists", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    const { runWithExecutionContext } =
      await import("../packages/vinext/src/shims/request-context.js");

    const waitUntilCalls: Promise<unknown>[] = [];
    const mockCtx = {
      waitUntil: (p: Promise<unknown>) => {
        waitUntilCalls.push(p);
      },
    };

    let called = false;
    await runWithExecutionContext(mockCtx, () => {
      after(() => {
        called = true;
      });
    });

    // waitUntil is called synchronously — no microtask delay needed
    expect(waitUntilCalls).toHaveLength(1);
    // Await the guarded promise to verify the callback ran
    await waitUntilCalls[0];
    expect(called).toBe(true);
  });

  it("after() falls back to fire-and-forget when no execution context exists", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");

    // Outside any execution context scope — should still run the task
    let called = false;
    after(() => {
      called = true;
    });
    // after() wraps function tasks in Promise.resolve().then(task) — two microtask
    // ticks are sufficient and more deterministic than a setTimeout.
    await Promise.resolve();
    await Promise.resolve();
    expect(called).toBe(true);
  });

  it('after() throws inside "use cache" scope', async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");

    cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      () => {
        expect(() => after(() => {})).toThrow(/cannot be called inside "use cache"/);
      },
    );
  });

  it("after() throws inside unstable_cache() scope", async () => {
    const { after } = await import("../packages/vinext/src/shims/server.js");
    const { AsyncLocalStorage } = await import("node:async_hooks");
    const key = Symbol.for("vinext.unstableCache.als");
    const g = globalThis as unknown as Record<symbol, unknown>;
    // Lazily register an ALS on globalThis if cache.ts hasn't been imported yet.
    // This test is intentionally isolated from the real cache.ts registration path —
    // it only validates that server.ts reads from the same Symbol key to detect the
    // unstable_cache scope. If cache.ts was already imported, the existing instance is
    // reused; if not, this standalone ALS is sufficient for the guard to work.
    if (!g[key]) g[key] = new AsyncLocalStorage();

    (g[key] as any).run(true, () => {
      expect(() => after(() => {})).toThrow(/unstable_cache/);
    });
  });

  it("connection() returns a resolved promise", async () => {
    const { connection } = await import("../packages/vinext/src/shims/server.js");
    const result = connection();
    expect(result).toBeInstanceOf(Promise);
    await expect(result).resolves.toBeUndefined();
  });

  it("connection() interrupts speculative probes before post-connection user code runs", async () => {
    const { runWithConnectionProbe } = await import("../packages/vinext/src/shims/headers.js");
    const { connection } = await import("../packages/vinext/src/shims/server.js");

    let ranAfterConnection = false;
    const outcome = await runWithConnectionProbe(async () => {
      await connection();
      ranAfterConnection = true;
      return "completed";
    });

    expect(outcome).toEqual({ completed: false });
    expect(ranAfterConnection).toBe(false);
  });

  it("URLPattern is exported and available in Node 20+", async () => {
    const { URLPattern } = await import("../packages/vinext/src/shims/server.js");
    // Node 22+ has URLPattern globally; if available, test it works
    if (globalThis.URLPattern) {
      expect(URLPattern).toBe(globalThis.URLPattern);
      const pattern = new URLPattern({ pathname: "/blog/:slug" });
      const match = pattern.exec({ pathname: "/blog/hello-world" });
      expect(match).toBeTruthy();
      expect(match!.pathname.groups.slug).toBe("hello-world");
    } else {
      // URLPattern not available — our export should be a fallback that throws
      expect(typeof URLPattern).toBe("function");
    }
  });
});

describe("next/config shim", () => {
  it("getConfig returns default empty config", async () => {
    const { default: getConfig } = await import("../packages/vinext/src/shims/config.js");
    const config = getConfig();
    expect(config).toEqual({
      serverRuntimeConfig: {},
      publicRuntimeConfig: {},
    });
  });

  it("setConfig updates the runtime config", async () => {
    const { default: getConfig, setConfig } =
      await import("../packages/vinext/src/shims/config.js");
    setConfig({
      serverRuntimeConfig: { secret: "s3cr3t" },
      publicRuntimeConfig: { appName: "test-app" },
    });
    const config = getConfig();
    expect(config.serverRuntimeConfig.secret).toBe("s3cr3t");
    expect(config.publicRuntimeConfig.appName).toBe("test-app");

    // Reset for other tests
    setConfig({ serverRuntimeConfig: {}, publicRuntimeConfig: {} });
  });
});

describe("next/cache shim", () => {
  it("exports revalidateTag, revalidatePath, unstable_cache", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.revalidateTag).toBe("function");
    expect(typeof mod.revalidatePath).toBe("function");
    expect(typeof mod.unstable_cache).toBe("function");
  });

  it("exports setCacheHandler and getCacheHandler", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.setCacheHandler).toBe("function");
    expect(typeof mod.getCacheHandler).toBe("function");
  });

  it("default handler is MemoryCacheHandler", async () => {
    const { getCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const handler = getCacheHandler();
    expect(handler).toBeInstanceOf(MemoryCacheHandler);
  });

  it("unstable_cache caches function results", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    // Fresh handler for isolation
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const expensive = async (x: number) => {
      callCount++;
      return x * 2;
    };

    const cached = unstable_cache(expensive, ["test-fn"], {
      tags: ["test-tag"],
    });

    const r1 = await cached(5);
    expect(r1).toBe(10);
    expect(callCount).toBe(1);

    const r2 = await cached(5);
    expect(r2).toBe(10);
    expect(callCount).toBe(1); // Should NOT call the function again

    const r3 = await cached(10);
    expect(r3).toBe(20);
    expect(callCount).toBe(2); // Different args = different cache key

    // Reset
    setCacheHandler(new MemoryCacheHandler());
  });

  it("unstable_cache caches undefined results", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = unstable_cache(async () => {
      callCount++;
      return undefined;
    }, ["undefined-result-test"]);

    await expect(cached()).resolves.toBeUndefined();
    expect(callCount).toBe(1);

    await expect(cached()).resolves.toBeUndefined();
    expect(callCount).toBe(1);

    setCacheHandler(new MemoryCacheHandler());
  });

  it("revalidateTag invalidates cached entries", async () => {
    const { unstable_cache, revalidateTag, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return "result-" + callCount;
    };

    const cached = unstable_cache(fn, ["revalidate-test"], {
      tags: ["my-tag"],
    });

    const r1 = await cached();
    expect(r1).toBe("result-1");
    expect(callCount).toBe(1);

    // Revalidate the tag
    await revalidateTag("my-tag");

    // Next call should re-execute the function
    const r2 = await cached();
    expect(r2).toBe("result-2");
    expect(callCount).toBe(2);

    // Reset
    setCacheHandler(new MemoryCacheHandler());
  });

  it("revalidateTag accepts optional cacheLife profile (Next.js 16)", async () => {
    const { revalidateTag, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    // Should not throw with profile argument
    await revalidateTag("my-tag", "max");
    await revalidateTag("my-tag", "hours");
    await revalidateTag("my-tag", { expire: 3600 });

    // Should still work without profile (deprecated single-arg form)
    await revalidateTag("my-tag");

    setCacheHandler(new MemoryCacheHandler());
  });

  it("exports updateTag function (Next.js 16)", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.updateTag).toBe("function");
  });

  it("updateTag invalidates cached entries when called inside a Server Action", async () => {
    const { unstable_cache, updateTag, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { setHeadersAccessPhase } = await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return "result-" + callCount;
    };

    const cached = unstable_cache(fn, ["update-tag-test"], {
      tags: ["user-1"],
    });

    const r1 = await cached();
    expect(r1).toBe("result-1");

    // updateTag expires the cache (must be called from inside a Server Action)
    const previousPhase = setHeadersAccessPhase("action");
    try {
      await updateTag("user-1");
    } finally {
      setHeadersAccessPhase(previousPhase);
    }

    const r2 = await cached();
    expect(r2).toBe("result-2");
    expect(callCount).toBe(2);

    setCacheHandler(new MemoryCacheHandler());
  });

  it("updateTag throws when called outside a Server Action (e.g. route handler)", async () => {
    const { updateTag } = await import("../packages/vinext/src/shims/cache.js");
    const { setHeadersAccessPhase } = await import("../packages/vinext/src/shims/headers.js");

    // Simulate a route handler phase
    const previousPhase = setHeadersAccessPhase("route-handler");
    try {
      await expect(updateTag("some-tag")).rejects.toThrow(
        /updateTag can only be called from within a Server Action/,
      );
    } finally {
      setHeadersAccessPhase(previousPhase);
    }
  });

  it("updateTag throws when called during render", async () => {
    const { updateTag } = await import("../packages/vinext/src/shims/cache.js");
    const { setHeadersAccessPhase } = await import("../packages/vinext/src/shims/headers.js");

    const previousPhase = setHeadersAccessPhase("render");
    try {
      await expect(updateTag("some-tag")).rejects.toThrow(
        /updateTag can only be called from within a Server Action/,
      );
    } finally {
      setHeadersAccessPhase(previousPhase);
    }
  });

  it("exports refresh function (Next.js 16)", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.refresh).toBe("function");
    // refresh() is a no-op on the server but should not throw
    mod.refresh();
  });

  // Ported from Next.js: packages/next/src/client/request/io.browser.ts
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/request/io.browser.ts
  it("exports io function", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.io).toBe("function");
  });

  it("io returns a resolved promise", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const result = io();
    expect(result).toBeInstanceOf(Promise);
    expect((result as any).status).toBe("fulfilled");
    expect((result as any).value).toBeUndefined();
    await expect(result).resolves.toBeUndefined();
  });

  it("io returns same instance (singleton)", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const r1 = io();
    const r2 = io();
    expect(r1).toBe(r2);
  });

  // Ported from Next.js: packages/next/src/server/request/io.ts
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/request/io.ts
  it("io returns a hanging promise during prerender", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const controller = new AbortController();

    // run() returns whatever the callback returns — a hanging promise.
    const hanging = workUnitAsyncStorage.run(
      { type: "prerender", renderSignal: controller.signal },
      io,
    );

    // The promise should not be resolved or rejected (it's "hanging")
    expect(hanging).toBeInstanceOf(Promise);

    // Verify it's still hanging using Promise.race (more portable than V8 internals)
    const result = await Promise.race([
      hanging.then(() => "resolved"),
      new Promise((r) => setTimeout(() => r("still-hanging"), 50)),
    ]);
    expect(result).toBe("still-hanging");

    // Clean up by aborting the signal
    controller.abort();
  });

  it("io resolves immediately with request store", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const promise = workUnitAsyncStorage.run({ type: "request" }, io);

    expect(promise).toBeInstanceOf(Promise);
    await expect(promise).resolves.toBeUndefined();
  });

  it("io resolves immediately with cache store", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const promise = workUnitAsyncStorage.run({ type: "cache" }, io);

    expect(promise).toBeInstanceOf(Promise);
    await expect(promise).resolves.toBeUndefined();
  });

  it("io rejects hanging promise on abort when prerendering", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const controller = new AbortController();

    const hanging = workUnitAsyncStorage.run(
      { type: "prerender", renderSignal: controller.signal },
      io,
    );

    expect(hanging).toBeInstanceOf(Promise);

    // Abort the signal — the hanging promise should reject
    controller.abort();
    await expect(hanging).rejects.toThrow(/`io\(\)`/);
  });

  it("io returns rejected promise when signal already aborted", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const controller = new AbortController();
    controller.abort(); // already aborted

    const promise = workUnitAsyncStorage.run(
      { type: "prerender", renderSignal: controller.signal },
      io,
    );

    await expect(promise).rejects.toThrow(/prerendering/i);
  });

  it("io does not emit unhandled rejection when signal already aborted", async () => {
    const { io } = await import("../packages/vinext/src/shims/cache.js");
    const { workUnitAsyncStorage } =
      await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");

    const unhandledRejections: unknown[] = [];
    const onUnhandledRejection = (reason: unknown) => {
      unhandledRejections.push(reason);
    };
    process.on("unhandledRejection", onUnhandledRejection);

    try {
      const controller = new AbortController();
      controller.abort(); // already aborted

      const promise = workUnitAsyncStorage.run(
        { type: "prerender", renderSignal: controller.signal, route: "/test" },
        io,
      );

      // Wait a tick for potential unhandled rejection to be detected
      await new Promise((resolve) => setTimeout(resolve, 10));

      // Await the promise to handle the rejection
      try {
        await promise;
      } catch {
        // expected
      }

      expect(unhandledRejections.length).toBe(0);
    } finally {
      process.off("unhandledRejection", onUnhandledRejection);
    }
  });

  it("unstable_io is exported as a deprecation alias for io", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.unstable_io).toBe("function");

    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    try {
      const result = mod.unstable_io();
      expect(result).toBeInstanceOf(Promise);
      await expect(result).resolves.toBeUndefined();
      // Warning fires at most once per process; at least one call must mention deprecation.
      const warned = warn.mock.calls.some((args) => String(args[0] ?? "").includes("unstable_io"));
      expect(warned).toBe(true);
    } finally {
      warn.mockRestore();
    }
  });

  it("setCacheHandler swaps the active handler", async () => {
    const { setCacheHandler, getCacheHandler, unstable_cache } =
      await import("../packages/vinext/src/shims/cache.js");

    // Create a custom handler that tracks calls
    const calls: string[] = [];
    const customHandler = {
      async get(key: string) {
        calls.push(`get:${key}`);
        return null;
      },
      async set(key: string) {
        calls.push(`set:${key}`);
      },
      async revalidateTag(tags: string | string[]) {
        const tagList = Array.isArray(tags) ? tags : [tags];
        calls.push(`revalidateTag:${tagList.join(",")}`);
      },
      resetRequestCache() {
        calls.push("reset");
      },
    };

    const originalHandler = getCacheHandler();
    setCacheHandler(customHandler);

    const cached = unstable_cache(async () => 42, ["custom-test"]);
    await cached();

    expect(calls.some((c) => c.startsWith("get:"))).toBe(true);
    expect(calls.some((c) => c.startsWith("set:"))).toBe(true);

    // Restore
    setCacheHandler(originalHandler);
  });

  it("MemoryCacheHandler.get/set round-trips values", async () => {
    const { MemoryCacheHandler } = await import("../packages/vinext/src/shims/cache.js");

    const handler = new MemoryCacheHandler();

    await handler.set("test-key", {
      kind: "FETCH",
      data: { headers: {}, body: '{"x":1}', url: "test" },
      tags: ["t1"],
      revalidate: 3600,
    });

    const result = await handler.get("test-key");
    expect(result).not.toBeNull();
    expect(result!.value).not.toBeNull();
    expect(result!.value!.kind).toBe("FETCH");
    if (result!.value!.kind === "FETCH") {
      expect(result!.value!.data.body).toBe('{"x":1}');
    }
  });

  it("MemoryCacheHandler evicts least-recently-used entries when max size is exceeded", async () => {
    const { MemoryCacheHandler } = await import("../packages/vinext/src/shims/cache.js");

    const handler = new MemoryCacheHandler({ cacheMaxMemorySize: 250 });
    const makeEntry = (body: string) => ({
      kind: "PAGES" as const,
      html: body,
      pageData: {},
      headers: undefined,
      status: 200,
    });

    await handler.set("a", makeEntry("a".repeat(80)));
    await handler.set("b", makeEntry("b".repeat(80)));

    expect(await handler.get("a")).toBeNull();
    expect(await handler.get("b")).not.toBeNull();
  });

  it("MemoryCacheHandler refreshes LRU order on get", async () => {
    const { MemoryCacheHandler } = await import("../packages/vinext/src/shims/cache.js");

    const handler = new MemoryCacheHandler({ cacheMaxMemorySize: 360 });
    const makeEntry = (body: string) => ({
      kind: "PAGES" as const,
      html: body,
      pageData: {},
      headers: undefined,
      status: 200,
    });

    await handler.set("a", makeEntry("a".repeat(80)));
    await handler.set("b", makeEntry("b".repeat(80)));
    expect(await handler.get("a")).not.toBeNull();
    await handler.set("c", makeEntry("c".repeat(80)));

    expect(await handler.get("a")).not.toBeNull();
    expect(await handler.get("b")).toBeNull();
    expect(await handler.get("c")).not.toBeNull();
  });

  it("MemoryCacheHandler with max size 0 disables storage", async () => {
    const { MemoryCacheHandler } = await import("../packages/vinext/src/shims/cache.js");

    const handler = new MemoryCacheHandler({ cacheMaxMemorySize: 0 });
    await handler.set("disabled", {
      kind: "FETCH",
      data: { headers: {}, body: '"cached"', url: "test" },
      tags: [],
      revalidate: 3600,
    });

    expect(await handler.get("disabled")).toBeNull();
  });

  it("MemoryCacheHandler respects tag invalidation", async () => {
    const { MemoryCacheHandler } = await import("../packages/vinext/src/shims/cache.js");

    const handler = new MemoryCacheHandler();

    await handler.set(
      "tagged-entry",
      {
        kind: "FETCH",
        data: { headers: {}, body: '"cached"', url: "test" },
        tags: ["fresh-tag"],
        revalidate: 3600,
      },
      { tags: ["fresh-tag"] },
    );

    // Should return the entry
    let result = await handler.get("tagged-entry");
    expect(result).not.toBeNull();

    // Invalidate the tag
    await handler.revalidateTag("fresh-tag");

    // Should now return null (invalidated)
    result = await handler.get("tagged-entry");
    expect(result).toBeNull();
  });

  it("exports unstable_noStore and noStore as no-ops", async () => {
    const { unstable_noStore, noStore } = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof unstable_noStore).toBe("function");
    expect(typeof noStore).toBe("function");
    // Both should run without throwing
    expect(() => unstable_noStore()).not.toThrow();
    expect(() => noStore()).not.toThrow();
  });

  it("exports cacheLife with built-in profiles", async () => {
    const { cacheLife, cacheLifeProfiles } = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof cacheLife).toBe("function");
    expect(typeof cacheLifeProfiles).toBe("object");

    // Built-in profiles should exist
    expect(cacheLifeProfiles).toHaveProperty("default");
    expect(cacheLifeProfiles).toHaveProperty("seconds");
    expect(cacheLifeProfiles).toHaveProperty("minutes");
    expect(cacheLifeProfiles).toHaveProperty("hours");
    expect(cacheLifeProfiles).toHaveProperty("days");
    expect(cacheLifeProfiles).toHaveProperty("weeks");
    expect(cacheLifeProfiles).toHaveProperty("max");

    // Profile shapes
    expect(cacheLifeProfiles.seconds).toEqual({ stale: 30, revalidate: 1, expire: 60 });
    expect(cacheLifeProfiles.max).toEqual({ stale: 300, revalidate: 2592000, expire: 31536000 });

    // Should run without throwing with valid inputs
    expect(() => cacheLife("default")).not.toThrow();
    expect(() => cacheLife("hours")).not.toThrow();
    expect(() => cacheLife({ stale: 60, revalidate: 300, expire: 3600 })).not.toThrow();
  });

  it("cacheLife warns on unknown profile", async () => {
    const { cacheLife } = await import("../packages/vinext/src/shims/cache.js");
    const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
    cacheLife("nonexistent-profile");
    expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining("unknown profile"));
    consoleWarn.mockRestore();
  });

  it("cacheLife warns when expire < revalidate", async () => {
    const { cacheLife } = await import("../packages/vinext/src/shims/cache.js");
    const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
    cacheLife({ revalidate: 3600, expire: 60 });
    expect(consoleWarn).toHaveBeenCalledWith(
      expect.stringContaining("expire must be >= revalidate"),
    );
    consoleWarn.mockRestore();
  });

  it("cacheLife inline configs inherit default profile values for omitted fields", async () => {
    const {
      cacheLife,
      _consumeRequestScopedCacheLife,
      _peekRequestScopedCacheLife,
      _runWithCacheState,
    } = await import("../packages/vinext/src/shims/cache.js");

    await _runWithCacheState(async () => {
      cacheLife({ expire: 60 });

      expect(_peekRequestScopedCacheLife()).toEqual({
        revalidate: 900,
        expire: 60,
      });
      expect(_consumeRequestScopedCacheLife()).toEqual({
        revalidate: 900,
        expire: 60,
      });
      expect(_peekRequestScopedCacheLife()).toBeNull();
    });
  });

  it("exports cacheTag as a no-op function", async () => {
    const { cacheTag } = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof cacheTag).toBe("function");
    // Should accept multiple tags without throwing
    expect(() => cacheTag("tag1", "tag2", "tag3")).not.toThrow();
  });

  // Ported from Next.js: test/e2e/app-dir/cache-components-errors/cache-components-unstable-deprecations.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components-errors/cache-components-unstable-deprecations.test.ts
  it("unstable_cacheLife is exported as a deprecation alias for cacheLife", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.unstable_cacheLife).toBe("function");

    // Matches the cacheLife signature: accepts a profile name or an inline config.
    const error = vi.spyOn(console, "error").mockImplementation(() => {});
    try {
      expect(() => mod.unstable_cacheLife("hours")).not.toThrow();
      expect(() =>
        mod.unstable_cacheLife({ stale: 60, revalidate: 300, expire: 3600 }),
      ).not.toThrow();

      // Next.js asserts CLI output contains "Error: `unstable_cacheLife` was recently stabilized".
      // That string comes from console.error(new Error(...)) — match the same surface here.
      const warned = error.mock.calls.some((args) => {
        const msg = args[0];
        const text = msg instanceof Error ? msg.message : String(msg ?? "");
        return text.includes("`unstable_cacheLife` was recently stabilized");
      });
      expect(warned).toBe(true);
    } finally {
      error.mockRestore();
    }
  });

  it("unstable_cacheTag is exported as a deprecation alias for cacheTag", async () => {
    const mod = await import("../packages/vinext/src/shims/cache.js");
    expect(typeof mod.unstable_cacheTag).toBe("function");

    const error = vi.spyOn(console, "error").mockImplementation(() => {});
    try {
      // Matches the cacheTag signature: variadic string tags. Outside a "use cache"
      // scope this is a no-op (same as cacheTag).
      expect(() => mod.unstable_cacheTag("tag1", "tag2", "tag3")).not.toThrow();

      const warned = error.mock.calls.some((args) => {
        const msg = args[0];
        const text = msg instanceof Error ? msg.message : String(msg ?? "");
        return text.includes("`unstable_cacheTag` was recently stabilized");
      });
      expect(warned).toBe(true);
    } finally {
      error.mockRestore();
    }
  });

  it("unstable_cache re-fetches when entry is stale (time-expired)", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return "value-" + callCount;
    };

    // Use a very short revalidate (1 second)
    const cached = unstable_cache(fn, ["stale-test"], {
      revalidate: 1,
    });

    const r1 = await cached();
    expect(r1).toBe("value-1");
    expect(callCount).toBe(1);

    // Cached call should still return the same value
    const r2 = await cached();
    expect(r2).toBe("value-1");
    expect(callCount).toBe(1);

    // Manually expire the entry by advancing time past revalidate window.
    // We do this by patching the entry's revalidateAt in the handler.
    const handler = (await import("../packages/vinext/src/shims/cache.js")).getCacheHandler();
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      if (entry.revalidateAt) {
        entry.revalidateAt = Date.now() - 1000; // expired 1 second ago
      }
    }

    // Next call should re-fetch because entry is now stale
    const r3 = await cached();
    expect(r3).toBe("value-2");
    expect(callCount).toBe(2);

    setCacheHandler(new MemoryCacheHandler());
  });

  it("unstable_cache serves stale entries and refreshes them in the background during App Router requests", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    const waitUntilPromises: Promise<unknown>[] = [];
    const setBodies: string[] = [];
    let callCount = 0;

    const handler: CacheHandler = {
      async get(): Promise<CacheHandlerValue> {
        return {
          lastModified: Date.now() - 2_000,
          cacheState: "stale",
          value: {
            kind: "FETCH",
            data: {
              headers: {},
              body: JSON.stringify({ v: "stale-value" }),
              url: "unstable_cache:stale-swr-test:[]",
            },
            tags: ["stale-swr"],
            revalidate: 1,
          },
        };
      },
      async set(_key: string, data: IncrementalCacheValue | null) {
        if (data?.kind === "FETCH") {
          setBodies.push(data.data.body);
        }
      },
      async revalidateTag(_tags: string | string[]) {},
    };

    setCacheHandler(handler);

    const cached = unstable_cache(
      async () => {
        callCount++;
        await new Promise((resolve) => setTimeout(resolve, 50));
        return "fresh-value";
      },
      ["stale-swr-test"],
      { tags: ["stale-swr"], revalidate: 1 },
    );

    // Matches Next.js App Router semantics: stale entries schedule a
    // pending revalidate and return the stale response immediately.
    // Source: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/spec-extension/unstable-cache.ts
    const requestContext = createRequestContext({
      unstableCacheRevalidation: "background",
      executionContext: {
        waitUntil(promise) {
          waitUntilPromises.push(promise);
        },
      },
    });

    try {
      const result = await Promise.race([
        runWithRequestContext(requestContext, () => cached()),
        new Promise((resolve) => setTimeout(() => resolve("blocked"), 10)),
      ]);

      expect(result).toBe("stale-value");
      expect(callCount).toBe(1);
      expect(waitUntilPromises).toHaveLength(1);
      expect(setBodies).toEqual([]);

      await Promise.all(waitUntilPromises);

      expect(setBodies).toEqual([JSON.stringify({ v: "fresh-value" })]);
    } finally {
      setCacheHandler(new MemoryCacheHandler());
    }
  });

  it("unstable_cache blocks on stale entries inside revalidation scopes", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    let callCount = 0;
    const handler: CacheHandler = {
      async get(): Promise<CacheHandlerValue> {
        return {
          lastModified: Date.now() - 2_000,
          cacheState: "stale",
          value: {
            kind: "FETCH",
            data: {
              headers: {},
              body: JSON.stringify({ v: "stale-value" }),
              url: "unstable_cache:foreground-test:[]",
            },
            tags: ["foreground"],
            revalidate: 1,
          },
        };
      },
      async set() {},
      async revalidateTag(_tags: string | string[]) {},
    };

    setCacheHandler(handler);

    const cached = unstable_cache(
      async () => {
        callCount++;
        return "fresh-value";
      },
      ["foreground-test"],
      { tags: ["foreground"], revalidate: 1 },
    );

    // Next.js foreground-revalidates stale unstable_cache entries while
    // regenerating a static/ISR page so the regenerated page stores fresh data.
    // Source test: https://github.com/vercel/next.js/blob/canary/test/production/app-dir/unstable-cache-foreground-revalidate/unstable-cache-foreground-revalidate.test.ts
    const requestContext = createRequestContext({
      unstableCacheRevalidation: "foreground",
    });

    try {
      await expect(runWithRequestContext(requestContext, () => cached())).resolves.toBe(
        "fresh-value",
      );
      expect(callCount).toBe(1);
    } finally {
      setCacheHandler(new MemoryCacheHandler());
    }
  });

  it("unstable_cache with no revalidate option caches indefinitely", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return "result-" + callCount;
    };

    // No revalidate option (should cache indefinitely, not expire at t=0)
    const cached = unstable_cache(fn, ["no-revalidate-test"]);

    const r1 = await cached();
    expect(r1).toBe("result-1");
    expect(callCount).toBe(1);

    // Should return cached value (not re-fetch)
    const r2 = await cached();
    expect(r2).toBe("result-1");
    expect(callCount).toBe(1);

    // Even after "time passes", should still be cached (not stale)
    const handler = (await import("../packages/vinext/src/shims/cache.js")).getCacheHandler();
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      // revalidateAt should be null (indefinite) not 0 or past timestamp
      expect(entry.revalidateAt).toBeNull();
    }

    setCacheHandler(new MemoryCacheHandler());
  });
});

// ---------------------------------------------------------------------------
// "use cache" runtime tests
// ---------------------------------------------------------------------------

describe('"use cache" runtime', () => {
  it("registerCachedFunction caches return values", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    // Reset state
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async (x: number) => {
      callCount++;
      return { result: x * 2 };
    };

    const cached = registerCachedFunction(fn, "test:double");

    const r1 = await cached(5);
    expect(r1).toEqual({ result: 10 });
    expect(callCount).toBe(1);

    // Second call with same args — should be cached
    const r2 = await cached(5);
    expect(r2).toEqual({ result: 10 });
    expect(callCount).toBe(1); // Not called again

    // Different args — cache miss
    const r3 = await cached(7);
    expect(r3).toEqual({ result: 14 });
    expect(callCount).toBe(2);
  });

  it("scopes shared cache entries by build ID", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    const previousBuildId = process.env.__VINEXT_BUILD_ID;
    try {
      let callCount = 0;

      process.env.__VINEXT_BUILD_ID = "build-one";
      const firstBuild = registerCachedFunction(async () => {
        callCount++;
        return { version: "old" };
      }, "test:same-id");

      expect(await firstBuild()).toEqual({ version: "old" });
      expect(callCount).toBe(1);

      process.env.__VINEXT_BUILD_ID = "build-two";
      const secondBuild = registerCachedFunction(async () => {
        callCount++;
        return { version: "new" };
      }, "test:same-id");

      expect(await secondBuild()).toEqual({ version: "new" });
      expect(callCount).toBe(2);
    } finally {
      if (previousBuildId === undefined) {
        delete process.env.__VINEXT_BUILD_ID;
      } else {
        process.env.__VINEXT_BUILD_ID = previousBuildId;
      }
    }
  });

  it("scopes shared cache entries by deployment ID when available", async () => {
    // Ported from Next.js: test/production/app-dir/use-cache-cross-deployment/use-cache-cross-deployment.test.ts
    // https://github.com/vercel/next.js/blob/07f76411b07de9417d4a6b816f3137cafe1045fc/test/production/app-dir/use-cache-cross-deployment/use-cache-cross-deployment.test.ts
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    const previousBuildId = process.env.__VINEXT_BUILD_ID;
    const previousDeploymentId = process.env.__VINEXT_DEPLOYMENT_ID;
    const previousNextDeploymentId = process.env.NEXT_DEPLOYMENT_ID;
    try {
      process.env.__VINEXT_BUILD_ID = "stable-build";
      delete process.env.NEXT_DEPLOYMENT_ID;

      let callCount = 0;
      const cached = registerCachedFunction(async () => {
        callCount++;
        return { count: callCount };
      }, "test:deployment-id");

      process.env.__VINEXT_DEPLOYMENT_ID = "deployment-one";
      expect(await cached()).toEqual({ count: 1 });
      expect(await cached()).toEqual({ count: 1 });

      process.env.__VINEXT_DEPLOYMENT_ID = "deployment-two";
      expect(await cached()).toEqual({ count: 2 });
      expect(await cached()).toEqual({ count: 2 });

      delete process.env.__VINEXT_DEPLOYMENT_ID;
      expect(await cached()).toEqual({ count: 3 });
      expect(await cached()).toEqual({ count: 3 });
    } finally {
      if (previousBuildId === undefined) {
        delete process.env.__VINEXT_BUILD_ID;
      } else {
        process.env.__VINEXT_BUILD_ID = previousBuildId;
      }
      if (previousDeploymentId === undefined) {
        delete process.env.__VINEXT_DEPLOYMENT_ID;
      } else {
        process.env.__VINEXT_DEPLOYMENT_ID = previousDeploymentId;
      }
      if (previousNextDeploymentId === undefined) {
        delete process.env.NEXT_DEPLOYMENT_ID;
      } else {
        process.env.NEXT_DEPLOYMENT_ID = previousNextDeploymentId;
      }
    }
  });

  it("uses NEXT_DEPLOYMENT_ID when the internal define is empty", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    const previousBuildId = process.env.__VINEXT_BUILD_ID;
    const previousDeploymentId = process.env.__VINEXT_DEPLOYMENT_ID;
    const previousNextDeploymentId = process.env.NEXT_DEPLOYMENT_ID;
    try {
      process.env.__VINEXT_BUILD_ID = "stable-build";
      process.env.__VINEXT_DEPLOYMENT_ID = "";

      let callCount = 0;
      const cached = registerCachedFunction(async () => {
        callCount++;
        return { count: callCount };
      }, "test:next-deployment-id");

      process.env.NEXT_DEPLOYMENT_ID = "env-deployment-one";
      expect(await cached()).toEqual({ count: 1 });
      expect(await cached()).toEqual({ count: 1 });

      process.env.NEXT_DEPLOYMENT_ID = "env-deployment-two";
      expect(await cached()).toEqual({ count: 2 });
      expect(await cached()).toEqual({ count: 2 });
      expect(callCount).toBe(2);
    } finally {
      if (previousBuildId === undefined) {
        delete process.env.__VINEXT_BUILD_ID;
      } else {
        process.env.__VINEXT_BUILD_ID = previousBuildId;
      }
      if (previousDeploymentId === undefined) {
        delete process.env.__VINEXT_DEPLOYMENT_ID;
      } else {
        process.env.__VINEXT_DEPLOYMENT_ID = previousDeploymentId;
      }
      if (previousNextDeploymentId === undefined) {
        delete process.env.NEXT_DEPLOYMENT_ID;
      } else {
        process.env.NEXT_DEPLOYMENT_ID = previousNextDeploymentId;
      }
    }
  });

  it("registerCachedFunction respects cacheLife inside cached function", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler, cacheLife } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      cacheLife("seconds"); // revalidate: 1s
      callCount++;
      return { ts: Date.now() };
    };

    const cached = registerCachedFunction(fn, "test:cachelife");

    await cached();
    expect(callCount).toBe(1);

    // Immediate second call — cached
    await cached();
    expect(callCount).toBe(1);
  });

  it("registerCachedFunction collects cacheTag", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler, cacheTag } =
      await import("../packages/vinext/src/shims/cache.js");
    const handler = new MemoryCacheHandler();
    setCacheHandler(handler);

    const fn = async () => {
      cacheTag("my-tag", "another-tag");
      return { data: "tagged" };
    };

    const cached = registerCachedFunction(fn, "test:tags");
    await cached();

    // The cache entry should have tags
    const entry = await handler.get("use-cache:test:tags");
    expect(entry).not.toBeNull();
    expect(entry?.value).toHaveProperty("kind", "FETCH");
    if (entry?.value && entry.value.kind === "FETCH") {
      expect(entry.value.tags).toContain("my-tag");
      expect(entry.value.tags).toContain("another-tag");
    }
  });

  it("revalidateTag invalidates cached entries", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler, cacheTag, revalidateTag } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      cacheTag("invalidate-me");
      callCount++;
      return { count: callCount };
    };

    const cached = registerCachedFunction(fn, "test:invalidate");

    const r1 = await cached();
    expect(r1).toEqual({ count: 1 });
    expect(callCount).toBe(1);

    // Cached
    const r2 = await cached();
    expect(r2).toEqual({ count: 1 });
    expect(callCount).toBe(1);

    // Invalidate the tag
    await revalidateTag("invalidate-me");

    // Should re-execute
    const r3 = await cached();
    expect(r3).toEqual({ count: 2 });
    expect(callCount).toBe(2);
  });

  it("nested cacheTag bubbles to the outer cache entry even when the inner HITs", async () => {
    // Regression for issue #1453 (nested-HIT path): when an inner "use cache"
    // function HITs the data cache while nested inside an outer cache that is
    // being (re)generated, the inner entry's stored tags must bubble into the
    // outer entry's tags. Otherwise revalidateTag("inner-tag") evicts the inner
    // entry but leaves the outer page/route-handler ISR entry stale.
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler, cacheTag } =
      await import("../packages/vinext/src/shims/cache.js");
    const handler = new MemoryCacheHandler();
    setCacheHandler(handler);

    const inner = registerCachedFunction(async () => {
      cacheTag("inner-tag");
      return { inner: true };
    }, "test:nested-inner");

    const outer = registerCachedFunction(async () => {
      cacheTag("outer-tag");
      await inner();
      return { outer: true };
    }, "test:nested-outer");

    // Warm the inner entry on its own so the next outer call HITs it.
    await inner();
    // Now build the outer entry; its body re-runs `inner()`, which HITs.
    await outer();

    const outerEntry = await handler.get("use-cache:test:nested-outer");
    expect(outerEntry?.value).toHaveProperty("kind", "FETCH");
    if (outerEntry?.value && outerEntry.value.kind === "FETCH") {
      // The outer entry must carry both its own tag and the nested inner tag.
      expect(outerEntry.value.tags).toContain("outer-tag");
      expect(outerEntry.value.tags).toContain("inner-tag");
    }
  });

  it("private variant uses per-request cache", async () => {
    const { registerCachedFunction, clearPrivateCache } =
      await import("../packages/vinext/src/shims/cache-runtime.js");

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return { count: callCount };
    };

    const cached = registerCachedFunction(fn, "test:private", "private");

    const r1 = await cached();
    expect(r1).toEqual({ count: 1 });

    // Same request — cached
    const r2 = await cached();
    expect(r2).toEqual({ count: 1 });

    // Clear private cache (simulates new request)
    clearPrivateCache();

    // Should re-execute
    const r3 = await cached();
    expect(r3).toEqual({ count: 2 });
  });

  it("private variant marks prerender output dynamic", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { consumeDynamicUsage } = await import("../packages/vinext/src/shims/headers.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    // Ported from Next.js: "use cache: private" is dynamic in prerendering contexts.
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/use-cache/use-cache-wrapper.ts
    const previousPrerender = process.env.VINEXT_PRERENDER;
    process.env.VINEXT_PRERENDER = "1";

    try {
      await runWithRequestContext(createRequestContext(), async () => {
        const cached = registerCachedFunction(
          async () => "private",
          "test:private-prerender",
          "private",
        );
        await cached();

        expect(consumeDynamicUsage()).toBe(true);
      });
    } finally {
      if (previousPrerender === undefined) {
        delete process.env.VINEXT_PRERENDER;
      } else {
        process.env.VINEXT_PRERENDER = previousPrerender;
      }
    }
  });

  it('rejects "use cache: private" nested inside public "use cache"', async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    // Ported from Next.js: "use cache: private" must not run inside public "use cache".
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/use-cache/use-cache-wrapper.ts
    await runWithRequestContext(createRequestContext(), async () => {
      try {
        setHeadersContext({
          headers: new Headers({ cookie: "test-cookie=leaked" }),
          cookies: new Map([["test-cookie", "leaked"]]),
        });

        const inner = registerCachedFunction(
          async () => (await cookies()).get("test-cookie")?.value,
          "test:private-nested-inner",
          "private",
        );
        const outer = registerCachedFunction(async () => inner(), "test:private-nested-outer", "");

        await expect(outer()).rejects.toThrow(
          /"use cache: private" must not be used within "use cache"/,
        );
      } finally {
        setHeadersContext(null);
      }
    });
  });

  it("cacheLife minimum-wins rule applies", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler, cacheLife } =
      await import("../packages/vinext/src/shims/cache.js");
    const handler = new MemoryCacheHandler();
    setCacheHandler(handler);

    const fn = async () => {
      cacheLife("hours"); // revalidate: 3600
      cacheLife("seconds"); // revalidate: 1  — this should win
      return { data: "min-wins" };
    };

    const cached = registerCachedFunction(fn, "test:min-wins");
    await cached();

    // The entry should have the minimum revalidate (1 second from "seconds" profile)
    const entry = await handler.get("use-cache:test:min-wins");
    expect(entry).not.toBeNull();
    if (entry?.value && entry.value.kind === "FETCH") {
      expect(entry.value.revalidate).toBe(1);
    }
  });

  it("getCacheContext returns null outside cache function", async () => {
    const { getCacheContext } = await import("../packages/vinext/src/shims/cache-runtime.js");
    expect(getCacheContext()).toBeNull();
  });

  it("consistent cache keys for same objects regardless of key order", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async (_opts: Record<string, unknown>) => {
      callCount++;
      return { result: "ok" };
    };

    const cached = registerCachedFunction(fn, "test:stable-key");

    // Different key order, same content — should be same cache key
    await cached({ b: 2, a: 1 });
    expect(callCount).toBe(1);

    await cached({ a: 1, b: 2 });
    expect(callCount).toBe(1); // Same cache key, still cached
  });

  it("cached function with no args works correctly", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async () => {
      callCount++;
      return { hello: "world" };
    };

    const cached = registerCachedFunction(fn, "test:no-args");
    const r1 = await cached();
    const r2 = await cached();
    expect(r1).toEqual({ hello: "world" });
    expect(r2).toEqual({ hello: "world" });
    expect(callCount).toBe(1);
  });

  it("preserves the original function arity on the cached wrapper", async () => {
    // The wrapper is declared as `(...args)` (arity 0). Callers like
    // `resolveModuleMetadata` rely on `fn.length` to decide whether to pass
    // optional arguments (e.g. the metadata `parent`), so the wrapper must
    // expose the original arity rather than 0.
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    const zero = registerCachedFunction(async () => ({}), "test:arity-0");
    const one = registerCachedFunction(async (_a: unknown) => ({}), "test:arity-1");
    const two = registerCachedFunction(async (_a: unknown, _b: unknown) => ({}), "test:arity-2");

    expect(zero.length).toBe(0);
    expect(one.length).toBe(1);
    expect(two.length).toBe(2);
  });

  it("falls back to JSON when RSC module is unavailable (test environment)", async () => {
    // In vitest, @vitejs/plugin-rsc/react/rsc is not available (no Vite RSC
    // environment). The runtime should gracefully fall back to JSON.stringify
    // for cache values and stableStringify for cache keys.
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const handler = new MemoryCacheHandler();
    setCacheHandler(handler);

    const fn = async (x: number) => ({ doubled: x * 2 });
    const cached = registerCachedFunction(fn, "test:json-fallback");

    const r1 = await cached(3);
    expect(r1).toEqual({ doubled: 6 });

    // Verify the stored value is JSON (no x-vinext-rsc header)
    // stableStringify wraps args as an array: [3]
    const entry = await handler.get("use-cache:test:json-fallback:[3]");
    expect(entry).not.toBeNull();
    if (entry?.value && entry.value.kind === "FETCH") {
      expect(entry.value.data.headers["x-vinext-rsc"]).toBeUndefined();
      expect(JSON.parse(entry.value.data.body)).toEqual({ doubled: 6 });
    }
  });

  it("skips caching for non-serializable args (functions)", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const fn = async (_cb: () => void) => {
      callCount++;
      return { called: true };
    };

    const cached = registerCachedFunction(fn, "test:fn-arg");

    // Functions can't be serialized — should execute every time (no caching)
    await cached(() => {});
    await cached(() => {});
    expect(callCount).toBe(2);
  });

  it("produces different cache entries for Promise-augmented params with different values", async () => {
    // Regression test: Next.js 16 params are created via
    // Object.assign(Promise.resolve(params), params) — a "thenable object".
    // encodeReply with temporaryReferences treats Promises as temp refs,
    // which excluded the actual param values from the cache key.
    // This caused all dynamic route pages with "use cache" to share one
    // cache entry (e.g., /layouts/sports showed /layouts/electronics data).
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    // Simulates a page component: async function Page({ params }) { ... }
    const fn = async (props: { params: any }) => {
      callCount++;
      const p = typeof props.params.then === "function" ? await props.params : props.params;
      return { section: p.section, data: `data-for-${p.section}` };
    };

    const cached = registerCachedFunction(fn, "test:thenable-params");

    // Create Promise-augmented params (same pattern as entries/app-rsc-entry.ts)
    const electronicsParams = { section: "electronics" };
    const asyncElectronics = Object.assign(Promise.resolve(electronicsParams), electronicsParams);

    const sportsParams = { section: "sports" };
    const asyncSports = Object.assign(Promise.resolve(sportsParams), sportsParams);

    // First call — electronics
    const r1 = await cached({ params: asyncElectronics });
    expect(r1).toEqual({ section: "electronics", data: "data-for-electronics" });
    expect(callCount).toBe(1);

    // Second call with SAME params — should be cached
    const asyncElectronics2 = Object.assign(Promise.resolve({ section: "electronics" }), {
      section: "electronics",
    });
    const r2 = await cached({ params: asyncElectronics2 });
    expect(r2).toEqual({ section: "electronics", data: "data-for-electronics" });
    expect(callCount).toBe(1); // Cache hit

    // Third call with DIFFERENT params — must be a cache MISS
    const r3 = await cached({ params: asyncSports });
    expect(r3).toEqual({ section: "sports", data: "data-for-sports" });
    expect(callCount).toBe(2); // Must have called the function again!
  });

  it("does not read page searchParams while deriving a use cache key", async () => {
    const { registerCachedFunction, markAppPagePropsForUseCache } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const observeSearchParams = vi.fn();
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<Record<string, unknown>>;
      }) => {
        callCount++;
        const params = await props.params;
        return { slug: params.slug };
      },
      "test:page-props-searchparams",
    );

    const first = await cached(
      markAppPagePropsForUseCache({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams(
          { q: "first" },
          { observeParamAccess: observeSearchParams },
        ),
      }),
    );
    expect(first).toEqual({ slug: "same" });
    expect(callCount).toBe(1);
    expect(observeSearchParams).not.toHaveBeenCalled();

    const second = await cached(
      markAppPagePropsForUseCache({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams(
          { q: "second" },
          { observeParamAccess: observeSearchParams },
        ),
      }),
    );
    expect(second).toEqual({ slug: "same" });
    expect(callCount).toBe(1);
    expect(observeSearchParams).not.toHaveBeenCalled();
  });

  it("omits page default export searchParams from use cache keys via transform metadata", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const observeSearchParams = vi.fn();
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<Record<string, unknown>>;
      }) => {
        callCount++;
        const params = await props.params;
        return { slug: params.slug };
      },
      "/fixture/app/cached/page.tsx:default",
      "",
      { appPageDefaultExport: true },
    );

    const first = await cached({
      params: makeThenableParams({ slug: "same" }),
      searchParams: makeThenableParams({ q: "first" }, { observeParamAccess: observeSearchParams }),
    });
    expect(first).toEqual({ slug: "same" });
    expect(callCount).toBe(1);
    expect(observeSearchParams).not.toHaveBeenCalled();

    const second = await cached({
      params: makeThenableParams({ slug: "same" }),
      searchParams: makeThenableParams(
        { q: "second" },
        { observeParamAccess: observeSearchParams },
      ),
    });
    expect(second).toEqual({ slug: "same" });
    expect(callCount).toBe(1);
    expect(observeSearchParams).not.toHaveBeenCalled();
  });

  it('rejects app page searchParams access inside page default "use cache"', async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    const { makeObservedAppPageSearchParamsThenable } =
      await import("../packages/vinext/src/server/app-page-search-params-observation.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<Record<string, string | string[]>>;
      }) => {
        callCount++;
        const searchParams = await props.searchParams;
        return { q: searchParams.q };
      },
      "/fixture/app/cached/page.tsx:default",
      "",
      { appPageDefaultExport: true },
    );

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeObservedAppPageSearchParamsThenable({ q: "first" }),
      }),
    ).rejects.toThrow(/cannot be called inside "use cache"/);
    expect(callCount).toBe(1);

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeObservedAppPageSearchParamsThenable({ q: "second" }),
      }),
    ).rejects.toThrow(/cannot be called inside "use cache"/);
    expect(callCount).toBe(2);
  });

  it('does not store a page default "use cache" result after caught searchParams access', async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    const { makeObservedAppPageSearchParamsThenable } =
      await import("../packages/vinext/src/server/app-page-search-params-observation.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<Record<string, string | string[]>>;
      }) => {
        callCount++;
        try {
          const searchParams = await props.searchParams;
          return { q: searchParams.q };
        } catch {
          return { q: `caught-${callCount}` };
        }
      },
      "/fixture/app/cached/caught/page.tsx:default",
      "",
      { appPageDefaultExport: true },
    );

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeObservedAppPageSearchParamsThenable({ q: "first" }),
      }),
    ).rejects.toThrow(/cannot be called inside "use cache"/);
    expect(callCount).toBe(1);

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeObservedAppPageSearchParamsThenable({ q: "second" }),
      }),
    ).rejects.toThrow(/cannot be called inside "use cache"/);
    expect(callCount).toBe(2);
  });

  it("does not infer page props from the cache id alone", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<{ q: string }>;
      }) => {
        callCount++;
        const searchParams = await props.searchParams;
        return { q: searchParams.q };
      },
      "/fixture/app/cached/page.tsx:default",
    );

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams({ q: "first" }),
      }),
    ).resolves.toEqual({ q: "first" });
    expect(callCount).toBe(1);

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams({ q: "second" }),
      }),
    ).resolves.toEqual({ q: "second" });
    expect(callCount).toBe(2);
  });

  it("keeps arbitrary params/searchParams objects distinct in use cache keys", async () => {
    const { registerCachedFunction } =
      await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { makeThenableParams } = await import("../packages/vinext/src/shims/thenable-params.js");
    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = registerCachedFunction(
      async (props: {
        params: Promise<{ slug: string }>;
        searchParams: Promise<{ q: string }>;
      }) => {
        callCount++;
        const searchParams = await props.searchParams;
        return { q: searchParams.q };
      },
      "test:userland-params-searchparams",
    );

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams({ q: "first" }),
      }),
    ).resolves.toEqual({ q: "first" });
    expect(callCount).toBe(1);

    await expect(
      cached({
        params: makeThenableParams({ slug: "same" }),
        searchParams: makeThenableParams({ q: "second" }),
      }),
    ).resolves.toEqual({ q: "second" });
    expect(callCount).toBe(2);
  });

  // -----------------------------------------------------------------------
  // Nested-dynamic cache life error tests — ported from Next.js PR #93707
  // https://github.com/vercel/next.js/pull/93707
  // -----------------------------------------------------------------------

  // These tests exercise the prerender-only throw path. The runtime check
  // is gated on `process.env.VINEXT_PRERENDER === "1"` (or NODE_ENV ===
  // "development") to match Next.js, which only throws when the work unit
  // type is `prerender` or `request` in dev. We set/restore the flag around
  // each test rather than relying on test ordering.
  function withPrerenderFlag<T>(fn: () => Promise<T>): Promise<T> {
    const previous = process.env.VINEXT_PRERENDER;
    process.env.VINEXT_PRERENDER = "1";
    return fn().finally(() => {
      if (previous === undefined) delete process.env.VINEXT_PRERENDER;
      else process.env.VINEXT_PRERENDER = previous;
    });
  }

  it("throws nested-dynamic error when inner cache has revalidate:0 and outer has no explicit cacheLife (prerender)", async () => {
    await withPrerenderFlag(async () => {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerFn = async () => {
        cacheLife({ revalidate: 0 });
        return "inner";
      };
      const innerCached = registerCachedFunction(innerFn, "test:nested-rev0-inner");

      const outerFn = async () => {
        const result = await innerCached();
        return `outer-${result}`;
      };
      const outerCached = registerCachedFunction(outerFn, "test:nested-rev0-outer");

      // Top-level call — outer has no explicit cacheLife, inner has revalidate:0
      // This should throw a nested-dynamic error
      let thrown: Error | undefined;
      try {
        await outerCached();
      } catch (e) {
        thrown = e as Error;
      }

      expect(thrown).toBeDefined();
      expect(thrown!.message).toContain("revalidate");
      expect(thrown!.message).toContain('"use cache"');
      expect(thrown!.message).toContain("not allowed");
      expect(thrown!.cause).toBeDefined();
      expect((thrown!.cause as Error).message).toContain("dynamic cache life");
      expect((thrown!.cause as Error).name).toContain("Nested dynamic");
    });
  });

  it("throws nested-dynamic error when inner cache has short expire and outer has no explicit cacheLife (prerender)", async () => {
    await withPrerenderFlag(async () => {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerFn = async () => {
        cacheLife({ expire: 60 }); // 1 minute, under 5 minute threshold
        return "inner";
      };
      const innerCached = registerCachedFunction(innerFn, "test:nested-short-inner");

      const outerFn = async () => {
        const result = await innerCached();
        return `outer-${result}`;
      };
      const outerCached = registerCachedFunction(outerFn, "test:nested-short-outer");

      let thrown: Error | undefined;
      try {
        await outerCached();
      } catch (e) {
        thrown = e as Error;
      }

      expect(thrown).toBeDefined();
      expect(thrown!.message).toContain("expire");
      expect(thrown!.message).toContain('"use cache"');
      expect(thrown!.message).toContain("not allowed");
      expect(thrown!.cause).toBeDefined();
      expect((thrown!.cause as Error).message).toContain("dynamic cache life");
    });
  });

  it("does not throw outside prerender/dev even with a nested dynamic inner cache", async () => {
    // Verifies the prerender gate: in production runtime SSR (no
    // VINEXT_PRERENDER, NODE_ENV !== "development"), nesting a dynamic
    // inner inside a non-cacheLife() outer must NOT throw. This matches
    // Next.js, which only surfaces the error during prerender or dev
    // requests (see use-cache-wrapper.ts).
    // `NODE_ENV` is typed read-only on `process.env`; cast to the indexable
    // signature to allow temporary mutation in this test.
    const env = process.env as Record<string, string | undefined>;
    const previousPrerender = env.VINEXT_PRERENDER;
    const previousNodeEnv = env.NODE_ENV;
    delete env.VINEXT_PRERENDER;
    env.NODE_ENV = "production";
    try {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerCached = registerCachedFunction(async () => {
        cacheLife({ revalidate: 0 });
        return "inner";
      }, "test:nested-prod-inner");

      // No explicit cacheLife on the outer — in prerender this would throw,
      // but in production runtime SSR it must just run and not be cached.
      const outerCached = registerCachedFunction(
        async () => `outer-${await innerCached()}`,
        "test:nested-prod-outer",
      );

      const result = await outerCached();
      expect(result).toBe("outer-inner");
    } finally {
      if (previousPrerender === undefined) delete env.VINEXT_PRERENDER;
      else env.VINEXT_PRERENDER = previousPrerender;
      if (previousNodeEnv === undefined) delete env.NODE_ENV;
      else env.NODE_ENV = previousNodeEnv;
    }
  });

  it("does throw in development mode even without VINEXT_PRERENDER set", async () => {
    // Companion to the production test above: verifies the *other side* of
    // the gate. With `NODE_ENV === "development"` (and no VINEXT_PRERENDER),
    // the throw must still fire. This catches a hypothetical regression
    // where the gate is broken to e.g. only check VINEXT_PRERENDER, or to
    // check `!== "production"` (which would falsely fire in Vitest's
    // default `NODE_ENV=test`). Together with the production test, this
    // pins down both branches of the gate.
    const env = process.env as Record<string, string | undefined>;
    const previousPrerender = env.VINEXT_PRERENDER;
    const previousNodeEnv = env.NODE_ENV;
    delete env.VINEXT_PRERENDER;
    env.NODE_ENV = "development";
    try {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerCached = registerCachedFunction(async () => {
        cacheLife({ revalidate: 0 });
        return "inner";
      }, "test:nested-dev-inner");

      const outerCached = registerCachedFunction(
        async () => `outer-${await innerCached()}`,
        "test:nested-dev-outer",
      );

      let thrown: Error | undefined;
      try {
        await outerCached();
      } catch (e) {
        thrown = e as Error;
      }

      expect(thrown).toBeDefined();
      expect(thrown!.message).toContain("revalidate");
      // In dev, the message should say "in development", not
      // "during prerendering", since no prerendering is happening.
      expect(thrown!.message).toContain("in development");
      expect(thrown!.message).not.toContain("during prerendering");
    } finally {
      if (previousPrerender === undefined) delete env.VINEXT_PRERENDER;
      else env.VINEXT_PRERENDER = previousPrerender;
      if (previousNodeEnv === undefined) delete env.NODE_ENV;
      else env.NODE_ENV = previousNodeEnv;
    }
  });

  it("does not throw when outer cache has explicit cacheLife", async () => {
    await withPrerenderFlag(async () => {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerFn = async () => {
        cacheLife({ revalidate: 0 });
        return "inner";
      };
      const innerCached = registerCachedFunction(innerFn, "test:nested-ok-inner");

      const outerFn = async () => {
        cacheLife({ revalidate: 60 }); // explicit on outer
        const result = await innerCached();
        return `outer-${result}`;
      };
      const outerCached = registerCachedFunction(outerFn, "test:nested-ok-outer");

      // Should NOT throw — outer made an explicit choice
      const result = await outerCached();
      expect(result).toBe("outer-inner");
    });
  });

  it("explicit cacheLife on outer suppresses throw but minimum-wins still applies to effective cache life", async () => {
    // Documents a potentially surprising behavior: when the outer has an
    // explicit `cacheLife({ revalidate: 60 })` and the inner is dynamic
    // (`revalidate: 0`), the throw is suppressed (outer made an explicit
    // choice), but the outer's effective `revalidate` resolves to 0 via
    // minimum-wins (because the inner's resolved life is pushed into the
    // outer's `lifeConfigs`). MemoryCacheHandler treats `revalidate: 0` as
    // not cached, so the outer function executes on every call. This matches
    // Next.js's `propagateCacheLifeAndTagsToRevalidateStore` semantics —
    // the outer's explicit cacheLife() opts it out of the throw, but does
    // not override the inner's minimum-wins contribution.
    await withPrerenderFlag(async () => {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      let innerCalls = 0;
      let outerCalls = 0;

      const innerCached = registerCachedFunction(async () => {
        innerCalls++;
        cacheLife({ revalidate: 0 });
        return "inner";
      }, "test:min-wins-inner");

      const outerCached = registerCachedFunction(async () => {
        outerCalls++;
        cacheLife({ revalidate: 60 });
        return `outer-${await innerCached()}`;
      }, "test:min-wins-outer");

      const first = await outerCached();
      const second = await outerCached();
      expect(first).toBe("outer-inner");
      expect(second).toBe("outer-inner");
      // The outer function must execute twice — minimum-wins resolved its
      // effective revalidate to 0, so the result was not cached despite the
      // explicit `cacheLife({ revalidate: 60 })`.
      expect(outerCalls).toBe(2);
      // Inner is also dynamic (revalidate: 0) so it re-executes too.
      expect(innerCalls).toBe(2);
    });
  });

  it("keeps first dynamic child as cause when multiple nested caches are dynamic", async () => {
    await withPrerenderFlag(async () => {
      const { registerCachedFunction } =
        await import("../packages/vinext/src/shims/cache-runtime.js");
      const { setCacheHandler, MemoryCacheHandler, cacheLife } =
        await import("../packages/vinext/src/shims/cache.js");
      setCacheHandler(new MemoryCacheHandler());

      const innerA = registerCachedFunction(async () => {
        cacheLife({ revalidate: 0 });
        return "a";
      }, "test:first-child-a");

      const innerB = registerCachedFunction(async () => {
        cacheLife({ expire: 120 }); // also dynamic
        return "b";
      }, "test:first-child-b");

      const outerFn = async () => {
        await innerA();
        await innerB();
        return "outer";
      };
      const outerCached = registerCachedFunction(outerFn, "test:first-child-outer");

      let thrown: Error | undefined;
      try {
        await outerCached();
      } catch (e) {
        thrown = e as Error;
      }

      expect(thrown).toBeDefined();
      expect(thrown!.cause).toBeDefined();
      // The cause should be from the FIRST dynamic child (innerA), which set
      // `revalidate: 0`. The outer error message reflects the first-firing
      // dynamic field — if innerA's revalidate propagated first, the outer
      // throws the revalidate (not expire) error, even though innerB's
      // `expire: 120` is also below DYNAMIC_EXPIRE. This rigorously verifies
      // first-child-wins semantics: dynamicNestedCacheError uses `??=` so the
      // first dynamic child's eager error is preserved as the cause, and the
      // revalidate check fires before the expire check in the outer.
      expect(thrown!.message).toContain("revalidate");
      expect(thrown!.message).not.toContain("expire");
    });
  });
});

describe("replyToCacheKey deterministic hashing", () => {
  it("returns string replies as-is", async () => {
    const { replyToCacheKey } = await import("../packages/vinext/src/shims/cache-runtime.js");
    expect(await replyToCacheKey("hello")).toBe("hello");
    expect(await replyToCacheKey("")).toBe("");
  });

  it("produces stable hash for FormData with string entries", async () => {
    const { replyToCacheKey } = await import("../packages/vinext/src/shims/cache-runtime.js");

    const fd1 = new FormData();
    fd1.append("a", "1");
    fd1.append("b", "2");

    const fd2 = new FormData();
    fd2.append("a", "1");
    fd2.append("b", "2");

    const key1 = await replyToCacheKey(fd1);
    const key2 = await replyToCacheKey(fd2);
    expect(key1).toBe(key2);
  });

  it("produces stable hash regardless of entry insertion order", async () => {
    const { replyToCacheKey } = await import("../packages/vinext/src/shims/cache-runtime.js");

    const fd1 = new FormData();
    fd1.append("b", "2");
    fd1.append("a", "1");

    const fd2 = new FormData();
    fd2.append("a", "1");
    fd2.append("b", "2");

    const key1 = await replyToCacheKey(fd1);
    const key2 = await replyToCacheKey(fd2);
    expect(key1).toBe(key2);
  });

  it("produces stable hash for FormData with Blob entries", async () => {
    const { replyToCacheKey } = await import("../packages/vinext/src/shims/cache-runtime.js");

    const blob = new Blob([new Uint8Array([1, 2, 3])], { type: "application/octet-stream" });

    const fd1 = new FormData();
    fd1.append("data", blob);

    const fd2 = new FormData();
    fd2.append("data", blob);

    const key1 = await replyToCacheKey(fd1);
    const key2 = await replyToCacheKey(fd2);
    expect(key1).toBe(key2);
  });

  it("produces different hashes for different FormData content", async () => {
    const { replyToCacheKey } = await import("../packages/vinext/src/shims/cache-runtime.js");

    const fd1 = new FormData();
    fd1.append("a", "1");

    const fd2 = new FormData();
    fd2.append("a", "2");

    const key1 = await replyToCacheKey(fd1);
    const key2 = await replyToCacheKey(fd2);
    expect(key1).not.toBe(key2);
  });
});

describe("middleware runner", () => {
  it("findMiddlewareFile finds middleware.ts at project root", async () => {
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");
    // pages-basic fixture has middleware.ts
    const result = findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher());
    expect(result).not.toBeNull();
    expect(result).toContain("middleware.ts");
  });

  it("findMiddlewareFile returns null when no middleware exists", async () => {
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");
    const result = findMiddlewareFile(
      "/tmp/nonexistent-dir-" + Date.now(),
      createValidFileMatcher(),
    );
    expect(result).toBeNull();
  });

  it("findMiddlewareFile does not find middleware.ts when ts is not a configured pageExtension", async () => {
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");
    // FIXTURE_DIR has middleware.ts — restricting to mdx only means it should not match
    const result = findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher(["mdx"]));
    expect(result).toBeNull();
  });

  it("findMiddlewareFile emits a deprecation warning when middleware.ts is found", async () => {
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    try {
      findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher());
      expect(warnSpy).toHaveBeenCalledOnce();
      // Match Next.js canonical wording: deprecation-warnings and
      // app-middleware e2e suites assert on this exact phrase via toContain.
      // See .nextjs-ref/packages/next/src/build/index.ts (the Log.warnOnce call
      // adjacent to MIDDLEWARE_FILENAME / PROXY_FILENAME).
      expect(warnSpy).toHaveBeenCalledWith(
        expect.stringContaining(
          'The "middleware" file convention is deprecated. Please use "proxy" instead.',
        ),
      );
      expect(warnSpy).toHaveBeenCalledWith(
        expect.stringContaining("https://nextjs.org/docs/messages/middleware-to-proxy"),
      );
    } finally {
      warnSpy.mockRestore();
    }
  });

  it("findMiddlewareFile prefers proxy.ts over middleware.ts (Next.js 16)", async () => {
    const fs = await import("node:fs");
    const path = await import("node:path");
    const os = await import("node:os");
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");

    // Create a temp directory with both proxy.ts and middleware.ts
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-proxy-test-"));
    try {
      fs.writeFileSync(path.join(tmpDir, "proxy.ts"), "export default function proxy() {}");
      fs.writeFileSync(path.join(tmpDir, "middleware.ts"), "export function middleware() {}");
      const result = findMiddlewareFile(tmpDir, createValidFileMatcher());
      expect(result).not.toBeNull();
      expect(result).toContain("proxy.ts");
    } finally {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("findMiddlewareFile finds proxy.js", async () => {
    const fs = await import("node:fs");
    const path = await import("node:path");
    const os = await import("node:os");
    const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js");
    const { createValidFileMatcher } =
      await import("../packages/vinext/src/routing/file-matcher.js");

    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-proxy-test-"));
    try {
      fs.writeFileSync(path.join(tmpDir, "proxy.js"), "module.exports = function proxy() {}");
      const result = findMiddlewareFile(tmpDir, createValidFileMatcher());
      expect(result).not.toBeNull();
      expect(result).toContain("proxy.js");
    } finally {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });
});

// ---------------------------------------------------------------------------
// Dev-mode runMiddleware header preservation tests
// Tests that the middleware.ts runner (used by the dev server) preserves
// x-middleware-request-* headers so the caller can unpack them into actual
// request headers. This is the dev/prod parity fix — the production inline
// codegen (pages-server-entry.ts) already preserved them correctly.

describe("runMiddleware preserves x-middleware-request-* headers (dev mode)", () => {
  it("keeps x-middleware-request-* headers on NextResponse.next()", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    // Mock runner that loads a fake middleware module
    const mockRunner = {
      import: async () => ({
        default: () =>
          NextResponse.next({
            request: {
              headers: new Headers({ "x-custom-injected": "from-middleware" }),
            },
          }),
        config: { matcher: "/" },
      }),
    };

    const request = new Request("http://localhost/");
    const result = await runMiddleware(mockRunner as any, "/fake/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(result.responseHeaders).toBeDefined();
    // x-middleware-request-* must survive so the dev server can unpack them
    expect(result.responseHeaders!.get("x-middleware-request-x-custom-injected")).toBe(
      "from-middleware",
    );
    // Other x-middleware-* internal headers must be stripped
    expect(result.responseHeaders!.has("x-middleware-next")).toBe(false);
  });

  it("preserves status from NextResponse.next()", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    const mockRunner = {
      import: async () => ({
        default: () => NextResponse.next({ status: 404 }),
        config: { matcher: "/" },
      }),
    };

    const request = new Request("http://localhost/");
    const result = await runMiddleware(mockRunner as any, "/fake/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(result.status).toBe(404);
  });

  it("keeps x-middleware-request-* headers on rewrite", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    const mockRunner = {
      import: async () => ({
        default: () => {
          const res = NextResponse.rewrite(new URL("/rewritten", "http://localhost"));
          // Simulate middleware also setting request headers on a rewrite
          res.headers.set("x-middleware-request-x-auth", "bearer-token");
          return res;
        },
        config: { matcher: "/" },
      }),
    };

    const request = new Request("http://localhost/");
    const result = await runMiddleware(mockRunner as any, "/fake/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(result.rewriteUrl).toBe("/rewritten");
    expect(result.responseHeaders).toBeDefined();
    expect(result.responseHeaders!.get("x-middleware-request-x-auth")).toBe("bearer-token");
    // x-middleware-rewrite must be stripped
    expect(result.responseHeaders!.has("x-middleware-rewrite")).toBe(false);
  });

  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-rewrites/test/index.test.ts
  it("preserves the full external URL for middleware rewrites", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    const mockRunner = {
      import: async () => ({
        default: () => NextResponse.rewrite("https://api.example.com/echo?from=middleware"),
        config: { matcher: "/:path*" },
      }),
    };

    const request = new Request("http://localhost/original?keep=1");
    const result = await runMiddleware(mockRunner as any, "/fake/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(result.rewriteUrl).toBe("https://api.example.com/echo?from=middleware");
  });

  it("strips x-middleware-set-cookie from custom middleware responses", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    const mockRunner = {
      import: async () => ({
        default: () => {
          const res = new NextResponse("blocked", { status: 403 });
          res.cookies.set("blocked", "1", { path: "/" });
          return res;
        },
        config: { matcher: "/:path*" },
      }),
    };

    const request = new Request("http://localhost/blocked");
    const result = await runMiddleware(mockRunner as any, "/fake/middleware.ts", request);

    expect(result.continue).toBe(false);
    expect(result.response).toBeDefined();
    expect(result.response!.headers.get("x-middleware-set-cookie")).toBeNull();
    expect(result.response!.headers.get("set-cookie")).toContain("blocked=1");
  });
});

// ---------------------------------------------------------------------------
// Middleware/proxy export validation tests
// Ported from Next.js: test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts

describe("middleware/proxy export validation", () => {
  it("isProxyFile returns true for proxy files", async () => {
    const { isProxyFile } = await import("../packages/vinext/src/server/middleware.js");
    expect(isProxyFile("/app/proxy.ts")).toBe(true);
    expect(isProxyFile("/app/proxy.js")).toBe(true);
    expect(isProxyFile("/app/proxy.mjs")).toBe(true);
    expect(isProxyFile("/app/src/proxy.ts")).toBe(true);
  });

  it("isProxyFile returns false for middleware files", async () => {
    const { isProxyFile } = await import("../packages/vinext/src/server/middleware.js");
    expect(isProxyFile("/app/middleware.ts")).toBe(false);
    expect(isProxyFile("/app/middleware.js")).toBe(false);
    expect(isProxyFile("/app/middleware.mjs")).toBe(false);
    expect(isProxyFile("/app/src/middleware.ts")).toBe(false);
  });

  it("resolveMiddlewareHandler: proxy.ts with named proxy export", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const fn = () => {};
    const handler = resolveMiddlewareHandler({ proxy: fn }, "/app/proxy.ts");
    expect(handler).toBe(fn);
  });

  it("resolveMiddlewareHandler: proxy.ts with default export", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const fn = () => {};
    const handler = resolveMiddlewareHandler({ default: fn }, "/app/proxy.ts");
    expect(handler).toBe(fn);
  });

  it("resolveMiddlewareHandler: proxy.ts prefers named proxy over default", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const proxyFn = () => {};
    const defaultFn = () => {};
    const handler = resolveMiddlewareHandler(
      { proxy: proxyFn, default: defaultFn },
      "/app/proxy.ts",
    );
    expect(handler).toBe(proxyFn);
  });

  it("resolveMiddlewareHandler: proxy.ts with default arrow function export", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const fn = () => {};
    const handler = resolveMiddlewareHandler({ default: fn }, "/app/proxy.ts");
    expect(handler).toBe(fn);
  });

  it("resolveMiddlewareHandler: proxy.ts throws when only 'middleware' is exported (wrong name)", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({ middleware: () => {} }, "/app/proxy.ts")).toThrow(
      "must export a function named `proxy` or a `default` function",
    );
  });

  it("resolveMiddlewareHandler: proxy.ts throws when export is aliased to wrong name", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({ handler: () => {} }, "/app/proxy.ts")).toThrow(
      "must export a function named `proxy` or a `default` function",
    );
  });

  it("resolveMiddlewareHandler: proxy.ts throws when no exports", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({}, "/app/proxy.ts")).toThrow(
      "must export a function named `proxy` or a `default` function",
    );
  });

  it("resolveMiddlewareHandler: proxy.ts throws when export is not a function", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({ proxy: "not a function" }, "/app/proxy.ts")).toThrow(
      "must export a function named `proxy` or a `default` function",
    );
  });

  it("resolveMiddlewareHandler: middleware.ts with named middleware export", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const fn = () => {};
    const handler = resolveMiddlewareHandler({ middleware: fn }, "/app/middleware.ts");
    expect(handler).toBe(fn);
  });

  it("resolveMiddlewareHandler: middleware.ts with default export", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    const fn = () => {};
    const handler = resolveMiddlewareHandler({ default: fn }, "/app/middleware.ts");
    expect(handler).toBe(fn);
  });

  it("resolveMiddlewareHandler: middleware.ts throws when only 'proxy' is exported (wrong name)", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({ proxy: () => {} }, "/app/middleware.ts")).toThrow(
      "must export a function named `middleware` or a `default` function",
    );
  });

  it("resolveMiddlewareHandler: middleware.ts throws when no exports", async () => {
    const { resolveMiddlewareHandler } =
      await import("../packages/vinext/src/server/middleware.js");
    expect(() => resolveMiddlewareHandler({}, "/app/middleware.ts")).toThrow(
      "must export a function named `middleware` or a `default` function",
    );
  });
});

// ---------------------------------------------------------------------------
// matchPattern / matchesMiddleware unit tests

describe("middleware matcher patterns", () => {
  it("matchPattern: exact path match", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchPattern("/about", "/about")).toBe(true);
    expect(matchPattern("/about", "/other")).toBe(false);
    expect(matchPattern("/", "/")).toBe(true);
  });

  it("matchPattern: named parameter (:param)", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchPattern("/user/123", "/user/:id")).toBe(true);
    expect(matchPattern("/user/abc", "/user/:id")).toBe(true);
    expect(matchPattern("/user/", "/user/:id")).toBe(false);
    expect(matchPattern("/user/123/posts", "/user/:id")).toBe(false);
  });

  // Ported from Next.js: middleware matchers support :param(constraint) syntax
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  it("matchPattern: :param(constraint) applies inline regex constraint", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    // :id(\d+) should match only digits
    expect(matchPattern("/blog/123", "/blog/:id(\\d+)")).toBe(true);
    expect(matchPattern("/blog/0", "/blog/:id(\\d+)")).toBe(true);
    expect(matchPattern("/blog/abc", "/blog/:id(\\d+)")).toBe(false);
    expect(matchPattern("/blog/12x", "/blog/:id(\\d+)")).toBe(false);

    // Locale-style alternation constraint: :locale(en|es|fr)
    expect(matchPattern("/en/about", "/:locale(en|es|fr)/about")).toBe(true);
    expect(matchPattern("/es/about", "/:locale(en|es|fr)/about")).toBe(true);
    expect(matchPattern("/de/about", "/:locale(en|es|fr)/about")).toBe(false);

    // Optional locale with ? after constraint
    expect(matchPattern("/about", "/:locale(en|es|fr)?/about")).toBe(true);
    expect(matchPattern("/en/about", "/:locale(en|es|fr)?/about")).toBe(true);
    expect(matchPattern("/de/about", "/:locale(en|es|fr)?/about")).toBe(false);
  });

  it("matchPattern: wildcard (:path*) matches zero or more segments", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchPattern("/dashboard", "/dashboard/:path*")).toBe(true);
    expect(matchPattern("/dashboard/settings", "/dashboard/:path*")).toBe(true);
    expect(matchPattern("/dashboard/settings/profile", "/dashboard/:path*")).toBe(true);
    expect(matchPattern("/other", "/dashboard/:path*")).toBe(false);
  });

  it("matchPattern: :param*(constraint) and :param+(constraint)", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    // /:path*(api|static) — optional segment constrained to api or static
    expect(matchPattern("/cdn", "/cdn/:path*(api|static)")).toBe(true);
    expect(matchPattern("/cdn/api", "/cdn/:path*(api|static)")).toBe(true);
    expect(matchPattern("/cdn/static", "/cdn/:path*(api|static)")).toBe(true);
    expect(matchPattern("/cdn/other", "/cdn/:path*(api|static)")).toBe(false);

    // /:path+(api|static) — required segment constrained
    expect(matchPattern("/cdn", "/cdn/:path+(api|static)")).toBe(false);
    expect(matchPattern("/cdn/api", "/cdn/:path+(api|static)")).toBe(true);
    expect(matchPattern("/cdn/static", "/cdn/:path+(api|static)")).toBe(true);
    expect(matchPattern("/cdn/other", "/cdn/:path+(api|static)")).toBe(false);
  });

  it("matchPattern: one-or-more (:path+) requires at least one segment", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchPattern("/api/users", "/api/:path+")).toBe(true);
    expect(matchPattern("/api/users/123", "/api/:path+")).toBe(true);
    expect(matchPattern("/api", "/api/:path+")).toBe(false);
    // /api/ has no actual segment after the slash
    expect(matchPattern("/api/", "/api/:path+")).toBe(false);
  });

  it("matchPattern: regex patterns with groups", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    // Common Next.js matcher: /((?!api|_next|favicon\.ico).*)
    expect(matchPattern("/about", "/((?!api|_next|favicon\\.ico).*)")).toBe(true);
    expect(matchPattern("/dashboard/settings", "/((?!api|_next|favicon\\.ico).*)")).toBe(true);
    expect(matchPattern("/api/hello", "/((?!api|_next|favicon\\.ico).*)")).toBe(false);
    expect(matchPattern("/_next/static/chunk.js", "/((?!api|_next|favicon\\.ico).*)")).toBe(false);
  });

  it("matchPattern: dots are escaped in paths", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchPattern("/files/data.json", "/files/data.json")).toBe(true);
    expect(matchPattern("/files/dataXjson", "/files/data.json")).toBe(false);
  });

  it("matchesMiddleware: no matcher — matches all paths (Next.js default)", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    // Next.js default: middleware runs on ALL paths when no matcher is configured.
    // Users opt out of specific paths by configuring a matcher pattern.
    expect(matchesMiddleware("/", undefined)).toBe(true);
    expect(matchesMiddleware("/about", undefined)).toBe(true);
    expect(matchesMiddleware("/dashboard/settings", undefined)).toBe(true);
    expect(matchesMiddleware("/_next/static/chunk.js", undefined)).toBe(true);
    expect(matchesMiddleware("/api/hello", undefined)).toBe(true);
    expect(matchesMiddleware("/favicon.ico", undefined)).toBe(true);
    expect(matchesMiddleware("/image.png", undefined)).toBe(true);
  });

  it("matchesMiddleware: single string matcher", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    expect(matchesMiddleware("/about", "/about")).toBe(true);
    expect(matchesMiddleware("/other", "/about")).toBe(false);
    // Ported from Next.js: test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
    expect(
      matchesMiddleware("/about", "/about", undefined, {
        locales: ["en", "fr"],
        defaultLocale: "en",
      }),
    ).toBe(true);
    expect(
      matchesMiddleware("/fr/about", "/about", undefined, {
        locales: ["en", "fr"],
        defaultLocale: "en",
      }),
    ).toBe(true);
    expect(
      matchesMiddleware("/", "/", undefined, {
        locales: ["en", "fr"],
        defaultLocale: "en",
      }),
    ).toBe(true);
  });

  it("matchesMiddleware: locale-prefixed negative-lookahead matchers keep internal paths excluded", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher = "/((?!api|_next|favicon\\.ico).*)";
    const i18nConfig = {
      locales: ["en", "fr"],
      defaultLocale: "en",
    };

    expect(matchesMiddleware("/fr/about", matcher, undefined, i18nConfig)).toBe(true);
    expect(matchesMiddleware("/fr/api/hello", matcher, undefined, i18nConfig)).toBe(false);
    expect(matchesMiddleware("/fr/_next/static/chunk.js", matcher, undefined, i18nConfig)).toBe(
      false,
    );
    expect(matchesMiddleware("/fr/favicon.ico", matcher, undefined, i18nConfig)).toBe(false);
  });

  it("matchesMiddleware: array of string matchers", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher = ["/about", "/dashboard/:path*"];
    expect(matchesMiddleware("/about", matcher)).toBe(true);
    expect(matchesMiddleware("/dashboard", matcher)).toBe(true);
    expect(matchesMiddleware("/dashboard/settings", matcher)).toBe(true);
    expect(matchesMiddleware("/other", matcher)).toBe(false);
  });

  it("matchesMiddleware: array of object matchers with source", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher = [{ source: "/about" }, { source: "/dashboard/:path*" }];
    expect(matchesMiddleware("/about", matcher)).toBe(true);
    expect(matchesMiddleware("/dashboard/settings", matcher)).toBe(true);
    expect(matchesMiddleware("/other", matcher)).toBe(false);
  });

  it("matchesMiddleware: object matchers respect has and missing conditions", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher: any = [
      {
        source: "/dashboard",
        has: [{ type: "header", key: "x-user-tier", value: "pro" }],
        missing: [{ type: "cookie", key: "blocked" }],
      },
    ];

    expect(matchesMiddleware("/dashboard", matcher)).toBe(false);
    expect(
      matchesMiddleware(
        "/dashboard",
        matcher,
        new Request("https://example.com/dashboard", {
          headers: { "x-user-tier": "pro" },
        }),
      ),
    ).toBe(true);
    expect(
      matchesMiddleware(
        "/dashboard",
        matcher,
        new Request("https://example.com/dashboard", {
          headers: { "x-user-tier": "free", cookie: "blocked=1" },
        }),
      ),
    ).toBe(false);
  });

  it("matchesMiddleware: rejects a single object matcher config", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher: any = {
      source: "/dashboard",
      has: [{ type: "header", key: "x-user-tier", value: "pro" }],
    };

    expect(matchesMiddleware("/dashboard", matcher)).toBe(false);
    expect(
      matchesMiddleware(
        "/dashboard",
        matcher,
        new Request("https://example.com/dashboard", {
          headers: { "x-user-tier": "pro" },
        }),
      ),
    ).toBe(false);
  });

  it("matchesMiddleware: rejects object matchers with unsupported fields", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher: any = [{ source: "/does-not-match", regexp: "^/dashboard(?:/.*)?$" }];

    expect(matchesMiddleware("/dashboard/settings", matcher)).toBe(false);
    expect(matchesMiddleware("/about", matcher)).toBe(false);
  });

  it("matchesMiddleware: matches default-locale and locale-prefixed paths unless locale is false", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const i18nConfig = {
      locales: ["en", "fr"],
      defaultLocale: "en",
    };

    // Ported from Next.js: test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-custom-matchers-i18n/test/index.test.ts
    expect(matchesMiddleware("/dashboard", [{ source: "/dashboard" }], undefined, i18nConfig)).toBe(
      true,
    );
    expect(
      matchesMiddleware("/fr/dashboard", [{ source: "/dashboard" }], undefined, i18nConfig),
    ).toBe(true);
    expect(
      matchesMiddleware(
        "/fr/dashboard",
        [{ source: "/dashboard", locale: false }],
        undefined,
        i18nConfig,
      ),
    ).toBe(false);
  });

  it("matchesMiddleware: mixed array of strings and objects", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const matcher = ["/about", { source: "/api/:path+" }] as any;
    expect(matchesMiddleware("/about", matcher)).toBe(true);
    expect(matchesMiddleware("/api/users", matcher)).toBe(true);
    expect(matchesMiddleware("/api", matcher)).toBe(false);
    expect(matchesMiddleware("/other", matcher)).toBe(false);
  });

  it("matchPattern: rejects pathological ReDoS patterns", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    // Pathological pattern: (a+)+ causes catastrophic backtracking
    // matchPattern should return false (no match) instead of hanging
    // lgtm[js/redos] — deliberate pathological regex to test safeRegExp guard
    expect(matchPattern("/aaaaaaaaaaaaaaaaaaaac", "(a+)+b")).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// normalizePath unit tests

describe("normalizePath", () => {
  it("returns root unchanged", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/")).toBe("/");
  });

  it("returns already-canonical paths unchanged", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/foo/bar")).toBe("/foo/bar");
    expect(normalizePath("/about")).toBe("/about");
    expect(normalizePath("/api/users/123")).toBe("/api/users/123");
  });

  it("collapses double slashes", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("//foo")).toBe("/foo");
    expect(normalizePath("/foo//bar")).toBe("/foo/bar");
    expect(normalizePath("/dashboard//settings")).toBe("/dashboard/settings");
    expect(normalizePath("///")).toBe("/");
    expect(normalizePath("/foo///bar///baz")).toBe("/foo/bar/baz");
  });

  it("resolves single-dot segments", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/foo/./bar")).toBe("/foo/bar");
    expect(normalizePath("/./foo")).toBe("/foo");
    expect(normalizePath("/foo/.")).toBe("/foo");
  });

  it("resolves double-dot segments", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/foo/../bar")).toBe("/bar");
    expect(normalizePath("/foo/bar/../baz")).toBe("/foo/baz");
    expect(normalizePath("/foo/..")).toBe("/");
  });

  it("clamps traversal above root", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/../../../etc/passwd")).toBe("/etc/passwd");
    expect(normalizePath("/..")).toBe("/");
  });

  it("ensures leading slash", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("foo/bar")).toBe("/foo/bar");
    expect(normalizePath("")).toBe("/");
  });

  it("preserves trailing slash on fast path", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    // Fast path: already canonical with trailing slash
    expect(normalizePath("/foo/bar/")).toBe("/foo/bar/");
  });

  it("handles complex combined cases", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    expect(normalizePath("/foo/./bar/../baz")).toBe("/foo/baz");
    expect(normalizePath("//foo/./bar//baz/../qux")).toBe("/foo/bar/qux");
  });
});

// ── escapePathDelimiters / decodePathParams ────────────────────────────────
// Ported from Next.js: packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
// and packages/next/src/server/lib/router-utils/decode-path-params.ts

describe("escapePathDelimiters", () => {
  let escapePathDelimiters: (segment: string, escapeEncoded?: boolean) => string;

  beforeEach(async () => {
    const mod = await import("../packages/vinext/src/server/normalize-path.js");
    escapePathDelimiters = mod.escapePathDelimiters;
  });

  it("re-encodes forward slash", () => {
    expect(escapePathDelimiters("admin/panel")).toBe("admin%2Fpanel");
  });

  it("re-encodes hash and question mark", () => {
    expect(escapePathDelimiters("foo#bar")).toBe("foo%23bar");
    expect(escapePathDelimiters("foo?bar")).toBe("foo%3Fbar");
  });

  it("re-encodes already-encoded delimiters when escapeEncoded is true", () => {
    expect(escapePathDelimiters("admin%2Fpanel", true)).toBe("admin%252Fpanel");
    expect(escapePathDelimiters("foo%23bar", true)).toBe("foo%2523bar");
    expect(escapePathDelimiters("foo%3Fbar", true)).toBe("foo%253Fbar");
    expect(escapePathDelimiters("foo%5Cbar", true)).toBe("foo%255Cbar");
  });

  it("leaves non-delimiter characters unchanged", () => {
    expect(escapePathDelimiters("café")).toBe("café");
    expect(escapePathDelimiters("hello world")).toBe("hello world");
  });
});

describe("decodePathParams", () => {
  let decodePathParams: (pathname: string) => string;

  beforeEach(async () => {
    const mod = await import("../packages/vinext/src/server/normalize-path.js");
    decodePathParams = mod.decodePathParams;
  });

  it("decodes non-ASCII characters within segments", () => {
    expect(decodePathParams("/caf%C3%A9")).toBe("/café");
    expect(decodePathParams("/%E6%97%A5%E6%9C%AC%E8%AA%9E")).toBe("/日本語");
  });

  it("preserves encoded slashes (%2F) - does not change path structure", () => {
    expect(decodePathParams("/admin%2Fpanel")).toBe("/admin%2Fpanel");
  });

  it("preserves encoded hash (%23) and question mark (%3F)", () => {
    expect(decodePathParams("/foo%23bar")).toBe("/foo%23bar");
    expect(decodePathParams("/foo%3Fbar")).toBe("/foo%3Fbar");
  });

  it("decodes encoded backslash (%5C) since it is not a path delimiter", () => {
    // Backslash is not a URL path delimiter (browsers normalize \ to / before
    // sending). The escapePathDelimiters function only re-encodes /, #, and ?.
    // %5C is decoded to \ and left as-is, matching Next.js behavior.
    expect(decodePathParams("/foo%5Cbar")).toBe("/foo\\bar");
  });

  it("decodes mixed paths correctly", () => {
    // Non-ASCII decoded, structural delimiters preserved
    expect(decodePathParams("/caf%C3%A9/admin%2Fpanel")).toBe("/café/admin%2Fpanel");
  });

  it("handles already-decoded paths", () => {
    expect(decodePathParams("/about")).toBe("/about");
    expect(decodePathParams("/foo/bar/baz")).toBe("/foo/bar/baz");
  });

  it("handles malformed percent-encoding gracefully", () => {
    // Should not throw, just return the segment as-is
    expect(decodePathParams("/%E0%A4%A")).toBe("/%E0%A4%A");
  });

  it("handles root path", () => {
    expect(decodePathParams("/")).toBe("/");
  });
});

// ---------------------------------------------------------------------------
// Integration: verify decodeURIComponent + normalizePath applied before matching

describe("middleware bypass prevention", () => {
  it("percent-encoded path is decoded before matching", async () => {
    const { matchPattern, matchesMiddleware } =
      await import("../packages/vinext/src/server/middleware.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");

    // /%61dmin decodes to /admin
    const encoded = "/%61dmin";
    const decoded = normalizePath(decodeURIComponent(encoded));
    expect(decoded).toBe("/admin");
    expect(matchPattern(decoded, "/admin")).toBe(true);
    expect(matchesMiddleware(decoded, "/admin")).toBe(true);
  });

  it("double-slash path is collapsed before matching", async () => {
    const { matchPattern, matchesMiddleware } =
      await import("../packages/vinext/src/server/middleware.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");

    // /dashboard//settings collapses to /dashboard/settings
    const doubleSlash = "/dashboard//settings";
    const normalized = normalizePath(doubleSlash);
    expect(normalized).toBe("/dashboard/settings");
    expect(matchPattern(normalized, "/dashboard/:path*")).toBe(true);
    expect(matchesMiddleware(normalized, "/dashboard/:path*")).toBe(true);
  });

  it("default matcher (no config) matches all paths including /api", async () => {
    const { matchesMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    // When no matcher is configured, middleware must run on ALL paths
    expect(matchesMiddleware("/api/hello", undefined)).toBe(true);
    expect(matchesMiddleware("/_next/data/build-id/page.json", undefined)).toBe(true);
    expect(matchesMiddleware("/favicon.ico", undefined)).toBe(true);
  });

  it("regex patterns are not corrupted by dot-escaping", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    // The common Next.js regex pattern must work correctly:
    // /((?!api|_next|favicon\.ico).*) should match /about but NOT /api/hello
    const pattern = "/((?!api|_next|favicon\\.ico).*)";
    expect(matchPattern("/about", pattern)).toBe(true);
    expect(matchPattern("/dashboard/settings", pattern)).toBe(true);
    expect(matchPattern("/api/hello", pattern)).toBe(false);
    expect(matchPattern("/_next/static/chunk.js", pattern)).toBe(false);
    expect(matchPattern("/favicon.ico", pattern)).toBe(false);
  });

  // ── Config matcher percent-encoding handling ──

  it("config redirect matcher works with decoded percent-encoded paths", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    const redirects = [
      { source: "/admin", destination: "/login", permanent: true },
      { source: "/old-blog/:slug", destination: "/blog/:slug", permanent: false },
    ];
    const reqCtx = {
      headers: new Headers(),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    };
    // Decoded path should match
    const decoded = normalizePath(decodeURIComponent("/%61dmin"));
    expect(decoded).toBe("/admin");
    const result = matchRedirect(decoded, redirects, reqCtx);
    expect(result).toBeTruthy();
    expect(result!.destination).toBe("/login");

    // Mixed encoding in parameterized route
    const slugDecoded = normalizePath(decodeURIComponent("/%6Fld-blog/my-p%6Fst"));
    expect(slugDecoded).toBe("/old-blog/my-post");
    const slugResult = matchRedirect(slugDecoded, redirects, reqCtx);
    expect(slugResult).toBeTruthy();
    expect(slugResult!.destination).toBe("/blog/my-post");

    // Raw encoded path must NOT match (matchers expect decoded paths)
    const rawResult = matchRedirect("/%61dmin", redirects, reqCtx);
    expect(rawResult).toBeNull();
  });

  it("preserveRedirectDestinationQuery merges the original request query onto the destination (#1529)", async () => {
    const { preserveRedirectDestinationQuery } =
      await import("../packages/vinext/src/config/config-matchers.js");

    // Carries the RSC cache-busting `_rsc` param onto a query-less destination.
    expect(preserveRedirectDestinationQuery("/dest", "?_rsc=abc123")).toBe("/dest?_rsc=abc123");

    // Merges with a destination that already has its own query; destination
    // params win on key conflicts (mirrors Next.js + proxyExternalRequest).
    expect(preserveRedirectDestinationQuery("/dest?from=cfg", "?from=req&_rsc=abc")).toBe(
      "/dest?from=cfg&_rsc=abc",
    );

    // No request query → destination untouched.
    expect(preserveRedirectDestinationQuery("/dest?a=1", "")).toBe("/dest?a=1");
    expect(preserveRedirectDestinationQuery("/dest", "?")).toBe("/dest");

    // External destinations are returned untouched (no query leak across origins).
    expect(preserveRedirectDestinationQuery("https://example.com/x", "?_rsc=abc")).toBe(
      "https://example.com/x",
    );
    expect(preserveRedirectDestinationQuery("//evil.com", "?_rsc=abc")).toBe("//evil.com");

    // Preserves a hash fragment on the destination.
    expect(preserveRedirectDestinationQuery("/dest#frag", "?_rsc=abc")).toBe("/dest?_rsc=abc#frag");
  });

  it("config header matcher works with decoded percent-encoded paths", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    const headers = [{ source: "/api/(.*)", headers: [{ key: "X-Custom", value: "true" }] }];
    const reqCtx = {
      headers: new Headers(),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    };
    // Decoded path should match
    const decoded = normalizePath(decodeURIComponent("/%61pi/hello"));
    expect(decoded).toBe("/api/hello");
    const result = matchHeaders(decoded, headers, reqCtx);
    expect(result).toHaveLength(1);
    expect(result[0].key).toBe("X-Custom");

    // Raw encoded path must NOT match
    const rawResult = matchHeaders("/%61pi/hello", headers, reqCtx);
    expect(rawResult).toHaveLength(0);
  });

  it("config rewrite matcher works with decoded percent-encoded paths", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    const rewrites = [{ source: "/before-rewrite", destination: "/about" }];
    const reqCtx = {
      headers: new Headers(),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    };
    // Decoded path should match
    const decoded = normalizePath(decodeURIComponent("/%62efore-rewrite"));
    expect(decoded).toBe("/before-rewrite");
    const result = matchRewrite(decoded, rewrites, reqCtx);
    expect(result).toBe("/about");

    // Raw encoded path must NOT match
    const rawResult = matchRewrite("/%62efore-rewrite", rewrites, reqCtx);
    expect(rawResult).toBeNull();
  });

  it("double-encoded paths are decoded only once", async () => {
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");
    // %2561dmin → first decode → %61dmin (literal text, not /admin)
    const doubleEncoded = "/%2561dmin";
    const decoded = normalizePath(decodeURIComponent(doubleEncoded));
    // Should decode to /%61dmin, NOT to /admin
    expect(decoded).toBe("/%61dmin");
    expect(decoded).not.toBe("/admin");
  });
});

describe("double-encoded path handling in middleware", () => {
  it("double-encoded path /%2564ashboard does not match /dashboard middleware pattern", async () => {
    const { matchPattern, matchesMiddleware } =
      await import("../packages/vinext/src/server/middleware.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");

    // /%2564ashboard with a single decode becomes /%64ashboard (NOT /dashboard).
    // The pathname should be decoded exactly once at the entry point.
    const testPath = "/%2564ashboard";
    const decoded = decodeURIComponent(testPath); // Single decode
    const normalized = normalizePath(decoded);
    // After one decode, this is NOT /dashboard — it's /%64ashboard
    expect(normalized).toBe("/%64ashboard");
    // Middleware should NOT match /dashboard for this path
    expect(matchPattern(normalized, "/dashboard")).toBe(false);
    expect(matchesMiddleware(normalized, "/dashboard")).toBe(false);
  });

  it("double-encoded slash /foo/..%252fdashboard does not resolve to /dashboard", async () => {
    const { matchPattern } = await import("../packages/vinext/src/server/middleware.js");
    const { normalizePath } = await import("../packages/vinext/src/server/normalize-path.js");

    // /foo/..%252fdashboard with a single decode becomes /foo/..%2fdashboard.
    // normalizePath does NOT treat %2f as a path separator, so no traversal occurs.
    const testPath = "/foo/..%252fdashboard";
    const decoded = decodeURIComponent(testPath); // Single decode
    const normalized = normalizePath(decoded);
    // After one decode + normalize, this should NOT resolve to /dashboard
    expect(normalized).not.toBe("/dashboard");
    // The .. only traverses if followed by a real /, not an encoded %2f
    expect(matchPattern(normalized, "/dashboard")).toBe(false);
  });

  it("RSC route matching does not double-decode pathnames", async () => {
    // Verify the generated entry delegates route matching to the typed helper.
    const { generateRscEntry } = await import("../packages/vinext/src/entries/app-rsc-entry.js");
    const code = generateRscEntry("/tmp/app", [
      {
        pattern: "/dashboard",
        patternParts: ["dashboard"],
        isDynamic: false,
        params: [],
        pagePath: null,
        routePath: null,
        layouts: [],
        routeSegments: [],
        layoutTreePositions: [],
        templates: [],
        loadingPath: null,
        errorPath: null,
        layoutErrorPaths: [],
        notFoundPath: null,
        notFoundPaths: [],
        forbiddenPaths: [],
        forbiddenPath: null,
        unauthorizedPaths: [],
        unauthorizedPath: null,
        parallelSlots: [],
        siblingIntercepts: [],
      },
    ]);
    expect(code).toContain("createAppRscRouteMatcher as __createAppRscRouteMatcher");
    expect(code).toContain("return __routeMatcher.matchRoute(url);");

    const routeMatchingSource = await readFile(
      new URL("../packages/vinext/src/server/app-rsc-route-matching.ts", import.meta.url),
      "utf8",
    );
    // Verify it does NOT call decodeURIComponent (the comment mentions it but
    // should not have an actual call like `decodeURIComponent(...)`)
    expect(routeMatchingSource).not.toMatch(/\bdecodeURIComponent\s*\(/);
  });

  it("App Router middleware receives a Request with the decoded pathname (not raw URL)", async () => {
    const { applyAppMiddleware } = await import("../packages/vinext/src/server/app-middleware.js");
    let capturedUrl: string | undefined;
    const module = {
      default: (req: Request) => {
        capturedUrl = req.url;
        return new Response(null, {
          headers: { "x-middleware-next": "1" },
        });
      },
    };

    const result = await applyAppMiddleware({
      cleanPathname: "/%64ashboard",
      context: { headers: null, requestHeaders: null, status: null },
      isProxy: false,
      module,
      request: new Request("http://localhost:3000/%2564ashboard"),
    });

    expect(result.kind).toBe("continue");
    expect(capturedUrl).toBeDefined();
    const mwPathname = new URL(capturedUrl!).pathname;
    expect(mwPathname).toBe("/%64ashboard");
    expect(mwPathname).not.toBe("/%2564ashboard");
  });

  it("App Router middleware preserves status from NextResponse.next()", async () => {
    const { applyAppMiddleware } = await import("../packages/vinext/src/server/app-middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const context = { headers: null, requestHeaders: null, status: null };

    const result = await applyAppMiddleware({
      cleanPathname: "/",
      context,
      isProxy: false,
      module: {
        default: () => NextResponse.next({ status: 404 }),
      },
      request: new Request("http://localhost:3000/"),
    });

    expect(result.kind).toBe("continue");
    expect(context.status).toBe(404);
  });

  it("App Router middleware does not see internal Flight headers", async () => {
    // Next.js strips FLIGHT_HEADERS before creating the middleware request:
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/web/adapter.ts
    const { applyAppMiddleware } = await import("../packages/vinext/src/server/app-middleware.js");
    let capturedHeaders: Headers | undefined;
    const module = {
      default: (req: Request) => {
        capturedHeaders = new Headers(req.headers);
        return new Response(null, {
          headers: { "x-middleware-next": "1" },
        });
      },
    };

    const result = await applyAppMiddleware({
      cleanPathname: "/dashboard",
      context: { headers: null, requestHeaders: null, status: null },
      isProxy: false,
      module,
      request: new Request("http://localhost:3000/dashboard", {
        headers: {
          rsc: "1",
          "next-router-state-tree": "%5B%5D",
          "next-router-prefetch": "1",
          "next-router-segment-prefetch": "/dashboard",
          "next-hmr-refresh": "1",
          "x-user-visible": "keep",
        },
      }),
    });

    expect(result.kind).toBe("continue");
    expect(capturedHeaders?.get("rsc")).toBeNull();
    expect(capturedHeaders?.get("next-router-state-tree")).toBeNull();
    expect(capturedHeaders?.get("next-router-prefetch")).toBeNull();
    expect(capturedHeaders?.get("next-router-segment-prefetch")).toBeNull();
    expect(capturedHeaders?.get("next-hmr-refresh")).toBeNull();
    expect(capturedHeaders?.get("x-user-visible")).toBe("keep");
  });

  it("App Router Flight header stripping does not lock the original request body", async () => {
    const { applyAppMiddleware } = await import("../packages/vinext/src/server/app-middleware.js");
    const request = new Request("http://localhost:3000/actions", {
      body: "action-payload",
      headers: {
        rsc: "1",
        "next-router-state-tree": "%5B%5D",
      },
      method: "POST",
    });
    const module = {
      default: () =>
        new Response(null, {
          headers: { "x-middleware-next": "1" },
        }),
    };

    const result = await applyAppMiddleware({
      cleanPathname: "/actions",
      context: { headers: null, requestHeaders: null, status: null },
      isProxy: false,
      module,
      request,
    });

    expect(result.kind).toBe("continue");
    expect(request.body?.locked).toBe(false);
    expect(await request.text()).toBe("action-payload");
  });

  it("App Router middleware can read the body without consuming the downstream request", async () => {
    const { applyAppMiddleware } = await import("../packages/vinext/src/server/app-middleware.js");
    const request = new Request("http://localhost:3000/api/echo", {
      body: JSON.stringify({ message: "hello" }),
      headers: { "content-type": "application/json" },
      method: "POST",
    });
    let middlewareBody: unknown;
    const module = {
      default: async (req: Request) => {
        middlewareBody = await req.json();
        return new Response(null, {
          headers: { "x-middleware-next": "1" },
        });
      },
    };

    const result = await applyAppMiddleware({
      cleanPathname: "/api/echo",
      context: { headers: null, requestHeaders: null, status: null },
      isProxy: false,
      module,
      request,
    });

    expect(result.kind).toBe("continue");
    expect(middlewareBody).toEqual({ message: "hello" });
    expect(request.bodyUsed).toBe(false);
    await expect(request.json()).resolves.toEqual({ message: "hello" });
  });

  it("external middleware rewrite proxy strips upstream x-middleware headers without middleware context", async () => {
    const { proxyExternalMiddlewareRewrite } =
      await import("../packages/vinext/src/server/app-middleware.js");
    const http = await import("node:http");
    const server = http.createServer((_req, res) => {
      res.writeHead(200, {
        "content-type": "text/plain",
        "x-middleware-rewrite": "/internal",
        "x-middleware-next": "1",
        "x-visible": "keep",
      });
      res.end("proxied");
    });

    await new Promise<void>((resolve) => server.listen(0, resolve));
    try {
      const address = server.address();
      expect(address && typeof address === "object").toBe(true);
      const port = address && typeof address === "object" ? address.port : 0;
      const response = await proxyExternalMiddlewareRewrite(
        new Request("http://localhost:3000/source"),
        `http://127.0.0.1:${port}/target`,
        { headers: null, requestHeaders: null, status: null },
      );

      expect(response.status).toBe(200);
      expect(await response.text()).toBe("proxied");
      expect(response.headers.get("x-middleware-rewrite")).toBeNull();
      expect(response.headers.get("x-middleware-next")).toBeNull();
      expect(response.headers.get("x-visible")).toBe("keep");
    } finally {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
  });

  it("external middleware rewrite proxy preserves credentials when applying partial request overrides", async () => {
    const { proxyExternalMiddlewareRewrite } =
      await import("../packages/vinext/src/server/app-middleware.js");
    const http = await import("node:http");
    let capturedHeaders: Record<string, string | string[] | undefined> = {};
    const server = http.createServer((req, res) => {
      capturedHeaders = req.headers;
      res.writeHead(200, { "content-type": "text/plain" });
      res.end("proxied");
    });

    await new Promise<void>((resolve) => server.listen(0, resolve));
    try {
      const address = server.address();
      expect(address && typeof address === "object").toBe(true);
      const port = address && typeof address === "object" ? address.port : 0;
      const response = await proxyExternalMiddlewareRewrite(
        new Request("http://localhost:3000/source", {
          headers: {
            authorization: "Bearer secret",
            cookie: "session=abc",
            "x-keep": "original",
          },
        }),
        `http://127.0.0.1:${port}/target`,
        {
          headers: null,
          requestHeaders: new Headers({
            "x-middleware-override-headers": "x-added",
            "x-middleware-request-x-added": "1",
          }),
          status: null,
        },
      );

      expect(response.status).toBe(200);
      expect(capturedHeaders.authorization).toBe("Bearer secret");
      expect(capturedHeaders.cookie).toBe("session=abc");
      expect(capturedHeaders["x-added"]).toBe("1");
      expect(capturedHeaders["x-keep"]).toBeUndefined();
    } finally {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
  });

  it("Pages Router runMiddleware passes decoded pathname to middleware function", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    // Create a mock Vite server that returns a middleware module
    let capturedUrl: string | undefined;
    const mockRunner = {
      import: async () => ({
        default: (req: Request) => {
          capturedUrl = req.url;
          return new Response("OK", {
            headers: { "x-middleware-next": "1" },
          });
        },
        config: { matcher: "/:path*" },
      }),
    };

    // Send a double-encoded path — after single decode, it should be /%64ashboard
    const testUrl = "http://localhost:3000/%2564ashboard";
    const request = new Request(testUrl);
    await runMiddleware(mockRunner as any, "/tmp/middleware.ts", request);

    // Middleware should have received the decoded+normalized URL
    expect(capturedUrl).toBeDefined();
    const mwPathname = new URL(capturedUrl!).pathname;
    // After single decode: %25 → %, so /%2564 → /%64
    expect(mwPathname).toBe("/%64ashboard");
    // It must NOT be the raw /%2564ashboard
    expect(mwPathname).not.toBe("/%2564ashboard");
    // It must NOT be double-decoded to /dashboard
    expect(mwPathname).not.toBe("/dashboard");
  });

  it("runMiddleware accepts named proxy export", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");

    const mockRunner = {
      import: async () => ({
        proxy: () => {
          const response = new Response(null, { status: 307 });
          response.headers.set("location", "/login");
          return response;
        },
        config: { matcher: ["/protected"] },
      }),
    };

    const request = new Request("http://localhost/protected");
    const result = await runMiddleware(mockRunner as any, "/tmp/proxy.js", request);

    expect(result.continue).toBe(false);
    expect(result.redirectUrl).toContain("/login");
    expect(result.redirectStatus).toBe(307);
  });

  it("runMiddleware bubbles up waitUntil promises in result", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");

    let capturedPromise: Promise<unknown> | null = null;
    const mockRunner = {
      import: async () => ({
        middleware: (_req: Request, event: { waitUntil: (p: Promise<unknown>) => void }) => {
          const p = Promise.resolve("background-work");
          capturedPromise = p;
          event.waitUntil(p);
          return Response.redirect("http://localhost/login", 307);
        },
        config: { matcher: ["/protected"] },
      }),
    };

    const request = new Request("http://localhost/protected");
    const result = await runMiddleware(mockRunner as any, "/tmp/middleware.ts", request);

    // The most critical behavior: waitUntil promises must appear in the result
    // so the runtime (e.g. Cloudflare Workers ctx.waitUntil) can keep them alive.
    expect(result.continue).toBe(false);
    expect(result.redirectUrl).toBeDefined();
    expect(result.waitUntilPromises).toBeDefined();
    expect(result.waitUntilPromises!.length).toBe(1);
    expect(result.waitUntilPromises![0]).toBe(capturedPromise);
  });

  it("runMiddleware bubbles up waitUntil promises on continue: true path", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    let capturedPromise: Promise<unknown> | null = null;
    const mockRunner = {
      import: async () => ({
        middleware: (_req: Request, event: { waitUntil: (p: Promise<unknown>) => void }) => {
          const p = Promise.resolve("analytics");
          capturedPromise = p;
          event.waitUntil(p);
          return NextResponse.next();
        },
        config: { matcher: ["/dashboard"] },
      }),
    };

    const request = new Request("http://localhost/dashboard");
    const result = await runMiddleware(mockRunner as any, "/tmp/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(result.waitUntilPromises).toBeDefined();
    expect(result.waitUntilPromises!.length).toBe(1);
    expect(result.waitUntilPromises![0]).toBe(capturedPromise);
  });

  it("app-router-entry.ts does not double-decode (delegates to RSC handler)", async () => {
    // Verify the Cloudflare Worker entry does not decode the pathname itself,
    // leaving that responsibility to the RSC handler.
    const fs = await import("node:fs");
    const entryCode = fs.readFileSync(
      new URL("../packages/vinext/src/server/app-router-entry.ts", import.meta.url),
      "utf-8",
    );
    // The entry should validate encoding but NOT normalize+reconstruct the request
    // (the RSC handler is the single decode point)
    expect(entryCode).not.toMatch(/normalizedRequest\s*=\s*new Request\(normalizedUrl/);
    // It should still validate malformed encoding (return 400)
    expect(entryCode).toMatch(
      /decodeURIComponent\(\s*(?:raw)?[pP]athname|decodeURIComponent\(url\.pathname\)/,
    );
    // The delegate call should pass the original request object through,
    // without reconstructing a normalized Request before delegation.
    expect(entryCode).toMatch(/rscHandler\(request(?:,\s*ctx)?\)/);
  });
});

// ---------------------------------------------------------------------------
// NextFetchEvent — middleware receives event with waitUntil support

describe("NextFetchEvent passed to middleware", () => {
  it("runMiddleware passes NextFetchEvent as second argument", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    // Middleware that accesses event.waitUntil — will throw if event is undefined
    let receivedEvent: any;
    const mockRunner = {
      import: async () => ({
        middleware: (_req: any, event: any) => {
          receivedEvent = event;
          event.waitUntil(Promise.resolve("done"));
          return new Response(null, {
            headers: { "x-middleware-next": "1" },
          });
        },
        config: { matcher: ["/test"] },
      }),
    };

    const request = new Request("http://localhost:3000/test");
    const result = await runMiddleware(mockRunner as any, "/tmp/middleware.ts", request);

    expect(result.continue).toBe(true);
    expect(receivedEvent).toBeDefined();
    expect(typeof receivedEvent.waitUntil).toBe("function");
    expect(receivedEvent.sourcePage).toBe("/test");
  });

  it("waitUntil promises are drained after middleware runs", async () => {
    const { runMiddleware } = await import("../packages/vinext/src/server/middleware.js");
    let sideEffectRan = false;
    const mockRunner = {
      import: async () => ({
        middleware: (_req: any, event: any) => {
          event.waitUntil(
            Promise.resolve().then(() => {
              sideEffectRan = true;
            }),
          );
          return new Response(null, {
            headers: { "x-middleware-next": "1" },
          });
        },
        config: { matcher: ["/drain"] },
      }),
    };

    const request = new Request("http://localhost:3000/drain");
    await runMiddleware(mockRunner as any, "/tmp/middleware.ts", request);

    // The waitUntil promise should have been resolved.
    // Flush the microtask queue so Promise.resolve().then(...) callbacks run.
    await new Promise((r) => queueMicrotask(r as () => void));
    expect(sideEffectRan).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// RequestCookies comprehensive tests

describe("RequestCookies API", () => {
  it("get() returns cookie by name", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=abc123; session=xyz" });
    const cookies = new RequestCookies(headers);

    const token = cookies.get("token");
    expect(token).toEqual({ name: "token", value: "abc123" });

    const session = cookies.get("session");
    expect(session).toEqual({ name: "session", value: "xyz" });
  });

  it("get() returns undefined for missing cookie", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=abc123" });
    const cookies = new RequestCookies(headers);

    expect(cookies.get("missing")).toBeUndefined();
  });

  it("getAll() returns all cookies", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2; c=3" });
    const cookies = new RequestCookies(headers);

    const all = cookies.getAll();
    expect(all).toHaveLength(3);
    expect(all).toContainEqual({ name: "a", value: "1" });
    expect(all).toContainEqual({ name: "b", value: "2" });
    expect(all).toContainEqual({ name: "c", value: "3" });
  });

  it("getAll(name) filters by cookie name", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; a=2; b=3" });
    const cookies = new RequestCookies(headers);

    expect(cookies.get("a")).toEqual({ name: "a", value: "2" });
    expect(cookies.getAll("a")).toEqual([{ name: "a", value: "2" }]);
    expect(cookies.getAll()).toEqual([
      { name: "a", value: "2" },
      { name: "b", value: "3" },
    ]);
  });

  it("getAll({ name }) filters by cookie name and missing names return []", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; a=2; token=abc=def" });
    const cookies = new RequestCookies(headers);

    expect(cookies.getAll({ name: "a", value: "ignored" })).toEqual([{ name: "a", value: "2" }]);
    expect(cookies.getAll("missing")).toEqual([]);
    expect(cookies.getAll({ name: "missing", value: "" })).toEqual([]);
    expect(cookies.get("token")).toEqual({ name: "token", value: "abc=def" });
  });

  it("parses encoded values, skips malformed values, and supports bare tokens", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=abc%3D123; bad=%E0%A4%A; flag; ok=yes" });
    const cookies = new RequestCookies(headers);

    expect(cookies.get("token")).toEqual({ name: "token", value: "abc=123" });
    expect(cookies.get("bad")).toBeUndefined();
    expect(cookies.get("flag")).toEqual({ name: "flag", value: "true" });
    expect(cookies.getAll()).toEqual([
      { name: "token", value: "abc=123" },
      { name: "flag", value: "true" },
      { name: "ok", value: "yes" },
    ]);
  });

  it("preserves explicit empty values", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "empty=; flag" });
    const cookies = new RequestCookies(headers);

    expect(cookies.get("empty")).toEqual({ name: "empty", value: "" });
    expect(cookies.get("flag")).toEqual({ name: "flag", value: "true" });
    expect(cookies.getAll()).toEqual([
      { name: "empty", value: "" },
      { name: "flag", value: "true" },
    ]);
  });

  it("preserves whitespace exactly like the Next.js parser", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a= 1 ; a =2" });
    const cookies = new RequestCookies(headers);

    expect(cookies.get("a")).toEqual({ name: "a", value: " 1 " });
    expect(cookies.get("a ")).toEqual({ name: "a ", value: "2" });
    expect(cookies.getAll()).toEqual([
      { name: "a", value: " 1 " },
      { name: "a ", value: "2" },
    ]);
  });

  it("has() checks cookie existence", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=abc" });
    const cookies = new RequestCookies(headers);

    expect(cookies.has("token")).toBe(true);
    expect(cookies.has("missing")).toBe(false);
  });

  it("iterator yields [name, entry] pairs", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "x=1; y=2" });
    const cookies = new RequestCookies(headers);

    const entries = [...cookies];
    expect(entries).toHaveLength(2);
    expect(entries[0][0]).toBe("x");
    expect(entries[0][1]).toEqual({ name: "x", value: "1" });
    expect(entries[1][0]).toBe("y");
    expect(entries[1][1]).toEqual({ name: "y", value: "2" });
  });

  it("handles empty cookie header", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    expect(cookies.getAll()).toHaveLength(0);
    expect(cookies.get("any")).toBeUndefined();
    expect(cookies.has("any")).toBe(false);
  });

  it("handles cookies with = in value", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "data=base64=encoded=value" });
    const cookies = new RequestCookies(headers);

    const data = cookies.get("data");
    expect(data).toBeDefined();
    expect(data!.value).toBe("base64=encoded=value");
  });

  it("set() adds a cookie and updates the Cookie header", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1" });
    const cookies = new RequestCookies(headers);

    cookies.set("b", "2");

    expect(cookies.get("b")).toEqual({ name: "b", value: "2" });
    // The underlying Cookie header should be updated
    expect(headers.get("cookie")).toContain("b=2");
    // Original cookie should still be there
    expect(cookies.get("a")).toEqual({ name: "a", value: "1" });
  });

  it("set() overwrites an existing cookie value", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=old" });
    const cookies = new RequestCookies(headers);

    cookies.set("token", "new");

    expect(cookies.get("token")).toEqual({ name: "token", value: "new" });
    expect(headers.get("cookie")).toContain("token=new");
    expect(headers.get("cookie")).not.toContain("token=old");
  });

  it("set() accepts an object with name and value", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    cookies.set({ name: "session", value: "abc" });

    expect(cookies.get("session")).toEqual({ name: "session", value: "abc" });
  });

  it("set() returns this for chaining", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    const result = cookies.set("a", "1").set("b", "2");

    expect(result).toBe(cookies);
    expect(cookies.get("a")).toEqual({ name: "a", value: "1" });
    expect(cookies.get("b")).toEqual({ name: "b", value: "2" });
  });

  it("set() rejects invalid cookie names", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    expect(() => cookies.set("foo=bar; Path=/", "val")).toThrow("Invalid cookie name");
    expect(() => cookies.set("foo; HttpOnly", "val")).toThrow("Invalid cookie name");
    expect(() => cookies.set("foo\r\nCookie: evil=1", "val")).toThrow("Invalid cookie name");
    expect(() => cookies.set("", "val")).toThrow("Invalid cookie name");
  });

  it("set({ name, value }) rejects invalid cookie names", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    expect(() => cookies.set({ name: "foo=bar", value: "val" })).toThrow("Invalid cookie name");
  });

  it("delete() removes a cookie from the Cookie header", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2; c=3" });
    const cookies = new RequestCookies(headers);

    cookies.delete("b");

    expect(cookies.has("b")).toBe(false);
    expect(cookies.get("b")).toBeUndefined();
    // Other cookies remain
    expect(cookies.get("a")).toEqual({ name: "a", value: "1" });
    expect(cookies.get("c")).toEqual({ name: "c", value: "3" });
  });

  it("delete() returns true when cookie existed, false otherwise", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2" });
    const cookies = new RequestCookies(headers);

    expect(cookies.delete("a")).toBe(true);
    expect(cookies.delete("nonexistent")).toBe(false);
  });

  it("delete() accepts an array of names", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2; c=3" });
    const cookies = new RequestCookies(headers);

    const results = cookies.delete(["a", "missing", "c"]);

    expect(results).toEqual([true, false, true]);
    expect(cookies.has("a")).toBe(false);
    expect(cookies.has("c")).toBe(false);
    expect(cookies.get("b")).toEqual({ name: "b", value: "2" });
  });

  it("delete() rejects invalid cookie names", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2" });
    const cookies = new RequestCookies(headers);

    expect(() => cookies.delete("foo=bar")).toThrow("Invalid cookie name");
    expect(() => cookies.delete(["a", "foo;bar"])).toThrow("Invalid cookie name");
  });

  it("delete() is a no-op for missing cookies", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1" });
    const cookies = new RequestCookies(headers);

    cookies.delete("nonexistent");

    expect(cookies.get("a")).toEqual({ name: "a", value: "1" });
  });

  it("size returns the number of cookies", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2; c=3" });
    const cookies = new RequestCookies(headers);

    expect(cookies.size).toBe(3);
  });

  it("size is 0 for empty cookie header", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    expect(cookies.size).toBe(0);
  });

  it("toString() serializes cookies back to a cookie header string", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2" });
    const cookies = new RequestCookies(headers);

    const str = cookies.toString();
    expect(str).toContain("a=1");
    expect(str).toContain("b=2");
  });

  it("set() round-trips values with special characters", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new RequestCookies(headers);

    cookies.set("data", "hello;world=foo");

    expect(cookies.get("data")).toEqual({ name: "data", value: "hello;world=foo" });
    // Header should be encoded
    expect(headers.get("cookie")).toBe("data=hello%3Bworld%3Dfoo");
  });

  it("set() preserves existing percent-encoded cookies", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "token=100%25done; sid=abc" });
    const cookies = new RequestCookies(headers);

    cookies.set("new", "value");

    expect(cookies.get("token")).toEqual({ name: "token", value: "100%done" });
    expect(cookies.get("new")).toEqual({ name: "new", value: "value" });
  });

  it("delete() after set() removes the cookie", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1" });
    const cookies = new RequestCookies(headers);

    cookies.set("b", "2");
    expect(cookies.get("b")).toEqual({ name: "b", value: "2" });

    cookies.delete("b");
    expect(cookies.has("b")).toBe(false);
    expect(cookies.get("a")).toEqual({ name: "a", value: "1" });
  });

  it("clear() removes all cookies", async () => {
    const { RequestCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers({ cookie: "a=1; b=2; c=3" });
    const cookies = new RequestCookies(headers);

    cookies.clear();

    expect(cookies.size).toBe(0);
    expect(cookies.getAll()).toHaveLength(0);
    expect(headers.get("cookie")).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// ResponseCookies comprehensive tests

describe("ResponseCookies API", () => {
  it("set() creates Set-Cookie header with options", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "abc123", {
      path: "/",
      httpOnly: true,
      secure: true,
      sameSite: "Lax",
      maxAge: 3600,
    });

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("token=abc123");
    expect(setCookie[0]).toContain("Path=/");
    expect(setCookie[0]).toContain("HttpOnly");
    expect(setCookie[0]).toContain("Secure");
    expect(setCookie[0]).toContain("SameSite=Lax");
    expect(setCookie[0]).toContain("Max-Age=3600");
  });

  it("set() multiple cookies appends multiple Set-Cookie headers", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");
    cookies.set("b", "2");

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(2);
    expect(setCookie[0]).toContain("a=1");
    expect(setCookie[1]).toContain("b=2");
  });

  it("get() retrieves a cookie from Set-Cookie headers", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "xyz");
    const result = cookies.get("token");
    expect(result).toEqual({ name: "token", value: "xyz" });
  });

  it("getAll() returns all set cookies", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");
    cookies.set("b", "2");
    cookies.set("c", "3");

    const all = cookies.getAll();
    expect(all).toHaveLength(3);
    expect(all).toContainEqual({ name: "a", value: "1" });
    expect(all).toContainEqual({ name: "b", value: "2" });
    expect(all).toContainEqual({ name: "c", value: "3" });
  });

  it("has() checks whether a response cookie exists", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("session", "abc");

    expect(cookies.has("session")).toBe(true);
    expect(cookies.has("missing")).toBe(false);
  });

  it("delete() expires the cookie (matching edge-runtime)", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.delete("session");

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("session=");
    expect(setCookie[0]).toContain("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
    expect(setCookie[0]).toContain("Path=/");
  });

  it("set() URL-encodes cookie values", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("data", "hello world; special=chars");

    const setCookie = headers.getSetCookie();
    expect(setCookie[0]).toContain("data=hello%20world%3B%20special%3Dchars");

    // get() should decode it back
    const result = cookies.get("data");
    expect(result?.value).toBe("hello world; special=chars");
  });

  it("iterator yields [name, entry] pairs", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("x", "1");
    cookies.set("y", "2");

    const entries = [...cookies];
    expect(entries).toHaveLength(2);
    expect(entries[0][0]).toBe("x");
    expect(entries[0][1]).toEqual({ name: "x", value: "1" });
    expect(entries[1][0]).toBe("y");
    expect(entries[1][1]).toEqual({ name: "y", value: "2" });
  });

  it("set() with domain option includes Domain directive", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "abc", { domain: ".example.com" });

    const setCookie = headers.getSetCookie();
    expect(setCookie[0]).toContain("Domain=.example.com");
  });

  it("set() with expires option includes Expires directive", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    const expires = new Date("2030-01-01T00:00:00Z");
    cookies.set("token", "abc", { expires });

    const setCookie = headers.getSetCookie();
    expect(setCookie[0]).toContain("Expires=");
    expect(setCookie[0]).toContain("2030");
  });
});

// ---------------------------------------------------------------------------
// ResponseCookies correctness (Next.js parity)
// Ported from @edge-runtime/cookies: packages/cookies/src/response-cookies.ts
// https://github.com/vercel/edge-runtime/blob/main/packages/cookies/src/response-cookies.ts

describe("ResponseCookies correctness", () => {
  // Bug 1: set() same cookie name twice should replace, not duplicate
  it("set() same cookie name twice replaces the header (no duplicates)", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("session", "old-value");
    cookies.set("session", "new-value");

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("session=new-value");
  });

  it("get() returns the latest value after set() overwrites", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "first");
    cookies.set("token", "second");

    const result = cookies.get("token");
    expect(result?.value).toBe("second");
  });

  it("set() replaces only the matching cookie, preserves others", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");
    cookies.set("b", "2");
    cookies.set("a", "3");

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(2);
    // 'a' was replaced, 'b' stays
    const aHeader = setCookie.find((h: string) => h.startsWith("a="));
    const bHeader = setCookie.find((h: string) => h.startsWith("b="));
    expect(aHeader).toContain("a=3");
    expect(bHeader).toContain("b=2");
  });

  // Bug 2: set() should accept object form
  it("set() accepts object form { name, value, ... }", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set({ name: "token", value: "abc", httpOnly: true, path: "/api" });

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("token=abc");
    expect(setCookie[0]).toContain("HttpOnly");
    expect(setCookie[0]).toContain("Path=/api");

    const result = cookies.get("token");
    expect(result?.value).toBe("abc");
  });

  it("set() object form replaces existing cookie of same name", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "old");
    cookies.set({ name: "token", value: "new", secure: true });

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("token=new");
    expect(setCookie[0]).toContain("Secure");
  });

  // Bug 3: getAll() should accept optional name filter
  it("getAll(name) filters by cookie name", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");
    cookies.set("b", "2");
    cookies.set("c", "3");

    const filtered = cookies.getAll("b");
    expect(filtered).toHaveLength(1);
    expect(filtered[0]).toEqual({ name: "b", value: "2" });
  });

  it("getAll({ name }) filters by cookie name (object form)", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("x", "10");
    cookies.set("y", "20");

    const filtered = cookies.getAll({ name: "x" });
    expect(filtered).toHaveLength(1);
    expect(filtered[0]).toEqual({ name: "x", value: "10" });
  });

  it("getAll() with no args returns all cookies", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");
    cookies.set("b", "2");

    const all = cookies.getAll();
    expect(all).toHaveLength(2);
  });

  it("getAll(name) returns empty array for non-existent cookie", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("a", "1");

    const filtered = cookies.getAll("missing");
    expect(filtered).toHaveLength(0);
  });

  // Bug 4: delete() should accept object and array forms
  it("delete({ name, path, domain }) sets correct expiry cookie with path and domain", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.delete({ name: "session", path: "/app", domain: ".example.com" });

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("session=");
    expect(setCookie[0]).toContain("Path=/app");
    expect(setCookie[0]).toContain("Domain=.example.com");
    // Should expire the cookie
    expect(setCookie[0]).toContain("Expires=");
  });

  it("delete() forwards httpOnly, secure, and sameSite attributes", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.delete({
      name: "session",
      path: "/app",
      httpOnly: true,
      secure: true,
      sameSite: "Lax",
    });

    const setCookie = headers.getSetCookie();
    expect(setCookie).toHaveLength(1);
    expect(setCookie[0]).toContain("HttpOnly");
    expect(setCookie[0]).toContain("Secure");
    expect(setCookie[0]).toContain("SameSite=Lax");
    expect(setCookie[0]).toContain("Path=/app");
    expect(setCookie[0]).toContain("Expires=");
  });

  it("delete() replaces existing cookie's Set-Cookie header", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("session", "abc123", { path: "/app" });
    cookies.delete("session");

    // Should have exactly one Set-Cookie for 'session' (the deletion one)
    const setCookie = headers.getSetCookie();
    const sessionHeaders = setCookie.filter((h: string) => h.startsWith("session="));
    expect(sessionHeaders).toHaveLength(1);
    expect(sessionHeaders[0]).toContain("Expires=");
  });

  // Constructor should parse existing Set-Cookie headers
  it("constructor parses existing Set-Cookie headers", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    headers.append("Set-Cookie", "existing=value; Path=/");

    const cookies = new ResponseCookies(headers);
    const result = cookies.get("existing");
    expect(result?.value).toBe("value");
  });

  // has() should work with internal map
  it("has() returns false after delete()", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("session", "abc");
    expect(cookies.has("session")).toBe(true);

    cookies.delete("session");
    // After delete, a new expired cookie replaces the old one.
    // The cookie still "exists" in the map (with empty value and past expiry),
    // matching edge-runtime behavior where delete() calls set().
    // has() returns true because the entry exists in the map.
    expect(cookies.has("session")).toBe(true);
  });

  // get() with object form (matching edge-runtime)
  it("get() accepts object { name } form", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);

    cookies.set("token", "abc");
    const result = cookies.get({ name: "token" });
    expect(result?.value).toBe("abc");
  });
});

// ---------------------------------------------------------------------------
// Cookie name/value injection prevention (RFC 6265)

describe("cookie name validation", () => {
  it("RequestCookies.set() rejects names with = (injection)", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.set("foo=bar; Path=/; Domain=evil.com", "val")).toThrow(
        "Invalid cookie name",
      );
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.set() rejects names with semicolons", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.set("foo; HttpOnly", "val")).toThrow("Invalid cookie name");
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.set() rejects names with newlines", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.set("foo\r\nSet-Cookie: evil=1", "val")).toThrow("Invalid cookie name");
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.set() rejects empty names", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.set("", "val")).toThrow("Invalid cookie name");
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.set() accepts valid cookie names", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      // These should not throw
      jar.set("valid-name", "value");
      jar.set("__Host-token", "value");
      jar.set("session_id", "value");
      jar.set("CSRF.Token", "value");
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.delete() rejects invalid names", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.delete("foo=bar")).toThrow("Invalid cookie name");
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("RequestCookies.set() rejects path with semicolons", async () => {
    const headersModule = await import("../packages/vinext/src/shims/headers.js");
    headersModule.setHeadersContext({ headers: new Headers(), cookies: new Map() });
    const previousPhase = headersModule.setHeadersAccessPhase("route-handler");
    try {
      const jar = await headersModule.cookies();
      expect(() => jar.set("name", "val", { path: "/; Domain=evil.com" })).toThrow(
        "Invalid cookie Path",
      );
    } finally {
      headersModule.setHeadersAccessPhase(previousPhase);
    }
  });

  it("ResponseCookies.set() rejects names with = (injection)", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);
    expect(() => cookies.set("foo=bar; Path=/", "val")).toThrow("Invalid cookie name");
  });

  it("ResponseCookies.set() rejects domain with control chars", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);
    expect(() => cookies.set("name", "val", { domain: "evil.com\r\nSet-Cookie: hack=1" })).toThrow(
      "Invalid cookie Domain",
    );
  });

  it("ResponseCookies.set() accepts valid cookie names and options", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);
    // These should not throw
    cookies.set("valid-name", "value", { path: "/", domain: ".example.com" });
    cookies.set("__Secure-token", "abc", { secure: true, httpOnly: true });
  });
});

// ---------------------------------------------------------------------------
// NextRequest API tests

describe("NextRequest API", () => {
  it("throws canonical 'Please use only absolute URLs' error for relative URL input", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    // Matches Next.js's documented behaviour — middleware tests assert on this
    // exact message via the middleware-relative-urls docs link.
    expect(() => new NextRequest("/foo")).toThrow(/Please use only absolute URLs/);
    expect(() => new NextRequest("/urls-b")).toThrow(
      /URL is malformed "\/urls-b"\. Please use only absolute URLs/,
    );
  });

  it("cookies reads request cookies", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/test", {
      headers: { cookie: "session=abc; theme=dark" },
    });

    expect(req.cookies.get("session")).toEqual({ name: "session", value: "abc" });
    expect(req.cookies.get("theme")).toEqual({ name: "theme", value: "dark" });
    expect(req.cookies.has("session")).toBe(true);
    expect(req.cookies.has("missing")).toBe(false);
  });

  it("nextUrl provides URL properties", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost:3000/api/test?key=value#hash");

    expect(req.nextUrl.pathname).toBe("/api/test");
    expect(req.nextUrl.search).toBe("?key=value");
    expect(req.nextUrl.searchParams.get("key")).toBe("value");
    expect(req.nextUrl.host).toBe("localhost:3000");
    expect(req.nextUrl.hostname).toBe("localhost");
    expect(req.nextUrl.protocol).toBe("http:");
    expect(req.nextUrl.hash).toBe("#hash");
  });

  it("nextUrl.clone() creates independent copy", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/test");
    const cloned = req.nextUrl.clone();

    cloned.pathname = "/other";
    expect(req.nextUrl.pathname).toBe("/test");
    expect(cloned.pathname).toBe("/other");
  });

  it("nextUrl supports all URL setters (port, host, hostname, protocol, href)", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://example.com:8080/path?q=1#hash");

    // port setter
    url.port = "9090";
    expect(url.port).toBe("9090");

    // hostname setter
    url.hostname = "other.com";
    expect(url.hostname).toBe("other.com");

    // host setter (hostname + port)
    url.host = "third.com:3000";
    expect(url.host).toBe("third.com:3000");
    expect(url.hostname).toBe("third.com");
    expect(url.port).toBe("3000");

    // protocol setter
    url.protocol = "https:";
    expect(url.protocol).toBe("https:");

    // href setter
    url.href = "http://new.com/new-path";
    expect(url.href).toBe("http://new.com/new-path");
    expect(url.hostname).toBe("new.com");
    expect(url.pathname).toBe("/new-path");
  });

  it("nextUrl.clone() setters work for next-intl compatibility", async () => {
    // next-intl's getAlternateLinksHeaderValue does: cloned.port = ""; cloned.host = h
    // This test ensures those setter operations work without throwing.
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://example.com:8080/en");
    const cloned = req.nextUrl.clone();

    // Should not throw — next-intl strips port when x-forwarded-host is present
    cloned.port = "";
    expect(cloned.port).toBe("");

    // Should not throw — next-intl sets host from x-forwarded-host header
    cloned.host = "example.com";
    expect(cloned.hostname).toBe("example.com");

    // Original should be unaffected
    expect(req.nextUrl.port).toBe("8080");
  });

  it("ip reads x-forwarded-for header", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/", {
      headers: { "x-forwarded-for": "1.2.3.4, 5.6.7.8" },
    });
    expect(req.ip).toBe("1.2.3.4");
  });

  it("ip returns undefined when no header", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/");
    expect(req.ip).toBeUndefined();
  });

  it("geo reads Cloudflare headers", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/", {
      headers: {
        "cf-ipcountry": "US",
        "cf-ipcity": "San Francisco",
      },
    });
    expect(req.geo?.country).toBe("US");
    expect(req.geo?.city).toBe("San Francisco");
  });

  it("geo returns undefined when no geo headers", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/");
    expect(req.geo).toBeUndefined();
  });

  it("nextUrl.buildId returns process.env.__VINEXT_BUILD_ID when set", async () => {
    const original = process.env.__VINEXT_BUILD_ID;
    try {
      process.env.__VINEXT_BUILD_ID = "test-build-123";
      const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
      const req = new NextRequest("http://localhost/");
      expect(req.nextUrl.buildId).toBe("test-build-123");
    } finally {
      if (original === undefined) {
        delete process.env.__VINEXT_BUILD_ID;
      } else {
        process.env.__VINEXT_BUILD_ID = original;
      }
    }
  });

  it("nextUrl.buildId returns undefined when __VINEXT_BUILD_ID is not set", async () => {
    const original = process.env.__VINEXT_BUILD_ID;
    try {
      delete process.env.__VINEXT_BUILD_ID;
      const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
      const req = new NextRequest("http://localhost/");
      expect(req.nextUrl.buildId).toBeUndefined();
    } finally {
      if (original !== undefined) {
        process.env.__VINEXT_BUILD_ID = original;
      }
    }
  });

  it("buildId pass-through on NextRequest delegates to nextUrl.buildId", async () => {
    const original = process.env.__VINEXT_BUILD_ID;
    try {
      process.env.__VINEXT_BUILD_ID = "test-build-456";
      const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
      const req = new NextRequest("http://localhost/");
      expect(req.buildId).toBe(req.nextUrl.buildId);
    } finally {
      if (original === undefined) {
        delete process.env.__VINEXT_BUILD_ID;
      } else {
        process.env.__VINEXT_BUILD_ID = original;
      }
    }
  });
});

// ---------------------------------------------------------------------------
// NextURL basePath and locale properties

describe("NextURL basePath and locale properties", () => {
  const i18nConfig = {
    nextConfig: {
      i18n: {
        locales: ["en", "fr", "de"],
        defaultLocale: "en",
      },
    },
  };

  it("basePath defaults to empty string when no config provided", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/dashboard");
    expect(url.basePath).toBe("");
  });

  it("basePath is empty when URL does not start with the configured prefix", async () => {
    // Matches Next.js getNextPathnameInfo behavior: basePath is only set when
    // the URL's pathname actually starts with the configured basePath prefix.
    // This is critical for middleware "absolute path" support — middleware
    // receiving a request without the basePath prefix should see basePath === "".
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.basePath).toBe("");
  });

  it("basePath is set when URL starts with the configured prefix", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.basePath).toBe("/app");
    // pathname is stripped of the basePath prefix
    expect(url.pathname).toBe("/dashboard");
  });

  it("basePath setter normalizes leading slash", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/dashboard");
    url.basePath = "app";
    expect(url.basePath).toBe("/app");
  });

  it("href reassignment re-derives basePath from the configured value", async () => {
    // Next.js's NextURL.analyze() re-runs getNextPathnameInfo on every href
    // set, so basePath toggles based on whether the new pathname carries the
    // configured prefix.
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.basePath).toBe("");

    // Moving inside the basePath re-activates it from the configured value.
    url.href = "http://localhost/app/settings";
    expect(url.basePath).toBe("/app");
    expect(url.pathname).toBe("/settings");

    // Moving back outside the basePath clears it again.
    url.href = "http://localhost/elsewhere";
    expect(url.basePath).toBe("");
    expect(url.pathname).toBe("/elsewhere");
  });

  it("basePath is empty in clone when URL does not have the prefix", async () => {
    // When the URL doesn't have the basePath prefix, basePath is cleared during
    // construction, and the clone reflects that empty basePath.
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/dashboard", undefined, {
      basePath: "/docs",
    });
    const cloned = url.clone();
    expect(cloned.basePath).toBe("");
  });

  it("basePath is preserved through clone() when URL has the prefix", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/docs/page", undefined, {
      basePath: "/docs",
    });
    const cloned = url.clone();
    expect(cloned.basePath).toBe("/docs");
  });

  it("locale defaults to empty string when no i18n config", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about");
    expect(url.locale).toBe("");
    expect(url.defaultLocale).toBeUndefined();
  });

  it("locale returns the detected locale from pathname", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    expect(url.defaultLocale).toBe("en");
    expect(url.pathname).toBe("/about");
  });

  it("locale falls back to defaultLocale when no locale in pathname", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about", undefined, i18nConfig);
    expect(url.locale).toBe("en");
    expect(url.pathname).toBe("/about");
  });

  it("uses the matching domain default locale", async () => {
    // Ported from Next.js: packages/next/src/server/web/next-url.ts
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.fr/about", undefined, {
      nextConfig: {
        i18n: {
          locales: ["en", "fr", "de"],
          defaultLocale: "en",
          domains: [{ domain: "example.fr", defaultLocale: "fr", locales: ["fr"] }],
        },
      },
    });
    expect(url.defaultLocale).toBe("fr");
    expect(url.locale).toBe("fr");
    expect(url.domainLocale).toEqual({
      domain: "example.fr",
      defaultLocale: "fr",
      locales: ["fr"],
    });
    expect(url.pathname).toBe("/about");
  });

  it("pathname locale overrides the matching domain default locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.fr/de/about", undefined, {
      nextConfig: {
        i18n: {
          locales: ["en", "fr", "de"],
          defaultLocale: "en",
          domains: [{ domain: "example.fr", defaultLocale: "fr", locales: ["fr"] }],
        },
      },
    });
    expect(url.defaultLocale).toBe("fr");
    expect(url.locale).toBe("de");
    expect(url.pathname).toBe("/about");
  });

  it("does not add locale prefixes when formatting API paths", async () => {
    // Ported from Next.js: packages/next/src/shared/lib/router/utils/add-locale.ts
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.com/fr/api/example", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    expect(url.pathname).toBe("/api/example");
    expect(url.href).toBe("https://example.com/api/example");
    expect(url.clone().href).toBe("https://example.com/api/example");
  });

  it("detects API paths case-insensitively when formatting locale URLs", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.com/fr/API/example", undefined, i18nConfig);
    expect(url.href).toBe("https://example.com/API/example");
  });

  it("re-analyzes domain locale after href reassignment", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.com/about", undefined, {
      nextConfig: {
        i18n: {
          locales: ["en", "fr"],
          defaultLocale: "en",
          domains: [{ domain: "example.fr", defaultLocale: "fr", locales: ["fr"] }],
        },
      },
    });
    expect(url.defaultLocale).toBe("en");
    url.href = "https://example.fr/about";
    expect(url.defaultLocale).toBe("fr");
    expect(url.locale).toBe("fr");
    expect(url.domainLocale?.domain).toBe("example.fr");
  });

  it("locale detection is case-insensitive", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/FR/about", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    expect(url.pathname).toBe("/about");
  });

  it("locale setter updates the locale and affects href", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    url.locale = "de";
    expect(url.locale).toBe("de");
    expect(url.href).toContain("/de/about");
  });

  it("locale setter throws on invalid locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(() => {
      url.locale = "es";
    }).toThrow(TypeError);
  });

  it("locales returns a copy of the configured locales array", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about", undefined, i18nConfig);
    const locales = url.locales!;
    expect(locales).toEqual(["en", "fr", "de"]);
    // Mutating the returned array must not affect internals
    locales.push("es");
    expect(url.locales).toEqual(["en", "fr", "de"]);
  });

  it("locales returns undefined without i18n config", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about");
    expect(url.locales).toBeUndefined();
  });

  // --- href / toString() reconstruction ---

  it("toString() preserves locale prefix in serialized URL", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(url.toString()).toBe("http://localhost/fr/about");
    expect(url.href).toBe("http://localhost/fr/about");
  });

  it("toString() omits defaultLocale prefix (matches Next.js)", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about", undefined, i18nConfig);
    expect(url.locale).toBe("en"); // defaultLocale
    expect(url.toString()).toBe("http://localhost/about");
  });

  it("setting locale changes the serialized href", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    url.locale = "de";
    expect(url.href).toBe("http://localhost/de/about");
  });

  it("href includes basePath prefix", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.pathname).toBe("/dashboard");
    expect(url.href).toBe("http://localhost/app/dashboard");
  });

  it("href includes both basePath and locale prefix", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/fr/about", undefined, {
      basePath: "/app",
      ...i18nConfig,
    });
    expect(url.pathname).toBe("/about");
    expect(url.href).toBe("http://localhost/app/fr/about");
  });

  it("href preserves port, search, and hash when basePath is active", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost:3000/app/dashboard?q=1#top", undefined, {
      basePath: "/app",
    });
    expect(url.pathname).toBe("/dashboard");
    expect(url.href).toBe("http://localhost:3000/app/dashboard?q=1#top");
  });

  it("root locale path /fr produces pathname /", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    expect(url.pathname).toBe("/");
    expect(url.href).toBe("http://localhost/fr");
  });

  it("href setter re-analyzes locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    url.href = "http://localhost/de/contact";
    expect(url.locale).toBe("de");
    expect(url.pathname).toBe("/contact");
  });

  it("href setter re-strips basePath before locale analysis", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/fr/about", undefined, {
      basePath: "/app",
      ...i18nConfig,
    });
    url.href = "http://localhost/app/de/contact";
    expect(url.locale).toBe("de");
    expect(url.pathname).toBe("/contact");
    expect(url.basePath).toBe("/app");
  });

  it("basePath setter to empty string clears basePath", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.basePath).toBe("/app");
    url.basePath = "";
    expect(url.basePath).toBe("");
    expect(url.href).toBe("http://localhost/dashboard");
  });

  it("basePath root path has no trailing slash", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app", undefined, {
      basePath: "/app",
    });
    expect(url.pathname).toBe("/");
    expect(url.href).toBe("http://localhost/app");
  });

  it("basePath is stripped from input URL (basePath-only, no i18n)", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/dashboard", undefined, {
      basePath: "/app",
    });
    expect(url.pathname).toBe("/dashboard");
    expect(url.basePath).toBe("/app");
    expect(url.href).toBe("http://localhost/app/dashboard");
  });

  it("pathname setter does not re-analyze locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    url.pathname = "/contact";
    expect(url.locale).toBe("fr"); // unchanged
    expect(url.pathname).toBe("/contact");
    expect(url.href).toBe("http://localhost/fr/contact");
  });

  it("basePath root path with default locale has no trailing slash", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app", undefined, {
      basePath: "/app",
      ...i18nConfig,
    });
    expect(url.locale).toBe("en"); // default locale, no prefix in output
    expect(url.pathname).toBe("/");
    expect(url.href).toBe("http://localhost/app");
  });

  it("locale setter resets to defaultLocale when set to undefined with i18n", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, i18nConfig);
    expect(url.locale).toBe("fr");
    url.locale = undefined;
    expect(url.locale).toBe("en"); // falls back to defaultLocale
    expect(url.href).toBe("http://localhost/about"); // default locale omitted from prefix
  });

  it("locale setter resets to defaultLocale when set to empty string with i18n", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/de/contact", undefined, i18nConfig);
    url.locale = "";
    expect(url.locale).toBe("en");
  });

  it("searchParams mutations are reflected in href with basePath and locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/fr/about", undefined, {
      basePath: "/app",
      ...i18nConfig,
    });
    url.searchParams.set("q", "2");
    expect(url.href).toBe("http://localhost/app/fr/about?q=2");
  });

  // --- clone() ---

  it("clone() preserves locale, basePath, and config through constructor", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/fr/about", undefined, {
      basePath: "/app",
      ...i18nConfig,
    });
    const cloned = url.clone();
    expect(cloned.basePath).toBe("/app");
    expect(cloned.locale).toBe("fr");
    expect(cloned.defaultLocale).toBe("en");
    expect(cloned.pathname).toBe("/about");
    expect(cloned.href).toBe("http://localhost/app/fr/about");
    // Mutations on clone don't affect original
    cloned.locale = "de";
    expect(url.locale).toBe("fr");
  });

  it("matches domain locale from the detected pathname locale", async () => {
    // Ported from Next.js: packages/next/src/shared/lib/i18n/detect-domain-locale.ts
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("https://example.com/fr/about", undefined, {
      nextConfig: {
        i18n: {
          locales: ["en", "fr", "de"],
          defaultLocale: "en",
          domains: [
            { domain: "example.fr", defaultLocale: "fr", locales: ["fr"] },
            { domain: "example.de", defaultLocale: "de", locales: ["de"] },
          ],
        },
      },
    });
    expect(url.domainLocale?.domain).toBe("example.fr");
    expect(url.defaultLocale).toBe("fr");
    expect(url.locale).toBe("fr");
    expect(url.pathname).toBe("/about");
  });

  // --- NextRequest integration ---

  it("NextRequest passes basePath and i18n config through to nextUrl", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/app/fr/dashboard", {
      nextConfig: {
        basePath: "/app",
        i18n: {
          locales: ["en", "fr"],
          defaultLocale: "en",
        },
      },
    });
    expect(req.nextUrl.basePath).toBe("/app");
    expect(req.nextUrl.locale).toBe("fr");
    expect(req.nextUrl.defaultLocale).toBe("en");
    expect(req.nextUrl.pathname).toBe("/dashboard");
    expect(req.nextUrl.href).toBe("http://localhost/app/fr/dashboard");
  });

  it("NextRequest.url reflects the normalized nextUrl href", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/app/fr/dashboard?tab=settings", {
      nextConfig: {
        basePath: "/app",
        i18n: { locales: ["en", "fr"], defaultLocale: "en" },
      },
    });

    expect(req.nextUrl.href).toBe("http://localhost/app/fr/dashboard?tab=settings");
    expect(req.url).toBe(req.nextUrl.href);
  });

  it("NextRequest.url preserves raw input when middleware URL normalization is disabled", async () => {
    const previous = process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE;
    process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE = "1";
    try {
      const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
      const req = new NextRequest("http://localhost/app/fr/dashboard?tab=settings", {
        nextConfig: {
          basePath: "/app",
          i18n: { locales: ["en", "fr"], defaultLocale: "en" },
        },
      });

      expect(req.nextUrl.href).toBe("http://localhost/app/fr/dashboard?tab=settings");
      expect(req.url).toBe("http://localhost/app/fr/dashboard?tab=settings");
    } finally {
      if (previous === undefined) {
        delete process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE;
      } else {
        process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE = previous;
      }
    }
  });

  it("NextRequest passes config when input is a Request object", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const raw = new Request("http://localhost/app/fr/dashboard");
    const req = new NextRequest(raw, {
      nextConfig: {
        basePath: "/app",
        i18n: { locales: ["en", "fr"], defaultLocale: "en" },
      },
    });
    expect(req.nextUrl.basePath).toBe("/app");
    expect(req.nextUrl.locale).toBe("fr");
    expect(req.nextUrl.pathname).toBe("/dashboard");
    expect(req.nextUrl.href).toBe("http://localhost/app/fr/dashboard");
  });

  it("NextRequest.url normalizes Request input through nextUrl", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const raw = new Request("http://localhost/app/fr/dashboard?tab=settings");
    const req = new NextRequest(raw, {
      nextConfig: {
        basePath: "/app",
        i18n: { locales: ["en", "fr"], defaultLocale: "en" },
      },
    });

    expect(req.nextUrl.href).toBe("http://localhost/app/fr/dashboard?tab=settings");
    expect(req.url).toBe(req.nextUrl.href);
  });
});

// ---------------------------------------------------------------------------
// NextURL trailingSlash policy (regression for issue #1332)
//
// The trailingSlash config must reach NextURL so that
// `NextResponse.redirect(request.nextUrl)` and friends emit a Location header
// that honours the user's slash policy. Mirrors Next.js's
// `formatNextPathnameInfo` behaviour.

describe("NextURL trailingSlash policy", () => {
  it("appends a trailing slash to href when trailingSlash is true", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about", undefined, {
      nextConfig: { trailingSlash: true },
    });
    expect(url.href).toBe("http://localhost/about/");
    expect(url.toString()).toBe("http://localhost/about/");
  });

  it("does not add a trailing slash to the root path", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/", undefined, {
      nextConfig: { trailingSlash: true },
    });
    expect(url.href).toBe("http://localhost/");
  });

  it("preserves trailing slash when trailingSlash is true and input already has one", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about/", undefined, {
      nextConfig: { trailingSlash: true },
    });
    expect(url.href).toBe("http://localhost/about/");
  });

  it("strips trailing slash when trailingSlash is false", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about/", undefined, {
      nextConfig: { trailingSlash: false },
    });
    expect(url.href).toBe("http://localhost/about");
  });

  it("defaults to no trailing slash when trailingSlash is not configured", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about/");
    expect(url.href).toBe("http://localhost/about");
  });

  it("applies trailingSlash after setting pathname", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/x", undefined, {
      nextConfig: { trailingSlash: true },
    });
    url.pathname = "/somewhere";
    expect(url.href).toBe("http://localhost/somewhere/");
  });

  it("applies trailingSlash with basePath", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/app/dashboard", undefined, {
      basePath: "/app",
      nextConfig: { trailingSlash: true },
    });
    expect(url.pathname).toBe("/dashboard");
    expect(url.href).toBe("http://localhost/app/dashboard/");
  });

  it("applies trailingSlash with locale", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/fr/about", undefined, {
      nextConfig: {
        i18n: { locales: ["en", "fr"], defaultLocale: "en" },
        trailingSlash: true,
      },
    });
    expect(url.locale).toBe("fr");
    expect(url.href).toBe("http://localhost/fr/about/");
  });

  it("preserves search and hash when adding trailing slash", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about?q=1#top", undefined, {
      nextConfig: { trailingSlash: true },
    });
    expect(url.href).toBe("http://localhost/about/?q=1#top");
  });

  it("clone() preserves the trailingSlash config", async () => {
    const { NextURL } = await import("../packages/vinext/src/shims/server.js");
    const url = new NextURL("http://localhost/about", undefined, {
      nextConfig: { trailingSlash: true },
    });
    const cloned = url.clone();
    cloned.pathname = "/contact";
    expect(cloned.href).toBe("http://localhost/contact/");
  });
});

describe("NextRequest plumbs trailingSlash into nextUrl", () => {
  it("forwards trailingSlash from nextConfig into the NextURL", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/about", {
      nextConfig: { trailingSlash: true },
    });
    expect(req.nextUrl.href).toBe("http://localhost/about/");
  });

  it("NextResponse.redirect(request.nextUrl) emits a Location that honours trailingSlash", async () => {
    const { NextRequest, NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/redirect", {
      nextConfig: { trailingSlash: true },
    });
    const url = req.nextUrl.clone();
    url.pathname = "/somewhere";
    const res = NextResponse.redirect(url);
    expect(res.status).toBe(307);
    const location = res.headers.get("location");
    expect(location).not.toBeNull();
    // Location is fully-qualified per the spec; the pathname must include the slash.
    expect(new URL(location!).pathname).toBe("/somewhere/");
  });

  it("NextResponse.redirect(request.nextUrl) drops trailing slash when trailingSlash is false", async () => {
    const { NextRequest, NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("http://localhost/redirect/", {
      nextConfig: { trailingSlash: false },
    });
    const url = req.nextUrl.clone();
    url.pathname = "/somewhere/";
    const res = NextResponse.redirect(url);
    const location = res.headers.get("location");
    expect(location).not.toBeNull();
    expect(new URL(location!).pathname).toBe("/somewhere");
  });
});

// ---------------------------------------------------------------------------
// NextResponse.next() with request header forwarding

describe("NextResponse.next() request header forwarding", () => {
  it("forwards request headers as x-middleware-request-* headers", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.next({
      request: {
        headers: new Headers({
          "x-custom-header": "custom-value",
          authorization: "Bearer token123",
        }),
      },
    });

    expect(res.headers.get("x-middleware-next")).toBe("1");
    expect(res.headers.get("x-middleware-request-x-custom-header")).toBe("custom-value");
    expect(res.headers.get("x-middleware-request-authorization")).toBe("Bearer token123");
  });

  it("serializes the full override set so omitted headers can be deleted downstream", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");

    const forwardedHeaders = new Headers({
      "x-custom-header": "custom-value",
      "x-added": "1",
    });

    const res = NextResponse.next({
      request: {
        headers: forwardedHeaders,
      },
    });

    const overrideHeaders = res.headers.get("x-middleware-override-headers");
    expect(overrideHeaders).not.toBeNull();
    expect(overrideHeaders!.split(",").sort()).toEqual([...forwardedHeaders.keys()].sort());
    expect(res.headers.get("x-middleware-request-x-custom-header")).toBe("custom-value");
    expect(res.headers.get("x-middleware-request-x-added")).toBe("1");
  });
});

describe("middleware request header overrides", () => {
  // Ported from Next.js: test/e2e/middleware-request-header-overrides/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-request-header-overrides/test/index.test.ts
  it("config-matchers applyMiddlewareRequestHeaders deletes omitted headers from the request", async () => {
    const { applyMiddlewareRequestHeaders } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const middlewareHeaders: Record<string, string> = {
      "x-middleware-override-headers": "x-keep,x-added",
      "x-middleware-request-x-keep": "updated",
      "x-middleware-request-x-added": "1",
      "x-middleware-next": "1",
    };

    const request = new Request("http://localhost/test", {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
        "x-keep": "original",
      },
    });

    const { request: nextRequest, postMwReqCtx } = applyMiddlewareRequestHeaders(
      middlewareHeaders,
      request,
    );

    expect(nextRequest.headers.get("authorization")).toBeNull();
    expect(nextRequest.headers.get("cookie")).toBeNull();
    expect(nextRequest.headers.get("x-keep")).toBe("updated");
    expect(nextRequest.headers.get("x-added")).toBe("1");
    expect(Object.keys(postMwReqCtx.cookies)).toEqual([]);
    expect(middlewareHeaders).toEqual({});
  });

  it("config-matchers applyMiddlewareRequestHeaders preserves existing headers in add-only overrides", async () => {
    const { applyMiddlewareRequestHeaders } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost/test", {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
        "x-keep": "original",
      },
    });

    const forwardedHeaders = new Headers(request.headers);
    forwardedHeaders.set("x-added", "1");

    const middlewareHeaders: Record<string, string> = {
      "x-middleware-override-headers": [...forwardedHeaders.keys()].join(","),
      "x-middleware-request-authorization": forwardedHeaders.get("authorization")!,
      "x-middleware-request-cookie": forwardedHeaders.get("cookie")!,
      "x-middleware-request-x-keep": forwardedHeaders.get("x-keep")!,
      "x-middleware-request-x-added": "1",
      "x-middleware-next": "1",
    };

    const { request: nextRequest, postMwReqCtx } = applyMiddlewareRequestHeaders(
      middlewareHeaders,
      request,
    );

    expect(nextRequest.headers.get("authorization")).toBe("Bearer secret");
    expect(nextRequest.headers.get("cookie")).toBe("a=1; b=2");
    expect(nextRequest.headers.get("x-keep")).toBe("original");
    expect(nextRequest.headers.get("x-added")).toBe("1");
    expect(postMwReqCtx.cookies).toEqual({ a: "1", b: "2" });
    expect(middlewareHeaders).toEqual({});
  });

  it("next/headers applyMiddlewareRequestHeaders replaces the live request header set", async () => {
    const {
      applyMiddlewareRequestHeaders,
      cookies,
      headers,
      headersContextFromRequest,
      runWithHeadersContext,
    } = await import("../packages/vinext/src/shims/headers.js");

    const request = new Request("http://localhost/test", {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
        "x-keep": "original",
      },
    });

    await runWithHeadersContext(headersContextFromRequest(request), async () => {
      applyMiddlewareRequestHeaders(
        new Headers({
          "x-middleware-override-headers": "x-keep,x-added",
          "x-middleware-request-x-keep": "updated",
          "x-middleware-request-x-added": "1",
        }),
      );

      const liveHeaders = await headers();
      const liveCookies = await cookies();

      expect(liveHeaders.get("authorization")).toBeNull();
      expect(liveHeaders.get("cookie")).toBeNull();
      expect(liveHeaders.get("x-keep")).toBe("updated");
      expect(liveHeaders.get("x-added")).toBe("1");
      expect(liveCookies.getAll()).toEqual([]);
    });
  });

  it("next/headers applyMiddlewareRequestHeaders invalidates headers() snapshot taken before the override", async () => {
    // Regression: a middleware that reads `headers()` (or `cookies()`) before
    // applying a request-header override would prime a sealed read-only
    // snapshot built from the *pre*-override request. Discovered with
    // @clerk/nextjs whose `clerkClient()` reads `headers()` via
    // `buildRequestLike()` during middleware execution; before the fix, the
    // Server Component subsequently received the stale snapshot and saw the
    // pre-override credentials and missing middleware-injected headers.
    const {
      applyMiddlewareRequestHeaders,
      cookies,
      headers,
      headersContextFromRequest,
      runWithHeadersContext,
    } = await import("../packages/vinext/src/shims/headers.js");

    const request = new Request("http://localhost/test", {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
        "x-keep": "original",
      },
    });

    await runWithHeadersContext(headersContextFromRequest(request), async () => {
      // 1. Prime the sealed snapshot — this is exactly what
      //    `clerkMiddleware()` does internally via `buildRequestLike()`.
      const preHeadersPromise = headers();
      const preCookiesPromise = cookies();
      const preHeaders = await preHeadersPromise;
      const preCookies = await preCookiesPromise;
      expect(preHeaders.get("authorization")).toBe("Bearer secret");
      expect(preHeaders.get("x-keep")).toBe("original");
      expect(preCookies.getAll()).toEqual([
        { name: "a", value: "1" },
        { name: "b", value: "2" },
      ]);

      // 2. Apply the override — drops `authorization`/`cookie`, adds `x-added`,
      //    and updates `x-keep`.
      applyMiddlewareRequestHeaders(
        new Headers({
          "x-middleware-override-headers": "x-keep,x-added",
          "x-middleware-request-x-keep": "updated",
          "x-middleware-request-x-added": "1",
        }),
      );

      // 3. A subsequent `headers()` call — for example from the Server
      //    Component's render — must observe the override, not the snapshot
      //    captured in step 1.
      const postHeadersPromise = headers();
      const postCookiesPromise = cookies();
      const postHeaders = await postHeadersPromise;
      const postCookies = await postCookiesPromise;

      expect(postHeadersPromise).not.toBe(preHeadersPromise);
      expect(postCookiesPromise).not.toBe(preCookiesPromise);
      expect(postHeaders.get("authorization")).toBeNull();
      expect(postHeaders.get("cookie")).toBeNull();
      expect(postHeaders.get("x-keep")).toBe("updated");
      expect(postHeaders.get("x-added")).toBe("1");
      expect(postCookies.getAll()).toEqual([]);
    });
  });
});

// ---------------------------------------------------------------------------
// NextResponse.redirect() with different status codes

describe("NextResponse.redirect() status codes", () => {
  it("defaults to 307 Temporary Redirect", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com");
    expect(res.status).toBe(307);
  });

  it("supports 301 Permanent Redirect", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com", 301);
    expect(res.status).toBe(301);
    // validateURL() normalizes via `new URL()`, adding a trailing slash for origin-only URLs.
    expect(res.headers.get("Location")).toBe("https://example.com/");
  });

  it("supports 302 Found", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com", 302);
    expect(res.status).toBe(302);
  });

  it("supports 308 Permanent Redirect", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com", 308);
    expect(res.status).toBe(308);
  });

  it("accepts URL object", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const url = new URL("https://example.com/target");
    const res = NextResponse.redirect(url);
    expect(res.headers.get("Location")).toBe("https://example.com/target");
  });

  it("supports 303 See Other", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    const res = NextResponse.redirect("https://example.com", 303);
    expect(res.status).toBe(303);
  });

  // Ported from Next.js: packages/next/src/server/web/spec-extension/response.ts
  // Next.js validates redirect status codes and throws RangeError for invalid ones.
  it("throws RangeError for non-redirect status code 200", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    expect(() => NextResponse.redirect("https://example.com", 200)).toThrow(RangeError);
  });

  it("throws RangeError for non-redirect status code 418", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    expect(() => NextResponse.redirect("https://example.com", 418)).toThrow(RangeError);
  });

  it("throws RangeError with descriptive message for invalid status", async () => {
    const { NextResponse } = await import("../packages/vinext/src/shims/server.js");
    expect(() => NextResponse.redirect("https://example.com", 200)).toThrow(
      /Failed to execute "redirect" on "response": Invalid status code/,
    );
  });
});

// ---------------------------------------------------------------------------
// matchConfigPattern unit tests (next.config.js redirects/rewrites)

describe("matchConfigPattern", () => {
  it("matches exact paths", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/about", "/about")).toEqual({});
    expect(matchConfigPattern("/", "/")).toEqual({});
    expect(matchConfigPattern("/about", "/other")).toBeNull();
  });

  it("matches single :param segments", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    const result = matchConfigPattern("/blog/hello-world", "/blog/:slug");
    expect(result).toEqual({ slug: "hello-world" });
  });

  it("matches multiple :param segments", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    const result = matchConfigPattern("/blog/2024/my-post", "/blog/:year/:slug");
    expect(result).toEqual({ year: "2024", slug: "my-post" });
  });

  it("rejects when segment count differs for non-wildcard patterns", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/blog/a/b", "/blog/:slug")).toBeNull();
    expect(matchConfigPattern("/blog", "/blog/:slug")).toBeNull();
  });

  it("matches :path* catch-all (zero or more segments)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Zero segments
    expect(matchConfigPattern("/docs", "/docs/:path*")).toEqual({ path: "" });
    // One segment
    expect(matchConfigPattern("/docs/intro", "/docs/:path*")).toEqual({ path: "intro" });
    // Multiple segments
    expect(matchConfigPattern("/docs/guide/getting-started", "/docs/:path*")).toEqual({
      path: "guide/getting-started",
    });
  });

  it("matches :path+ catch-all (one or more segments)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // One segment
    expect(matchConfigPattern("/api/users", "/api/:path+")).toEqual({ path: "users" });
    // Multiple segments
    expect(matchConfigPattern("/api/users/123", "/api/:path+")).toEqual({ path: "users/123" });
    // Zero segments — should NOT match
    expect(matchConfigPattern("/api", "/api/:path+")).toBeNull();
  });

  it("matches regex group patterns", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Common Next.js pattern: /:path(\\d+) for numeric paths
    const result = matchConfigPattern("/123", "/:id(\\d+)");
    if (result) {
      expect(result.id).toBe("123");
    }
    // Non-numeric should not match
    expect(matchConfigPattern("/abc", "/:id(\\d+)")).toBeNull();
  });

  it("handles dots in patterns", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/feed.xml", "/feed.xml")).toEqual({});
    // Dot should not match any character
    expect(matchConfigPattern("/feedXxml", "/feed.xml")).toBeNull();
  });

  it("matches :path* with literal suffix (e.g. /:path*.md)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Should match URLs ending in .md
    expect(matchConfigPattern("/article.md", "/:path*.md")).toEqual({ path: "article" });
    expect(matchConfigPattern("/news/my-article.md", "/:path*.md")).toEqual({
      path: "news/my-article",
    });
    expect(matchConfigPattern("/docs/guide/intro.md", "/:path*.md")).toEqual({
      path: "docs/guide/intro",
    });
    // Should NOT match URLs without .md suffix
    expect(matchConfigPattern("/", "/:path*.md")).toBeNull();
    expect(matchConfigPattern("/about", "/:path*.md")).toBeNull();
    expect(matchConfigPattern("/news", "/:path*.md")).toBeNull();
    expect(matchConfigPattern("/article.txt", "/:path*.md")).toBeNull();
  });

  it("matches :path+ with literal suffix (e.g. /:path+.json)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Should match URLs ending in .json with at least one path segment
    expect(matchConfigPattern("/data.json", "/:path+.json")).toEqual({ path: "data" });
    expect(matchConfigPattern("/api/users.json", "/:path+.json")).toEqual({ path: "api/users" });
    // Should NOT match bare .json (zero segments before suffix)
    expect(matchConfigPattern("/.json", "/:path+.json")).toBeNull();
    // Should NOT match URLs without .json suffix
    expect(matchConfigPattern("/data", "/:path+.json")).toBeNull();
    expect(matchConfigPattern("/", "/:path+.json")).toBeNull();
  });

  it("matches :path* with prefix and suffix (e.g. /docs/:path*.md)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/docs/intro.md", "/docs/:path*.md")).toEqual({ path: "intro" });
    expect(matchConfigPattern("/docs/guide/getting-started.md", "/docs/:path*.md")).toEqual({
      path: "guide/getting-started",
    });
    // Should NOT match without .md
    expect(matchConfigPattern("/docs/intro", "/docs/:path*.md")).toBeNull();
    // Should NOT match different prefix
    expect(matchConfigPattern("/blog/intro.md", "/docs/:path*.md")).toBeNull();
  });

  it("matches :param with literal suffix (e.g. /:slug.md)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Should match URLs with the .md suffix and extract the param
    expect(matchConfigPattern("/hello-world.md", "/:slug.md")).toEqual({ slug: "hello-world" });
    expect(matchConfigPattern("/my-post.md", "/:slug.md")).toEqual({ slug: "my-post" });
    // Should NOT match URLs without .md suffix
    expect(matchConfigPattern("/", "/:slug.md")).toBeNull();
    expect(matchConfigPattern("/hello-world", "/:slug.md")).toBeNull();
    expect(matchConfigPattern("/hello-world.txt", "/:slug.md")).toBeNull();
    // Should NOT match paths with extra segments
    expect(matchConfigPattern("/blog/hello-world.md", "/:slug.md")).toBeNull();
  });

  it("matches :param with literal suffix via config-matchers module", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/hello-world.md", "/:slug.md")).toEqual({ slug: "hello-world" });
    expect(matchConfigPattern("/", "/:slug.md")).toBeNull();
    expect(matchConfigPattern("/hello-world", "/:slug.md")).toBeNull();
  });

  it("still matches plain :path* catch-all (no suffix) correctly", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Ensure the fix doesn't regress existing catch-all behavior
    expect(matchConfigPattern("/docs", "/docs/:path*")).toEqual({ path: "" });
    expect(matchConfigPattern("/docs/intro", "/docs/:path*")).toEqual({ path: "intro" });
    expect(matchConfigPattern("/docs/guide/getting-started", "/docs/:path*")).toEqual({
      path: "guide/getting-started",
    });
  });

  // Regression test for: catch-all prefix overmatch
  // /foobar was incorrectly matched by /foo/:path* because startsWith("/foo")
  // passed without checking for a segment boundary after the prefix.
  // https://github.com/cloudflare/vinext/pull/368
  it("regression: does not overmatch catch-all when pathname shares a prefix but not a segment boundary", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Core regression case: /foobar must NOT match /foo/:path*
    expect(matchConfigPattern("/foobar", "/foo/:path*")).toBeNull();
    // Similarly for :path+
    expect(matchConfigPattern("/foobar", "/foo/:path+")).toBeNull();
    // A legitimate sub-path still matches
    expect(matchConfigPattern("/foo/bar", "/foo/:path*")).toEqual({ path: "bar" });
    // An exact prefix (zero segments) still matches for :path*
    expect(matchConfigPattern("/foo", "/foo/:path*")).toEqual({ path: "" });
    // An exact prefix (zero segments) still does NOT match for :path+
    expect(matchConfigPattern("/foo", "/foo/:path+")).toBeNull();
    // Deeper false-prefix: /football must NOT match /foot/:path*
    expect(matchConfigPattern("/football", "/foot/:path*")).toBeNull();
    // But /foot/ball should match
    expect(matchConfigPattern("/foot/ball", "/foot/:path*")).toEqual({ path: "ball" });
  });

  it("regression: catch-all prefix overmatch via config-matchers module", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Same cases exercised against the standalone config-matchers module
    expect(matchConfigPattern("/foobar", "/foo/:path*")).toBeNull();
    expect(matchConfigPattern("/foobar", "/foo/:path+")).toBeNull();
    expect(matchConfigPattern("/foo/bar", "/foo/:path*")).toEqual({ path: "bar" });
    expect(matchConfigPattern("/foo", "/foo/:path*")).toEqual({ path: "" });
    expect(matchConfigPattern("/foo", "/foo/:path+")).toBeNull();
  });

  // Regression for #1331: a trailing slash on the request pathname must not
  // hide a config rewrite/redirect/header source written without one. This
  // mirrors Next.js's conditional `source + '(/)?'` suffix under
  // `trailingSlash: true` — see `stripTrailingSlashForConfigMatch`.
  it("strips a trailing slash on the incoming pathname before matching", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Exact match: trailing slash on the pathname is ignored.
    expect(matchConfigPattern("/about/", "/about")).toEqual({});
    // :param match across the segment with a trailing slash on the pathname.
    expect(matchConfigPattern("/blog/hello-world/", "/blog/:slug")).toEqual({
      slug: "hello-world",
    });
    // Multi-segment :param with a trailing slash.
    expect(matchConfigPattern("/blog/2024/my-post/", "/blog/:year/:slug")).toEqual({
      year: "2024",
      slug: "my-post",
    });
    // Regex-group pattern with a trailing slash.
    expect(matchConfigPattern("/123/", "/:id(\\d+)")).toEqual({ id: "123" });
  });

  it("preserves the root path and catch-all semantics under trailing slash", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // The root pathname "/" stays "/" — never stripped to "".
    expect(matchConfigPattern("/", "/")).toEqual({});
    // Catch-alls already consume the trailing slash; the strip must not turn
    // `/docs/` into `/docs` in a way that breaks zero-segment matching.
    expect(matchConfigPattern("/docs/", "/docs/:path*")).toEqual({ path: "" });
    // One-segment catch-all with trailing slash on the pathname.
    expect(matchConfigPattern("/docs/intro/", "/docs/:path*")).toEqual({ path: "intro" });
    // :path+ still requires at least one segment when the only "extra" was a slash.
    expect(matchConfigPattern("/api/", "/api/:path+")).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// isSafeRegex / safeRegExp unit tests (ReDoS prevention)

describe("isSafeRegex", () => {
  it("accepts simple patterns", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("^/about$")).toBe(true);
    expect(isSafeRegex("^/blog/[^/]+$")).toBe(true);
    expect(isSafeRegex("^/docs/(.*)$")).toBe(true);
    expect(isSafeRegex("^/api/(.+)$")).toBe(true);
    expect(isSafeRegex("\\d+")).toBe(true);
    expect(isSafeRegex("^/feed\\.xml$")).toBe(true);
  });

  it("accepts non-nested quantifiers inside groups", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // A single quantifier inside a group without a quantifier on the group itself
    expect(isSafeRegex("(a+)")).toBe(true);
    expect(isSafeRegex("([^/]+)")).toBe(true);
    expect(isSafeRegex("(\\d{2,4})")).toBe(true);
  });

  it("rejects nested quantifiers: (a+)+", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(a+)+")).toBe(false);
  });

  it("rejects nested quantifiers: (a+)*", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(a+)*")).toBe(false);
  });

  it("rejects nested quantifiers: (.*)*", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(.*)*")).toBe(false);
  });

  it("rejects nested quantifiers: (a*)+", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(a*)+")).toBe(false);
  });

  it("rejects nested quantifiers: ([^/]+)+", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("([^/]+)+")).toBe(false);
  });

  it("rejects nested quantifiers with braces: (a+){2,}", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(a+){2,}")).toBe(false);
  });

  it("accepts quantifier on group without inner quantifier", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // (ab)+ is fine — no inner quantifier
    expect(isSafeRegex("(ab)+")).toBe(true);
    expect(isSafeRegex("(foo|bar)*")).toBe(true);
  });

  it("treats escaped characters as safe", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // \\+ is a literal +, not a quantifier
    expect(isSafeRegex("(a\\+)+")).toBe(true);
  });

  it("treats quantifiers inside character classes as safe", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // [+*] is a character class, not a quantifier
    expect(isSafeRegex("([+*])+")).toBe(true);
  });

  it("rejects nested optional quantifiers: (a?)+", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // '?' inside group + quantifier on group = catastrophic backtracking
    expect(isSafeRegex("(a?)+")).toBe(false);
    expect(isSafeRegex("(a?)+b")).toBe(false);
  });

  it("rejects nested optional quantifiers: (.?)+", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(.?)+")).toBe(false);
  });

  it("rejects nested optional quantifiers: (a?)*", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isSafeRegex("(a?)*")).toBe(false);
  });

  it("accepts outer '?' on group (zero-or-one is not unbounded repetition)", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // '?' means zero or one — only 2 paths, not exponential backtracking
    // This is safe even with inner quantifiers (e.g. URL patterns like (?:/.*)?  )
    expect(isSafeRegex("(a+)?")).toBe(true);
    expect(isSafeRegex("(?:/.*)?")).toBe(true);
  });

  it("treats non-greedy modifier as safe, not as quantifier", async () => {
    const { isSafeRegex } = await import("../packages/vinext/src/config/config-matchers.js");
    // a+? is non-greedy '+', not a nested quantifier
    expect(isSafeRegex("(a+?)")).toBe(true);
    // (a*?) is non-greedy '*', still just one quantifier
    expect(isSafeRegex("(a*?)")).toBe(true);
  });
});

describe("safeRegExp", () => {
  it("returns RegExp for safe patterns", async () => {
    const { safeRegExp } = await import("../packages/vinext/src/config/config-matchers.js");
    const re = safeRegExp("^/about$");
    expect(re).toBeInstanceOf(RegExp);
    expect(re!.test("/about")).toBe(true);
  });

  it("returns null for pathological patterns", async () => {
    const { safeRegExp } = await import("../packages/vinext/src/config/config-matchers.js");
    // lgtm[js/redos] — deliberate pathological regex to test safeRegExp guard
    const re = safeRegExp("(a+)+b");
    expect(re).toBeNull();
  });

  it("returns null for invalid regex syntax", async () => {
    const { safeRegExp } = await import("../packages/vinext/src/config/config-matchers.js");
    const re = safeRegExp("(?P<name>");
    expect(re).toBeNull();
  });
});

describe("escapeHeaderSource", () => {
  it("passes through literal paths unchanged", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/api/users")).toBe("/api/users");
  });

  it("escapes dots", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/file.txt")).toBe("/file\\.txt");
  });

  it("converts named param to [^/]+", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/user/:id")).toBe("/user/[^/]+");
  });

  it("converts glob * to .*", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/api/*")).toBe("/api/.*");
  });

  it("escapes + and ?", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/path+query")).toBe("/path\\+query");
    expect(escapeHeaderSource("/maybe?")).toBe("/maybe\\?");
  });

  it("handles constrained param :param(constraint)", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/api/:version(\\d+)/users")).toBe("/api/(\\d+)/users");
  });

  it("handles constrained param with alternation", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/:lang(en|fr)/page")).toBe("/(en|fr)/page");
  });

  it("preserves standalone regex groups", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/api/(v1|v2)/users")).toBe("/api/(v1|v2)/users");
  });

  it("handles multiple groups and params", async () => {
    const { escapeHeaderSource } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(escapeHeaderSource("/:lang(en|fr)/:id(\\d+)/page")).toBe("/(en|fr)/(\\d+)/page");
  });
});

describe("matchConfigPattern rejects ReDoS patterns", () => {
  it("returns null for pathological source patterns", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // This pattern has nested quantifiers: the compiled regex would be (a+)+b
    // which causes catastrophic backtracking. matchConfigPattern should return
    // null (no match) rather than hanging.
    // lgtm[js/redos] — deliberate pathological regex to test safeRegExp guard
    const result = matchConfigPattern("/aaaaaaaaaaaaaaaaaaaac", "/:id((a+)+b)");
    expect(result).toBeNull();
  });
});

describe("matchConfigPattern compiled pattern cache", () => {
  it("returns consistent results when the same pattern is called multiple times", async () => {
    // Regression test for the per-request recompilation bug: patterns that
    // enter the regex branch (containing `(`, `\`, or param suffixes) were
    // previously re-running isSafeRegex + new RegExp() on every call, which
    // dominated CPU profiles on apps with many locale-prefixed redirect rules.
    // After the fix the compiled RegExp is cached at module level.
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");

    // Locale capture-group pattern — the kind that triggered the bottleneck.
    const localePattern = "/:locale(en|es|fr|id|ja|ko|pt-br|pt|ro|ta|tr|uk|zh-cn|zh-tw)?/security";

    // First call — populates the cache.
    const first = matchConfigPattern("/en/security", localePattern);
    expect(first).not.toBeNull();
    expect(first!.locale).toBe("en");

    // Second call — must hit the cache and return the same result.
    const second = matchConfigPattern("/en/security", localePattern);
    expect(second).toEqual(first);

    // Different pathname, same pattern — still uses the cached RegExp.
    const third = matchConfigPattern("/fr/security", localePattern);
    expect(third).not.toBeNull();
    expect(third!.locale).toBe("fr");

    // Non-matching pathname — cache must not corrupt the null path.
    const fourth = matchConfigPattern("/de/security", localePattern);
    expect(fourth).toBeNull();

    // Plain no-match when locale omitted and path wrong.
    const fifth = matchConfigPattern("/security/extra", localePattern);
    expect(fifth).toBeNull();
  });

  it("caches rejection for unsafe (ReDoS) patterns and returns null on repeat calls", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // lgtm[js/redos] — deliberate pathological regex to test cache-of-null path
    const unsafe = "/:id((a+)+b)";
    expect(matchConfigPattern("/x", unsafe)).toBeNull();
    // Second call must not re-run isSafeRegex — just return null from cache.
    expect(matchConfigPattern("/x", unsafe)).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// matchRedirect locale-static index tests
// Verifies the O(1) locale-prefix optimization in matchRedirect.

describe("matchRedirect locale-static index", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  // 63 locale-prefix rules — matches the profiled bottleneck scenario.
  const locales = "en|es|fr|id|ja|ko|pt-br|pt|ro|ta|tr|uk|zh-cn|zh-tw|";
  function makeLocaleRules(suffixes: string[]) {
    return suffixes.map((s) => ({
      source: `/:locale(${locales})?${s}`,
      destination: `/:locale${s}-dest`,
      permanent: false as const,
    }));
  }

  it("matches a locale-prefixed pathname (locale present)", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = makeLocaleRules(["/security", "/advisory-board"]);
    const result = matchRedirect("/en/security", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/en/security-dest");
    expect(result!.permanent).toBe(false);
  });

  it("matches a locale-prefixed pathname (locale omitted)", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = makeLocaleRules(["/security", "/advisory-board"]);
    // When locale is omitted the destination :locale param substitutes to "".
    // sanitizeDestination collapses the leading double slash.
    const result = matchRedirect("/security", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/security-dest");
  });

  it("returns null when pathname does not match any rule", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = makeLocaleRules(["/security", "/advisory-board"]);
    // /blog is not in any indexed suffix
    expect(matchRedirect("/blog", redirects, emptyCtx)).toBeNull();
    expect(matchRedirect("/en/blog", redirects, emptyCtx)).toBeNull();
  });

  it("returns null when locale segment is not in the alternation", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = makeLocaleRules(["/security"]);
    // "de" is not in the locales alternation
    expect(matchRedirect("/de/security", redirects, emptyCtx)).toBeNull();
  });

  it("matches multi-segment locale codes like pt-br and zh-cn", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = makeLocaleRules(["/security"]);
    const ptBr = matchRedirect("/pt-br/security", redirects, emptyCtx);
    expect(ptBr).not.toBeNull();
    expect(ptBr!.destination).toBe("/pt-br/security-dest");

    const zhCn = matchRedirect("/zh-cn/security", redirects, emptyCtx);
    expect(zhCn).not.toBeNull();
    expect(zhCn!.destination).toBe("/zh-cn/security-dest");
  });

  it("preserves ordering: linear rule earlier than locale-static wins", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // Rule 0 is a linear catch-all; rule 1 is locale-static.
    // For /en/security, the linear rule (index 0) matches first.
    const redirects = [
      { source: "/:path*", destination: "/catchall", permanent: false as const },
      ...makeLocaleRules(["/security"]),
    ];
    const result = matchRedirect("/en/security", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/catchall");
  });

  it("preserves ordering: locale-static rule earlier than linear wins", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // Rule 0 is locale-static; rule 1 is linear.
    // For /en/security, the locale-static rule (index 0) matches first.
    const redirects = [
      ...makeLocaleRules(["/security"]),
      { source: "/:path*", destination: "/catchall", permanent: false as const },
    ];
    const result = matchRedirect("/en/security", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/en/security-dest");
  });

  it("returns null efficiently for 63 rules on a non-matching path (no regex exec on hot path)", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // Construct 63 locale-prefixed rules (matches the profiled bottleneck).
    const suffixes = Array.from({ length: 63 }, (_, i) => `/page-${i}`);
    const redirects = makeLocaleRules(suffixes);
    // /blog does not match any rule.
    const result = matchRedirect("/blog", redirects, emptyCtx);
    expect(result).toBeNull();
    // /en/blog also does not match.
    const result2 = matchRedirect("/en/blog", redirects, emptyCtx);
    expect(result2).toBeNull();
  });

  it("respects has/missing conditions on locale-static rules", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [
      {
        source: `/:locale(en|fr)?/gated`,
        destination: `/:locale/gated-dest`,
        permanent: false as const,
        has: [{ type: "header" as const, key: "x-auth", value: "1" }],
      },
    ];

    // Without the header — should NOT match.
    const noHeader = matchRedirect("/en/gated", redirects, emptyCtx);
    expect(noHeader).toBeNull();

    // With the header — should match.
    const withHeader = matchRedirect("/en/gated", redirects, {
      headers: new Headers({ "x-auth": "1" }),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    });
    expect(withHeader).not.toBeNull();
    expect(withHeader!.destination).toBe("/en/gated-dest");
  });

  it("lets has captures override locale params in locale-static redirects", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [
      {
        source: `/:locale(en|fr)?/docs`,
        destination: `/target/:locale`,
        permanent: false as const,
        has: [{ type: "header" as const, key: "x-locale", value: "(?<locale>forced)" }],
      },
    ];

    const withLocalePrefix = matchRedirect("/en/docs", redirects, {
      headers: new Headers({ "x-locale": "forced" }),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    });
    expect(withLocalePrefix).toEqual({ destination: "/target/forced", permanent: false });

    const withoutLocalePrefix = matchRedirect("/docs", redirects, {
      headers: new Headers({ "x-locale": "forced" }),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    });
    expect(withoutLocalePrefix).toEqual({ destination: "/target/forced", permanent: false });
  });

  it("falls back to linear matching for rules that are not locale-static", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // A mix: some locale-static rules and one catch-all that matches /other.
    const redirects = [
      ...makeLocaleRules(["/security", "/advisory-board"]),
      { source: "/other", destination: "/other-dest", permanent: true as const },
    ];
    const result = matchRedirect("/other", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/other-dest");
    expect(result!.permanent).toBe(true);
  });
});

describe("matchConfigPattern handles parameterized suffix patterns", () => {
  it("matches :path* with literal suffix (e.g. /:path*.md)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    // Should match URLs ending in .md
    expect(matchConfigPattern("/article.md", "/:path*.md")).toEqual({ path: "article" });
    expect(matchConfigPattern("/news/my-article.md", "/:path*.md")).toEqual({
      path: "news/my-article",
    });
    // Should NOT match URLs without .md suffix
    expect(matchConfigPattern("/", "/:path*.md")).toBeNull();
    expect(matchConfigPattern("/about", "/:path*.md")).toBeNull();
    expect(matchConfigPattern("/news", "/:path*.md")).toBeNull();
  });

  it("matches :path+ with literal suffix (e.g. /:path+.json)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/data.json", "/:path+.json")).toEqual({ path: "data" });
    expect(matchConfigPattern("/api/users.json", "/:path+.json")).toEqual({ path: "api/users" });
    // Zero segments before suffix — should NOT match for :path+
    expect(matchConfigPattern("/.json", "/:path+.json")).toBeNull();
    expect(matchConfigPattern("/", "/:path+.json")).toBeNull();
  });

  it("does not regress plain :path* catch-all (no suffix)", async () => {
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(matchConfigPattern("/docs", "/docs/:path*")).toEqual({ path: "" });
    expect(matchConfigPattern("/docs/intro", "/docs/:path*")).toEqual({ path: "intro" });
    expect(matchConfigPattern("/docs/guide/getting-started", "/docs/:path*")).toEqual({
      path: "guide/getting-started",
    });
  });
});

// ---------------------------------------------------------------------------
// has/missing condition matching unit tests (next.config.js redirects/rewrites)

describe("parseCookies", () => {
  it("parses standard cookie header", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(parseCookies("a=1; b=2; c=three")).toEqual({ a: "1", b: "2", c: "three" });
  });

  it("returns empty object for null", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(parseCookies(null)).toEqual({});
  });

  it("returns empty object for empty string", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(parseCookies("")).toEqual({});
  });

  it("handles cookies with = in value", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(parseCookies("token=abc=def")).toEqual({ token: "abc=def" });
  });

  it("trims whitespace around keys and values", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(parseCookies("  a = 1 ;  b = 2 ")).toEqual({ a: "1", b: "2" });
  });

  // Next.js uses the same compiled `cookie` parser for request cookies.
  // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/api-utils/get-cookie-parser.ts
  it("matches Next.js duplicate, prototype-key, and empty-name semantics", async () => {
    const { parseCookies } = await import("../packages/vinext/src/config/config-matchers.js");
    const cookies = parseCookies(
      'session=trusted; session=attacker; =empty-name; __proto__=prototype-cookie; constructor=constructor-cookie; toString=string-cookie; encoded=hello%20world; malformed=%E0%A4%A; quoted="quoted value"',
    );

    expect(Object.getPrototypeOf(cookies)).toBe(Object.prototype);
    expect(cookies.hasOwnProperty("session")).toBe(true);
    expect(Object.hasOwn(cookies, "toString")).toBe(false);
    expect(Object.prototype.toString.call(cookies)).toBe("[object Object]");
    expect(cookies.session).toBe("trusted");
    expect(cookies[""]).toBe("empty-name");
    expect(cookies.encoded).toBe("hello world");
    expect(cookies.malformed).toBe("%E0%A4%A");
    expect(cookies.quoted).toBe("quoted value");
    expect(Object.hasOwn(cookies, "__proto__")).toBe(false);
    expect(Object.hasOwn(cookies, "constructor")).toBe(false);
    expect(Object.hasOwn(cookies, "toString")).toBe(false);
  });
});

describe("checkHasConditions", () => {
  function makeCtx(
    overrides: Partial<{
      headers: Record<string, string>;
      cookies: Record<string, string>;
      query: Record<string, string>;
      host: string;
    }> = {},
  ) {
    const headers = new Headers(overrides.headers ?? {});
    if (overrides.cookies) {
      headers.set(
        "cookie",
        Object.entries(overrides.cookies)
          .map(([k, v]) => `${k}=${v}`)
          .join("; "),
      );
    }
    const query = new URLSearchParams(overrides.query ?? {});
    return {
      headers,
      cookies: overrides.cookies ?? {},
      query,
      host: overrides.host ?? "localhost",
    };
  }

  it("returns true when no conditions", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(checkHasConditions(undefined, undefined, makeCtx())).toBe(true);
  });

  // -- header conditions --
  it("has header: passes when header present", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ headers: { "x-custom": "yes" } });
    expect(checkHasConditions([{ type: "header", key: "x-custom" }], undefined, ctx)).toBe(true);
  });

  it("has header: fails when header absent", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({});
    expect(checkHasConditions([{ type: "header", key: "x-custom" }], undefined, ctx)).toBe(false);
  });

  it("has header with value: matches regex", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ headers: { "x-auth": "yes" } });
    expect(
      checkHasConditions(
        [{ type: "header", key: "x-auth", value: "(?:yes|true)" }],
        undefined,
        ctx,
      ),
    ).toBe(true);
    expect(
      checkHasConditions(
        [{ type: "header", key: "x-auth", value: "(?:no|false)" }],
        undefined,
        ctx,
      ),
    ).toBe(false);
  });

  // -- cookie conditions --
  it("has cookie: passes when cookie present", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: { session: "abc" } });
    expect(checkHasConditions([{ type: "cookie", key: "session" }], undefined, ctx)).toBe(true);
  });

  it("has cookie: fails when cookie absent", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: {} });
    expect(checkHasConditions([{ type: "cookie", key: "session" }], undefined, ctx)).toBe(false);
  });

  it("has cookie with exact value", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: { authorized: "true" } });
    expect(
      checkHasConditions([{ type: "cookie", key: "authorized", value: "true" }], undefined, ctx),
    ).toBe(true);
    expect(
      checkHasConditions([{ type: "cookie", key: "authorized", value: "false" }], undefined, ctx),
    ).toBe(false);
  });

  it("config cookie conditions ignore inherited names", async () => {
    const { checkHasConditions, requestContextFromRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = requestContextFromRequest(
      new Request("https://example.com/account", {
        headers: {
          cookie:
            "session=trusted; session=attacker; constructor=constructor-cookie; toString=string-cookie",
        },
      }),
    );

    expect(
      checkHasConditions([{ type: "cookie", key: "session", value: "trusted" }], undefined, ctx),
    ).toBe(true);
    expect(
      checkHasConditions([{ type: "cookie", key: "session", value: "attacker" }], undefined, ctx),
    ).toBe(false);
    expect(checkHasConditions([{ type: "cookie", key: "constructor" }], undefined, ctx)).toBe(
      false,
    );
    expect(checkHasConditions([{ type: "cookie", key: "toString" }], undefined, ctx)).toBe(false);
  });

  // -- query conditions --
  it("has query: passes when query param present", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ query: { page: "home" } });
    expect(checkHasConditions([{ type: "query", key: "page" }], undefined, ctx)).toBe(true);
  });

  it("has query: fails when query param absent", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ query: {} });
    expect(checkHasConditions([{ type: "query", key: "page" }], undefined, ctx)).toBe(false);
  });

  it("has query with regex value", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ query: { page: "home" } });
    expect(
      checkHasConditions([{ type: "query", key: "page", value: "home|about" }], undefined, ctx),
    ).toBe(true);
    expect(
      checkHasConditions([{ type: "query", key: "page", value: "^settings$" }], undefined, ctx),
    ).toBe(false);
  });

  // -- host conditions --
  it("has host: matches exact value", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ host: "example.com" });
    expect(
      checkHasConditions([{ type: "host", key: "", value: "example.com" }], undefined, ctx),
    ).toBe(true);
    expect(
      checkHasConditions([{ type: "host", key: "", value: "other.com" }], undefined, ctx),
    ).toBe(false);
  });

  it("has host: matches regex value", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ host: "staging.example.com" });
    expect(
      checkHasConditions([{ type: "host", key: "", value: ".*\\.example\\.com" }], undefined, ctx),
    ).toBe(true);
  });

  // -- missing conditions --
  it("missing header: passes when header absent", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({});
    expect(checkHasConditions(undefined, [{ type: "header", key: "x-block" }], ctx)).toBe(true);
  });

  it("missing header: fails when header present", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ headers: { "x-block": "1" } });
    expect(checkHasConditions(undefined, [{ type: "header", key: "x-block" }], ctx)).toBe(false);
  });

  it("missing cookie: passes when cookie absent", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: {} });
    expect(checkHasConditions(undefined, [{ type: "cookie", key: "stay-here" }], ctx)).toBe(true);
  });

  it("missing cookie: fails when cookie present", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: { "stay-here": "1" } });
    expect(checkHasConditions(undefined, [{ type: "cookie", key: "stay-here" }], ctx)).toBe(false);
  });

  // -- combined has + missing --
  it("both has and missing must pass", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: { auth: "yes" } });
    // has: cookie auth present (passes), missing: cookie block absent (passes)
    expect(
      checkHasConditions(
        [{ type: "cookie", key: "auth" }],
        [{ type: "cookie", key: "block" }],
        ctx,
      ),
    ).toBe(true);
    // has: cookie auth present (passes), missing: cookie auth absent (fails — it's present)
    expect(
      checkHasConditions([{ type: "cookie", key: "auth" }], [{ type: "cookie", key: "auth" }], ctx),
    ).toBe(false);
  });

  it("all has conditions must match (conjunction)", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const ctx = makeCtx({ cookies: { a: "1" }, query: { page: "home" } });
    // Both match
    expect(
      checkHasConditions(
        [
          { type: "cookie", key: "a" },
          { type: "query", key: "page" },
        ],
        undefined,
        ctx,
      ),
    ).toBe(true);
    // One doesn't match
    expect(
      checkHasConditions(
        [
          { type: "cookie", key: "a" },
          { type: "query", key: "missing" },
        ],
        undefined,
        ctx,
      ),
    ).toBe(false);
  });
});

describe("matchHeaders", () => {
  function makeCtx(
    overrides: Partial<{
      headers: Record<string, string>;
      cookies: Record<string, string>;
      query: Record<string, string>;
      host: string;
    }> = {},
  ) {
    const headers = new Headers(overrides.headers ?? {});
    if (overrides.cookies) {
      headers.set(
        "cookie",
        Object.entries(overrides.cookies)
          .map(([k, v]) => `${k}=${v}`)
          .join("; "),
      );
    }
    const query = new URLSearchParams(overrides.query ?? {});
    return {
      headers,
      cookies: overrides.cookies ?? {},
      query,
      host: overrides.host ?? "localhost",
    };
  }

  it("applies headers when has header condition is satisfied", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/about",
        has: [{ type: "header", key: "x-user-tier", value: "pro" }],
        headers: [{ key: "x-conditional-header", value: "enabled" }],
      },
    ];

    const matched = matchHeaders("/about", rules, makeCtx({ headers: { "x-user-tier": "pro" } }));
    expect(matched).toEqual([{ key: "x-conditional-header", value: "enabled" }]);
  });

  it("does not apply headers when missing cookie condition fails", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/about",
        missing: [{ type: "cookie", key: "no-config-header" }],
        headers: [{ key: "x-conditional-header", value: "enabled" }],
      },
    ];

    const matched = matchHeaders(
      "/about",
      rules,
      makeCtx({ cookies: { "no-config-header": "1" } }),
    );
    expect(matched).toEqual([]);
  });

  it("applies headers when has query condition is satisfied", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/about",
        has: [{ type: "query", key: "preview", value: "1" }],
        headers: [{ key: "x-preview-header", value: "true" }],
      },
    ];

    const matched = matchHeaders("/about", rules, makeCtx({ query: { preview: "1" } }));
    expect(matched).toEqual([{ key: "x-preview-header", value: "true" }]);
  });

  it("skips conditional header rule when has condition is not met", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/about",
        has: [{ type: "header", key: "x-user-tier", value: "pro" }],
        headers: [{ key: "x-conditional-header", value: "enabled" }],
      },
    ];

    // Request without the required header should not match
    const matched = matchHeaders("/about", rules, makeCtx());
    expect(matched).toEqual([]);
  });

  // Regression for #1331: under `trailingSlash: true` the incoming pathname
  // arrives as `/about/`, but header source patterns are written without a
  // trailing slash. `matchHeaders` must strip the slash before matching.
  it("matches when the request pathname has a trailing slash", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/about",
        headers: [{ key: "x-static-header", value: "1" }],
      },
      {
        source: "/api/:path*",
        headers: [{ key: "x-api-header", value: "1" }],
      },
    ];

    const aboutMatched = matchHeaders("/about/", rules, makeCtx());
    expect(aboutMatched).toEqual([{ key: "x-static-header", value: "1" }]);

    const apiMatched = matchHeaders("/api/users/", rules, makeCtx());
    expect(apiMatched).toEqual([{ key: "x-api-header", value: "1" }]);
  });
});

describe("matchHeaders compiled source cache", () => {
  // Regression test: escapeHeaderSource() + safeRegExp() were re-run on every
  // request for every header rule. The result is now cached in _compiledHeaderSourceCache
  // keyed by rule.source so subsequent calls skip the tokeniser and isSafeRegex.
  function makeCtx(h: Record<string, string> = {}) {
    return {
      headers: new Headers(h),
      cookies: {},
      query: new URLSearchParams(),
      host: "localhost",
    };
  }

  it("returns consistent results when the same source is matched multiple times", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/blog/:slug",
        headers: [{ key: "x-content-type", value: "article" }],
      },
    ];

    // First call — populates _compiledHeaderSourceCache.
    const first = matchHeaders("/blog/hello-world", rules, makeCtx());
    expect(first).toEqual([{ key: "x-content-type", value: "article" }]);

    // Second call — must hit the cache and return the same result.
    const second = matchHeaders("/blog/hello-world", rules, makeCtx());
    expect(second).toEqual(first);

    // Different matching pathname, same rule — still uses cached regex.
    const third = matchHeaders("/blog/another-post", rules, makeCtx());
    expect(third).toEqual([{ key: "x-content-type", value: "article" }]);

    // Non-matching pathname.
    const fourth = matchHeaders("/about", rules, makeCtx());
    expect(fourth).toEqual([]);
  });

  it("caches regex-bearing source patterns (containing `(`)", async () => {
    const { matchHeaders } = await import("../packages/vinext/src/config/config-matchers.js");
    const rules: any[] = [
      {
        source: "/:locale(en|fr|de)/blog",
        headers: [{ key: "x-locale-blog", value: "1" }],
      },
    ];

    const first = matchHeaders("/en/blog", rules, makeCtx());
    expect(first).toEqual([{ key: "x-locale-blog", value: "1" }]);

    // Cache hit — same source pattern, different matching pathname.
    const second = matchHeaders("/fr/blog", rules, makeCtx());
    expect(second).toEqual([{ key: "x-locale-blog", value: "1" }]);

    // Non-matching locale.
    const third = matchHeaders("/zh/blog", rules, makeCtx());
    expect(third).toEqual([]);
  });
});

describe("checkHasConditions condition value cache", () => {
  // Regression test: safeRegExp(condition.value) was called on every request
  // for every has/missing condition. The result is now cached in
  // _compiledConditionCache keyed by value so isSafeRegex runs at most once.
  function makeCtx(
    overrides: {
      headers?: Record<string, string>;
      cookies?: Record<string, string>;
      query?: Record<string, string>;
      host?: string;
    } = {},
  ) {
    return {
      headers: new Headers(overrides.headers ?? {}),
      cookies: overrides.cookies ?? {},
      query: new URLSearchParams(overrides.query ?? {}),
      host: overrides.host ?? "localhost",
    };
  }

  it("returns consistent results when the same condition value is evaluated multiple times", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const has = [{ type: "header" as const, key: "x-tier", value: "pro|enterprise" }];

    // First call — populates _compiledConditionCache for "pro|enterprise".
    expect(checkHasConditions(has, undefined, makeCtx({ headers: { "x-tier": "pro" } }))).toBe(
      true,
    );

    // Second call — must hit the cache.
    expect(
      checkHasConditions(has, undefined, makeCtx({ headers: { "x-tier": "enterprise" } })),
    ).toBe(true);

    // Non-matching value.
    expect(checkHasConditions(has, undefined, makeCtx({ headers: { "x-tier": "free" } }))).toBe(
      false,
    );
  });

  it("caches condition values across all condition types (header, cookie, query, host)", async () => {
    const { checkHasConditions } = await import("../packages/vinext/src/config/config-matchers.js");
    const sharedPattern = "^v\\d+$"; // a pattern that will be cached once and reused

    // header
    const hasHeader = [{ type: "header" as const, key: "x-version", value: sharedPattern }];
    expect(
      checkHasConditions(hasHeader, undefined, makeCtx({ headers: { "x-version": "v3" } })),
    ).toBe(true);
    expect(
      checkHasConditions(hasHeader, undefined, makeCtx({ headers: { "x-version": "v3" } })),
    ).toBe(true);

    // cookie — same pattern string, should hit cache populated by header call above
    const hasCookie = [{ type: "cookie" as const, key: "ver", value: sharedPattern }];
    expect(checkHasConditions(hasCookie, undefined, makeCtx({ cookies: { ver: "v1" } }))).toBe(
      true,
    );
    expect(checkHasConditions(hasCookie, undefined, makeCtx({ cookies: { ver: "beta" } }))).toBe(
      false,
    );

    // query
    const hasQuery = [{ type: "query" as const, key: "v", value: sharedPattern }];
    expect(checkHasConditions(hasQuery, undefined, makeCtx({ query: { v: "v2" } }))).toBe(true);

    // host
    const hasHost = [{ type: "host" as const, key: "", value: sharedPattern }];
    expect(checkHasConditions(hasHost, undefined, makeCtx({ host: "v9" }))).toBe(true);
    expect(checkHasConditions(hasHost, undefined, makeCtx({ host: "prod" }))).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// isExternalUrl unit tests (external rewrite detection)

describe("isExternalUrl", () => {
  it("returns true for https:// URLs", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("https://example.com/path")).toBe(true);
    expect(isExternalUrl("https://us.i.posthog.com/decide?v=3")).toBe(true);
  });

  it("returns true for http:// URLs", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("http://example.com/api")).toBe(true);
  });

  it("returns false for relative paths", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("/about")).toBe(false);
    expect(isExternalUrl("/api/test")).toBe(false);
    expect(isExternalUrl("/")).toBe(false);
  });

  it("returns true for protocol-relative URLs", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("//example.com")).toBe(true);
    expect(isExternalUrl("//cdn.example.com/image.png")).toBe(true);
  });

  it("returns true for exotic URL schemes (data:, javascript:, blob:, ftp:)", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("data:text/html,<h1>hi</h1>")).toBe(true);
    expect(isExternalUrl("javascript:alert(1)")).toBe(true);
    expect(isExternalUrl("blob:http://localhost/abc")).toBe(true);
    expect(isExternalUrl("ftp://files.example.com/pub")).toBe(true);
  });

  it("returns false for hash-only and bare strings", async () => {
    const { isExternalUrl } = await import("../packages/vinext/src/config/config-matchers.js");
    expect(isExternalUrl("#section")).toBe(false);
    expect(isExternalUrl("about")).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// proxyExternalRequest unit tests

describe("proxyExternalRequest", () => {
  it("proxies request to external URL and returns upstream response", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    // Use a well-known public URL that returns a predictable response
    const request = new Request("http://localhost:3000/test?extra=1", {
      method: "GET",
      headers: { "user-agent": "vinext-test" },
    });

    // We test the function constructs the right request by mocking fetch
    const originalFetch = globalThis.fetch;
    let capturedUrl: string | undefined;
    let capturedInit: any;
    globalThis.fetch = async (url: any, init: any) => {
      capturedUrl = typeof url === "string" ? url : url.toString();
      capturedInit = init;
      return new Response("proxied body", {
        status: 200,
        headers: { "content-type": "text/plain", "x-upstream": "true" },
      });
    };

    try {
      const response = await proxyExternalRequest(request, "https://api.example.com/endpoint");
      expect(capturedUrl).toContain("https://api.example.com/endpoint");
      // Extra query param from original request should be merged
      expect(capturedUrl).toContain("extra=1");
      expect(capturedInit.method).toBe("GET");
      expect(capturedInit.redirect).toBe("manual");
      // Host header should be set to the external target
      expect(capturedInit.headers.get("host")).toBe("api.example.com");
      expect(response.status).toBe(200);
      expect(response.headers.get("x-upstream")).toBe("true");
      const body = await response.text();
      expect(body).toBe("proxied body");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("preserves query parameters from the rewrite destination", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test", {
      method: "GET",
    });

    const originalFetch = globalThis.fetch;
    let capturedUrl: string | undefined;
    globalThis.fetch = async (url: any, _init: any) => {
      capturedUrl = typeof url === "string" ? url : url.toString();
      return new Response("ok", { status: 200 });
    };

    try {
      await proxyExternalRequest(request, "https://api.example.com/v1?key=abc");
      expect(capturedUrl).toContain("key=abc");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("preserves repeated original query params when the destination does not define that key", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test?a=1&a=2&b=3", {
      method: "GET",
    });

    const originalFetch = globalThis.fetch;
    let capturedEntries: Array<[string, string]> | undefined;
    globalThis.fetch = async (url: any, _init: any) => {
      capturedEntries = [...new URL(typeof url === "string" ? url : url.toString()).searchParams];
      return new Response("ok", { status: 200 });
    };

    try {
      await proxyExternalRequest(request, "https://api.example.com/v1");
      expect(capturedEntries).toEqual([
        ["a", "1"],
        ["a", "2"],
        ["b", "3"],
      ]);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("keeps destination query params authoritative while preserving repeated values for other keys", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test?a=1&a=2&b=3&b=4", {
      method: "GET",
    });

    const originalFetch = globalThis.fetch;
    let capturedEntries: Array<[string, string]> | undefined;
    globalThis.fetch = async (url: any, _init: any) => {
      capturedEntries = [...new URL(typeof url === "string" ? url : url.toString()).searchParams];
      return new Response("ok", { status: 200 });
    };

    try {
      await proxyExternalRequest(request, "https://api.example.com/v1?a=dest");
      expect(capturedEntries).toEqual([
        ["a", "dest"],
        ["b", "3"],
        ["b", "4"],
      ]);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("strips hop-by-hop headers from upstream response", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test");

    const originalFetch = globalThis.fetch;
    globalThis.fetch = async (_url: any, _init: any) =>
      new Response("ok", {
        status: 200,
        headers: {
          "content-type": "text/plain",
          "x-custom": "preserved",
          // Note: "transfer-encoding" and "connection" are hop-by-hop headers
          // that should be stripped. However, the fetch API may not allow
          // setting them on Response, so we test with headers that can be set.
        },
      });

    try {
      const response = await proxyExternalRequest(request, "https://api.example.com/test");
      expect(response.headers.get("content-type")).toBe("text/plain");
      expect(response.headers.get("x-custom")).toBe("preserved");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("passes through non-200 status codes", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test");

    const originalFetch = globalThis.fetch;
    globalThis.fetch = async (_url: any, _init: any) => new Response("Not Found", { status: 404 });

    try {
      const response = await proxyExternalRequest(request, "https://api.example.com/missing");
      expect(response.status).toBe(404);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("forwards credential headers and strips x-middleware-* headers from proxied requests", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/proxy", {
      method: "GET",
      headers: {
        cookie: "session=secret123",
        authorization: "Bearer tok_secret",
        "x-api-key": "sk_live_secret",
        "proxy-authorization": "Basic cHJveHk=",
        "x-middleware-rewrite": "/internal",
        "x-middleware-next": "1",
        "x-vinext-prerender-secret": "build-secret-123",
        "x-vinext-prerender-route-params": "%7B%22id%22%3A%22forged%22%7D",
        "x-custom-header": "keep-me",
        "user-agent": "vinext-test",
      },
    });

    const originalFetch = globalThis.fetch;
    let capturedHeaders: Headers | undefined;
    globalThis.fetch = async (_url: any, init: any) => {
      capturedHeaders = init.headers;
      return new Response("ok", { status: 200 });
    };

    try {
      await proxyExternalRequest(request, "https://api.example.com/data");
      expect(capturedHeaders).toBeDefined();
      // Credential headers must be forwarded to match Next.js external rewrite proxying.
      expect(capturedHeaders!.get("cookie")).toBe("session=secret123");
      expect(capturedHeaders!.get("authorization")).toBe("Bearer tok_secret");
      expect(capturedHeaders!.get("x-api-key")).toBe("sk_live_secret");
      expect(capturedHeaders!.get("proxy-authorization")).toBe("Basic cHJveHk=");
      // Internal middleware headers must be stripped
      expect(capturedHeaders!.get("x-middleware-rewrite")).toBeNull();
      expect(capturedHeaders!.get("x-middleware-next")).toBeNull();
      expect(capturedHeaders!.get("x-vinext-prerender-secret")).toBeNull();
      expect(capturedHeaders!.get("x-vinext-prerender-route-params")).toBeNull();
      // Non-sensitive headers must be preserved
      expect(capturedHeaders!.get("x-custom-header")).toBe("keep-me");
      expect(capturedHeaders!.get("user-agent")).toBe("vinext-test");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("strips hop-by-hop request headers before proxying external rewrites", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/proxy", {
      method: "DELETE",
      headers: {
        connection: "keep-alive, x-custom-hop",
        "keep-alive": "timeout=5",
        te: "trailers",
        trailers: "x-trailer",
        "transfer-encoding": "chunked",
        upgrade: "websocket",
        "x-custom-hop": "secret",
        "proxy-authorization": "Basic cHJveHk=",
        "x-custom-header": "keep-me",
      },
    });

    const originalFetch = globalThis.fetch;
    let capturedHeaders: Headers | undefined;
    globalThis.fetch = async (_url: any, init: any) => {
      capturedHeaders = init.headers;
      return new Response("ok", { status: 200 });
    };

    try {
      await proxyExternalRequest(request, "https://api.example.com/data");
      expect(capturedHeaders).toBeDefined();
      expect(capturedHeaders!.get("connection")).toBeNull();
      expect(capturedHeaders!.get("keep-alive")).toBeNull();
      expect(capturedHeaders!.get("te")).toBeNull();
      expect(capturedHeaders!.get("trailers")).toBeNull();
      expect(capturedHeaders!.get("transfer-encoding")).toBeNull();
      expect(capturedHeaders!.get("upgrade")).toBeNull();
      expect(capturedHeaders!.get("x-custom-hop")).toBeNull();
      // Request credentials that are not connection-scoped should still forward.
      expect(capturedHeaders!.get("proxy-authorization")).toBe("Basic cHJveHk=");
      expect(capturedHeaders!.get("x-custom-header")).toBe("keep-me");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("forwards redirect responses without following them", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test");

    const originalFetch = globalThis.fetch;
    globalThis.fetch = async (_url: any, init: any) => {
      // Verify redirect: "manual" was set
      expect(init.redirect).toBe("manual");
      return new Response(null, {
        status: 301,
        headers: { location: "https://other.example.com/new" },
      });
    };

    try {
      const response = await proxyExternalRequest(request, "https://api.example.com/old");
      expect(response.status).toBe(301);
      expect(response.headers.get("location")).toBe("https://other.example.com/new");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("strips content-encoding and content-length from proxied response to avoid double-decompression", async () => {
    const { proxyExternalRequest } =
      await import("../packages/vinext/src/config/config-matchers.js");

    const request = new Request("http://localhost:3000/test");

    const originalFetch = globalThis.fetch;
    globalThis.fetch = async (_url: any, _init: any) =>
      new Response("decompressed body", {
        status: 200,
        headers: {
          "content-type": "application/json",
          "content-encoding": "gzip",
          "content-length": "42",
          "x-custom": "keep",
        },
      });

    try {
      const response = await proxyExternalRequest(request, "https://api.example.com/data");
      // content-encoding and content-length must be stripped to prevent
      // ERR_CONTENT_DECODING_FAILED in the browser (double-decompression bug).
      expect(response.headers.get("content-encoding")).toBeNull();
      expect(response.headers.get("content-length")).toBeNull();
      // Other headers must be preserved
      expect(response.headers.get("content-type")).toBe("application/json");
      expect(response.headers.get("x-custom")).toBe("keep");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });
});

// ---------------------------------------------------------------------------
// matchRewrite + isExternalUrl integration (config-matchers)

describe("matchRewrite with external URLs", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  it("returns full external URL when destination is external", async () => {
    const { matchRewrite, isExternalUrl } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/ph/:path*", destination: "https://us.i.posthog.com/:path*" }];
    const result = matchRewrite("/ph/decide", rewrites, emptyCtx);
    expect(result).toBe("https://us.i.posthog.com/decide");
    expect(isExternalUrl(result!)).toBe(true);
  });

  it("returns full external URL for static path rewrites", async () => {
    const { matchRewrite, isExternalUrl } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [
      { source: "/ph/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*" },
    ];
    const result = matchRewrite("/ph/static/array.js", rewrites, emptyCtx);
    expect(result).toBe("https://us-assets.i.posthog.com/static/array.js");
    expect(isExternalUrl(result!)).toBe(true);
  });

  it("returns internal path for non-external rewrites", async () => {
    const { matchRewrite, isExternalUrl } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/posts/:id", destination: "/blog/:id" }];
    const result = matchRewrite("/posts/hello", rewrites, emptyCtx);
    expect(result).toBe("/blog/hello");
    expect(isExternalUrl(result!)).toBe(false);
  });

  it("appends unused rewrite source params to the destination query", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/blog-:param", destination: "/blog/post-3" }];
    const result = matchRewrite("/blog-post-3", rewrites, emptyCtx);
    expect(result).toBe("/blog/post-3?param=post-3");
  });

  it("does not append rewrite source params consumed by the destination hash", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/old/:id", destination: "/new#section-:id" }];
    const result = matchRewrite("/old/123", rewrites, emptyCtx);
    expect(result).toBe("/new#section-123");
  });

  it("replaces repeated params in rewrite destinations", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/post/:id", destination: "/api/:id/:id" }];
    const result = matchRewrite("/post/123", rewrites, emptyCtx);
    expect(result).toBe("/api/123/123");
  });

  it("replaces adjacent params separated by literal characters", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/legacy/:year/:month", destination: "/archive/:year-:month" }];
    const result = matchRewrite("/legacy/2024/06", rewrites, emptyCtx);
    expect(result).toBe("/archive/2024-06");
  });

  it("replaces hyphenated param names without truncating them", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/auth/:auth-method", destination: "/signin/:auth-method" }];
    const result = matchRewrite("/auth/google", rewrites, emptyCtx);
    expect(result).toBe("/signin/google");
  });

  it("treats hyphen as a literal delimiter when only the shorter param key exists", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/item/:foo", destination: "/dest/:foo-bar" }];
    const result = matchRewrite("/item/123", rewrites, emptyCtx);
    expect(result).toBe("/dest/123-bar");
  });

  it("substitutes named captures from has conditions into rewrite destinations", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    // Ported from documented Next.js behavior:
    // https://github.com/vercel/next.js/blob/canary/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx
    const rewrites = [
      {
        source: "/:path*",
        has: [{ type: "header" as const, key: "x-authorized", value: "(?<authorized>yes|true)" }],
        destination: "/home?authorized=:authorized&path=:path*",
      },
    ];
    const result = matchRewrite("/docs/intro", rewrites, {
      ...emptyCtx,
      headers: new Headers({ "x-authorized": "yes" }),
    });
    expect(result).toBe("/home?authorized=yes&path=docs/intro");
  });

  it("appends source params not referenced by the rewrite destination", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const result = matchRewrite(
      "/query-rewrite/hello/world",
      [
        {
          source: "/query-rewrite/:section/:name",
          destination: "/with-params?first=:section&second=:name",
        },
      ],
      emptyCtx,
    );

    expect(result).toBe("/with-params?first=hello&second=world&section=hello&name=world");
  });

  it("appends unused condition captures to the rewrite destination query", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const result = matchRewrite(
      "/source",
      [
        {
          source: "/source",
          destination: "/target",
          has: [{ type: "query", key: "preview", value: "(?<mode>draft)" }],
        },
      ],
      { ...emptyCtx, query: new URLSearchParams("preview=draft") },
    );

    expect(result).toBe("/target?mode=draft");
  });

  it("does not append rewrite params consumed by the destination fragment", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const result = matchRewrite(
      "/docs/500",
      [{ source: "/docs/:code", destination: "/status#:code" }],
      emptyCtx,
    );

    expect(result).toBe("/status#500");
  });
});

describe("matchRedirect destination param substitution", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  it("replaces repeated params in redirect destinations", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [{ source: "/post/:id", destination: "/api/:id/:id", permanent: false }];
    const result = matchRedirect("/post/123", redirects, emptyCtx);
    expect(result).toEqual({ destination: "/api/123/123", permanent: false });
  });

  it("replaces adjacent params separated by literal characters in redirect destinations", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [
      { source: "/legacy/:year/:month", destination: "/archive/:year-:month", permanent: true },
    ];
    const result = matchRedirect("/legacy/2024/06", redirects, emptyCtx);
    expect(result).toEqual({ destination: "/archive/2024-06", permanent: true });
  });

  it("replaces repeated locale params in locale-static redirect destinations", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [
      {
        source: "/:locale(en|fr)?/docs",
        destination: "/:locale/:locale/docs",
        permanent: false,
      },
    ];
    const result = matchRedirect("/en/docs", redirects, emptyCtx);
    expect(result).toEqual({ destination: "/en/en/docs", permanent: false });
  });

  it("substitutes named captures from has conditions into redirect destinations", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // Ported from documented Next.js behavior:
    // https://github.com/vercel/next.js/blob/canary/docs/01-app/03-api-reference/05-config/01-next-config-js/redirects.mdx
    const redirects = [
      {
        source: "/",
        has: [{ type: "header" as const, key: "x-authorized", value: "(?<authorized>yes|true)" }],
        destination: "/home?authorized=:authorized",
        permanent: false,
      },
    ];
    const result = matchRedirect("/", redirects, {
      ...emptyCtx,
      headers: new Headers({ "x-authorized": "yes" }),
    });
    expect(result).toEqual({ destination: "/home?authorized=yes", permanent: false });
  });
});

// ---------------------------------------------------------------------------
// sanitizeDestination — protocol-relative URL handling

describe("sanitizeDestination", () => {
  it("collapses leading // to / for relative URLs", async () => {
    const { sanitizeDestination } =
      await import("../packages/vinext/src/config/config-matchers.js");
    expect(sanitizeDestination("//evil.com")).toBe("/evil.com");
    expect(sanitizeDestination("///evil.com")).toBe("/evil.com");
    expect(sanitizeDestination("////evil.com/path")).toBe("/evil.com/path");
  });

  it("preserves external http:// and https:// URLs", async () => {
    const { sanitizeDestination } =
      await import("../packages/vinext/src/config/config-matchers.js");
    expect(sanitizeDestination("https://example.com/path")).toBe("https://example.com/path");
    expect(sanitizeDestination("http://example.com")).toBe("http://example.com");
  });

  it("normalizes leading backslashes (browsers treat \\ as /)", async () => {
    const { sanitizeDestination } =
      await import("../packages/vinext/src/config/config-matchers.js");
    expect(sanitizeDestination("\\/evil.com")).toBe("/evil.com");
    expect(sanitizeDestination("\\\\evil.com")).toBe("/evil.com");
    expect(sanitizeDestination("\\\\/evil.com")).toBe("/evil.com");
    expect(sanitizeDestination("/\\evil.com")).toBe("/evil.com");
  });

  it("preserves normal relative paths", async () => {
    const { sanitizeDestination } =
      await import("../packages/vinext/src/config/config-matchers.js");
    expect(sanitizeDestination("/about")).toBe("/about");
    expect(sanitizeDestination("/blog/hello")).toBe("/blog/hello");
    expect(sanitizeDestination("/")).toBe("/");
  });
});

// ---------------------------------------------------------------------------
// Catch-all redirect destination sanitization

describe("open redirect prevention in catch-all redirects", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  it("matchRedirect sanitizes decoded %2F that would produce //evil.com", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    // In the real request flow, the entry point decodes %2F to / and
    // normalizePath collapses // to /. So /old/%2Fevil.com arrives as
    // /old/evil.com (after decode + normalize).
    // Test with the already-decoded path (how matchRedirect is actually called).
    const redirects = [{ source: "/old/:path*", destination: "/:path*", permanent: false }];
    const result = matchRedirect("/old/evil.com", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("/evil.com");
    // Verify it does NOT start with // (protocol-relative)
    expect(result!.destination.startsWith("//")).toBe(false);
  });

  it("matchRedirect sanitizes double-slash in already-decoded paths", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [{ source: "/old/:path*", destination: "/:path*", permanent: false }];
    // Even if an already-decoded path somehow contains //, the sanitizer should handle it
    const result = matchRedirect("/old//evil.com", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination.startsWith("//")).toBe(false);
  });

  it("matchRedirect preserves valid external redirect destinations", async () => {
    const { matchRedirect } = await import("../packages/vinext/src/config/config-matchers.js");
    const redirects = [
      { source: "/go/:path*", destination: "https://example.com/:path*", permanent: false },
    ];
    const result = matchRedirect("/go/page", redirects, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!.destination).toBe("https://example.com/page");
  });

  it("matchRewrite sanitizes decoded %2F that would produce //evil.com", async () => {
    const { matchRewrite } = await import("../packages/vinext/src/config/config-matchers.js");
    const rewrites = [{ source: "/old/:path*", destination: "/:path*" }];
    // In the real request flow, the entry point decodes and normalizePath
    // collapses //. Test with already-decoded path.
    const result = matchRewrite("/old/evil.com", rewrites, emptyCtx);
    expect(result).not.toBeNull();
    expect(result!).toBe("/evil.com");
    expect(result!.startsWith("//")).toBe(false);
  });
});

describe("next/form shim", () => {
  it("exports default Form component", async () => {
    const mod = await import("../packages/vinext/src/shims/form.js");
    expect(mod.default).toBeDefined();
    expect(typeof mod.default).toBe("object"); // forwardRef returns an object
  });

  it("re-exports useActionState from React", async () => {
    const mod = await import("../packages/vinext/src/shims/form.js");
    expect(typeof mod.useActionState).toBe("function");
  });

  it("renders a form element with string action in SSR", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { default: Form } = await import("../packages/vinext/src/shims/form.js");

    const html = renderToStaticMarkup(
      React.createElement(
        Form,
        { action: "/search" },
        React.createElement("input", { name: "q" }),
        React.createElement("button", { type: "submit" }, "Search"),
      ),
    );
    expect(html).toContain("<form");
    expect(html).toContain('action="/search"');
    expect(html).toContain('name="q"');
    expect(html).toContain("Search");
  });

  it("renders a form with method prop — strips method and warns", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { default: Form } = await import("../packages/vinext/src/shims/form.js");

    const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    const html = renderToStaticMarkup(
      React.createElement(
        Form,
        // Cast to any: testing that a disallowed prop is stripped and warned about
        { action: "/api/submit", method: "POST" } as any,
        React.createElement("button", { type: "submit" }, "Submit"),
      ),
    );
    expect(html).toContain("<form");
    // method is a DISALLOWED_FORM_PROP — it must be stripped from the rendered output
    expect(html).not.toContain('method="POST"');
    // a dev warning must be emitted
    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining("<Form> does not support changing `method`"),
    );
    warnSpy.mockRestore();
  });

  it("renders children inside the form", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { default: Form } = await import("../packages/vinext/src/shims/form.js");

    const html = renderToStaticMarkup(
      React.createElement(
        Form,
        { action: "/search" },
        React.createElement("label", null, "Query:"),
        React.createElement("input", { name: "q", placeholder: "Search..." }),
        React.createElement("button", null, "Go"),
      ),
    );
    expect(html).toContain("Query:");
    expect(html).toContain('placeholder="Search..."');
    expect(html).toContain("Go");
  });

  it("passes className and id through to form element", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { default: Form } = await import("../packages/vinext/src/shims/form.js");

    const html = renderToStaticMarkup(
      React.createElement(Form, { action: "/search", className: "search-form", id: "main-search" }),
    );
    expect(html).toContain('class="search-form"');
    expect(html).toContain('id="main-search"');
  });
});

describe("next/font/google shim", () => {
  it("returns className and style without variable unless requested for a Google Font", async () => {
    const { createFontLoader } = await import("../packages/vinext/src/shims/font-google.js");
    const Inter = createFontLoader("Inter");
    const result = Inter({ subsets: ["latin"], weight: ["400", "700"] });

    expect(result.className).toMatch(/^__font_inter_/);
    expect(result.style.fontFamily).toContain("Inter");
    expect(result.variable).toBeUndefined();
  });

  it("Proxy returns font loaders for any family", async () => {
    const mod = await import("../packages/vinext/src/shims/font-google.js");
    const googleFonts = mod.default;
    const loader = googleFonts.Poppins;
    expect(typeof loader).toBe("function");

    const result = loader({ weight: "400" });
    expect(result.className).toMatch(/^__font_poppins_/);
    expect(result.style.fontFamily).toContain("Poppins");
  });

  it("converts PascalCase to font family name", async () => {
    const mod = await import("../packages/vinext/src/shims/font-google.js");
    const googleFonts = mod.default;
    const result = googleFonts.RobotoMono({ weight: "400" });

    expect(result.style.fontFamily).toContain("Roboto Mono");
    expect(result.variable).toBeUndefined();
  });

  it("uses custom variable name when provided", async () => {
    const { createFontLoader } = await import("../packages/vinext/src/shims/font-google.js");
    const Inter = createFontLoader("Inter");
    const result = Inter({ variable: "--custom-font" });
    // When custom variable is provided, the generated class still sets that variable
    // The returned value is still a class name, not the CSS variable name itself
    expect(result.variable).toMatch(/^__variable_inter_/);
  });

  it("uses custom fallback fonts", async () => {
    const { createFontLoader } = await import("../packages/vinext/src/shims/font-google.js");
    const Inter = createFontLoader("Inter");
    const result = Inter({ fallback: ["Helvetica", "Arial", "sans-serif"] });
    expect(result.style.fontFamily).toContain("Helvetica");
    expect(result.style.fontFamily).toContain("Arial");
  });

  it("generates CSS rules for className (SSR)", async () => {
    const { createFontLoader, getSSRFontStyles } =
      await import("../packages/vinext/src/shims/font-google.js");
    // Clear any previously collected styles
    getSSRFontStyles();

    const Inter = createFontLoader("Inter");
    const result = Inter({ subsets: ["latin"], weight: ["400"] });

    // getSSRFontStyles should return CSS rules mapping className to font-family
    const styles = getSSRFontStyles();
    const allCss = styles.join("\n");
    expect(allCss).toContain(`.${result.className}`);
    expect(allCss).toContain("font-family:");
    expect(allCss).toContain("Inter");
  });

  it("generates CSS variable rule when variable is specified", async () => {
    const { createFontLoader, getSSRFontStyles } =
      await import("../packages/vinext/src/shims/font-google.js");
    getSSRFontStyles(); // clear

    const Inter = createFontLoader("Inter");
    Inter({ variable: "--font-inter" });
    const styles = getSSRFontStyles();
    const allCss = styles.join("\n");
    expect(allCss).toContain("--font-inter:");
  });
});

describe("next/font/local shim", () => {
  it("returns className, style for a local font", async () => {
    const { default: localFont } = await import("../packages/vinext/src/shims/font-local.js");
    const result = localFont({ src: "./my-font.woff2" });

    expect(result.className).toMatch(/^__font_local_/);
    expect(result.style.fontFamily).toMatch(/__local_font_/);
  });

  it("includes variable as generated class name when specified", async () => {
    const { default: localFont } = await import("../packages/vinext/src/shims/font-local.js");
    const result = localFont({
      src: "./my-font.woff2",
      variable: "--font-custom",
    });
    // variable should be a generated class name, not the raw CSS variable
    expect(result.variable).toMatch(/^__variable_local_/);
    expect(result.variable).not.toBe("--font-custom");
  });

  it("accepts array of font sources", async () => {
    const { default: localFont } = await import("../packages/vinext/src/shims/font-local.js");
    const result = localFont({
      src: [
        { path: "./regular.woff2", weight: "400" },
        { path: "./bold.woff2", weight: "700" },
      ],
    });

    expect(result.className).toMatch(/^__font_local_/);
    expect(result.style.fontFamily).toBeTruthy();
  });

  it("does not include variable when not specified", async () => {
    const { default: localFont } = await import("../packages/vinext/src/shims/font-local.js");
    const result = localFont({ src: "./no-var.woff2" });
    expect(result.variable).toBeUndefined();
  });

  it("generates SSR font styles for className rules", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    const localFont = fontLocal.default;
    // In test (Node), typeof document === "undefined", so SSR path is used
    const result = localFont({
      src: "./ssr-test.woff2",
      variable: "--font-ssr-test",
    });

    const ssrStyles = fontLocal.getSSRFontStyles();
    expect(ssrStyles.length).toBeGreaterThan(0);

    // Should contain @font-face rule
    const allCSS = ssrStyles.join("\n");
    expect(allCSS).toContain("@font-face");
    expect(allCSS).toContain("ssr-test.woff2");

    // Should contain className rule
    expect(allCSS).toContain(`.${result.className}`);
    expect(allCSS).toContain("font-family:");

    // Should contain variable class rule with :root fallback
    expect(allCSS).toContain(`.${result.variable}`);
    expect(allCSS).toContain("--font-ssr-test");
    expect(allCSS).toContain(":root");
  });

  it("generates unique classNames and variableClassNames", async () => {
    const { default: localFont } = await import("../packages/vinext/src/shims/font-local.js");
    const a = localFont({ src: "./a.woff2", variable: "--font-a" });
    const b = localFont({ src: "./b.woff2", variable: "--font-b" });

    expect(a.className).not.toBe(b.className);
    expect(a.variable).not.toBe(b.variable);
    expect(a.variable).toMatch(/^__variable_local_/);
    expect(b.variable).toMatch(/^__variable_local_/);
  });

  it("exports getSSRFontPreloads function", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    expect(typeof fontLocal.getSSRFontPreloads).toBe("function");
  });

  it("collects preload data for fonts with absolute URLs", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    const localFont = fontLocal.default;

    // Simulate a font with an absolute URL (as resolved by Vite transform)
    localFont({ src: "/assets/my-font-abc123.woff2" });

    const preloads = fontLocal.getSSRFontPreloads();
    const match = preloads.find((p: any) => p.href === "/assets/my-font-abc123.woff2");
    expect(match).toBeDefined();
    expect(match!.type).toBe("font/woff2");
  });

  it("collects preload data for array font sources with absolute URLs", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    const localFont = fontLocal.default;

    localFont({
      src: [
        { path: "/assets/regular-abc.woff2", weight: "400" },
        { path: "/assets/bold-def.woff", weight: "700" },
      ],
    });

    const preloads = fontLocal.getSSRFontPreloads();
    const woff2 = preloads.find((p: any) => p.href === "/assets/regular-abc.woff2");
    const woff = preloads.find((p: any) => p.href === "/assets/bold-def.woff");
    expect(woff2).toBeDefined();
    expect(woff2!.type).toBe("font/woff2");
    expect(woff).toBeDefined();
    expect(woff!.type).toBe("font/woff");
  });

  it("does not collect preload data for relative URLs", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    const localFont = fontLocal.default;

    const preloadsBefore = fontLocal.getSSRFontPreloads().length;
    localFont({ src: "./relative-font.woff2" });
    const preloadsAfter = fontLocal.getSSRFontPreloads().length;

    // Relative URLs should NOT be added to preloads
    expect(preloadsAfter).toBe(preloadsBefore);
  });

  it("deduplicates preload entries by href", async () => {
    const fontLocal = await import("../packages/vinext/src/shims/font-local.js");
    const localFont = fontLocal.default;

    // Call twice with the same font URL
    localFont({ src: "/assets/dedup-test.woff2" });
    localFont({ src: "/assets/dedup-test.woff2" });

    const preloads = fontLocal.getSSRFontPreloads();
    const matches = preloads.filter((p: any) => p.href === "/assets/dedup-test.woff2");
    expect(matches.length).toBe(1);
  });
});

describe("next/og shim", () => {
  it("exports ImageResponse class", async () => {
    const og = await import("../packages/vinext/src/shims/og.js");
    expect(og.ImageResponse).toBeDefined();
    expect(typeof og.ImageResponse).toBe("function");
  });

  it("ImageResponse extends Response", async () => {
    const og = await import("../packages/vinext/src/shims/og.js");
    // Check the prototype chain
    expect(og.ImageResponse.prototype instanceof Response).toBe(true);
  });

  it("generates a PNG image from JSX", async () => {
    const React = await import("react");
    const og = await import("../packages/vinext/src/shims/og.js");

    // Simple colored div — no text so no font needed
    const element = React.createElement("div", {
      style: {
        display: "flex",
        width: "100%",
        height: "100%",
        backgroundColor: "#ff6600",
        alignItems: "center",
        justifyContent: "center",
      },
    });

    const response = new og.ImageResponse(element, {
      width: 100,
      height: 100,
    });

    expect(response).toBeInstanceOf(Response);
    expect(response.status).toBe(200);
    expect(response.headers.get("content-type")).toBe("image/png");

    // Read the response body — should be valid PNG data
    const buffer = await response.arrayBuffer();
    const bytes = new Uint8Array(buffer);

    // PNG magic bytes: 0x89 0x50 0x4E 0x47
    expect(bytes[0]).toBe(0x89);
    expect(bytes[1]).toBe(0x50); // P
    expect(bytes[2]).toBe(0x4e); // N
    expect(bytes[3]).toBe(0x47); // G
  });

  it("sets Next.js metadata image cache headers by default", async () => {
    const React = await import("react");
    const og = await import("../packages/vinext/src/shims/og.js");

    const element = React.createElement("div", {
      style: { display: "flex", width: "100%", height: "100%", backgroundColor: "blue" },
    });

    const response = new og.ImageResponse(element, {
      width: 50,
      height: 50,
    });

    expect(response.headers.get("cache-control")).toBe("public, max-age=0, must-revalidate");
  });

  it("respects custom status and headers", async () => {
    const React = await import("react");
    const og = await import("../packages/vinext/src/shims/og.js");

    const element = React.createElement("div", {
      style: { display: "flex", width: "100%", height: "100%", backgroundColor: "blue" },
    });

    const response = new og.ImageResponse(element, {
      width: 50,
      height: 50,
      status: 201,
      headers: { "x-custom": "test-value" },
    });

    expect(response.status).toBe(201);
    expect(response.headers.get("x-custom")).toBe("test-value");
    expect(response.headers.get("content-type")).toBe("image/png");
  });

  it("uses default dimensions of 1200x630", async () => {
    const React = await import("react");
    const og = await import("../packages/vinext/src/shims/og.js");

    const element = React.createElement("div", {
      style: { display: "flex", width: "100%", height: "100%", backgroundColor: "green" },
    });

    // No width/height specified — should use defaults
    const response = new og.ImageResponse(element);
    expect(response).toBeInstanceOf(Response);

    // Verify it produces valid PNG
    const buffer = await response.arrayBuffer();
    const bytes = new Uint8Array(buffer);
    expect(bytes[0]).toBe(0x89); // PNG magic
  });
});

describe("metadata route serializers", () => {
  it("sitemapToXml converts sitemap entries to valid XML", async () => {
    const { sitemapToXml } = await import("../packages/vinext/src/server/metadata-routes.js");
    const xml = sitemapToXml([
      { url: "https://example.com", lastModified: "2025-01-01", priority: 1 },
      { url: "https://example.com/about", changeFrequency: "monthly" as const },
    ]);
    expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
    expect(xml).toContain("<urlset");
    expect(xml).toContain("<loc>https://example.com</loc>");
    expect(xml).toContain("<lastmod>2025-01-01</lastmod>");
    expect(xml).toContain("<priority>1</priority>");
    expect(xml).toContain("<loc>https://example.com/about</loc>");
    expect(xml).toContain("<changefreq>monthly</changefreq>");
  });

  it("sitemapToXml handles Date objects", async () => {
    const { sitemapToXml } = await import("../packages/vinext/src/server/metadata-routes.js");
    const xml = sitemapToXml([
      { url: "https://example.com", lastModified: new Date("2025-06-15") },
    ]);
    expect(xml).toContain("<lastmod>2025-06-15T00:00:00.000Z</lastmod>");
  });

  it("robotsToText converts robots config to text", async () => {
    const { robotsToText } = await import("../packages/vinext/src/server/metadata-routes.js");
    const text = robotsToText({
      rules: { userAgent: "*", allow: "/", disallow: "/private/" },
      sitemap: "https://example.com/sitemap.xml",
    });
    expect(text).toContain("User-Agent: *");
    expect(text).toContain("Allow: /");
    expect(text).toContain("Disallow: /private/");
    expect(text).toContain("Sitemap: https://example.com/sitemap.xml");
  });

  it("robotsToText handles multiple rules", async () => {
    const { robotsToText } = await import("../packages/vinext/src/server/metadata-routes.js");
    const text = robotsToText({
      rules: [
        { userAgent: "Googlebot", allow: "/" },
        { userAgent: "Bingbot", disallow: "/secret" },
      ],
    });
    expect(text).toContain("User-Agent: Googlebot");
    expect(text).toContain("User-Agent: Bingbot");
    expect(text).toContain("Disallow: /secret");
  });

  it("manifestToJson converts manifest config to JSON", async () => {
    const { manifestToJson } = await import("../packages/vinext/src/server/metadata-routes.js");
    const json = manifestToJson({
      name: "Test App",
      short_name: "Test",
      start_url: "/",
      display: "standalone",
    });
    const parsed = JSON.parse(json);
    expect(parsed.name).toBe("Test App");
    expect(parsed.display).toBe("standalone");
  });

  it("scanMetadataFiles discovers metadata files in app directory", async () => {
    const { scanMetadataFiles } = await import("../packages/vinext/src/server/metadata-routes.js");
    const appDir = path.resolve(import.meta.dirname, "./fixtures/app-basic/app");
    const routes = scanMetadataFiles(appDir);

    // Should find our test fixture files
    const types = routes.map((r: { type: string }) => r.type);
    expect(types).toContain("sitemap");
    expect(types).toContain("robots");
    expect(types).toContain("manifest");
    expect(types).toContain("favicon");

    // Root sitemap should be dynamic (.ts)
    const sitemap = routes.find(
      (r: { type: string; servedUrl: string }) =>
        r.type === "sitemap" && r.servedUrl === "/sitemap.xml",
    );
    expect(sitemap).toBeDefined();
    expect(sitemap!.isDynamic).toBe(true);
    expect(sitemap!.contentType).toBe("application/xml");

    // Favicon should be static (.ico)
    const favicon = routes.find((r: { type: string }) => r.type === "favicon");
    expect(favicon).toBeDefined();
    expect(favicon!.isDynamic).toBe(false);
    expect(favicon!.servedUrl).toBe("/favicon.ico");
    expect(favicon!.contentType).toBe("image/x-icon");
  });
});

describe("next/dynamic shim", () => {
  it("exports a default function", async () => {
    const mod = await import("../packages/vinext/src/shims/dynamic.js");
    expect(typeof mod.default).toBe("function");
  });

  it("exports flushPreloads", async () => {
    const mod = await import("../packages/vinext/src/shims/dynamic.js");
    expect(typeof mod.flushPreloads).toBe("function");
  });

  it("returns a component for SSR-enabled dynamic imports", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const FakeComponent = () => React.createElement("div", null, "Hello from dynamic");
    const DynamicComponent = dynamic(() => Promise.resolve({ default: FakeComponent }));

    // renderToReadableStream handles React.lazy + Suspense
    const stream = await renderToReadableStream(React.createElement(DynamicComponent));
    await stream.allReady;
    const html = await new Response(stream).text();
    expect(html).toContain("Hello from dynamic");
  });

  it("renders loading state for ssr: false on server", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    const FakeComponent = () => React.createElement("div", null, "Should not appear");
    const Loading = () => React.createElement("span", null, "Loading...");
    const DynamicComponent = dynamic(() => Promise.resolve({ default: FakeComponent }), {
      ssr: false,
      loading: Loading,
    });

    // On server with ssr: false, should render loading, not the component
    const html = renderToStaticMarkup(React.createElement(DynamicComponent));
    expect(html).toContain("Loading...");
    expect(html).not.toContain("Should not appear");
  });

  it("renders nothing for ssr: false without loading on server", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    const FakeComponent = () => React.createElement("div", null, "Should not appear");
    const DynamicComponent = dynamic(() => Promise.resolve({ default: FakeComponent }), {
      ssr: false,
    });

    // On server with ssr: false and no loading component, should render nothing
    const html = renderToStaticMarkup(React.createElement(DynamicComponent));
    expect(html).toBe("");
  });

  it("accepts module without default export (bare component)", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const BareComponent = () => React.createElement("p", null, "Bare export");
    const DynamicComponent = dynamic(() => Promise.resolve(BareComponent));

    const stream = await renderToReadableStream(React.createElement(DynamicComponent));
    await stream.allReady;
    const html = await new Response(stream).text();
    expect(html).toContain("Bare export");
  });

  it("forwards props to the underlying component", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const Greeter = ({ name }: { name: string }) =>
      React.createElement("span", null, `Hello ${name}`);
    const DynamicGreeter = dynamic(() => Promise.resolve({ default: Greeter }));

    const stream = await renderToReadableStream(
      React.createElement(DynamicGreeter, { name: "World" }),
    );
    await stream.allReady;
    const html = await new Response(stream).text();
    expect(html).toContain("Hello World");
  });

  it("renders loading fallback when component not yet resolved (SSR)", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    let resolveLoader!: (val: any) => void;
    const loaderPromise = new Promise((r) => {
      resolveLoader = r;
    });
    const SlowComponent = () => React.createElement("div", null, "Loaded");
    const Loading = () => React.createElement("span", null, "Please wait...");

    const DynamicSlow = dynamic(() => loaderPromise as any, { loading: Loading });

    // Start streaming — the shell includes the Suspense fallback
    const stream = await renderToReadableStream(React.createElement(DynamicSlow));
    // Resolve the loader so the stream can complete
    resolveLoader({ default: SlowComponent });
    await stream.allReady;
    const html = await new Response(stream).text();
    // The final HTML should contain the resolved component, but the Suspense
    // fallback ("Please wait...") was sent as part of the shell before it resolved
    expect(html).toContain("Loaded");
  });

  it("streaming renderer resolves multiple dynamic components", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const CompA = () => React.createElement("div", null, "Component A");
    const CompB = () => React.createElement("div", null, "Component B");

    const DynA = dynamic(
      () => new Promise<any>((r) => setTimeout(() => r({ default: CompA }), 10)),
    );
    const DynB = dynamic(
      () => new Promise<any>((r) => setTimeout(() => r({ default: CompB }), 10)),
    );

    // renderToReadableStream handles React.lazy via Suspense
    const streamA = await renderToReadableStream(React.createElement(DynA));
    await streamA.allReady;
    const htmlA = await new Response(streamA).text();

    const streamB = await renderToReadableStream(React.createElement(DynB));
    await streamB.allReady;
    const htmlB = await new Response(streamB).text();

    expect(htmlA).toContain("Component A");
    expect(htmlB).toContain("Component B");
  });

  it("flushPreloads second call resolves immediately (queue drained)", async () => {
    const { flushPreloads } = await import("../packages/vinext/src/shims/dynamic.js");

    // First call should drain whatever's in the queue
    await flushPreloads();

    // Second call should resolve immediately with empty array
    const result = await flushPreloads();
    expect(result).toEqual([]);
  });

  it("loading component receives Next.js noSSR loading props", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    let receivedProps: any = null;
    const Loading = (props: any) => {
      receivedProps = props;
      return React.createElement("span", null, "Loading");
    };

    const FakeComp = () => React.createElement("div", null, "Content");
    const DynComp = dynamic(() => Promise.resolve({ default: FakeComp }), {
      ssr: false,
      loading: Loading,
    });

    renderToStaticMarkup(React.createElement(DynComp));
    expect(receivedProps).not.toBeNull();
    expect(receivedProps.isLoading).toBe(true);
    expect(receivedProps.pastDelay).toBe(false);
    expect(receivedProps.error).toBeNull();
  });

  it("renders loading fallback for ssr: false with props forwarded", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    const HeavyChart = ({ title }: { title: string }) => React.createElement("canvas", null, title);
    const Loading = () => React.createElement("div", null, "Chart loading...");

    const DynamicChart = dynamic(() => Promise.resolve({ default: HeavyChart }), {
      ssr: false,
      loading: Loading,
    });

    // On server: should show loading, not the chart
    const html = renderToStaticMarkup(React.createElement(DynamicChart, { title: "Revenue" }));
    expect(html).toContain("Chart loading...");
    expect(html).not.toContain("Revenue");
  });

  it("handles module with both default and named exports", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const MainComponent = () => React.createElement("div", null, "Main");
    const namedHelper = () => "helper";

    const DynComp = dynamic(() => Promise.resolve({ default: MainComponent, namedHelper }));

    const stream = await renderToReadableStream(React.createElement(DynComp));
    await stream.allReady;
    const html = await new Response(stream).text();
    expect(html).toContain("Main");
  });

  it("loader rejection does not crash flushPreloads", async () => {
    const { default: dynamic, flushPreloads } =
      await import("../packages/vinext/src/shims/dynamic.js");

    dynamic(() => Promise.reject(new Error("Module not found")));

    // flushPreloads should not throw (it's now a no-op for the server lazy path,
    // but kept for backward compatibility with Pages Router)
    await expect(flushPreloads()).resolves.not.toThrow();
  });

  it("loader rejection renders loading component with error", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const LoadingComp = (props: { error?: Error | null; isLoading?: boolean }) => {
      if (props.error) {
        return React.createElement("div", null, `Error: ${props.error.message}`);
      }
      return React.createElement("div", null, "Loading...");
    };

    const DynComp = dynamic(() => Promise.reject(new Error("chunk load fail")), {
      loading: LoadingComp,
    });

    // The error boundary renders the loading component with the error
    const stream = await renderToReadableStream(React.createElement(DynComp));
    await stream.allReady;
    const html = await new Response(stream).text();
    expect(html).toContain("Error: chunk load fail");
  });

  it("loader rejection without loading component propagates via onError", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const DynComp = dynamic(() => Promise.reject(new Error("fail")));

    // Without a loading component, the Suspense fallback is null.
    // The rejected loader throws during rendering, caught by onError.
    const errors: Error[] = [];
    const stream = await renderToReadableStream(React.createElement(DynComp), {
      onError(err: unknown) {
        if (err instanceof Error) errors.push(err);
      },
    });
    await stream.allReady.catch(() => {});
    expect(errors.some((e) => e.message === "fail")).toBe(true);
  });

  it("loader rejection with non-Error value is caught during SSR", async () => {
    const { default: dynamic } = await import("../packages/vinext/src/shims/dynamic.js");
    const React = await import("react");
    const { renderToReadableStream } = await import("react-dom/server.edge");

    const DynComp = dynamic(() => Promise.reject("string error"));

    // Non-Error rejection values are caught by React's SSR error handling
    const errors: unknown[] = [];
    const stream = await renderToReadableStream(React.createElement(DynComp), {
      onError(err: unknown) {
        errors.push(err);
      },
    });
    await stream.allReady.catch(() => {});
    expect(errors.length).toBeGreaterThan(0);
  });
});

describe("basePath config validation", () => {
  it("resolveNextConfig preserves basePath with leading slash", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({ basePath: "/my-app" });
    expect(config.basePath).toBe("/my-app");
  });

  it("resolveNextConfig handles nested basePath", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({ basePath: "/a/b/c" });
    expect(config.basePath).toBe("/a/b/c");
  });

  it("resolveNextConfig defaults to empty string", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.basePath).toBe("");
  });

  it("resolveNextConfig handles undefined basePath", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({ basePath: undefined });
    expect(config.basePath).toBe("");
  });
});

describe("pageExtensions config", () => {
  it("resolveNextConfig defaults pageExtensions to Next.js defaults", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.pageExtensions).toEqual(["tsx", "ts", "jsx", "js"]);
  });

  it("resolveNextConfig reads pageExtensions from config", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({
      pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"],
    });
    expect(config.pageExtensions).toEqual(["js", "jsx", "ts", "tsx", "mdx"]);
  });

  it("resolveNextConfig strips leading dots and whitespace from pageExtensions entries", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({
      pageExtensions: [".tsx", " ts ", "tsx", "", ".mdx"],
    });
    expect(config.pageExtensions).toEqual(["tsx", "ts", "tsx", "mdx"]);
  });
});

describe("cacheComponents config (Next.js 16)", () => {
  it("resolveNextConfig defaults cacheComponents to false", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.cacheComponents).toBe(false);
  });

  it("resolveNextConfig reads cacheComponents: true", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({ cacheComponents: true });
    expect(config.cacheComponents).toBe(true);
  });

  it("resolveNextConfig reads cacheComponents: false", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({ cacheComponents: false });
    expect(config.cacheComponents).toBe(false);
  });

  it("resolveNextConfig handles null input with cacheComponents default", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig(null);
    expect(config.cacheComponents).toBe(false);
  });

  it("resolveNextConfig defaults mdx to null", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.mdx).toBeNull();
  });

  it("resolveNextConfig returns null mdx for null input", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig(null);
    expect(config.mdx).toBeNull();
  });

  it("resolveNextConfig resolves serverActionsAllowedOrigins from experimental.serverActions", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({
      experimental: {
        serverActions: {
          allowedOrigins: ["my-proxy.com", "*.my-domain.com"],
        },
      },
    });
    expect(config.serverActionsAllowedOrigins).toEqual(["my-proxy.com", "*.my-domain.com"]);
  });

  it("resolveNextConfig resolves allowedDevOrigins from top-level config", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({
      allowedDevOrigins: ["staging.example.com", "*.preview.dev"],
    });
    expect(config.allowedDevOrigins).toEqual(["staging.example.com", "*.preview.dev"]);
  });

  it("resolveNextConfig keeps allowedDevOrigins separate from serverActionsAllowedOrigins", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({
      allowedDevOrigins: ["dev.example.com"],
      experimental: {
        serverActions: {
          allowedOrigins: ["actions.example.com"],
        },
      },
    });
    expect(config.allowedDevOrigins).toEqual(["dev.example.com"]);
    expect(config.serverActionsAllowedOrigins).toEqual(["actions.example.com"]);
  });

  it("resolveNextConfig defaults allowedDevOrigins to empty array", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.allowedDevOrigins).toEqual([]);
  });

  it("resolveNextConfig handles null input with empty allowedDevOrigins", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig(null);
    expect(config.allowedDevOrigins).toEqual([]);
  });

  it("resolveNextConfig defaults serverActionsAllowedOrigins to empty array", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig({});
    expect(config.serverActionsAllowedOrigins).toEqual([]);
  });

  it("resolveNextConfig handles null input with empty serverActionsAllowedOrigins", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const config = await resolveNextConfig(null);
    expect(config.serverActionsAllowedOrigins).toEqual([]);
  });
});

describe("loadNextConfig CJS support", () => {
  let tmpDir: string;

  beforeEach(async () => {
    const os = await import("node:os");
    const fsp = await import("node:fs/promises");
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-cjs-cfg-"));
  });

  afterEach(async () => {
    const fsp = await import("node:fs/promises");
    await fsp.rm(tmpDir, { recursive: true, force: true });
  });

  it("loads a CJS next.config.js that uses module.exports", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    await fsp.writeFile(
      path.join(tmpDir, "next.config.js"),
      `module.exports = { basePath: "/cjs-app", trailingSlash: true };`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    expect(config!.basePath).toBe("/cjs-app");
    expect(config!.trailingSlash).toBe(true);
  });

  it("loads a CJS next.config.js with require() plugin wrapper", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    // Simulate a CJS plugin wrapper like nextra/next-intl/etc.
    // Create a fake plugin module that wraps the config
    await fsp.mkdir(path.join(tmpDir, "node_modules", "fake-plugin"), {
      recursive: true,
    });
    await fsp.writeFile(
      path.join(tmpDir, "node_modules", "fake-plugin", "index.js"),
      `module.exports = function fakePlugin(pluginOpts) {
        return function withPlugin(nextConfig) {
          return Object.assign({}, nextConfig, { env: { PLUGIN: "loaded" } });
        };
      };`,
    );
    await fsp.writeFile(
      path.join(tmpDir, "node_modules", "fake-plugin", "package.json"),
      JSON.stringify({ name: "fake-plugin", version: "1.0.0", main: "index.js" }),
    );

    // Write a next.config.js that uses require() — this is the pattern that fails
    await fsp.writeFile(
      path.join(tmpDir, "next.config.js"),
      `const withPlugin = require('fake-plugin')({ theme: 'docs' });
module.exports = withPlugin({ basePath: "/wrapped" });`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    expect(config!.basePath).toBe("/wrapped");
    expect(config!.env).toEqual({ PLUGIN: "loaded" });
  });

  it("loads a CJS function-form next.config.js", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    await fsp.writeFile(
      path.join(tmpDir, "next.config.js"),
      `module.exports = function(phase, { defaultConfig }) {
        return { basePath: "/fn-" + phase.split("-")[1] };
      };`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    // phase is "phase-development-server", split("-")[1] = "development"
    expect(config!.basePath).toBe("/fn-development");
  });

  it("loads a .cjs config file", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    await fsp.writeFile(
      path.join(tmpDir, "next.config.cjs"),
      `module.exports = { basePath: "/cjs-ext" };`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    expect(config!.basePath).toBe("/cjs-ext");
  });

  it("loads an ESM next.config.mjs normally", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    await fsp.writeFile(
      path.join(tmpDir, "next.config.mjs"),
      `export default { basePath: "/esm-app" };`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    expect(config!.basePath).toBe("/esm-app");
  });

  it("loads next.config.ts with extensionless local imports", async () => {
    const fsp = await import("node:fs/promises");
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    await fsp.writeFile(path.join(tmpDir, "env.ts"), `export const BASE_PATH = "/from-env";`);
    await fsp.writeFile(
      path.join(tmpDir, "next.config.ts"),
      `import { BASE_PATH } from "./env";

export default {
  basePath: BASE_PATH,
  trailingSlash: true,
};`,
    );

    const config = await loadNextConfig(tmpDir);
    expect(config).not.toBeNull();
    expect(config!.basePath).toBe("/from-env");
    expect(config!.trailingSlash).toBe(true);
  });

  it("returns null when no config file exists", async () => {
    const { loadNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    const config = await loadNextConfig(tmpDir);
    expect(config).toBeNull();
  });
});

describe("extractMdxOptions", () => {
  it("returns null when no webpack function", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    await expect(extractMdxOptions({})).resolves.toBeNull();
    await expect(extractMdxOptions({ webpack: "not a function" })).resolves.toBeNull();
  });

  it("extracts remarkPlugins from webpack rule", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const fakeRemarkPlugin = () => {};
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.mdx$/,
          use: [
            {
              loader: "@next/mdx/mdx-js-loader",
              options: {
                remarkPlugins: [[fakeRemarkPlugin, { option: true }]],
                rehypePlugins: [],
              },
            },
          ],
        });
        return webpackConfig;
      },
    };
    const result = await extractMdxOptions(config);
    expect(result).not.toBeNull();
    expect(result!.remarkPlugins).toHaveLength(1);
    expect(result!.remarkPlugins![0]).toEqual([fakeRemarkPlugin, { option: true }]);
    expect(result!.rehypePlugins).toBeUndefined();
  });

  it("extracts rehypePlugins from webpack rule", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const fakeRehypePlugin = () => {};
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.mdx$/,
          use: [
            {
              loader: "@next/mdx/mdx-js-loader",
              options: {
                remarkPlugins: [],
                rehypePlugins: [fakeRehypePlugin],
              },
            },
          ],
        });
        return webpackConfig;
      },
    };
    const result = await extractMdxOptions(config);
    expect(result).not.toBeNull();
    expect(result!.rehypePlugins).toHaveLength(1);
    expect(result!.remarkPlugins).toBeUndefined();
  });

  it("extracts recmaPlugins from webpack rule", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const fakeRecmaPlugin = () => {};
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.mdx$/,
          use: [
            {
              loader: "@next/mdx/mdx-js-loader",
              options: {
                recmaPlugins: [fakeRecmaPlugin],
              },
            },
          ],
        });
        return webpackConfig;
      },
    };
    const result = await extractMdxOptions(config);
    expect(result).not.toBeNull();
    expect(result!.recmaPlugins).toHaveLength(1);
  });

  it("handles oneOf nested rules", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const fakeRemarkPlugin = () => {};
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          oneOf: [
            {
              test: /\.mdx$/,
              use: [
                {
                  loader: "@next/mdx/mdx-js-loader",
                  options: {
                    remarkPlugins: [fakeRemarkPlugin],
                  },
                },
              ],
            },
          ],
        });
        return webpackConfig;
      },
    };
    const result = await extractMdxOptions(config);
    expect(result).not.toBeNull();
    expect(result!.remarkPlugins).toHaveLength(1);
  });

  it("returns null when webpack throws", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const config = {
      webpack: () => {
        throw new Error("some webpack error");
      },
    };
    await expect(extractMdxOptions(config)).resolves.toBeNull();
  });

  it("returns null when webpack has no MDX loader", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.css$/,
          use: [{ loader: "css-loader" }],
        });
        return webpackConfig;
      },
    };
    await expect(extractMdxOptions(config)).resolves.toBeNull();
  });

  it("returns null when MDX loader has empty plugin arrays", async () => {
    const { extractMdxOptions } = await import("../packages/vinext/src/config/next-config.js");
    const config = {
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.mdx$/,
          use: [
            {
              loader: "@next/mdx/mdx-js-loader",
              options: {
                remarkPlugins: [],
                rehypePlugins: [],
              },
            },
          ],
        });
        return webpackConfig;
      },
    };
    await expect(extractMdxOptions(config)).resolves.toBeNull();
  });

  it("resolveNextConfig extracts mdx from webpack closure", async () => {
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");
    const fakeRemarkPlugin = () => {};
    const config = await resolveNextConfig({
      webpack: (webpackConfig: any) => {
        webpackConfig.module.rules.push({
          test: /\.mdx$/,
          use: [
            {
              loader: "@next/mdx/mdx-js-loader",
              options: {
                remarkPlugins: [fakeRemarkPlugin],
              },
            },
          ],
        });
        return webpackConfig;
      },
    });
    expect(config.mdx).not.toBeNull();
    expect(config.mdx!.remarkPlugins).toHaveLength(1);
  });
});

describe("next/web-vitals shim", () => {
  it("exports useReportWebVitals as a hook function", async () => {
    const { useReportWebVitals } = await import("../packages/vinext/src/shims/web-vitals.js");
    expect(typeof useReportWebVitals).toBe("function");
  });
});

describe("next/amp shim", () => {
  it("exports useAmp and isInAmpMode as no-op functions", async () => {
    const { useAmp, isInAmpMode } = await import("../packages/vinext/src/shims/amp.js");
    expect(typeof useAmp).toBe("function");
    expect(typeof isInAmpMode).toBe("function");
    // Both always return false
    expect(useAmp()).toBe(false);
    expect(isInAmpMode()).toBe(false);
  });
});

describe("app router scroll intent state", () => {
  it("clears a staged scroll intent when a same-document navigation supersedes it", async () => {
    const {
      beginAppRouterScrollIntent,
      clearAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    clearAppRouterScrollIntent();
    beginAppRouterScrollIntent(null);
    expect(getPendingAppRouterScrollIntent()).not.toBeNull();

    clearAppRouterScrollIntent();
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });

  it("marks only the claimed commit's intent as head-hoisted", async () => {
    const {
      beginAppRouterScrollIntent,
      claimAppRouterScrollIntentForCommit,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
      markAppRouterScrollIntentHeadHoisted,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    clearAppRouterScrollIntent();

    const intent = beginAppRouterScrollIntent(null);
    expect(intent.targetHoistedInHead).toBe(false);

    // The committed scroll target only runs once a commit id is claimed, so a
    // not-yet-claimed intent must never be marked.
    markAppRouterScrollIntentHeadHoisted(intent, 7);
    expect(getPendingAppRouterScrollIntent()?.targetHoistedInHead).toBe(false);

    claimAppRouterScrollIntentForCommit(intent, 7);

    // A scroll target from a different (e.g. stale) commit must not mark it.
    markAppRouterScrollIntentHeadHoisted(intent, 6);
    expect(getPendingAppRouterScrollIntent()?.targetHoistedInHead).toBe(false);

    markAppRouterScrollIntentHeadHoisted(intent, 7);
    const consumed = consumeAppRouterScrollIntent(intent, 7);
    expect(consumed?.targetHoistedInHead).toBe(true);
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });

  it("does not mark a stale intent reference as head-hoisted", async () => {
    const {
      beginAppRouterScrollIntent,
      claimAppRouterScrollIntentForCommit,
      clearAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
      markAppRouterScrollIntentHeadHoisted,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    clearAppRouterScrollIntent();

    const staleIntent = beginAppRouterScrollIntent(null);
    const latestIntent = beginAppRouterScrollIntent(null);
    claimAppRouterScrollIntentForCommit(latestIntent, 3);

    // A scroll target committed for an earlier navigation must not poison the
    // navigation that replaced it.
    markAppRouterScrollIntentHeadHoisted(staleIntent, 3);
    expect(getPendingAppRouterScrollIntent()?.id).toBe(latestIntent.id);
    expect(getPendingAppRouterScrollIntent()?.targetHoistedInHead).toBe(false);
  });

  it("only lets the claimed render commit consume its own scroll intent", async () => {
    const {
      beginAppRouterScrollIntent,
      claimAppRouterScrollIntentForCommit,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    clearAppRouterScrollIntent();

    const staleIntent = beginAppRouterScrollIntent(null);
    const latestIntent = beginAppRouterScrollIntent(null);

    claimAppRouterScrollIntentForCommit(staleIntent, 1);
    expect(getPendingAppRouterScrollIntent()?.commitId).toBeNull();
    expect(consumeAppRouterScrollIntent(undefined)).toBeNull();
    expect(getPendingAppRouterScrollIntent()?.id).toBe(latestIntent.id);
    expect(consumeAppRouterScrollIntent(null)).toBeNull();
    expect(getPendingAppRouterScrollIntent()?.id).toBe(latestIntent.id);
    expect(consumeAppRouterScrollIntent(latestIntent, 1)).toBeNull();
    expect(getPendingAppRouterScrollIntent()?.id).toBe(latestIntent.id);

    claimAppRouterScrollIntentForCommit(latestIntent, 2);
    expect(consumeAppRouterScrollIntent(latestIntent, 1)).toBeNull();

    const consumed = consumeAppRouterScrollIntent(latestIntent, 2);
    expect(consumed?.id).toBe(latestIntent.id);
    expect(consumed?.commitId).toBe(2);
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });

  it("consuming an unclaimed intent without commitId matches navigateClientSide fallback contract", async () => {
    const {
      beginAppRouterScrollIntent,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    // no-commit path: intent was created but never claimed by a render commit.
    // consumeAppRouterScrollIntent(intent) without a commitId must consume it
    // so the navigateClientSide fallback (which calls consume without commitId)
    // finds null and does not apply a scroll for an abandoned navigation.
    clearAppRouterScrollIntent();

    const intent = beginAppRouterScrollIntent(null);
    expect(getPendingAppRouterScrollIntent()?.id).toBe(intent.id);

    const consumed = consumeAppRouterScrollIntent(intent);
    expect(consumed?.id).toBe(intent.id);
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });

  it("consuming an intent with a stale intent reference is a no-op", async () => {
    const {
      beginAppRouterScrollIntent,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    // When a newer navigation replaces the pending intent, attempting to consume
    // the stale reference must be a no-op — this is the safety check that prevents
    // the fallback from consuming a different navigation's intent.
    clearAppRouterScrollIntent();

    const firstIntent = beginAppRouterScrollIntent(null);
    const secondIntent = beginAppRouterScrollIntent(null);

    expect(consumeAppRouterScrollIntent(firstIntent)).toBeNull();
    expect(getPendingAppRouterScrollIntent()?.id).toBe(secondIntent.id);
    expect(consumeAppRouterScrollIntent(secondIntent)).not.toBeNull();
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });

  it("consuming a claimed intent without commitId is always allowed for cleanup paths", async () => {
    const {
      beginAppRouterScrollIntent,
      claimAppRouterScrollIntentForCommit,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");

    // The fallback in navigateClientSide calls consume without a commitId.
    // An intent claimed by a newer commit must still be consumable without a
    // commitId check so that explicit cleanup paths (no-commit, hard-navigate)
    // can clear it. The commitId gate only applies when a commitId is passed.
    clearAppRouterScrollIntent();

    const intent = beginAppRouterScrollIntent(null);
    claimAppRouterScrollIntentForCommit(intent, 42);
    expect(getPendingAppRouterScrollIntent()?.commitId).toBe(42);

    // Consume without commitId — always allowed (no-commit / hard-navigate cleanup).
    const consumed = consumeAppRouterScrollIntent(intent);
    expect(consumed?.id).toBe(intent.id);
    expect(getPendingAppRouterScrollIntent()).toBeNull();
  });
});

describe("app router scroll document-top fallback", () => {
  // applyAppRouterScrollFallback runs in navigateClientSide after a committed
  // navigation declined to consume its scroll intent. It must scroll the
  // document to the top UNLESS this navigation's committed target was a
  // React-hoisted node in <head> (marked per-intent by AppRouterScrollTarget).
  function withScrollFallbackEnv(
    head: { querySelectorAll: () => unknown[] },
    run: (documentElement: { scrollTop: number }) => void,
  ): void {
    const documentElement = { clientHeight: 800, scrollTop: 500 };
    const originalDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
    const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");

    Object.defineProperty(globalThis, "document", {
      configurable: true,
      value: { documentElement, head },
    });
    Object.defineProperty(globalThis, "window", { configurable: true, value: {} });

    try {
      run(documentElement);
    } finally {
      if (originalDocument) {
        Object.defineProperty(globalThis, "document", originalDocument);
      } else {
        Reflect.deleteProperty(globalThis, "document");
      }
      if (originalWindow) {
        Object.defineProperty(globalThis, "window", originalWindow);
      } else {
        Reflect.deleteProperty(globalThis, "window");
      }
    }
  }

  // A document head that already contains a React-hoisted precedence stylesheet
  // — exactly the state in which the old global head scan wrongly suppressed the
  // fallback for every navigation.
  const headWithHoistedStylesheet = {
    querySelectorAll: () => [
      {
        getAttribute: () => null,
        localName: "link",
      },
    ],
  };

  it("scrolls to the document top even when hoisted stylesheets exist in <head>", async () => {
    const { beginAppRouterScrollIntent, clearAppRouterScrollIntent } =
      await import("../packages/vinext/src/shims/app-router-scroll-state.js");
    const { applyAppRouterScrollFallback } =
      await import("../packages/vinext/src/shims/navigation.js");

    // Regression: an ordinary navigation whose intent was never marked as
    // head-hoisted must still scroll to the document top. The presence of a
    // hoisted stylesheet in <head> for some unrelated reason must not suppress
    // it — the suppression decision is per-intent, not a global head scan.
    clearAppRouterScrollIntent();
    const intent = beginAppRouterScrollIntent(null);
    expect(intent.targetHoistedInHead).toBe(false);

    withScrollFallbackEnv(headWithHoistedStylesheet, (documentElement) => {
      applyAppRouterScrollFallback(intent);
      expect(documentElement.scrollTop).toBe(0);
    });
  });

  it("does not scroll to the document top when this navigation's target was hoisted into <head>", async () => {
    const { applyAppRouterScrollFallback } =
      await import("../packages/vinext/src/shims/navigation.js");

    const intent = {
      commitId: 1,
      hash: null,
      id: 1,
      targetHoistedInHead: true,
    };

    withScrollFallbackEnv({ querySelectorAll: () => [] }, (documentElement) => {
      applyAppRouterScrollFallback(intent);
      expect(documentElement.scrollTop).toBe(500);
    });
  });

  it("full chain: marked head-hoisted intent survives claim→consume and suppresses the document-top fallback", async () => {
    const {
      beginAppRouterScrollIntent,
      claimAppRouterScrollIntentForCommit,
      clearAppRouterScrollIntent,
      consumeAppRouterScrollIntent,
      getPendingAppRouterScrollIntent,
      markAppRouterScrollIntentHeadHoisted,
    } = await import("../packages/vinext/src/shims/app-router-scroll-state.js");
    const { applyAppRouterScrollFallback } =
      await import("../packages/vinext/src/shims/navigation.js");

    // Start with a clean slate — no leftover intent from a prior test.
    clearAppRouterScrollIntent();

    // ── begin ──────────────────────────────────────────────────────────
    // navigateClientSide stages an intent before sending the RSC navigation.
    const originalIntent = beginAppRouterScrollIntent(null);
    expect(originalIntent.targetHoistedInHead).toBe(false);

    // ── claim ──────────────────────────────────────────────────────────
    // The render commit claims the intent, writing its commit id.
    claimAppRouterScrollIntentForCommit(originalIntent, 9);

    // ── mark (head-hoisted) ───────────────────────────────────────────
    // AppRouterScrollTarget finds the committed node is a React-hoisted
    // resource in <head> and marks this navigation's intent.
    markAppRouterScrollIntentHeadHoisted(originalIntent, 9);

    // ── consume ────────────────────────────────────────────────────────
    // navigateClientSide reads the consumable result.
    const consumed = consumeAppRouterScrollIntent(originalIntent, 9);
    expect(consumed).not.toBeNull();
    expect(consumed!.targetHoistedInHead).toBe(true);
    expect(consumed!.id).toBe(originalIntent.id);
    expect(getPendingAppRouterScrollIntent()).toBeNull();

    // ── apply fallback ─────────────────────────────────────────────────
    // navigateClientSide calls the fallback with the consumed intent.
    withScrollFallbackEnv({ querySelectorAll: () => [] }, (documentElement) => {
      applyAppRouterScrollFallback(consumed!);
      // targetHoistedInHead is true → fallback declines document-top scroll.
      expect(documentElement.scrollTop).toBe(500);
    });

    // ══════════════════════════════════════════════════════════════════
    // Regression guard
    // ══════════════════════════════════════════════════════════════════
    // markAppRouterScrollIntentHeadHoisted replaces the store.pending
    // object with a spread copy to keep the type Readonly. The reference
    // handed back by beginAppRouterScrollIntent still points at the old
    // object where targetHoistedInHead is false. If a future refactor
    // accidentally passes the original (stale) intent to the fallback
    // instead of the consumed one, the flag check silently passes and the
    // document scrolls to top — incorrectly masking the old-handler
    // behaviour whose observable contract the upstream deploy-suite
    // depends on.
    expect(originalIntent.targetHoistedInHead).toBe(false);
  });
});

describe("next/compat/router shim", () => {
  it("exports useRouter as a function", async () => {
    const mod = await import("../packages/vinext/src/shims/compat-router.js");
    // useRouter should be a named export, not a default export (unlike next/router).
    // Returns null in App Router context instead of throwing.
    expect(typeof mod.useRouter).toBe("function");
    expect((mod as Record<string, unknown>).default).toBeUndefined();
  });

  it("useRouter returns null when no RouterContext.Provider wraps the tree", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter } = await import("../packages/vinext/src/shims/compat-router.js");

    let captured: unknown = "NOT_SET";
    function Probe() {
      captured = useRouter();
      return React.createElement("div", null, "probe");
    }

    renderToStaticMarkup(React.createElement(Probe));
    expect(captured).toBeNull();
  });

  it("useRouter returns the router when wrapWithRouterContext wraps the tree", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext } = await import("../packages/vinext/src/shims/router.js");

    let captured: unknown = "NOT_SET";
    function Probe() {
      captured = useCompatRouter();
      return React.createElement("div", null, "probe");
    }

    const element = wrapWithRouterContext(React.createElement(Probe));
    renderToStaticMarkup(element);
    expect(captured).not.toBeNull();
    expect(typeof (captured as any).pathname).toBe("string");
    expect(typeof (captured as any).push).toBe("function");
    expect(typeof (captured as any).replace).toBe("function");
    expect(typeof (captured as any).back).toBe("function");
    expect(typeof (captured as any).reload).toBe("function");
  });

  it("useRouter returns router reflecting SSR context when set", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext, setSSRContext } =
      await import("../packages/vinext/src/shims/router.js");

    setSSRContext({
      pathname: "/posts/42",
      query: { id: "42" },
      asPath: "/posts/42?tab=comments",
    });

    let captured: unknown = "NOT_SET";
    function Probe() {
      captured = useCompatRouter();
      return React.createElement("div", null, "probe");
    }

    const element = wrapWithRouterContext(React.createElement(Probe));
    renderToStaticMarkup(element);

    setSSRContext(null);

    expect(captured).not.toBeNull();
    expect((captured as any).pathname).toBe("/posts/42");
    expect((captured as any).asPath).toBe("/posts/42?tab=comments");
    expect((captured as any).query.id).toBe("42");
  });

  it("preserves array query values from SSR context", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext, setSSRContext } =
      await import("../packages/vinext/src/shims/router.js");

    setSSRContext({
      pathname: "/docs/a/b",
      query: { slug: ["a", "b"] },
      asPath: "/docs/a/b",
    });

    let captured: unknown = "NOT_SET";
    function Probe() {
      captured = useCompatRouter();
      return React.createElement("div", null, "probe");
    }

    const element = wrapWithRouterContext(React.createElement(Probe));
    renderToStaticMarkup(element);

    setSSRContext(null);

    expect(captured).not.toBeNull();
    expect((captured as any).query.slug).toEqual(["a", "b"]);
  });

  // Regression for issue #1466: React's `useSyncExternalStore` calls
  // `getServerSnapshot` multiple times during SSR/hydration and expects a
  // stable reference (Object.is) — otherwise it logs:
  //   "The result of getServerSnapshot should be cached to avoid an infinite loop"
  // and may re-render until it stabilizes. `useParams`/`useSearchParams`/
  // `usePathname` from `next/navigation` derive their server snapshot from
  // `getPagesNavigationContext()`, so its SSR-side return value must be
  // reference-stable within a single request. Dynamic params and search params
  // intentionally follow the Pages Router pre-ready snapshot here so SSR and
  // client hydration agree before the queued ready notification publishes live
  // values.
  it("getPagesNavigationContext returns stable pre-ready SSR snapshots in one request", async () => {
    const { getPagesNavigationContext, setSSRContext } =
      await import("../packages/vinext/src/shims/router.js");

    setSSRContext({
      pathname: "/pages-dir/[dynamic]",
      query: { dynamic: "foobar" },
      asPath: "/pages-dir/foobar?tab=comments",
      navigationIsReady: false,
      nextData: {
        props: {},
        page: "/pages-dir/[dynamic]",
        query: { dynamic: "foobar", tab: "comments" },
        autoExport: true,
      },
    });
    try {
      const first = getPagesNavigationContext();
      const second = getPagesNavigationContext();
      expect(first).not.toBeNull();
      expect(first).toBe(second);
      expect(first!.pathname).toBeNull();
      expect(first!.params).toBeNull();
      expect(first!.searchParams.toString()).toBe("");
      // searchParams must also be reference-stable across calls so hook-level
      // snapshots (`pagesCtx.searchParams`) are Object.is-equal between renders.
      expect(first!.searchParams).toBe(second!.searchParams);
    } finally {
      setSSRContext(null);
    }
  });

  it("client Pages navigation context prefers the current URL over stale __NEXT_DATA__ params", async () => {
    const { getPagesNavigationContext } = await import("../packages/vinext/src/shims/router.js");

    const previousWindow = (globalThis as any).window;
    (globalThis as any).window = {
      location: {
        pathname: "/search-params-pages/bar",
        search: "",
        hash: "",
      },
      __NEXT_DATA__: {
        page: "/search-params-pages/[foo]",
        query: { foo: "foo" },
        isFallback: false,
      },
      __VINEXT_PAGE_LOADERS__: {
        "/search-params-pages/[foo]": async () => ({ default: () => null }),
      },
    };

    try {
      expect(getPagesNavigationContext()?.params).toEqual({ foo: "bar" });
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("server Pages navigation context prefers the resolved URL over stale query params", async () => {
    const { getPagesNavigationContext, setSSRContext } =
      await import("../packages/vinext/src/shims/router.js");

    setSSRContext({
      pathname: "/search-params-pages/[foo]",
      query: { foo: "foo" },
      asPath: "/search-params-pages/bar",
      navigationIsReady: true,
    });
    try {
      expect(getPagesNavigationContext()?.params).toEqual({ foo: "bar" });
    } finally {
      setSSRContext(null);
    }
  });

  // Regression: `useRouter().isReady` flows through getRouterSnapshot →
  // PagesRouterProvider. On the server it must reflect the SSR navigation
  // readiness context, not unconditionally report `true`. Otherwise a pre-ready
  // route (auto-export dynamic / query string / rewrite-capable build) would
  // render `isReady: true` on the server while the client hydrates with
  // `isReady: false`, a hydration mismatch for components reading it in JSX.
  // Mirrors Next.js render.tsx's server readiness rule.
  it("useRouter().isReady reflects SSR navigation readiness during server render", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter, wrapWithRouterContext, setSSRContext } =
      await import("../packages/vinext/src/shims/router.js");

    function Probe() {
      return React.createElement("span", null, String(useRouter().isReady));
    }

    // Pre-ready route: navigationIsReady false → isReady false.
    setSSRContext({
      pathname: "/pages-dir/[dynamic]",
      query: { dynamic: "foobar" },
      asPath: "/pages-dir/foobar?tab=comments",
      navigationIsReady: false,
    });
    try {
      const html = renderToStaticMarkup(wrapWithRouterContext(React.createElement(Probe)));
      expect(html).toBe("<span>false</span>");
    } finally {
      setSSRContext(null);
    }

    // Ready route: navigationIsReady true → isReady true.
    setSSRContext({
      pathname: "/blog/[slug]",
      query: { slug: "hello" },
      asPath: "/blog/hello",
      navigationIsReady: true,
    });
    try {
      const html = renderToStaticMarkup(wrapWithRouterContext(React.createElement(Probe)));
      expect(html).toBe("<span>true</span>");
    } finally {
      setSSRContext(null);
    }
  });

  it("does not defer Pages Router readiness for dynamic getStaticProps routes without search", async () => {
    const { getPagesNavigationIsReadyFromSerializedState } =
      await import("../packages/vinext/src/shims/router.js");

    expect(
      getPagesNavigationIsReadyFromSerializedState("/nav-compat-gsp/[slug]", "", {
        props: {},
        page: "/nav-compat-gsp/[slug]",
        query: { slug: "foobar" },
        gsp: true,
      }),
    ).toBe(true);
    expect(
      getPagesNavigationIsReadyFromSerializedState("/nav-compat/[slug]", "", {
        props: {},
        page: "/nav-compat/[slug]",
        query: { slug: "foobar" },
        autoExport: true,
      }),
    ).toBe(false);
    expect(
      getPagesNavigationIsReadyFromSerializedState("/nav-compat-gsp/[slug]", "?q=pages", {
        props: {},
        page: "/nav-compat-gsp/[slug]",
        query: { slug: "foobar", q: "pages" },
        gsp: true,
      }),
    ).toBe(false);
    expect(
      getPagesNavigationIsReadyFromSerializedState("/search-params-static-rewrites", "", {
        props: {},
        page: "/search-params-static-rewrites",
        query: {},
        autoExport: true,
        __vinext: { hasRewrites: true },
      }),
    ).toBe(false);
  });

  it("preserves route param arrays, repeated search params, and hash in client router state", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext } = await import("../packages/vinext/src/shims/router.js");

    const previousWindow = (globalThis as any).window;
    (globalThis as any).window = {
      location: {
        pathname: "/docs/a/b",
        search: "?tag=a&tag=b",
        hash: "#section",
      },
      __NEXT_DATA__: {
        page: "/docs/[...slug]",
        query: { slug: ["a", "b"] },
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };

    try {
      let captured: unknown = "NOT_SET";
      function Probe() {
        captured = useCompatRouter();
        return React.createElement("div", null, "probe");
      }

      const element = wrapWithRouterContext(React.createElement(Probe));
      renderToStaticMarkup(element);

      expect(captured).not.toBeNull();
      expect((captured as any).query.slug).toEqual(["a", "b"]);
      expect((captured as any).query.tag).toEqual(["a", "b"]);
      expect((captured as any).asPath).toBe("/docs/a/b?tag=a&tag=b#section");
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("prefers dynamic route params over same-key search params in client router state", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext } = await import("../packages/vinext/src/shims/router.js");

    const previousWindow = (globalThis as any).window;
    (globalThis as any).window = {
      location: {
        pathname: "/docs/a/b",
        search: "?slug=c",
        hash: "",
      },
      __NEXT_DATA__: {
        page: "/docs/[...slug]",
        query: { slug: ["a", "b"] },
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };

    try {
      let captured: unknown = "NOT_SET";
      function Probe() {
        captured = useCompatRouter();
        return React.createElement("div", null, "probe");
      }

      const element = wrapWithRouterContext(React.createElement(Probe));
      renderToStaticMarkup(element);

      expect(captured).not.toBeNull();
      expect((captured as any).query.slug).toEqual(["a", "b"]);
      expect((captured as any).asPath).toBe("/docs/a/b?slug=c");
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("treats prototype property names as ordinary query keys in client router state", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const { wrapWithRouterContext } = await import("../packages/vinext/src/shims/router.js");

    const previousWindow = (globalThis as any).window;
    (globalThis as any).window = {
      location: {
        pathname: "/shallow-test",
        search: "?toString=a&constructor=b&__proto__=c",
        hash: "",
      },
      __NEXT_DATA__: {
        page: "/shallow-test",
        query: {},
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };

    try {
      let captured: unknown = "NOT_SET";
      function Probe() {
        captured = useCompatRouter();
        return React.createElement("div", null, "probe");
      }

      const element = wrapWithRouterContext(React.createElement(Probe));
      renderToStaticMarkup(element);

      expect(captured).not.toBeNull();
      expect((captured as any).query.toString).toBe("a");
      expect((captured as any).query.constructor).toBe("b");
      expect((captured as any).query.__proto__).toBe("c");
      expect(Object.getPrototypeOf((captured as any).query)).toBe(Object.prototype);
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });
});

describe("Pages Router router helpers", () => {
  it("exports wrapWithRouterContext function", async () => {
    const mod = await import("../packages/vinext/src/shims/router.js");
    expect(typeof mod.wrapWithRouterContext).toBe("function");
  });

  it("serializes array query values as repeated params for object-form router URLs", async () => {
    const previousWindow = (globalThis as any).window;
    const pushState = vi.fn();
    const replaceState = vi.fn();

    (globalThis as any).window = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: {
        state: null,
        pushState,
        replaceState,
        back: vi.fn(),
      },
      dispatchEvent: vi.fn(),
      scrollTo: vi.fn(),
      scrollX: 0,
      scrollY: 0,
      __NEXT_DATA__: {
        page: "/",
        query: {},
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      await routerModule.default.push(
        { pathname: "/search", query: { tag: ["a", "b"], q: "x" } },
        undefined,
        { shallow: true },
      );

      expect(pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/search?tag=a&tag=b&q=x",
      );
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("stringifies scalar query values like Next.js for object-form router URLs", async () => {
    const previousWindow = (globalThis as any).window;
    const pushState = vi.fn();
    const replaceState = vi.fn();

    (globalThis as any).window = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: {
        state: null,
        pushState,
        replaceState,
        back: vi.fn(),
      },
      dispatchEvent: vi.fn(),
      scrollTo: vi.fn(),
      scrollX: 0,
      scrollY: 0,
      __NEXT_DATA__: {
        page: "/",
        query: {},
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      await routerModule.default.push(
        {
          pathname: "/search",
          query: { page: 2, draft: false, empty: null, missing: undefined, tag: ["a", "b"] },
        },
        undefined,
        { shallow: true },
      );

      expect(pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/search?page=2&draft=false&empty=&missing=&tag=a&tag=b",
      );
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("updates dynamic route params from the URL after shallow navigation", async () => {
    // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-rewrites/test/index.test.ts
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const routerModule = await import("../packages/vinext/src/shims/router.js");

    const previousWindow = (globalThis as any).window;
    const win = {
      location: {
        pathname: "/posts/42",
        search: "",
        hash: "",
        href: "http://localhost/posts/42",
        hostname: "localhost",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: {
        state: null,
        pushState: vi.fn((_state: unknown, _title: string, url: string) => {
          const nextUrl = new URL(url, win.location.href);
          win.location.pathname = nextUrl.pathname;
          win.location.search = nextUrl.search;
          win.location.hash = nextUrl.hash;
          win.location.href = nextUrl.href;
        }),
        replaceState: vi.fn(),
        back: vi.fn(),
      },
      dispatchEvent: vi.fn(),
      scrollTo: vi.fn(),
      scrollX: 0,
      scrollY: 0,
      __NEXT_DATA__: {
        page: "/posts/[id]",
        query: { id: "42" },
        isFallback: false,
      },
      __VINEXT_LOCALE__: undefined,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
    };
    (globalThis as any).window = win;

    try {
      await routerModule.default.push("/posts/43", undefined, { shallow: true });

      let captured: unknown = "NOT_SET";
      function Probe() {
        captured = useCompatRouter();
        return React.createElement("div", null, "probe");
      }

      renderToStaticMarkup(routerModule.wrapWithRouterContext(React.createElement(Probe)));

      expect(captured).not.toBeNull();
      expect((captured as any).pathname).toBe("/posts/[id]");
      expect((captured as any).asPath).toBe("/posts/43");
      expect((captured as any).query).toEqual({ id: "43" });
      expect(win.__NEXT_DATA__.query).toEqual({ id: "42" });
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("exposes beforePopState on both the Router singleton and wrapped router context", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const mod = await import("../packages/vinext/src/shims/router.js");
    const { useRouter: useCompatRouter } =
      await import("../packages/vinext/src/shims/compat-router.js");
    const routerSingleton = mod.default;

    let captured: unknown = "NOT_SET";
    function Probe() {
      captured = useCompatRouter();
      return React.createElement("div", null, "probe");
    }

    renderToStaticMarkup(mod.wrapWithRouterContext(React.createElement(Probe)));

    expect(typeof (routerSingleton as any).beforePopState).toBe("function");
    expect(typeof (captured as any).beforePopState).toBe("function");
  });

  describe("isExternalUrl", () => {
    it("detects https:// as external", () => {
      expect(isExternalUrl("https://example.com")).toBe(true);
      expect(isExternalUrl("https://example.com/path")).toBe(true);
    });

    it("detects http:// as external", () => {
      expect(isExternalUrl("http://example.com")).toBe(true);
    });

    it("detects protocol-relative // as external", () => {
      expect(isExternalUrl("//cdn.example.com/img.png")).toBe(true);
    });

    it("detects native URI schemes as external", () => {
      expect(isExternalUrl("mailto:hello@example.com")).toBe(true);
      expect(isExternalUrl("tel:+123456789")).toBe(true);
      expect(isExternalUrl("sms:+123456789")).toBe(true);
    });

    it("returns false for relative paths", () => {
      expect(isExternalUrl("/about")).toBe(false);
      expect(isExternalUrl("/")).toBe(false);
      expect(isExternalUrl("about")).toBe(false);
    });

    it("returns false for hash-only", () => {
      expect(isExternalUrl("#section")).toBe(false);
    });

    it("returns false for query-only", () => {
      expect(isExternalUrl("?foo=1")).toBe(false);
    });
  });

  describe("isHashOnlyChange", () => {
    it("returns true for hash-only strings starting with #", () => {
      // This works even without window because of the startsWith check
      expect(isHashOnlyChange("#foo")).toBe(true);
      expect(isHashOnlyChange("#")).toBe(true);
      expect(isHashOnlyChange("#section-2")).toBe(true);
    });

    it("returns false for absolute paths without window context", () => {
      // Without a real browser window, URL-based comparison returns false
      // because typeof window === "undefined" → returns false
      expect(isHashOnlyChange("/about")).toBe(false);
      expect(isHashOnlyChange("/about#foo")).toBe(false);
    });

    it("returns false for full URLs without window context", () => {
      expect(isHashOnlyChange("https://example.com#foo")).toBe(false);
    });
  });

  describe("applyNavigationLocale", () => {
    it("does not prefix absolute https:// URLs", async () => {
      const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
      // Simulate a browser-like window so the locale guard is reached
      (globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
      try {
        expect(applyNavigationLocale("https://example.com/about", "fr")).toBe(
          "https://example.com/about",
        );
      } finally {
        delete (globalThis as any).window;
      }
    });

    it("does not prefix absolute http:// URLs", async () => {
      const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
      (globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
      try {
        expect(applyNavigationLocale("http://example.com/path", "de")).toBe(
          "http://example.com/path",
        );
      } finally {
        delete (globalThis as any).window;
      }
    });

    it("does not prefix protocol-relative // URLs", async () => {
      const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
      (globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
      try {
        expect(applyNavigationLocale("//cdn.example.com/img.png", "fr")).toBe(
          "//cdn.example.com/img.png",
        );
      } finally {
        delete (globalThis as any).window;
      }
    });

    it("does not prefix native URI schemes", async () => {
      const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
      (globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
      try {
        expect(applyNavigationLocale("mailto:hello@example.com", "fr")).toBe(
          "mailto:hello@example.com",
        );
        expect(applyNavigationLocale("tel:+123456789", "fr")).toBe("tel:+123456789");
      } finally {
        delete (globalThis as any).window;
      }
    });

    it("prefixes local paths with locale", async () => {
      const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
      (globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
      try {
        expect(applyNavigationLocale("/about", "fr")).toBe("/fr/about");
      } finally {
        delete (globalThis as any).window;
      }
    });
  });
});

describe("Pages Router concurrent navigation", () => {
  it("installs the Pages Router popstate runtime before exposing window.next.router", async () => {
    const previousWindow = (globalThis as any).window;
    const { win } = createNavWindow();
    const calls: string[] = [];
    win.addEventListener = vi.fn((type: string) => {
      const next = Reflect.get(win, "next");
      const router =
        typeof next === "object" && next !== null ? Reflect.get(next, "router") : undefined;
      calls.push(
        `${type}:${router === undefined ? "before-window-next-router" : "after-window-next-router"}`,
      );
    });
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");

      expect(calls).toEqual(["popstate:before-window-next-router"]);
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  it("installPagesRouterRuntime registers the Pages Router popstate handler once", async () => {
    const previousWindow = (globalThis as any).window;
    const { win } = createNavWindow();
    win.addEventListener = vi.fn();
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");

      installPagesRouterRuntime();
      installPagesRouterRuntime();

      expect(win.addEventListener).toHaveBeenCalledTimes(1);
      expect(win.addEventListener).toHaveBeenCalledWith("popstate", expect.any(Function));
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });

  /**
   * Helper: create a mock window suitable for non-shallow Router.push().
   * Returns an object with the window mock plus helpers for controlling
   * the fetch responses (deferred promises).
   */
  function createNavWindow() {
    const pushState = vi.fn();
    const replaceState = vi.fn();
    const render = vi.fn((element: unknown) => commitRenderedPagesRouterElement(element));

    const win = {
      location: {
        pathname: "/",
        search: "",
        hash: "",
        href: "http://localhost/",
        hostname: "localhost",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: {
        state: null,
        pushState: pushState as any,
        replaceState: replaceState as any,
        back: vi.fn(),
      },
      dispatchEvent: vi.fn(),
      scrollTo: vi.fn(),
      scrollX: 0,
      scrollY: 0,
      addEventListener: vi.fn(),
      __NEXT_DATA__: {
        page: "/",
        query: {},
        isFallback: false,
        props: { pageProps: {} },
        __vinext: { pageModuleUrl: "/@fs/pages/index.js" },
      },
      __VINEXT_ROOT__: { render },
      __VINEXT_APP__: undefined,
      __VINEXT_LOCALE__: undefined as string | undefined,
      __VINEXT_LOCALES__: undefined as string[] | undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined as string | undefined,
    };

    // Make pushState update location to simulate real browser behavior
    pushState.mockImplementation((_state: unknown, _title: string, url: string) => {
      try {
        const parsed = new URL(url, "http://localhost");
        win.location.pathname = parsed.pathname;
        win.location.search = parsed.search;
        win.location.hash = parsed.hash;
        win.location.href = parsed.href;
      } catch {
        // Relative URL — just set pathname
        win.location.pathname = url;
        win.location.href = "http://localhost" + url;
      }
    });

    replaceState.mockImplementation((_state: unknown, _title: string, url?: string) => {
      if (!url) return;
      try {
        const parsed = new URL(url, "http://localhost");
        win.location.pathname = parsed.pathname;
        win.location.search = parsed.search;
        win.location.hash = parsed.hash;
        win.location.href = parsed.href;
      } catch {
        win.location.pathname = url;
        win.location.href = "http://localhost" + url;
      }
    });

    return { win, pushState, replaceState, render };
  }

  /**
   * Build a minimal HTML response that navigateClient can parse.
   * Includes __NEXT_DATA__ with a pageModuleUrl pointing to the given path.
   */
  function buildNavHtml(
    page: string,
    pageModuleUrl: string,
    query: Record<string, unknown> = {},
    i18n?: { locale: string; locales: string[]; defaultLocale: string },
  ): string {
    const nextDataScript = buildPagesNextDataScript({
      buildId: null,
      i18n: i18n ?? {},
      pageProps: { page },
      params: query,
      routePattern: page,
      safeJsonStringify,
      vinext: { pageModuleUrl },
    });
    return `<html><head></head><body>${nextDataScript}</body></html>`;
  }

  function buildNavHtmlWithVinext(
    page: string,
    vinext: { pageModuleUrl?: string; appModuleUrl?: string; hasMiddleware?: boolean },
  ): string {
    const nextDataScript = buildPagesNextDataScript({
      buildId: null,
      i18n: {},
      pageProps: { page },
      params: {},
      routePattern: page,
      safeJsonStringify,
      vinext,
    });
    return `<html><head></head><body>${nextDataScript}</body></html>`;
  }

  function getFetchHref(url: RequestInfo | URL): string {
    if (typeof url === "string") return url;
    if (url instanceof URL) return url.href;
    return url.url;
  }

  /**
   * Create a deferred promise for controlling fetch timing.
   */
  function createDeferred<T>() {
    let resolve!: (value: T) => void;
    let reject!: (reason: unknown) => void;
    const promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  }

  it("preserves cross-origin domain-locale targets during query-only same-segment navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const { win } = createNavWindow();
    process.env.__NEXT_ROUTER_BASEPATH = "/app";
    Object.assign(win.location, {
      pathname: "/app/rewrite-navigation-same/0",
      href: "https://example.com/app/rewrite-navigation-same/0",
      hostname: "example.com",
      origin: "https://example.com",
    });
    Object.assign(win, {
      __VINEXT_LOCALE__: "en",
      __VINEXT_LOCALES__: ["en", "fr"],
      __VINEXT_DEFAULT_LOCALE__: "en",
      __NEXT_DATA__: {
        ...win.__NEXT_DATA__,
        page: "/rewrite-navigation-same/[id]",
        query: { id: "0" },
        domainLocales: [
          { domain: "example.com", defaultLocale: "en" },
          { domain: "example.fr", defaultLocale: "fr", http: true },
        ],
      },
    });
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { default: Router } = await import("../packages/vinext/src/shims/router.js");

      await Router.push({ query: { id: "1" } }, undefined, { shallow: true, locale: "fr" });

      expect(win.location.assign).toHaveBeenCalledWith(
        "http://example.fr/app/rewrite-navigation-same/0?id=1",
      );
      expect(win.history.pushState).not.toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousBasePath === undefined) delete process.env.__NEXT_ROUTER_BASEPATH;
      else process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
    }
  });

  it("throws href-interpolation-failed when a required same-segment param is missing", async () => {
    const previousWindow = (globalThis as any).window;
    const { win } = createNavWindow();
    Object.assign(win.location, {
      pathname: "/catalog/books/old",
      href: "http://localhost/catalog/books/old",
    });
    Object.assign(win, {
      __NEXT_DATA__: {
        ...win.__NEXT_DATA__,
        page: "/catalog/[category]/[item]",
        query: { category: "books", item: "old" },
      },
    });
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { default: Router } = await import("../packages/vinext/src/shims/router.js");

      await expect(
        Router.push({ query: { category: "music" } }, undefined, { shallow: true }),
      ).rejects.toThrow(
        "The provided `href` (/catalog/[category]/[item]?category=music) value is missing query values (item) to be interpolated properly. Read more: https://nextjs.org/docs/messages/href-interpolation-failed",
      );
      expect(win.history.pushState).not.toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
    }
  });

  it("allows an omitted optional catch-all during same-segment interpolation", async () => {
    const previousWindow = (globalThis as any).window;
    const { win } = createNavWindow();
    Object.assign(win.location, {
      pathname: "/docs/old",
      href: "http://localhost/docs/old",
    });
    Object.assign(win, {
      __NEXT_DATA__: {
        ...win.__NEXT_DATA__,
        page: "/docs/[section]/[[...slug]]",
        query: { section: "old" },
      },
    });
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { default: Router } = await import("../packages/vinext/src/shims/router.js");

      await expect(
        Router.push({ query: { section: "guide" } }, undefined, { shallow: true }),
      ).resolves.toBe(true);
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/guide",
      );
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
    }
  });

  it("interpolates required catch-all params during same-segment navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const { win } = createNavWindow();
    Object.assign(win.location, {
      pathname: "/docs/old",
      href: "http://localhost/docs/old",
    });
    Object.assign(win, {
      __NEXT_DATA__: {
        ...win.__NEXT_DATA__,
        page: "/docs/[...slug]",
        query: { slug: ["old"] },
      },
    });
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const { default: Router } = await import("../packages/vinext/src/shims/router.js");

      await expect(
        Router.push({ query: { slug: ["guide", "intro"] } }, undefined, { shallow: true }),
      ).resolves.toBe(true);
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/guide/intro",
      );
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
    }
  });

  for (const mode of ["push", "replace"] as const) {
    it(`keeps hash-only UrlObject ${mode} on the current dynamic route`, async () => {
      const previousWindow = (globalThis as any).window;
      const previousDocument = (globalThis as any).document;
      const originalFetch = globalThis.fetch;
      const { win } = createNavWindow();
      Object.assign(win.location, {
        pathname: "/docs/old",
        href: "http://localhost/docs/old",
      });
      Object.assign(win, {
        __NEXT_DATA__: {
          ...win.__NEXT_DATA__,
          page: "/docs/[id]",
          query: { id: "old" },
        },
      });
      (globalThis as any).window = win;
      (globalThis as any).document = {
        getElementById: vi.fn(() => null),
        getElementsByName: vi.fn(() => []),
      };
      globalThis.fetch = vi.fn(async () => {
        throw new Error("hash-only dynamic navigation must not fetch page HTML");
      });

      try {
        vi.resetModules();
        const { default: Router } = await import("../packages/vinext/src/shims/router.js");

        await expect(Router[mode]({ hash: "section" })).resolves.toBe(true);
        expect(globalThis.fetch).not.toHaveBeenCalled();
        expect(win.history[`${mode}State`]).toHaveBeenCalledWith(
          expect.objectContaining({ __N: true }),
          "",
          "/docs/old#section",
        );
      } finally {
        vi.resetModules();
        if (previousWindow === undefined) delete (globalThis as any).window;
        else (globalThis as any).window = previousWindow;
        if (previousDocument === undefined) delete (globalThis as any).document;
        else (globalThis as any).document = previousDocument;
        globalThis.fetch = originalFetch;
      }
    });
  }

  function trackHrefAssignments(win: {
    location: {
      href: string;
    };
  }): string[] {
    let currentHref = win.location.href;
    const assignments: string[] = [];

    Object.defineProperty(win.location, "href", {
      configurable: true,
      enumerable: true,
      get: () => currentHref,
      set: (value: string) => {
        currentHref = value;
        assignments.push(value);
      },
    });

    return assignments;
  }

  async function expectPagesRouterPushTrailingSlashNormalization({
    target,
    expectedBrowserUrl,
  }: {
    target: string;
    expectedBrowserUrl: string;
  }): Promise<void> {
    const previousWindow = (globalThis as any).window;
    const previousTrailingSlash = process.env.__VINEXT_TRAILING_SLASH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;
    process.env.__VINEXT_TRAILING_SLASH = "true";
    vi.resetModules();

    globalThis.fetch = vi.fn(async () => {
      throw new Error("shallow trailing-slash normalization test must not fetch page HTML");
    });

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push(target, undefined, { shallow: true });

      expect(result).toBe(true);
      // History state now follows Next.js shape ({ url, as, options, __N, key });
      // assert via partial match so test stays focused on URL normalization.
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        expectedBrowserUrl,
      );
    } finally {
      if (previousTrailingSlash === undefined) {
        delete process.env.__VINEXT_TRAILING_SLASH;
      } else {
        process.env.__VINEXT_TRAILING_SLASH = previousTrailingSlash;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  }

  it("Pages Router push normalizes query-bearing paths when trailingSlash is true", async () => {
    await expectPagesRouterPushTrailingSlashNormalization({
      target: "/about?hello=world",
      expectedBrowserUrl: "/about/?hello=world",
    });
  });

  it("Pages Router push preserves canonical query-bearing paths when trailingSlash is true", async () => {
    await expectPagesRouterPushTrailingSlashNormalization({
      target: "/about/?hello=world",
      expectedBrowserUrl: "/about/?hello=world",
    });
  });

  it("Pages Router push strips file-looking path slashes when trailingSlash is true", async () => {
    await expectPagesRouterPushTrailingSlashNormalization({
      target: "/catch-all/hello.world/",
      expectedBrowserUrl: "/catch-all/hello.world",
    });
  });

  // Ported from Next.js:
  // test/e2e/basepath/error-pages.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/basepath/error-pages.test.ts
  it("Pages Router fetches the error route while preserving the masked URL under basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/docs/slug-1";
    win.location.href = "http://localhost/docs/slug-1";
    (globalThis as any).window = win;
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";

    const fetch = vi.fn(
      async () =>
        new Response(buildNavHtml("/404", pageModuleUrl), {
          status: 404,
        }),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/404", "/slug-2");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/docs/404", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/slug-2",
      );
      expect(win.location.pathname).toBe("/docs/slug-2");
      expect(win.__NEXT_DATA__.page).toBe("/404");
    } finally {
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("Pages Router maps masked /_error client navigations to the 404 page under basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/docs/slug-1";
    win.location.href = "http://localhost/docs/slug-1";
    (globalThis as any).window = win;
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";

    const fetch = vi.fn(
      async () =>
        new Response(buildNavHtml("/404", pageModuleUrl), {
          status: 404,
        }),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/_error", "/slug-2");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/docs/404", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/slug-2",
      );
      expect(win.location.pathname).toBe("/docs/slug-2");
      expect(win.__NEXT_DATA__.page).toBe("/404");
    } finally {
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("Pages Router fetches /404 through a non-default locale while preserving the masked URL", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/docs/fr/slug-1";
    win.location.href = "http://localhost/docs/fr/slug-1";
    win.__VINEXT_LOCALE__ = "fr";
    win.__VINEXT_LOCALES__ = ["en", "fr"];
    win.__VINEXT_DEFAULT_LOCALE__ = "en";
    (globalThis as any).window = win;
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/404",
            pageModuleUrl,
            {},
            {
              locale: "fr",
              locales: ["en", "fr"],
              defaultLocale: "en",
            },
          ),
          { status: 404 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/404", "/slug-2");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/docs/fr/404", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/fr/slug-2",
      );
      expect(win.location.pathname).toBe("/docs/fr/slug-2");
      expect(win.__NEXT_DATA__.page).toBe("/404");
    } finally {
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("Pages Router maps /_error through a non-default locale while preserving the masked URL", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/docs/fr/slug-1";
    win.location.href = "http://localhost/docs/fr/slug-1";
    win.__VINEXT_LOCALE__ = "fr";
    win.__VINEXT_LOCALES__ = ["en", "fr"];
    win.__VINEXT_DEFAULT_LOCALE__ = "en";
    (globalThis as any).window = win;
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/404",
            pageModuleUrl,
            {},
            {
              locale: "fr",
              locales: ["en", "fr"],
              defaultLocale: "en",
            },
          ),
          { status: 404 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/_error", "/slug-2");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/docs/fr/404", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/docs/fr/slug-2",
      );
      expect(win.location.pathname).toBe("/docs/fr/slug-2");
      expect(win.__NEXT_DATA__.page).toBe("/404");
    } finally {
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("Pages Router HTML fallback prefers the registered route loader", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const { win, render } = createNavWindow();
    const LoaderPage = () => null;
    const loader = vi.fn(async () => ({ default: LoaderPage }));
    Object.assign(win, {
      __VINEXT_PAGE_LOADERS__: {
        "/same": loader,
      },
      __VINEXT_PAGE_PATTERNS__: ["/same"],
    });
    (globalThis as any).window = win;
    (globalThis as any).document = {
      createElement: vi.fn(),
      head: { appendChild: vi.fn() },
    };

    const fetch = vi.fn(
      async () => new Response(buildNavHtml("/same", "javascript:duplicate-or-invalid")),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/same");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/same", expect.any(Object));
      expect(loader).toHaveBeenCalledTimes(1);
      expect(render).toHaveBeenCalled();
      expect(win.location.href).toBe("http://localhost/same");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("last push() wins when two overlap — superseded navigation does not render", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    // Two deferred fetches so we control resolution order
    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      // Start two navigations — don't await yet
      const navA = Router.push("/page-a");
      // Let microtask queue process so navA's fetch has been called
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Resolve B first (the winning navigation)
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));

      // Resolve A after B (stale — should be ignored)
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));

      await Promise.allSettled([navA, navB]);

      // The superseded navigation (page-a) must NOT have committed its data.
      // In a real browser page-b would render; in the test env B's dynamic import
      // may fail, so we verify the important invariant: A never writes.
      expect(win.__NEXT_DATA__.page).not.toBe("/page-a");
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("routeChangeComplete does not fire for the superseded navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    const completedUrls: string[] = [];
    const onRouteChangeComplete = (...args: unknown[]) => {
      completedUrls.push(String(args[0]));
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeComplete", onRouteChangeComplete);

      // Start two overlapping navigations
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Resolve B first, then A
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));

      await Promise.allSettled([navA, navB]);

      // The superseded navigation (page-a) must NOT fire routeChangeComplete.
      // page-b may or may not complete fully (dynamic import may fail in test
      // env), but that's a separate concern. The critical fix is that the
      // cancelled navigation never fires routeChangeComplete.
      expect(completedUrls).not.toContain("/page-a");
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeComplete", onRouteChangeComplete);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("routeChangeError fires for superseded navigation with cancelled error", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    const errors: Array<{ err: unknown; url: string }> = [];
    const onRouteChangeError = (...args: unknown[]) => {
      errors.push({ err: args[0], url: String(args[1]) });
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeError", onRouteChangeError);

      // Start two overlapping navigations
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Resolve both
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));

      await Promise.allSettled([navA, navB]);

      // The superseded navigation (page-a) should emit routeChangeError
      // with a cancelled error, matching Next.js behavior
      const cancelledError = errors.find((e) => e.url === "/page-a");
      expect(cancelledError).toBeDefined();
      const errObj = cancelledError?.err;
      expect(errObj).toHaveProperty("cancelled", true);
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeError", onRouteChangeError);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("failed navigation (non-OK response) does not emit routeChangeComplete", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    globalThis.fetch = async (_url: any, _init: any) =>
      new Response("Internal Server Error", { status: 500 });

    const completedUrls: string[] = [];
    const errorUrls: string[] = [];
    const onRouteChangeComplete = (...args: unknown[]) => {
      completedUrls.push(String(args[0]));
    };
    const onRouteChangeError = (...args: unknown[]) => {
      errorUrls.push(String(args[1]));
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeComplete", onRouteChangeComplete);
      Router.events.on("routeChangeError", onRouteChangeError);

      await Router.push("/failing-page");

      // Should NOT have fired routeChangeComplete for a failed navigation
      expect(completedUrls).not.toContain("/failing-page");
      // Should have fired routeChangeError
      expect(errorUrls).toContain("/failing-page");
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeComplete", onRouteChangeComplete);
      Router.events.off("routeChangeError", onRouteChangeError);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("known navigation failures schedule exactly one hard-navigation fallback", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const hrefAssignments = trackHrefAssignments(win);
    (globalThis as any).window = win;

    globalThis.fetch = async (_url: any, _init: any) =>
      new Response("Internal Server Error", { status: 500 });

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/failing-page");
      // Distinguish the history update from the hard-navigation fallback:
      // pushState writes the absolute browser URL, while the fallback helper
      // writes the raw app-relative URL. The guard is correct only if each
      // happens exactly once.
      const fallbackAssignments = hrefAssignments.filter((value) => value === "/failing-page");
      const pushStateAssignments = hrefAssignments.filter(
        (value) => value === "http://localhost/failing-page",
      );

      expect(result).toBe(false);
      expect(fallbackAssignments).toHaveLength(1);
      expect(pushStateAssignments).toHaveLength(1);
      // Catch-all: exactly one history write plus exactly one hard-nav fallback.
      expect(hrefAssignments).toHaveLength(2);
    } finally {
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("fetches default-locale root through a locale-qualified URL without changing the browser URL", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/new";
    win.location.href = "http://localhost/new";
    Object.assign(win, {
      __VINEXT_LOCALE__: "en",
      __VINEXT_LOCALES__: ["en", "id"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/",
            pageModuleUrl,
            {},
            {
              locale: "en",
              locales: ["en", "id"],
              defaultLocale: "en",
            },
          ),
          { status: 200 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/en", expect.any(Object));
      // Next.js-shaped history state (`{ url, as, options, __N, key }`) is now
      // written on every router push; assert via partial match so this test
      // stays focused on the locale-qualified fetch / browser-URL contract.
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true, options: expect.objectContaining({ locale: "en" }) }),
        "",
        "/",
      );
      expect(win.location.href).toBe("http://localhost/");
      expect(win.__VINEXT_LOCALE__).toBe("en");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("keeps the active locale for root Link navigations from a non-locale-prefixed path", async () => {
    // i18n sticky-locale (issue #1336): a default-locale path served under a
    // non-default locale must keep reporting its active `__VINEXT_LOCALE__` so
    // a subsequent client-side root navigation stays in that locale rather
    // than snapping back to the configured default. See
    // tests/pages-router-i18n-sticky-locale.test.ts for the broader contract.
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/new";
    win.location.href = "http://localhost/new";
    Object.assign(win, {
      __VINEXT_LOCALE__: "id",
      __VINEXT_LOCALES__: ["en", "id"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/",
            pageModuleUrl,
            {},
            {
              locale: "id",
              locales: ["en", "id"],
              defaultLocale: "en",
            },
          ),
          { status: 200 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/id", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true, options: expect.objectContaining({ locale: "id" }) }),
        "",
        "/id",
      );
      expect(win.location.href).toBe("http://localhost/id");
      expect(win.__VINEXT_LOCALE__).toBe("id");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("keeps a locale-prefixed current path locale for root Link navigations", async () => {
    // Ported from Next.js:
    // test/e2e/i18n-preferred-locale-detection/i18n-preferred-locale-detection.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-preferred-locale-detection/i18n-preferred-locale-detection.test.ts
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.location.pathname = "/id/new";
    win.location.href = "http://localhost/id/new";
    Object.assign(win, {
      __VINEXT_LOCALE__: "id",
      __VINEXT_LOCALES__: ["en", "id"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/",
            pageModuleUrl,
            {},
            {
              locale: "id",
              locales: ["en", "id"],
              defaultLocale: "en",
            },
          ),
          { status: 200 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/id", expect.any(Object));
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/id",
      );
      expect(win.location.href).toBe("http://localhost/id");
      expect(win.__VINEXT_LOCALE__).toBe("id");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("popstate fetches default-locale root through a locale-qualified URL", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const originalCustomEvent = globalThis.CustomEvent;
    const listeners = new Map<string, (event: any) => void>();
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");

    win.location.pathname = "/about";
    win.location.href = "http://localhost/about";
    Object.assign(win, {
      __VINEXT_LOCALE__: "en",
      __VINEXT_LOCALES__: ["en", "id"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    win.addEventListener = vi.fn((type: string, handler: (event: any) => void) => {
      listeners.set(type, handler);
    });

    (globalThis as any).window = win;
    (globalThis as any).CustomEvent = class CustomEventMock {
      constructor(public type: string) {}
    } as any;

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/",
            pageModuleUrl,
            {},
            {
              locale: "en",
              locales: ["en", "id"],
              defaultLocale: "en",
            },
          ),
          { status: 200 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      const popstateHandler = listeners.get("popstate");
      expect(popstateHandler).toBeDefined();

      win.location.pathname = "/";
      win.location.href = "http://localhost/";
      popstateHandler!({ state: null });
      await new Promise((resolve) => setTimeout(resolve, 0));

      expect(fetch).toHaveBeenCalledWith("/en", expect.any(Object));
      expect(win.location.href).toBe("http://localhost/");
      expect(win.__VINEXT_LOCALE__).toBe("en");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
      (globalThis as any).CustomEvent = originalCustomEvent;
    }
  });

  // Without experimental.scrollRestoration, popstate must still restore the
  // per-entry scroll saved into history state on push (__vinext_scrollX/Y):
  // a soft popstate re-renders content the browser's native "auto" scroll
  // restoration can't position correctly.
  it("popstate restores history-state scroll when manual scroll restoration is off", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const originalCustomEvent = globalThis.CustomEvent;
    const listeners = new Map<string, (event: any) => void>();
    const { win, render } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    win.scrollTo.mockImplementation((x: number, y: number) => {
      win.scrollX = x;
      win.scrollY = y;
    });

    win.addEventListener = vi.fn((type: string, handler: (event: any) => void) => {
      listeners.set(type, handler);
    });

    (globalThis as any).window = win;
    (globalThis as any).document = {
      documentElement: {
        dataset: {},
        style: {},
        getClientRects: vi.fn(),
      },
    };
    (globalThis as any).CustomEvent = class CustomEventMock {
      constructor(public type: string) {}
    } as any;

    globalThis.fetch = vi.fn(
      async () => new Response(buildNavHtml("/other", pageModuleUrl), { status: 200 }),
    );

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      const popstateHandler = listeners.get("popstate");
      expect(popstateHandler).toBeDefined();

      const routeChangeComplete = new Promise<void>((resolve, reject) => {
        (win as any).next.router.events.on("routeChangeComplete", () => resolve());
        (win as any).next.router.events.on("routeChangeError", (err: unknown) => reject(err));
      });

      win.location.pathname = "/other";
      win.location.href = "http://localhost/other";
      // The router-owned state shape saveScrollPosition() writes on push:
      // the Next.js state stamp plus the merged __vinext_scrollX/Y fields.
      popstateHandler!({
        state: {
          __N: true,
          url: "/other",
          as: "/other",
          options: {},
          __vinext_scrollX: 12,
          __vinext_scrollY: 345,
        },
      });

      // The scroll target is applied inside the render-commit callback; the
      // mocked root.render never mounts React, so fire the commit manually.
      await vi.waitFor(() => expect(render).toHaveBeenCalled());
      const committed = render.mock.calls.at(-1)![0] as { props: { onCommit: () => void } };
      committed.props.onCommit();
      await routeChangeComplete;

      expect(win.scrollTo).toHaveBeenCalledWith(12, 345);
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
      (globalThis as any).CustomEvent = originalCustomEvent;
    }
  });

  // When beforePopState cancels a popstate the app stays on the previous
  // history entry, so the internal current-key tracker must keep pointing at
  // that entry — otherwise later scroll bookkeeping saves the outgoing
  // position under the wrong (never-visited) key. Observable via the next
  // popstate's sessionStorage scroll save, which is keyed off the tracker.
  it("beforePopState cancellation leaves the current history key unchanged", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const originalScrollRestorationEnv = process.env.__NEXT_SCROLL_RESTORATION;
    const listeners = new Map<string, (event: any) => void>();
    const { win } = createNavWindow();

    win.addEventListener = vi.fn((type: string, handler: (event: any) => void) => {
      listeners.set(type, handler);
    });
    // Enable the manual scroll restoration path: the flag also requires
    // history.scrollRestoration support and a working sessionStorage.
    (win.history as any).scrollRestoration = "auto";
    const sessionStore = new Map<string, string>();
    (win as any).sessionStorage = {
      getItem: (key: string) => sessionStore.get(key) ?? null,
      setItem: (key: string, value: string) => void sessionStore.set(key, value),
      removeItem: (key: string) => void sessionStore.delete(key),
    };
    // Pre-stamped router-owned entry so install picks up its key.
    (win.history as any).state = {
      __N: true,
      url: "/",
      as: "/",
      options: {},
      key: "key-initial",
    };

    (globalThis as any).window = win;
    globalThis.fetch = vi.fn();
    process.env.__NEXT_SCROLL_RESTORATION = "true";

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      const popstateHandler = listeners.get("popstate");
      expect(popstateHandler).toBeDefined();

      const beforePopState = vi.fn(() => false);
      (win as any).next.router.beforePopState(beforePopState);

      // Back/forward to another entry, cancelled by beforePopState.
      win.location.pathname = "/other";
      win.location.href = "http://localhost/other";
      popstateHandler!({
        state: { __N: true, url: "/other", as: "/other", options: {}, key: "key-target" },
      });
      expect(beforePopState).toHaveBeenCalledTimes(1);
      expect(globalThis.fetch).not.toHaveBeenCalled();

      // A later popstate snapshots the outgoing scroll position under the
      // tracked current key. The app never left the initial entry, so the
      // snapshot must land on key-initial — not the cancelled key-target.
      win.scrollX = 120;
      win.scrollY = 450;
      win.location.pathname = "/third";
      win.location.href = "http://localhost/third";
      popstateHandler!({
        state: { __N: true, url: "/third", as: "/third", options: {}, key: "key-other" },
      });

      expect(sessionStore.get("__next_scroll_key-initial")).toBe(
        JSON.stringify({ x: 120, y: 450 }),
      );
      expect(sessionStore.has("__next_scroll_key-target")).toBe(false);
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
      if (originalScrollRestorationEnv === undefined) {
        delete process.env.__NEXT_SCROLL_RESTORATION;
      } else {
        process.env.__NEXT_SCROLL_RESTORATION = originalScrollRestorationEnv;
      }
    }
  });

  // A hash-only back/forward under manual scroll restoration honors only the
  // hash anchor: the target entry's `__next_scroll_<key>` snapshot is read but
  // intentionally not applied, matching Next.js where change()'s
  // onlyAHashChange branch passes `null` to this.set() and only scrollToHash
  // runs (forcedScroll feeds the full-navigation path exclusively). The
  // departed entry's position is still snapshotted, and the unconsumed target
  // snapshot stays in sessionStorage for a later non-hash popstate.
  it("hash-only popstate scrolls to the hash anchor and leaves the entry snapshot for later", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const originalScrollRestorationEnv = process.env.__NEXT_SCROLL_RESTORATION;
    const listeners = new Map<string, (event: any) => void>();
    const { win, pushState } = createNavWindow();

    win.addEventListener = vi.fn((type: string, handler: (event: any) => void) => {
      listeners.set(type, handler);
    });
    // Enable the manual scroll restoration path: the flag also requires
    // history.scrollRestoration support and a working sessionStorage.
    (win.history as any).scrollRestoration = "auto";
    const sessionStore = new Map<string, string>();
    (win as any).sessionStorage = {
      getItem: (key: string) => sessionStore.get(key) ?? null,
      setItem: (key: string, value: string) => void sessionStore.set(key, value),
      removeItem: (key: string) => void sessionStore.delete(key),
    };
    // Pre-stamped router-owned entry so install picks up its key.
    (win.history as any).state = {
      __N: true,
      url: "/",
      as: "/",
      options: {},
      key: "key-initial",
    };

    (globalThis as any).window = win;
    globalThis.fetch = vi.fn();
    process.env.__NEXT_SCROLL_RESTORATION = "true";

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      // Positive control: a Pages Router document with the flag enabled
      // switches the browser to manual restoration at install time.
      expect((win.history as any).scrollRestoration).toBe("manual");

      const popstateHandler = listeners.get("popstate");
      expect(popstateHandler).toBeDefined();

      // The position the user reached on the initial entry; the hash-only
      // push below snapshots it under key-initial on departure.
      win.scrollX = 5;
      win.scrollY = 500;

      // Hash-only push mints a new history entry ("#top" avoids the
      // document-dependent getElementById branch of scrollToHashTarget).
      await (win as any).next.router.push("/#top");
      expect(sessionStore.get("__next_scroll_key-initial")).toBe(JSON.stringify({ x: 5, y: 500 }));
      const pushedKey = pushState.mock.calls.at(-1)![0].key as string;
      expect(typeof pushedKey).toBe("string");

      const hashEvents: string[] = [];
      (win as any).next.router.events.on("hashChangeStart", () =>
        hashEvents.push("hashChangeStart"),
      );
      (win as any).next.router.events.on("hashChangeComplete", () =>
        hashEvents.push("hashChangeComplete"),
      );

      // The live position on the hash entry, snapshotted on departure.
      win.scrollX = 7;
      win.scrollY = 70;
      win.scrollTo.mockClear();

      // Back to the initial entry: same pathname+search, only the hash
      // differs, so this is a hash-only popstate.
      win.location.hash = "";
      win.location.pathname = "/";
      win.location.href = "http://localhost/";
      popstateHandler!({
        state: { __N: true, url: "/", as: "/", options: {}, key: "key-initial" },
      });

      // Hash-only: no page fetch, hash events fired in order.
      expect(globalThis.fetch).not.toHaveBeenCalled();
      expect(hashEvents).toEqual(["hashChangeStart", "hashChangeComplete"]);

      // Only the hash anchor is honored (empty hash scrolls to top); the
      // target entry's saved {x: 5, y: 500} is not applied (upstream parity).
      expect(win.scrollTo).toHaveBeenCalledWith(0, 0);
      expect(win.scrollTo).not.toHaveBeenCalledWith(5, 500);

      // The departed hash entry's live position was snapshotted under its
      // key, and the unconsumed target snapshot survives for a later
      // non-hash popstate to this entry.
      expect(sessionStore.get(`__next_scroll_${pushedKey}`)).toBe(JSON.stringify({ x: 7, y: 70 }));
      expect(sessionStore.get("__next_scroll_key-initial")).toBe(JSON.stringify({ x: 5, y: 500 }));
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
      if (originalScrollRestorationEnv === undefined) {
        delete process.env.__NEXT_SCROLL_RESTORATION;
      } else {
        process.env.__NEXT_SCROLL_RESTORATION = originalScrollRestorationEnv;
      }
    }
  });

  // A hash-only push must snapshot the outgoing entry's scroll under its key
  // before updateHistory mints the new entry's key — otherwise a later
  // back-popstate to the departed entry restores {x: 0, y: 0} instead of the
  // position the user had reached there. Upstream snapshots in Router.push()
  // itself, ahead of change()'s onlyAHashChange short-circuit, so hash-only
  // pushes still write `__next_scroll_<key>` for the departed entry.
  it("hash-only push snapshots the outgoing entry's scroll under the old key", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const originalScrollRestorationEnv = process.env.__NEXT_SCROLL_RESTORATION;
    const { win, pushState } = createNavWindow();

    // Enable the manual scroll restoration path: the flag also requires
    // history.scrollRestoration support and a working sessionStorage.
    (win.history as any).scrollRestoration = "auto";
    const sessionStore = new Map<string, string>();
    (win as any).sessionStorage = {
      getItem: (key: string) => sessionStore.get(key) ?? null,
      setItem: (key: string, value: string) => void sessionStore.set(key, value),
      removeItem: (key: string) => void sessionStore.delete(key),
    };
    // Pre-stamped router-owned entry so install picks up its key.
    (win.history as any).state = {
      __N: true,
      url: "/",
      as: "/",
      options: {},
      key: "key-initial",
    };

    (globalThis as any).window = win;
    globalThis.fetch = vi.fn();
    process.env.__NEXT_SCROLL_RESTORATION = "true";

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      // The live position on the outgoing entry at push time ("#top" avoids
      // the document-dependent getElementById branch of scrollToHashTarget).
      win.scrollX = 30;
      win.scrollY = 300;
      await (win as any).next.router.push("/#top");

      // Hash-only: no page fetch; a new entry was minted with a fresh key.
      expect(globalThis.fetch).not.toHaveBeenCalled();
      const pushedKey = pushState.mock.calls.at(-1)![0].key as string;
      expect(typeof pushedKey).toBe("string");
      expect(pushedKey).not.toBe("key-initial");

      // The departed entry's position was snapshotted under its own key —
      // not the freshly minted one — so a later back-popstate restores it.
      expect(sessionStore.get("__next_scroll_key-initial")).toBe(JSON.stringify({ x: 30, y: 300 }));
      expect(sessionStore.has(`__next_scroll_${pushedKey}`)).toBe(false);
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
      if (originalScrollRestorationEnv === undefined) {
        delete process.env.__NEXT_SCROLL_RESTORATION;
      } else {
        process.env.__NEXT_SCROLL_RESTORATION = originalScrollRestorationEnv;
      }
    }
  });

  // The experimental.scrollRestoration manual opt-in is Pages-Router-only:
  // an App Router document (window.next.appDir === true, stamped by the App
  // Router bootstrap before any client-component module evaluates) that
  // value-imports next/router for compat must not have the browser's native
  // restoration disabled. Upstream only flips history.scrollRestoration in
  // the Pages Router constructor, which never runs on App Router documents.
  //
  // This test guards the bootstrap eval-order invariant: the
  // manualScrollRestoration constant in router.ts reads window.next?.appDir
  // at module-eval time, which is only correct because installWindowNext in
  // app-browser-entry.ts stamps appDir: true before the first next/router
  // value-import evaluates. The failure mode is silent — if that ordering
  // regressed, manual scroll restoration would flip on under App Router
  // documents with no error, only subtly broken back/forward scroll.
  it("does not flip history.scrollRestoration to manual on App Router documents", async () => {
    const previousWindow = (globalThis as any).window;
    const originalScrollRestorationEnv = process.env.__NEXT_SCROLL_RESTORATION;
    const { win } = createNavWindow();

    (win.history as any).scrollRestoration = "auto";
    const sessionStore = new Map<string, string>();
    (win as any).sessionStorage = {
      getItem: (key: string) => sessionStore.get(key) ?? null,
      setItem: (key: string, value: string) => void sessionStore.set(key, value),
      removeItem: (key: string) => void sessionStore.delete(key),
    };
    // The App Router bootstrap stamp, in place before next/router evaluates.
    (win as any).next = { version: "test", appDir: true };

    (globalThis as any).window = win;
    process.env.__NEXT_SCROLL_RESTORATION = "true";

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      expect((win.history as any).scrollRestoration).toBe("auto");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (originalScrollRestorationEnv === undefined) {
        delete process.env.__NEXT_SCROLL_RESTORATION;
      } else {
        process.env.__NEXT_SCROLL_RESTORATION = originalScrollRestorationEnv;
      }
    }
  });

  it("does not prefix a locale-qualified target with the current locale", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    Object.assign(win, {
      __VINEXT_LOCALE__: "id",
      __VINEXT_LOCALES__: ["en", "id", "fr"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(
      async () =>
        new Response(
          buildNavHtml(
            "/about",
            pageModuleUrl,
            {},
            {
              locale: "fr",
              locales: ["en", "id", "fr"],
              defaultLocale: "en",
            },
          ),
          { status: 200 },
        ),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/fr/about");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledWith("/fr/about", expect.any(Object));
      // Next.js-shaped history state — assert partially.
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/fr/about",
      );
      expect(win.location.href).toBe("http://localhost/fr/about");
      expect(win.__VINEXT_LOCALE__).toBe("fr");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("handles Pages Router middleware internal redirects as client-side redirects", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win, replaceState, render } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    Object.assign(win.location, { origin: "http://localhost" });
    Object.assign(win, {
      __VINEXT_LOCALE__: "en",
      __VINEXT_LOCALES__: ["en", "fr", "nl", "es"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    Object.assign(win, {
      __VINEXT_PAGE_LOADERS__: {
        "/new-home": async () => import(pageModuleUrl),
      },
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(async (url: RequestInfo | URL) => {
      const href = getFetchHref(url);
      if (href === "/_next/data/build-1/old-home.json") {
        return new Response("{}", {
          headers: { "x-nextjs-redirect": "/new-home" },
          status: 200,
        });
      }
      if (href === "/new-home") {
        return new Response(buildNavHtmlWithVinext("/new-home", { hasMiddleware: true }));
      }
      throw new Error(`Unexpected fetch: ${href}`);
    });
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/old-home");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenNthCalledWith(
        1,
        "/_next/data/build-1/old-home.json",
        expect.objectContaining({
          headers: expect.objectContaining({ "x-nextjs-data": "1" }),
        }),
      );
      expect(fetch).toHaveBeenNthCalledWith(2, "/new-home", expect.any(Object));
      expect(replaceState).toHaveBeenLastCalledWith({}, "", "/new-home");
      expect(win.location.pathname).toBe("/new-home");
      expect(render).toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("hydrates middleware rewrites to fallback pages from the data response", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win, render } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    Object.assign(win.location, { origin: "http://localhost" });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    Object.assign(win, {
      __VINEXT_PAGE_PATTERNS__: ["/fallback-true-blog/[slug]"],
      __VINEXT_PAGE_LOADERS__: {
        "/fallback-true-blog/[slug]": async () => import(pageModuleUrl),
      },
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(async (url: RequestInfo | URL) => {
      const href = getFetchHref(url);
      if (href === "/_next/data/build-1/to-blog/another-post.json") {
        return new Response(
          JSON.stringify({ pageProps: { slug: "another-post", generated: true } }),
          {
            headers: {
              "content-type": "application/json",
              "x-nextjs-rewrite": "/fallback-true-blog/another-post",
            },
          },
        );
      }
      throw new Error(`Unexpected fetch: ${href}`);
    });
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/to-blog/another-post");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledOnce();
      expect(win.history.pushState).toHaveBeenLastCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        "/to-blog/another-post",
      );
      expect(win.location.pathname).toBe("/to-blog/another-post");
      expect(win.__NEXT_DATA__).toMatchObject({
        page: "/fallback-true-blog/[slug]",
        query: { slug: "another-post" },
        props: { pageProps: { slug: "another-post", generated: true } },
        isFallback: false,
      });
      expect(render).toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("uses the rewritten page loader when the visible URL also matches a page", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win, render } = createNavWindow();
    const sourceLoader = vi.fn(async () => ({ default: () => null }));
    const destinationLoader = vi.fn(async () => ({ default: () => null }));
    Object.assign(win.location, { origin: "http://localhost" });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    Object.assign(win, {
      __VINEXT_PAGE_PATTERNS__: ["/source/[slug]", "/destination"],
      __VINEXT_PAGE_LOADERS__: {
        "/source/[slug]": sourceLoader,
        "/destination": destinationLoader,
      },
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(
      async () =>
        new Response(JSON.stringify({ pageProps: { slug: "post", rewritten: true } }), {
          headers: {
            "content-type": "application/json",
            "x-nextjs-rewrite": "/destination?rewriteSlug=post",
          },
        }),
    );
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/source/post");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenCalledOnce();
      expect(sourceLoader).not.toHaveBeenCalled();
      expect(destinationLoader).toHaveBeenCalledOnce();
      expect(win.location.pathname).toBe("/source/post");
      expect(win.__NEXT_DATA__).toMatchObject({
        page: "/destination",
        query: { rewriteSlug: "post" },
        props: { pageProps: { slug: "post", rewritten: true } },
      });
      expect(Router.query).toEqual({ rewriteSlug: "post" });
      expect(render).toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("does not double-prefix basePath for middleware data redirects", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const { win, replaceState, render } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    Object.assign(win.location, {
      origin: "http://localhost",
      pathname: "/docs",
      href: "http://localhost/docs",
    });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    Object.assign(win, {
      __VINEXT_PAGE_LOADERS__: {
        "/new-home": async () => import(pageModuleUrl),
      },
    });
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";
    (globalThis as any).window = win;

    const fetch = vi.fn(async (url: RequestInfo | URL) => {
      const href = getFetchHref(url);
      if (href === "/docs/_next/data/build-1/old-home.json") {
        return new Response("{}", {
          headers: { "x-nextjs-redirect": "/docs/new-home" },
          status: 200,
        });
      }
      if (href === "/docs/new-home") {
        return new Response(buildNavHtmlWithVinext("/new-home", { hasMiddleware: true }));
      }
      throw new Error(`Unexpected fetch: ${href}`);
    });
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/old-home");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenNthCalledWith(
        1,
        "/docs/_next/data/build-1/old-home.json",
        expect.objectContaining({
          headers: expect.objectContaining({ "x-nextjs-data": "1" }),
        }),
      );
      expect(fetch).toHaveBeenNthCalledWith(2, "/docs/new-home", expect.any(Object));
      expect(fetch).not.toHaveBeenCalledWith("/docs/docs/new-home", expect.any(Object));
      expect(replaceState).toHaveBeenLastCalledWith({}, "", "/docs/new-home");
      expect(win.location.href).toBe("http://localhost/docs/new-home");
      expect(render).toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("falls through to normal page navigation when the middleware data probe fails", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win, render } = createNavWindow();
    const pageModuleUrl = path.resolve(import.meta.dirname, "fixtures/client-navigation-page.tsx");
    Object.assign(win.location, { origin: "http://localhost" });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(async (url: RequestInfo | URL) => {
      const href = getFetchHref(url);
      if (href === "/_next/data/build-1/old-home.json") {
        throw new TypeError("probe failed");
      }
      if (href === "/old-home") {
        return new Response(buildNavHtml("/old-home", pageModuleUrl));
      }
      throw new Error(`Unexpected fetch: ${href}`);
    });
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/old-home");

      expect(result).toBe(true);
      expect(fetch).toHaveBeenNthCalledWith(
        1,
        "/_next/data/build-1/old-home.json",
        expect.objectContaining({
          headers: expect.objectContaining({ "x-nextjs-data": "1" }),
        }),
      );
      expect(fetch).toHaveBeenNthCalledWith(2, "/old-home", expect.any(Object));
      expect(win.location.href).toBe("http://localhost/old-home");
      expect(render).toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("hard-navigates to the final middleware redirect URL when it is not a page", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    const hrefAssignments = trackHrefAssignments(win);
    Object.assign(win.location, { origin: "http://localhost" });
    Object.assign(win, {
      __VINEXT_LOCALE__: "en",
      __VINEXT_LOCALES__: ["en", "fr", "nl", "es"],
      __VINEXT_DEFAULT_LOCALE__: "en",
    });
    Object.assign(win.__NEXT_DATA__, {
      buildId: "build-1",
      __vinext: { ...win.__NEXT_DATA__.__vinext, hasMiddleware: true },
    });
    (globalThis as any).window = win;

    const fetch = vi.fn(async (url: RequestInfo | URL) => {
      const href = getFetchHref(url);
      if (href === "/_next/data/build-1/nl/to.json?pathname=/api/ok") {
        return new Response("{}", {
          headers: { "x-nextjs-redirect": "/nl/api/ok" },
          status: 200,
        });
      }
      if (href === "/api/ok") return new Response("ok", { status: 200 });
      throw new Error(`Unexpected fetch: ${href}`);
    });
    globalThis.fetch = fetch;

    try {
      vi.resetModules();
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("/to?pathname=/api/ok", undefined, { locale: "nl" });

      expect(result).toBe(false);
      expect(fetch).toHaveBeenNthCalledWith(
        1,
        "/_next/data/build-1/nl/to.json?pathname=/api/ok",
        expect.objectContaining({
          headers: expect.objectContaining({ "x-nextjs-data": "1" }),
        }),
      );
      expect(hrefAssignments).toContain("http://localhost/nl/to?pathname=/api/ok");
      expect(hrefAssignments).toContain("/api/ok");
      expect(hrefAssignments).not.toContain("/nl/api/ok");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  async function expectBasePathHashOnlyPush({
    browserPath,
    target,
    expectedBrowserUrl,
    expectedEventUrl,
  }: {
    browserPath: string;
    target: string;
    expectedBrowserUrl: string;
    expectedEventUrl: string;
  }) {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const { win } = createNavWindow();
    win.location.pathname = browserPath;
    win.location.href = `http://localhost${browserPath}`;
    (globalThis as any).window = win;
    (globalThis as any).document = {
      getElementById: vi.fn(() => ({ scrollIntoView: vi.fn() })),
    };
    process.env.__NEXT_ROUTER_BASEPATH = "/app";
    vi.resetModules();

    const fetch = vi.fn(async () => {
      throw new Error("hash-only navigations must not fetch page HTML");
    });
    globalThis.fetch = fetch;

    const hashEvents: string[] = [];
    const routeEvents: string[] = [];
    const onHashChangeStart = (...args: unknown[]) => {
      hashEvents.push(`start:${String(args[0])}`);
    };
    const onHashChangeComplete = (...args: unknown[]) => {
      hashEvents.push(`complete:${String(args[0])}`);
    };
    const onRouteChangeStart = (...args: unknown[]) => {
      routeEvents.push(`start:${String(args[0])}`);
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.on("hashChangeStart", onHashChangeStart);
      Router.events.on("hashChangeComplete", onHashChangeComplete);
      Router.events.on("routeChangeStart", onRouteChangeStart);

      const result = await Router.push(target);

      expect(result).toBe(true);
      expect(fetch).not.toHaveBeenCalled();
      // Hash-only push now records Next.js-shaped history state too.
      expect(win.history.pushState).toHaveBeenCalledWith(
        expect.objectContaining({ __N: true }),
        "",
        expectedBrowserUrl,
      );
      expect(hashEvents).toEqual([`start:${expectedEventUrl}`, `complete:${expectedEventUrl}`]);
      expect(routeEvents).toEqual([]);
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("hashChangeStart", onHashChangeStart);
      Router.events.off("hashChangeComplete", onHashChangeComplete);
      Router.events.off("routeChangeStart", onRouteChangeStart);
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
    }
  }

  it("treats app-relative hash navigations as hash-only when basePath is configured", async () => {
    await expectBasePathHashOnlyPush({
      browserPath: "/app/router-events-test",
      target: "/router-events-test#section-1",
      expectedBrowserUrl: "/app/router-events-test#section-1",
      expectedEventUrl: "/router-events-test#section-1",
    });
  });

  it("scrolls hash-only pushes to URI-decoded id targets", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const target = { scrollIntoView: vi.fn() };
    const getElementById = vi.fn((id: string) => (id === "hello world" ? target : null));
    const getElementsByName = vi.fn(() => []);
    (globalThis as any).document = { getElementById, getElementsByName };
    globalThis.fetch = vi.fn(async () => {
      throw new Error("hash-only navigations must not fetch page HTML");
    });
    vi.resetModules();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("#hello%20world");

      expect(result).toBe(true);
      expect(getElementById).toHaveBeenCalledWith("hello world");
      expect(getElementsByName).not.toHaveBeenCalled();
      expect(target.scrollIntoView).toHaveBeenCalledWith({ behavior: "auto" });
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("scrolls hash-only pushes to named anchors when no id matches", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const target = { scrollIntoView: vi.fn() };
    const getElementById = vi.fn(() => null);
    const getElementsByName = vi.fn((name: string) => (name === "legacy-anchor" ? [target] : []));
    (globalThis as any).document = { getElementById, getElementsByName };
    globalThis.fetch = vi.fn(async () => {
      throw new Error("hash-only navigations must not fetch page HTML");
    });
    vi.resetModules();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("#legacy-anchor");

      expect(result).toBe(true);
      expect(getElementById).toHaveBeenCalledWith("legacy-anchor");
      expect(getElementsByName).toHaveBeenCalledWith("legacy-anchor");
      expect(target.scrollIntoView).toHaveBeenCalledWith({ behavior: "auto" });
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("does not scroll hash-only pushes when scroll is false", async () => {
    const previousWindow = (globalThis as any).window;
    const previousDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const target = { scrollIntoView: vi.fn() };
    const getElementById = vi.fn(() => target);
    const getElementsByName = vi.fn(() => []);
    (globalThis as any).document = { getElementById, getElementsByName };
    globalThis.fetch = vi.fn(async () => {
      throw new Error("hash-only navigations must not fetch page HTML");
    });
    vi.resetModules();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      const result = await Router.push("#legacy-anchor", undefined, { scroll: false });

      expect(result).toBe(true);
      expect(getElementById).not.toHaveBeenCalled();
      expect(getElementsByName).not.toHaveBeenCalled();
      expect(target.scrollIntoView).not.toHaveBeenCalled();
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      if (previousDocument === undefined) {
        delete (globalThis as any).document;
      } else {
        (globalThis as any).document = previousDocument;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("does not strip app-relative targets that start with the basePath segment", async () => {
    await expectBasePathHashOnlyPush({
      browserPath: "/app/app/foo",
      target: "/app/foo#section-1",
      expectedBrowserUrl: "/app/app/foo#section-1",
      expectedEventUrl: "/app/foo#section-1",
    });
  });

  it("popstate known failures schedule a single hard navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const originalCustomEvent = globalThis.CustomEvent;
    const listeners = new Map<string, (event: any) => void>();
    const { win } = createNavWindow();
    win.addEventListener = vi.fn((type: string, handler: (event: any) => void) => {
      listeners.set(type, handler);
    });

    (globalThis as any).window = win;
    (globalThis as any).CustomEvent = class CustomEventMock {
      constructor(public type: string) {}
    } as any;

    globalThis.fetch = async (_url: any, _init: any) =>
      new Response("Internal Server Error", { status: 500 });

    try {
      vi.resetModules();
      await import("../packages/vinext/src/shims/router.js");
      const { installPagesRouterRuntime } =
        await import("../packages/vinext/src/shims/pages-router-runtime.js");
      installPagesRouterRuntime();

      const popstateHandler = listeners.get("popstate");
      expect(popstateHandler).toBeDefined();

      win.location.pathname = "/failing-page";
      win.location.href = "http://localhost/failing-page";
      // Install tracking after test setup so we only capture popstate-driven
      // writes, not the setup assignment above.
      const hrefAssignments = trackHrefAssignments(win);
      popstateHandler!({ state: null });
      // Cross a task boundary so the async popstate chain has fully settled.
      await new Promise((resolve) => setTimeout(resolve, 0));

      expect(hrefAssignments.filter((value) => value === "/failing-page")).toHaveLength(1);
      expect(hrefAssignments).toHaveLength(1);
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
      (globalThis as any).CustomEvent = originalCustomEvent;
    }
  });

  it("replace() also cancels superseded navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    const completedUrls: string[] = [];
    const errors: Array<{ err: unknown; url: string }> = [];
    const onRouteChangeComplete = (...args: unknown[]) => {
      completedUrls.push(String(args[0]));
    };
    const onRouteChangeError = (...args: unknown[]) => {
      errors.push({ err: args[0], url: String(args[1]) });
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeComplete", onRouteChangeComplete);
      Router.events.on("routeChangeError", onRouteChangeError);

      // First push, then replace overlapping
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.replace("/page-b");

      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));

      await Promise.allSettled([navA, navB]);

      // The superseded push (page-a) should be cancelled, not completed
      expect(completedUrls).not.toContain("/page-a");
      // page-a should have a cancelled error
      const cancelledA = errors.find((e) => e.url === "/page-a");
      expect(cancelledA).toBeDefined();
      expect(cancelledA?.err).toHaveProperty("cancelled", true);
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeComplete", onRouteChangeComplete);
      Router.events.off("routeChangeError", onRouteChangeError);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("abort signal fires when navigation is superseded — AbortError becomes NavigationCancelledError", async () => {
    // Verify that the AbortController signal passed to fetch actually fires when a
    // newer navigation starts, and that the resulting AbortError is converted into
    // a NavigationCancelledError (the routeChangeError path, not a plain rejection).
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    // Signal-aware mock: the first fetch rejects with AbortError when its signal fires.
    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) {
        return new Promise<Response>((resolve, reject) => {
          fetchA.promise.then(resolve, reject);
          _init?.signal?.addEventListener("abort", () => {
            reject(new DOMException("The operation was aborted.", "AbortError"));
          });
        });
      }
      return fetchB.promise;
    };

    const errors: Array<{ err: unknown; url: string }> = [];
    const onRouteChangeError = (...args: unknown[]) => {
      errors.push({ err: args[0], url: String(args[1]) });
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeError", onRouteChangeError);

      // Start navigation A then immediately supersede it with B
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Resolve B; A is aborted via its signal — no manual resolution needed
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));

      await Promise.allSettled([navA, navB]);

      // navA's fetch was aborted via signal → AbortError → NavigationCancelledError
      const cancelledError = errors.find((e) => e.url === "/page-a");
      expect(cancelledError).toBeDefined();
      expect(cancelledError?.err).toHaveProperty("cancelled", true);
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeError", onRouteChangeError);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("stale response arriving first does not render before the winning navigation", async () => {
    // fetchA (stale, page-a) resolves before fetchB (winning, page-b).
    // assertStillCurrent() in navigateClient must catch the stale navigation
    // after it processes the response, so page-a's data never reaches the DOM.
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    const completedUrls: string[] = [];
    const onRouteChangeComplete = (...args: unknown[]) => {
      completedUrls.push(String(args[0]));
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeComplete", onRouteChangeComplete);

      // Start two navigations
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Stale fetch (A) resolves first this time
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));
      // Winning fetch (B) resolves after
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));

      await Promise.allSettled([navA, navB]);

      // Stale navigation must not have committed its data to the DOM.
      // B may also fail at dynamic import in test env, so we only verify A never wrote.
      expect(win.__NEXT_DATA__.page).not.toBe("/page-a");
      // Stale navigation must not fire routeChangeComplete
      expect(completedUrls).not.toContain("/page-a");
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeComplete", onRouteChangeComplete);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  it("__NEXT_DATA__ is not stale when routeChangeError fires for a cancelled navigation", async () => {
    // Regression test: __NEXT_DATA__ must not reflect the cancelled route's data
    // at the moment routeChangeError fires.  The fix defers the global write until
    // just before root.render(), after all assertStillCurrent() checks pass.
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();
    (globalThis as any).window = win;

    const fetchA = createDeferred<Response>();
    const fetchB = createDeferred<Response>();
    let fetchCount = 0;

    globalThis.fetch = async (_url: any, _init: any) => {
      fetchCount++;
      if (fetchCount === 1) return fetchA.promise;
      return fetchB.promise;
    };

    // Track __NEXT_DATA__.page at the moment of each routeChangeError
    const nextDataPageAtError: string[] = [];
    const onRouteChangeError = () => {
      nextDataPageAtError.push(win.__NEXT_DATA__.page);
    };

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;

      Router.events.on("routeChangeError", onRouteChangeError);

      // Start two navigations — A will be superseded by B
      const navA = Router.push("/page-a");
      await Promise.resolve();
      const navB = Router.push("/page-b");

      // Resolve stale (A) first, then winning (B)
      fetchA.resolve(new Response(buildNavHtml("/page-a", "/@fs/pages/page-a.js")));
      fetchB.resolve(new Response(buildNavHtml("/page-b", "/@fs/pages/page-b.js")));

      await Promise.allSettled([navA, navB]);

      // At the moment routeChangeError fired for nav A, __NEXT_DATA__ must NOT
      // have been overwritten with page-a's data
      for (const page of nextDataPageAtError) {
        expect(page).not.toBe("/page-a");
      }
    } finally {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.off("routeChangeError", onRouteChangeError);
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      globalThis.fetch = originalFetch;
    }
  });

  // Ported from Next.js: test/e2e/middleware-dynamic-basepath-matcher-rewrites
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-dynamic-basepath-matcher-rewrites
  // Issue #1196 — catch-all router.query must not be corrupted by basePath + rewrites + middleware.
  it("initializes catch-all router.query from __NEXT_DATA__ with basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const { win } = createNavWindow();

    // Simulate being on a catch-all page under basePath
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";
    win.location.pathname = "/docs/first";
    win.location.href = "http://localhost/docs/first";

    // Server-rendered __NEXT_DATA__ for the catch-all page
    win.__NEXT_DATA__ = {
      page: "/[...path]",
      query: { path: ["first"] },
      isFallback: false,
      props: { pageProps: {} },
      __vinext: { pageModuleUrl: "/@fs/pages/[...path].js" },
    };

    (globalThis as any).window = win;
    vi.resetModules();

    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const routerModule = await import("../packages/vinext/src/shims/router.js");

    let capturedRouter: any;
    function Probe() {
      capturedRouter = routerModule.useRouter();
      return React.createElement("span", null, "ok");
    }

    try {
      renderToStaticMarkup(routerModule.wrapWithRouterContext(React.createElement(Probe)));

      expect(capturedRouter.query.path).toEqual(["first"]);
      expect(capturedRouter.pathname).toBe("/[...path]");
      expect(capturedRouter.asPath).toBe("/first");
    } finally {
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      vi.resetModules();
    }
  });

  // Ported from Next.js: test/e2e/middleware-dynamic-basepath-matcher-rewrites
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-dynamic-basepath-matcher-rewrites
  // Issue #1196 — during a client-side <Link>/Router.push() navigation on a
  // catch-all route under basePath, router.query.<catchAll> must reflect the
  // destination URL segments and not be corrupted by intermediate data-URL
  // handling. In Next.js the corruption originated in getMiddlewareData where
  // basePath wasn't stripped from the _next/data/... URL before the catch-all
  // route regex matched against it. vinext's navigateClient reads __NEXT_DATA__
  // directly from the response HTML rather than constructing _next/data URLs,
  // so it doesn't have that architectural pattern, but the route-param
  // extraction path that ultimately produces router.query must still strip
  // basePath from the resolved pathname before matching the catch-all pattern.
  // This test exercises Router.push() through performNavigation → pushState
  // and verifies that router.query.path reflects the destination segments.
  it("preserves catch-all router.query after client navigation with basePath", async () => {
    const previousWindow = (globalThis as any).window;
    const previousBasePath = process.env.__NEXT_ROUTER_BASEPATH;
    const originalFetch = globalThis.fetch;
    const { win } = createNavWindow();

    // Simulate being on a catch-all page under basePath when navigation starts.
    process.env.__NEXT_ROUTER_BASEPATH = "/docs";
    win.location.pathname = "/docs/first";
    win.location.href = "http://localhost/docs/first";
    win.__NEXT_DATA__ = {
      page: "/[...path]",
      query: { path: ["first"] },
      isFallback: false,
      props: { pageProps: {} },
      // The catch-all module file is referenced through a Vite-style path that
      // passes isValidModulePath() (no ".." directory-traversal segments).
      __vinext: { pageModuleUrl: "/@fs/pages/catchall.js" },
    };

    (globalThis as any).window = win;
    vi.resetModules();

    // Capture router state mid-navigation, after pushState has updated
    // window.location but before navigateClient's dynamic import runs (which
    // would fail in this test env). useRouter() reads from window.location
    // and window.__NEXT_DATA__ at provider-mount time, so rendering a Probe
    // inside the mocked fetch handler observes the post-pushState state —
    // exactly the code path that #1196 corrupts in Next.js.
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const routerModule = await import("../packages/vinext/src/shims/router.js");

    let capturedRouter: any;
    function Probe() {
      capturedRouter = routerModule.useRouter();
      return React.createElement("span", null, "ok");
    }

    globalThis.fetch = async (_url: any, _init: any) => {
      renderToStaticMarkup(routerModule.wrapWithRouterContext(React.createElement(Probe)));
      // Return HTML containing the destination's __NEXT_DATA__. The dynamic
      // import of the page module fails in this test env, which is fine — the
      // assertion above has already captured router.query.
      return new Response(
        buildNavHtml("/[...path]", "/@fs/pages/catchall.js", { path: ["second"] }),
      );
    };

    // Silence the expected routeChangeError from the page-module import failure.
    const onRouteChangeError = vi.fn();

    try {
      const Router = routerModule.default;
      Router.events.on("routeChangeError", onRouteChangeError);

      // Router.push() takes an app-relative path; basePath is added internally
      // by performNavigation → toBrowserNavigationHref before pushState.
      await Router.push("/second");

      // pushState has fired, so location.pathname is "/docs/second".
      // stripBasePath() removes "/docs", and the catch-all pattern
      // "/[...path]" from __NEXT_DATA__.page extracts { path: ["second"] }.
      expect(capturedRouter.pathname).toBe("/[...path]");
      expect(capturedRouter.asPath).toBe("/second");
      expect(capturedRouter.query.path).toEqual(["second"]);
      // Negative assertion mirroring #1196: query.path must NOT contain
      // _next/data segments.
      expect(capturedRouter.query.path).not.toContain("_next");
      expect(capturedRouter.query.path).not.toContain("data");
    } finally {
      routerModule.default.events.off("routeChangeError", onRouteChangeError);
      globalThis.fetch = originalFetch;
      if (previousBasePath === undefined) {
        delete process.env.__NEXT_ROUTER_BASEPATH;
      } else {
        process.env.__NEXT_ROUTER_BASEPATH = previousBasePath;
      }
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
      vi.resetModules();
    }
  });
});

describe("deprecated Router.on<Event> property bridge", () => {
  // These tests verify that assigning a function to a deprecated property such
  // as `Router.onRouteChangeComplete` causes that function to be invoked when
  // the corresponding event fires via Router.events.emit().  The bridge is the
  // code added in fix/with-router-deprecated-nav-events.

  beforeEach(() => {
    vi.resetModules();
  });

  afterEach(() => {
    vi.resetModules();
  });

  it("Router.onRouteChangeComplete is called when routeChangeComplete fires", async () => {
    const routerModule = await import("../packages/vinext/src/shims/router.js");
    const Router = routerModule.default;

    const calls: unknown[][] = [];
    (Router as any).onRouteChangeComplete = (...args: unknown[]) => {
      calls.push(args);
    };

    try {
      Router.events.emit("routeChangeComplete", "/test-path", { shallow: false });

      expect(calls).toHaveLength(1);
      expect(calls[0][0]).toBe("/test-path");
    } finally {
      delete (Router as any).onRouteChangeComplete;
    }
  });

  it("Router.onRouteChangeStart is called when routeChangeStart fires", async () => {
    const routerModule = await import("../packages/vinext/src/shims/router.js");
    const Router = routerModule.default;

    const calls: unknown[][] = [];
    (Router as any).onRouteChangeStart = (...args: unknown[]) => {
      calls.push(args);
    };

    try {
      Router.events.emit("routeChangeStart", "/start-path", { shallow: false });

      expect(calls).toHaveLength(1);
      expect(calls[0][0]).toBe("/start-path");
    } finally {
      delete (Router as any).onRouteChangeStart;
    }
  });

  it("emitting an event with no on<Event> handler assigned does not throw", async () => {
    const routerModule = await import("../packages/vinext/src/shims/router.js");
    const Router = routerModule.default;

    // Ensure no handler is set
    delete (Router as any).onRouteChangeComplete;

    expect(() => {
      Router.events.emit("routeChangeComplete", "/no-handler", { shallow: false });
    }).not.toThrow();
  });

  it("previously-cleared on<Event> handler is not called after removal", async () => {
    const routerModule = await import("../packages/vinext/src/shims/router.js");
    const Router = routerModule.default;

    const calls: unknown[][] = [];
    (Router as any).onRouteChangeComplete = (...args: unknown[]) => {
      calls.push(args);
    };

    // Fire once — should be called
    Router.events.emit("routeChangeComplete", "/first", { shallow: false });
    expect(calls).toHaveLength(1);

    // Clear the handler
    delete (Router as any).onRouteChangeComplete;

    // Fire again — should NOT be called
    Router.events.emit("routeChangeComplete", "/second", { shallow: false });
    expect(calls).toHaveLength(1);
  });
});

describe("Pages Router _next/data client navigation", () => {
  // These tests exercise the JSON path in navigateClient: when
  // `__VINEXT_PAGE_LOADERS__` is populated (prod-style hydration), navigation
  // fetches `/_next/data/<buildId>/<page>.json` instead of the full HTML.
  // The HTML path tests above still cover the dev fallback.

  // Local mirror of trackHrefAssignments in the concurrent-navigation suite —
  // duplicated rather than hoisted to avoid disturbing the existing scopes.
  function trackHrefAssignmentsLocal(win: { location: { href: string } }): string[] {
    let currentHref = win.location.href;
    const assignments: string[] = [];
    Object.defineProperty(win.location, "href", {
      configurable: true,
      enumerable: true,
      get: () => currentHref,
      set: (value: string) => {
        currentHref = value;
        assignments.push(value);
      },
    });
    return assignments;
  }

  // Local mirror of buildNavHtml — the HTML-fallback assertion needs a
  // minimal HTML response that the navigateClientHtml path can parse.
  function buildNavHtmlLocal(page: string, pageModuleUrl: string): string {
    const nextData = {
      props: { pageProps: {} },
      page,
      query: {},
      buildId: null,
      isFallback: false,
      __vinext: { pageModuleUrl },
    };
    return `<html><head></head><body><script>window.__NEXT_DATA__ = ${JSON.stringify(nextData)}</script></body></html>`;
  }

  function createDataNavWindow(
    opts: {
      buildId?: string;
      page?: string;
      locale?: string;
      loaders?: Record<string, () => Promise<unknown>>;
      appLoader?: () => Promise<unknown>;
      /** Initial pathname for window.location. */
      pathname?: string;
    } = {},
  ) {
    const pushState = vi.fn();
    const replaceState = vi.fn();
    const render = vi.fn((element: unknown) => commitRenderedPagesRouterElement(element));
    const buildId = opts.buildId ?? "test-build";

    const win = {
      location: {
        pathname: opts.pathname ?? "/",
        search: "",
        hash: "",
        href: `http://localhost${opts.pathname ?? "/"}`,
        origin: "http://localhost",
        hostname: "localhost",
        assign: vi.fn(),
        replace: vi.fn(),
        reload: vi.fn(),
      },
      history: {
        state: null,
        pushState: pushState as any,
        replaceState: replaceState as any,
        back: vi.fn(),
      },
      dispatchEvent: vi.fn(),
      scrollTo: vi.fn(),
      scrollX: 0,
      scrollY: 0,
      addEventListener: vi.fn(),
      __NEXT_DATA__: {
        page: opts.page ?? "/",
        query: {},
        isFallback: false,
        props: { pageProps: {} },
        buildId,
      } as any,
      __VINEXT_ROOT__: { render },
      __VINEXT_APP__: undefined,
      __VINEXT_LOCALE__: opts.locale,
      __VINEXT_LOCALES__: undefined,
      __VINEXT_DEFAULT_LOCALE__: undefined,
      __VINEXT_PAGE_LOADERS__: opts.loaders,
      __VINEXT_PAGE_PATTERNS__: opts.loaders ? Object.keys(opts.loaders) : undefined,
      __VINEXT_APP_LOADER__: opts.appLoader,
    };

    pushState.mockImplementation((_state: unknown, _title: string, url: string) => {
      try {
        const parsed = new URL(url, "http://localhost");
        win.location.pathname = parsed.pathname;
        win.location.search = parsed.search;
        win.location.hash = parsed.hash;
        win.location.href = parsed.href;
      } catch {
        win.location.pathname = url;
        win.location.href = "http://localhost" + url;
      }
    });
    replaceState.mockImplementation((_state: unknown, _title: string, url?: string) => {
      if (!url) return;
      try {
        const parsed = new URL(url, "http://localhost");
        win.location.pathname = parsed.pathname;
        win.location.search = parsed.search;
        win.location.hash = parsed.hash;
        win.location.href = parsed.href;
      } catch {
        win.location.pathname = url;
        win.location.href = "http://localhost" + url;
      }
    });

    return { win, pushState, replaceState, render, buildId };
  }

  // Fake React-component placeholder. The mocked __VINEXT_ROOT__.render
  // captures whatever element navigateClient produces; we only assert on the
  // surrounding state (page, query, fetch URL, etc.), so the page modules
  // returned by loaders just need a default export of any function value.
  const FakePage = (() => "fake-page") as unknown as { default: unknown };
  (FakePage as { default: unknown }).default = () => "fake-page";

  function makePageModule(name: string): { default: unknown } {
    return { default: ((): string => `page:${name}`) as unknown };
  }

  it("fetches /_next/data/<buildId>/<page>.json instead of the HTML page", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const loaderAbout = vi.fn(async () => makePageModule("about"));
    const { win, buildId } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")), "/about": loaderAbout },
    });
    (globalThis as any).window = win;
    vi.resetModules();

    const fetchMock = vi.fn(
      async (_url: any, _init: any) =>
        new Response(JSON.stringify({ pageProps: { hello: "world" } }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    );
    globalThis.fetch = fetchMock as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      const result = await Router.push("/about");

      expect(result).toBe(true);
      expect(fetchMock).toHaveBeenCalledTimes(1);
      const [calledUrl, calledInit] = fetchMock.mock.calls[0];
      expect(String(calledUrl)).toBe(`/_next/data/${buildId}/about.json`);
      expect((calledInit as RequestInit).headers).toMatchObject({
        Accept: "application/json",
        "x-nextjs-data": "1",
      });
      expect(loaderAbout).toHaveBeenCalledTimes(1);
      expect(win.__NEXT_DATA__.page).toBe("/about");
      expect(win.__NEXT_DATA__.props.pageProps).toEqual({ hello: "world" });
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("dedupes deferred identical pushes while cancelling only the stale navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const slowLoader = vi.fn(async () => makePageModule("slow"));
    const { win, render, pushState } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")), "/slow": slowLoader },
    });
    (globalThis as any).window = win;
    vi.resetModules();

    let resolveFetch: (response: Response) => void = () => {};
    const deferredResponse = new Promise<Response>((resolve) => {
      resolveFetch = resolve;
    });
    const fetchMock = vi.fn(() => deferredResponse);
    globalThis.fetch = fetchMock as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      const routeChangeErrors: unknown[] = [];
      const routeChangeComplete = vi.fn();
      Router.events.on("routeChangeError", (error) => routeChangeErrors.push(error));
      Router.events.on("routeChangeComplete", routeChangeComplete);

      const firstPush = Router.push("/slow?value=one");
      const secondPush = Router.push("/slow?value=one");

      expect(fetchMock).toHaveBeenCalledTimes(1);
      expect(pushState).toHaveBeenCalledTimes(2);

      resolveFetch(
        new Response(JSON.stringify({ pageProps: { hit: 1 } }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
      );

      await expect(Promise.all([firstPush, secondPush])).resolves.toEqual([true, true]);
      expect(routeChangeErrors).toHaveLength(1);
      expect(routeChangeErrors[0]).toMatchObject({ cancelled: true });
      expect(routeChangeComplete).toHaveBeenCalledTimes(1);
      expect(slowLoader).toHaveBeenCalledTimes(1);
      expect(render).toHaveBeenCalledTimes(1);
      expect(win.__NEXT_DATA__).toMatchObject({
        page: "/slow",
        query: { value: "one" },
        props: { pageProps: { hit: 1 } },
      });
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("threads full Pages props through _app during data navigation", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const PageComponent = () => "page:about";
    const AppComponent = () => "app";
    const loaderAbout = vi.fn(async () => ({ default: PageComponent }));
    const appLoader = vi.fn(async () => ({ default: AppComponent }));
    const { win, render } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")), "/about": loaderAbout },
      appLoader,
    });
    (globalThis as any).window = win;
    vi.resetModules();

    const props = {
      appProps: { source: "_app.getInitialProps" },
      pageProps: { hello: "world" },
    };
    globalThis.fetch = vi.fn(
      async () =>
        new Response(JSON.stringify(props), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    ) as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      const result = await Router.push("/about");

      expect(result).toBe(true);
      expect(loaderAbout).toHaveBeenCalledTimes(1);
      expect(appLoader).toHaveBeenCalledTimes(1);
      const committed = render.mock.calls.at(-1)![0] as {
        props: {
          children: { props: { children: { type: unknown; props: Record<string, unknown> } } };
        };
      };
      const appElement = committed.props.children.props.children;
      expect(appElement.type).toBe(AppComponent);
      expect(appElement.props).toMatchObject({
        appProps: { source: "_app.getInitialProps" },
        pageProps: { hello: "world" },
      });
      expect(appElement.props.Component).toBe(PageComponent);
      expect(win.__NEXT_DATA__.props).toEqual(props);
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("hard-reloads when the data endpoint returns 404 (build-skew safety net)", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const { win } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/about": vi.fn(async () => makePageModule("about")),
      },
    });
    (globalThis as any).window = win;
    const hrefAssignments = trackHrefAssignmentsLocal(win);
    vi.resetModules();

    globalThis.fetch = vi.fn(
      async () =>
        new Response("{}", { status: 404, headers: { "Content-Type": "application/json" } }),
    ) as any;

    const onRouteChangeError = vi.fn();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.on("routeChangeError", onRouteChangeError);

      const result = await Router.push("/about");
      expect(result).toBe(false);
      // Hard reload was queued via window.location.href = url.
      expect(hrefAssignments).toContain("/about");
      Router.events.off("routeChangeError", onRouteChangeError);
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("hard-reloads to the redirect target on x-nextjs-redirect", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const { win } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/about": vi.fn(async () => makePageModule("about")),
      },
    });
    (globalThis as any).window = win;
    const hrefAssignments = trackHrefAssignmentsLocal(win);
    vi.resetModules();

    globalThis.fetch = vi.fn(
      async () =>
        new Response("{}", {
          status: 200,
          headers: { "Content-Type": "application/json", "x-nextjs-redirect": "/login" },
        }),
    ) as any;

    const onRouteChangeError = vi.fn();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.on("routeChangeError", onRouteChangeError);

      await Router.push("/about");
      // The soft redirect destination is what gets hard-loaded.
      expect(hrefAssignments).toContain("/login");
      Router.events.off("routeChangeError", onRouteChangeError);
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("falls back to the HTML path when no loader is registered for the target route", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    // Loader map has only `/` — no loader for `/about`, so the data path
    // resolves to null and navigateClient falls through to HTML extraction.
    const { win } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")) },
    });
    (globalThis as any).window = win;
    vi.resetModules();

    const fetchMock = vi.fn(
      async (_url: any, _init?: any) =>
        new Response(buildNavHtmlLocal("/about", "/@fs/pages/about.js"), {
          status: 200,
          headers: { "Content-Type": "text/html" },
        }),
    );
    globalThis.fetch = fetchMock as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      await Router.push("/about");

      // The fetch URL must be the HTML page, not the data endpoint.
      const [calledUrl] = fetchMock.mock.calls[0];
      expect(String(calledUrl)).not.toMatch(/^\/_next\/data\//);
      expect(String(calledUrl)).toContain("/about");
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("extracts dynamic route params from the URL and stores them on __NEXT_DATA__.query", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const blogLoader = vi.fn(async () => makePageModule("blog"));
    const { win, buildId } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")), "/blog/[slug]": blogLoader },
    });
    (globalThis as any).window = win;
    vi.resetModules();

    const fetchMock = vi.fn(
      async (_url: any, _init?: any) =>
        new Response(JSON.stringify({ pageProps: { post: "hi" } }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    );
    globalThis.fetch = fetchMock as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      await Router.push("/blog/my-post");

      const [calledUrl] = fetchMock.mock.calls[0];
      expect(String(calledUrl)).toBe(`/_next/data/${buildId}/blog/my-post.json`);
      expect(blogLoader).toHaveBeenCalledTimes(1);
      expect(win.__NEXT_DATA__.page).toBe("/blog/[slug]");
      expect(win.__NEXT_DATA__.query).toEqual({ slug: "my-post" });
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("merges URL search params and route params into __NEXT_DATA__.query (route params win)", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const { win } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/blog/[slug]": vi.fn(async () => makePageModule("blog")),
      },
    });
    (globalThis as any).window = win;
    vi.resetModules();

    globalThis.fetch = vi.fn(
      async () =>
        new Response(JSON.stringify({ pageProps: {} }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    ) as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      // Search param `slug` should NOT shadow the dynamic route param `slug`.
      // Search params `q` and `tag` should appear in query alongside `slug`.
      await Router.push("/blog/my-post?slug=spoofed&q=hello&tag=a&tag=b");

      expect(win.__NEXT_DATA__.page).toBe("/blog/[slug]");
      expect(win.__NEXT_DATA__.query).toEqual({
        slug: "my-post",
        q: "hello",
        tag: ["a", "b"],
      });
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("hard-reloads on a malformed JSON response", async () => {
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const { win } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/about": vi.fn(async () => makePageModule("about")),
      },
    });
    (globalThis as any).window = win;
    const hrefAssignments = trackHrefAssignmentsLocal(win);
    vi.resetModules();

    globalThis.fetch = vi.fn(
      async () =>
        new Response("not json {{{", {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    ) as any;

    const onRouteChangeError = vi.fn();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      Router.events.on("routeChangeError", onRouteChangeError);

      await Router.push("/about");
      expect(hrefAssignments).toContain("/about");
      Router.events.off("routeChangeError", onRouteChangeError);
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("uses the JSON path for prefetch when a loader is registered", async () => {
    const previousWindow = (globalThis as any).window;
    const originalDocument = (globalThis as any).document;
    const originalFetch = globalThis.fetch;

    const aboutLoader = vi.fn(async () => makePageModule("about"));
    const { win, buildId } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")), "/about": aboutLoader },
    });
    (globalThis as any).window = win;

    const appendedLinks: Array<{ rel: string; as: string; href: string; crossOrigin?: string }> =
      [];
    (globalThis as any).document = {
      createElement: (tag: string) => {
        if (tag !== "link") return {};
        const link: { rel: string; as: string; href: string; crossOrigin?: string } = {
          rel: "",
          as: "",
          href: "",
        };
        return link;
      },
      head: {
        appendChild: (node: any) => {
          appendedLinks.push({
            rel: node.rel,
            as: node.as,
            href: node.href,
            crossOrigin: node.crossOrigin,
          });
        },
      },
    };
    const fetchMock = vi.fn(async () => new Response("{}"));
    globalThis.fetch = fetchMock;
    vi.resetModules();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      await Router.prefetch("/about");

      await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce());
      expect(fetchMock).toHaveBeenCalledWith(`/_next/data/${buildId}/about.json`, {
        headers: {
          Accept: "application/json",
          purpose: "prefetch",
          "x-nextjs-data": "1",
        },
        signal: expect.any(AbortSignal),
      });
      expect(appendedLinks).toEqual([]);
      // The loader was warmed (chunk fetch kicked off).
      expect(aboutLoader).toHaveBeenCalledTimes(1);
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      (globalThis as any).document = originalDocument;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("falls back to as=document prefetch when no loader matches", async () => {
    const previousWindow = (globalThis as any).window;
    const originalDocument = (globalThis as any).document;

    const { win } = createDataNavWindow({
      loaders: { "/": vi.fn(async () => makePageModule("home")) },
    });
    (globalThis as any).window = win;

    const appendedLinks: Array<{ rel: string; as: string; href: string }> = [];
    (globalThis as any).document = {
      createElement: () => ({ rel: "", as: "", href: "" }),
      head: {
        appendChild: (node: any) => {
          appendedLinks.push({ rel: node.rel, as: node.as, href: node.href });
        },
      },
    };
    vi.resetModules();

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      await Router.prefetch("/unmapped");

      const prefetchLink = appendedLinks.find((l) => l.rel === "prefetch");
      expect(prefetchLink?.as).toBe("document");
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      (globalThis as any).document = originalDocument;
      vi.resetModules();
    }
  });

  it("strips the locale prefix before matching loaders and updates __NEXT_DATA__.locale", async () => {
    // Regression: route patterns in __VINEXT_PAGE_PATTERNS__ are
    // locale-unaware (e.g. /about, not /fr/about). Without stripping the
    // locale prefix from the URL before pattern matching, locale transitions
    // would always miss the loader map and fall through to the slower HTML
    // path. The data URL itself must keep the locale prefix so the server
    // returns locale-specific gSSP data.
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const loaderAbout = vi.fn(async () => makePageModule("about"));
    const { win, buildId } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/about": loaderAbout,
      },
      locale: "en",
    });
    (win as any).__VINEXT_LOCALES__ = ["en", "fr"];
    (win as any).__VINEXT_DEFAULT_LOCALE__ = "en";
    (win.__NEXT_DATA__ as any).locale = "en";
    (win.__NEXT_DATA__ as any).locales = ["en", "fr"];
    (win.__NEXT_DATA__ as any).defaultLocale = "en";
    (globalThis as any).window = win;
    vi.resetModules();

    const fetchMock = vi.fn(
      async () =>
        new Response(JSON.stringify({ pageProps: { hello: "bonjour" } }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    );
    globalThis.fetch = fetchMock as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      const result = await Router.push("/fr/about");

      expect(result).toBe(true);
      // URL kept the locale prefix — the server uses it to pick the locale.
      const [calledUrl] = (fetchMock.mock.calls[0] ?? []) as unknown as [unknown, RequestInit?];
      expect(String(calledUrl)).toBe(`/_next/data/${buildId}/fr/about.json`);
      // Loader matched on the locale-stripped pattern.
      expect(loaderAbout).toHaveBeenCalledTimes(1);
      // Page resolved to the unprefixed route pattern.
      expect(win.__NEXT_DATA__.page).toBe("/about");
      // Locale globals updated to the new locale so useRouter().locale reflects it.
      expect((win.__NEXT_DATA__ as any).locale).toBe("fr");
      expect(win.__VINEXT_LOCALE__).toBe("fr");
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });

  it("restores the default locale on transitions back to an unprefixed URL", async () => {
    // `Router.push("/about", undefined, { locale: "en" })` from a French page
    // switches back to the default locale. The data URL drops the locale
    // prefix (default locale is unprefixed) and __NEXT_DATA__.locale flips
    // back to "en" so useRouter().locale tracks the new locale.
    const previousWindow = (globalThis as any).window;
    const originalFetch = globalThis.fetch;

    const loaderAbout = vi.fn(async () => makePageModule("about"));
    const { win, buildId } = createDataNavWindow({
      loaders: {
        "/": vi.fn(async () => makePageModule("home")),
        "/about": loaderAbout,
      },
      locale: "fr",
      pathname: "/fr",
    });
    (win as any).__VINEXT_LOCALES__ = ["en", "fr"];
    (win as any).__VINEXT_DEFAULT_LOCALE__ = "en";
    (win.__NEXT_DATA__ as any).locale = "fr";
    (win.__NEXT_DATA__ as any).locales = ["en", "fr"];
    (win.__NEXT_DATA__ as any).defaultLocale = "en";
    (globalThis as any).window = win;
    vi.resetModules();

    globalThis.fetch = vi.fn(
      async () =>
        new Response(JSON.stringify({ pageProps: {} }), {
          status: 200,
          headers: { "Content-Type": "application/json" },
        }),
    ) as any;

    try {
      const routerModule = await import("../packages/vinext/src/shims/router.js");
      const Router = routerModule.default;
      await Router.push("/about", undefined, { locale: "en" });

      const [calledUrl] = (globalThis.fetch as any).mock.calls[0];
      expect(String(calledUrl)).toBe(`/_next/data/${buildId}/about.json`);
      expect((win.__NEXT_DATA__ as any).locale).toBe("en");
      expect(win.__VINEXT_LOCALE__).toBe("en");
    } finally {
      if (previousWindow === undefined) delete (globalThis as any).window;
      else (globalThis as any).window = previousWindow;
      globalThis.fetch = originalFetch;
      vi.resetModules();
    }
  });
});

describe("next/server enhancements", () => {
  it("NextRequest.ip extracts from x-forwarded-for header", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("https://example.com", {
      headers: { "x-forwarded-for": "1.2.3.4, 5.6.7.8" },
    });
    expect(req.ip).toBe("1.2.3.4");
  });

  it("NextRequest.ip returns undefined without forwarded header", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("https://example.com");
    expect(req.ip).toBeUndefined();
  });

  it("NextRequest.geo extracts from Cloudflare/Vercel headers", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("https://example.com", {
      headers: {
        "cf-ipcountry": "US",
        "cf-ipcity": "San Francisco",
      },
    });
    const geo = req.geo;
    expect(geo).toBeDefined();
    expect(geo!.country).toBe("US");
    expect(geo!.city).toBe("San Francisco");
  });

  it("NextRequest.geo returns undefined without geo headers", async () => {
    const { NextRequest } = await import("../packages/vinext/src/shims/server.js");
    const req = new NextRequest("https://example.com");
    expect(req.geo).toBeUndefined();
  });

  it("ResponseCookies.getAll returns all set cookies", async () => {
    const { ResponseCookies } = await import("../packages/vinext/src/shims/server.js");
    const headers = new Headers();
    const cookies = new ResponseCookies(headers);
    cookies.set("a", "1");
    cookies.set("b", "2");
    const all = cookies.getAll();
    expect(all).toHaveLength(2);
    expect(all.find((c: any) => c.name === "a")?.value).toBe("1");
    expect(all.find((c: any) => c.name === "b")?.value).toBe("2");
  });
});

describe("next/image enhancements", () => {
  it("exports StaticImageData type", async () => {
    const imageModule = await import("../packages/vinext/src/shims/image.js");
    // StaticImageData is an interface, so we can't check at runtime
    // but getImageProps uses it — verify that function exists
    expect(typeof imageModule.getImageProps).toBe("function");
  });

  it("getImageProps returns img props from Image props", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Test",
      width: 800,
      height: 600,
      priority: true,
    });
    // Local images now route through the optimization endpoint
    expect(result.props.src).toContain("/_next/image");
    expect(result.props.src).toContain("url=%2Fphoto.jpg");
    expect(result.props.src).toContain("w=800");
    expect(result.props.alt).toBe("Test");
    expect(result.props.width).toBe(800);
    expect(result.props.height).toBe(600);
    expect(result.props.loading).toBe("eager");
  });

  it("getImageProps handles fill mode", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/bg.jpg",
      alt: "Background",
      fill: true,
    });
    expect(result.props.width).toBeUndefined();
    expect(result.props.height).toBeUndefined();
    expect(result.props.style?.position).toBe("absolute");
  });

  it("getImageProps handles StaticImageData", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: { src: "/imported.jpg", width: 1200, height: 800, blurDataURL: "data:..." },
      alt: "Imported",
    });
    expect(result.props.src).toContain("/_next/image");
    expect(result.props.src).toContain("url=%2Fimported.jpg");
    expect(result.props.src).toContain("w=1200");
    expect(result.props.width).toBe(1200);
    expect(result.props.height).toBe(800);
  });

  it("getImageProps generates srcSet for local images with width", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Test",
      width: 1200,
      height: 800,
    });
    expect(result.props.srcSet).toBeDefined();
    // srcSet entries point to /_next/image optimization endpoint
    expect(result.props.srcSet).toContain("/_next/image");
    expect(result.props.srcSet).toContain("url=%2Fphoto.jpg");
    expect(result.props.srcSet).toContain("w");
  });

  it("getImageProps does not generate srcSet for fill images", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/bg.jpg",
      alt: "Background",
      fill: true,
    });
    expect(result.props.srcSet).toBeUndefined();
    expect(result.props.sizes).toBe("100vw"); // fill implies 100vw
  });

  it("getImageProps includes fetchPriority for priority images", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/hero.jpg",
      alt: "Hero",
      width: 1200,
      height: 800,
      priority: true,
    });
    expect(result.props.fetchPriority).toBe("high");
    expect(result.props.loading).toBe("eager");
  });

  it("getImageProps includes data-nimg attribute", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Photo",
      width: 800,
      height: 600,
    });
    expect((result.props as any)["data-nimg"]).toBe("1");

    const fillResult = getImageProps({
      src: "/bg.jpg",
      alt: "BG",
      fill: true,
    });
    expect((fillResult.props as any)["data-nimg"]).toBe("fill");
  });

  it("getImageProps includes blur placeholder background styles", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const blurUrl = "data:image/jpeg;base64,/9j/4AAQ";
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Blurry",
      width: 800,
      height: 600,
      placeholder: "blur",
      blurDataURL: blurUrl,
    });
    expect(result.props.style?.backgroundImage).toContain(blurUrl);
    expect(result.props.style?.backgroundSize).toBe("cover");
  });

  it("getImageProps uses custom loader function", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Custom",
      width: 800,
      height: 600,
      loader: ({ src, width, quality }) => `https://cdn.example.com${src}?w=${width}&q=${quality}`,
    });
    // Custom loader bypasses the /_next/image endpoint
    expect(result.props.src).toBe("https://cdn.example.com/photo.jpg?w=800&q=75");
    expect(result.props.src).not.toContain("/_next/image");
  });

  it("unoptimized prop bypasses /_next/image endpoint", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.jpg",
      alt: "Unoptimized",
      width: 800,
      height: 600,
      unoptimized: true,
    });
    // unoptimized=true should serve the raw src, not the optimization endpoint
    expect(result.props.src).toBe("/photo.jpg");
    expect(result.props.src).not.toContain("/_next/image");
  });

  it("SVG src auto-skips optimization endpoint (default behavior)", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/logo.svg",
      alt: "SVG logo",
      width: 200,
      height: 200,
    });
    // By default (dangerouslyAllowSVG not set), .svg sources bypass the optimizer
    expect(result.props.src).toBe("/logo.svg");
    expect(result.props.src).not.toContain("/_next/image");
  });

  it("non-SVG src still uses optimization endpoint", async () => {
    const { getImageProps } = await import("../packages/vinext/src/shims/image.js");
    const result = getImageProps({
      src: "/photo.png",
      alt: "PNG photo",
      width: 256,
      height: 256,
    });
    expect(result.props.src).toContain("/_next/image");
  });
});

describe("next/image component rendering", () => {
  it("renders basic image with src, alt, width, height", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/photo.jpg", alt: "Test photo", width: 800, height: 600 }),
    );
    // Local images route through the optimization endpoint
    expect(html).toContain("/_next/image");
    expect(html).toContain("url=%2Fphoto.jpg");
    expect(html).toContain('alt="Test photo"');
    expect(html).toContain('width="800"');
    expect(html).toContain('height="600"');
  });

  it("renders fill image with absolute positioning", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/bg.jpg", alt: "Background", fill: true }),
    );
    expect(html).toContain("position:absolute");
    expect(html).toContain('data-nimg="fill"');
    // fill images should not have width/height attributes
    expect(html).not.toContain("width=");
    expect(html).not.toContain("height=");
  });

  it("renders priority image with fetchpriority=high and loading=eager", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/hero.jpg",
        alt: "Hero",
        width: 1200,
        height: 800,
        priority: true,
      }),
    );
    expect(html).toContain('fetchPriority="high"');
    expect(html).toContain('loading="eager"');
  });

  it("renders lazy loading by default (no priority)", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/photo.jpg", alt: "Photo", width: 800, height: 600 }),
    );
    expect(html).toContain('loading="lazy"');
    expect(html).not.toContain("fetchPriority");
  });

  it("renders srcSet for local images with width", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/photo.jpg", alt: "Photo", width: 1200, height: 800 }),
    );
    expect(html).toContain("srcSet");
    // srcSet entries point to /_next/image optimization endpoint
    expect(html).toContain("/_next/image");
    expect(html).toContain("url=%2Fphoto.jpg");
  });

  it("renders blur placeholder with background-image", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const blurUrl = "data:image/jpeg;base64,/9j/4AAQ";
    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Blurry",
        width: 800,
        height: 600,
        placeholder: "blur",
        blurDataURL: blurUrl,
      }),
    );
    expect(html).toContain(blurUrl);
    expect(html).toContain("background-image");
    expect(html).toContain("background-size:cover");
  });

  it("renders with custom loader function", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Custom",
        width: 800,
        height: 600,
        loader: ({ src, width, quality }: { src: string; width: number; quality?: number }) =>
          `https://cdn.example.com${src}?w=${width}&q=${quality}`,
      }),
    );
    expect(html).toContain('src="https://cdn.example.com/photo.jpg?w=800&amp;q=75"');
  });

  it("renders with custom sizes attribute", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Responsive",
        width: 1200,
        height: 800,
        sizes: "(max-width: 768px) 100vw, 50vw",
      }),
    );
    expect(html).toContain('sizes="(max-width: 768px) 100vw, 50vw"');
  });

  it("renders fill image with sizes=100vw by default", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/bg.jpg", alt: "BG", fill: true }),
    );
    expect(html).toContain('sizes="100vw"');
  });

  it("handles StaticImageData import objects", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const staticImport = {
      src: "/imported.jpg",
      width: 1200,
      height: 800,
      blurDataURL: "data:...",
    };
    const html = renderToStaticMarkup(
      React.createElement(Image, { src: staticImport, alt: "Imported" }),
    );
    expect(html).toContain("/_next/image");
    expect(html).toContain("url=%2Fimported.jpg");
    expect(html).toContain('width="1200"');
    expect(html).toContain('height="800"');
  });

  it("renders with className", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Styled",
        width: 800,
        height: 600,
        className: "rounded-lg shadow-md",
      }),
    );
    expect(html).toContain('class="rounded-lg shadow-md"');
  });

  it("includes data-nimg=1 for non-fill images", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/photo.jpg", alt: "Test", width: 800, height: 600 }),
    );
    expect(html).toContain('data-nimg="1"');
  });

  it("always sets decoding=async", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, { src: "/photo.jpg", alt: "Test", width: 800, height: 600 }),
    );
    expect(html).toContain('decoding="async"');
  });

  // ── SSR-only smoke tests: verify SSR output does not crash when onLoad / onError /
  // ref are provided. The hydration replay logic (useLayoutEffect, img.src = img.src,
  // mergedRef DOM node capture) requires a client-side mount; the handler wiring is
  // tested in the Playwright E2E test suite.

  it("renders with onError callback attached (SSR smoke test)", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/broken.jpg",
        alt: "Broken",
        width: 400,
        height: 300,
        onError: () => {},
      }),
    );
    // SSR should render without errors — the onError replay is client-side only
    expect(html).toContain("<img");
    expect(html).toContain("/_next/image");
  });

  it("renders with both onLoad and onError callbacks (SSR smoke test)", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Photo",
        width: 800,
        height: 600,
        onLoad: () => {},
        onError: () => {},
      }),
    );
    expect(html).toContain("<img");
    expect(html).toContain("/_next/image");
  });

  it("forwards ref to img element via mergedRef (SSR smoke test)", async () => {
    const React = await import("react");
    const { renderToString } = await import("react-dom/server");

    const ref = React.createRef<HTMLImageElement>();
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToString(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Ref test",
        width: 800,
        height: 600,
        ref,
      }),
    );
    expect(html).toContain("<img");
    expect(html).toContain("/_next/image");
    // ref.current is null after SSR — that's expected since refs don't hydrate in SSR
    expect(ref.current).toBeNull();
  });

  it("renders with onError in loader path (SSR smoke test)", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Image = (await import("../packages/vinext/src/shims/image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Image, {
        src: "/photo.jpg",
        alt: "Loader",
        width: 800,
        height: 600,
        onError: () => {},
        loader: ({ src, width, quality }) =>
          `https://cdn.example.com${src}?w=${width}&q=${quality}`,
      }),
    );
    expect(html).toContain("<img");
    expect(html).not.toContain("/_next/image");
    expect(html).toContain("cdn.example.com");
  });
});

describe("image remote pattern matching", () => {
  it("matchRemotePattern matches exact hostname", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "cdn.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    expect(matchRemotePattern(pattern, new URL("https://other.example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern matches wildcard hostname with *", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "*.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    expect(matchRemotePattern(pattern, new URL("https://images.example.com/img.jpg"))).toBe(true);
    // Single * should NOT match nested subdomains
    expect(matchRemotePattern(pattern, new URL("https://a.b.example.com/img.jpg"))).toBe(false);
    // Should not match bare domain
    expect(matchRemotePattern(pattern, new URL("https://example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern matches ** hostname for any depth", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "**.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    expect(matchRemotePattern(pattern, new URL("https://a.b.c.example.com/img.jpg"))).toBe(true);
    // ** requires at least one segment
    expect(matchRemotePattern(pattern, new URL("https://example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern checks protocol when specified", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { protocol: "https", hostname: "cdn.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    expect(matchRemotePattern(pattern, new URL("http://cdn.example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern checks port when specified", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "localhost", port: "3000" };
    expect(matchRemotePattern(pattern, new URL("http://localhost:3000/img.jpg"))).toBe(true);
    expect(matchRemotePattern(pattern, new URL("http://localhost:8080/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern checks pathname when specified", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "cdn.example.com", pathname: "/images/**" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/images/photo.jpg"))).toBe(
      true,
    );
    expect(
      matchRemotePattern(pattern, new URL("https://cdn.example.com/images/nested/photo.jpg")),
    ).toBe(true);
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/other/photo.jpg"))).toBe(
      false,
    );
  });

  it("matchRemotePattern checks search when specified", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "cdn.example.com", search: "?v=123" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg?v=123"))).toBe(
      true,
    );
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg?v=456"))).toBe(
      false,
    );
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern defaults pathname to ** when not specified", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "cdn.example.com" };
    expect(
      matchRemotePattern(pattern, new URL("https://cdn.example.com/any/deep/path/img.jpg")),
    ).toBe(true);
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/"))).toBe(true);
  });

  it("matchRemotePattern handles protocol with trailing colon", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { protocol: "https:", hostname: "cdn.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
  });

  it("hasRemoteMatch checks domains list", async () => {
    const { hasRemoteMatch } = await import("../packages/vinext/src/shims/image-config.js");
    const domains = ["cdn.example.com", "images.example.com"];
    expect(hasRemoteMatch(domains, [], new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    expect(hasRemoteMatch(domains, [], new URL("https://images.example.com/img.jpg"))).toBe(true);
    expect(hasRemoteMatch(domains, [], new URL("https://other.example.com/img.jpg"))).toBe(false);
  });

  it("hasRemoteMatch checks remotePatterns when domains don't match", async () => {
    const { hasRemoteMatch } = await import("../packages/vinext/src/shims/image-config.js");
    const patterns = [{ protocol: "https", hostname: "**.cdn.example.com" }];
    expect(hasRemoteMatch([], patterns, new URL("https://us.cdn.example.com/img.jpg"))).toBe(true);
    expect(hasRemoteMatch([], patterns, new URL("http://us.cdn.example.com/img.jpg"))).toBe(false);
  });

  it("matchRemotePattern handles single * in pathname", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    const pattern = { hostname: "cdn.example.com", pathname: "/images/*" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/images/photo.jpg"))).toBe(
      true,
    );
    // Single * should not match nested paths
    expect(
      matchRemotePattern(pattern, new URL("https://cdn.example.com/images/nested/photo.jpg")),
    ).toBe(false);
  });

  it("matchRemotePattern handles regex special chars in hostname", async () => {
    const { matchRemotePattern } = await import("../packages/vinext/src/shims/image-config.js");
    // Dots in hostname are literal (escaped for regex)
    const pattern = { hostname: "cdn.example.com" };
    expect(matchRemotePattern(pattern, new URL("https://cdn.example.com/img.jpg"))).toBe(true);
    // "cdnXexampleXcom" should not match "cdn.example.com"
    expect(matchRemotePattern(pattern, new URL("https://cdnXexample.com/img.jpg"))).toBe(false);
  });
});

describe("image optimization URL generation", () => {
  it("imageOptimizationUrl generates correct URL", async () => {
    const { imageOptimizationUrl } = await import("../packages/vinext/src/shims/image.js");
    const url = imageOptimizationUrl("/images/hero.webp", 1200, 75);
    expect(url).toBe("/_next/image?url=%2Fimages%2Fhero.webp&w=1200&q=75");
  });

  it("imageOptimizationUrl encodes special characters", async () => {
    const { imageOptimizationUrl } = await import("../packages/vinext/src/shims/image.js");
    const url = imageOptimizationUrl("/images/my photo.jpg", 800, 80);
    expect(url).toContain("url=%2Fimages%2Fmy%20photo.jpg");
    expect(url).toContain("w=800");
    expect(url).toContain("q=80");
  });

  it("imageOptimizationUrl uses default quality of 75", async () => {
    const { imageOptimizationUrl } = await import("../packages/vinext/src/shims/image.js");
    const url = imageOptimizationUrl("/img.png", 640);
    expect(url).toContain("q=75");
  });
});

describe("image optimization request parsing", () => {
  it("parseImageParams extracts url, width, quality", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const url = new URL("http://localhost/_next/image?url=%2Fimages%2Fhero.webp&w=1200&q=75");
    const params = parseImageParams(url);
    expect(params).not.toBeNull();
    expect(params!.imageUrl).toBe("/images/hero.webp");
    expect(params!.width).toBe(1200);
    expect(params!.quality).toBe(75);
  });

  it("parseImageParams returns null when url is missing", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const url = new URL("http://localhost/_next/image?w=640&q=75");
    expect(parseImageParams(url)).toBeNull();
  });

  it("parseImageParams blocks absolute http URLs", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const url = new URL(
      "http://localhost/_next/image?url=http%3A%2F%2Fevil.com%2Fimg.jpg&w=640&q=75",
    );
    expect(parseImageParams(url)).toBeNull();
  });

  it("parseImageParams blocks absolute https URLs", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const url = new URL(
      "http://localhost/_next/image?url=https%3A%2F%2Fevil.com%2Fimg.jpg&w=640&q=75",
    );
    expect(parseImageParams(url)).toBeNull();
  });

  it("parseImageParams blocks protocol-relative URLs", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const url = new URL("http://localhost/_next/image?url=%2F%2Fevil.com%2Fimg.jpg&w=640&q=75");
    expect(parseImageParams(url)).toBeNull();
  });

  // Ported from Next.js: test/integration/image-optimizer/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/integration/image-optimizer/test/index.test.ts
  it("parseImageParams requires width and quality", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg"))).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640")),
    ).toBeNull();
  });

  it("parseImageParams rejects cache-key aliases with integer suffixes", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640junk&q=75")),
    ).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75junk")),
    ).toBeNull();
  });

  it("parseImageParams rejects zero, signed, and decimal widths", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    for (const width of ["0", "-640", "+640", "640.0"]) {
      expect(
        parseImageParams(new URL(`http://localhost/_next/image?url=%2Fimg.jpg&w=${width}&q=75`)),
      ).toBeNull();
    }
  });

  it("parseImageParams rejects leading-zero cache-key aliases", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=0640&q=75")),
    ).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=075")),
    ).toBeNull();
  });

  it("parseImageParams rejects duplicate and unknown query parameters", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&w=640&q=75")),
    ).toBeNull();
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75&cacheBust=1"),
      ),
    ).toBeNull();
  });

  it("parseImageParams rejects source URLs longer than 3072 characters", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const imageUrl = `/${"a".repeat(3072)}`;
    const url = new URL("http://localhost/_next/image");
    url.searchParams.set("url", imageUrl);
    url.searchParams.set("w", "640");
    url.searchParams.set("q", "75");
    expect(parseImageParams(url)).toBeNull();
  });

  it("parseImageParams blocks data: URIs (exotic scheme bypass)", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(
        new URL(
          "http://localhost/_next/image?url=data%3Atext%2Fhtml%2C%3Cscript%3Ealert(1)%3C%2Fscript%3E&w=640&q=75",
        ),
      ),
    ).toBeNull();
  });

  it("parseImageParams blocks javascript: URIs", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=javascript%3Aalert(1)&w=640&q=75"),
      ),
    ).toBeNull();
  });

  it("parseImageParams blocks bare filenames (no leading slash)", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=img.jpg&w=640&q=75")),
    ).toBeNull();
  });

  it("parseImageParams rejects quality outside 1-100", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=0")),
    ).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=101")),
    ).toBeNull();
  });

  it("parseImageParams permits any quality 1-100 when qualities is unset", async () => {
    // Matches Next.js: an unset `images.qualities` is not restricted to a single
    // value. Regression test for non-75 qualities (e.g. `<Image quality={90}>`)
    // returning 400 from the image optimization endpoint.
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    for (const q of [1, 50, 75, 90, 100]) {
      const params = parseImageParams(
        new URL(`http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=${q}`),
      );
      expect(params).not.toBeNull();
      expect(params!.quality).toBe(q);
    }
  });

  it("parseImageParams enforces the allowlist only when qualities is configured", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const allowed = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
    // Quality in the configured allowlist passes.
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=90"),
        allowed,
        [75, 90],
      ),
    ).not.toBeNull();
    // Quality outside the configured allowlist is rejected.
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=50"),
        allowed,
        [75, 90],
      ),
    ).toBeNull();
  });

  it("parseImageParams blocks backslash-based open redirect (/\\evil.com)", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    // /\evil.com — browsers and the URL constructor treat this as //evil.com
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2F%5Cevil.com&w=640&q=75")),
    ).toBeNull();
  });

  it("parseImageParams blocks encoded backslash variants", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    // /\evil.com/img.jpg
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2F%5Cevil.com%2Fimg.jpg&w=640&q=75"),
      ),
    ).toBeNull();
    // /\\evil.com (double backslash)
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2F%5C%5Cevil.com&w=640&q=75")),
    ).toBeNull();
  });

  it("parseImageParams validates origin hasn't changed after URL construction", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    // This tests defense-in-depth: even if a future parser differential is found,
    // the origin check catches it.
    // A valid relative URL should pass
    const good = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimages%2Fhero.webp&w=640&q=75"),
    );
    expect(good).not.toBeNull();
    expect(good!.imageUrl).toBe("/images/hero.webp");
  });

  it("parseImageParams normalizes backslashes in returned imageUrl", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    // /images\hero.webp should be normalized to /images/hero.webp
    // (backslash in the middle of a valid path)
    const result = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimages%5Chero.webp&w=640&q=75"),
    );
    expect(result).not.toBeNull();
    expect(result!.imageUrl).toBe("/images/hero.webp");
  });

  it("parseImageParams rejects width exceeding absolute maximum (3840)", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=3841&q=75")),
    ).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=999999999&q=75")),
    ).toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=2147483647&q=75")),
    ).toBeNull();
  });

  it("parseImageParams accepts width at the absolute maximum (3840)", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const params = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=3840&q=75"),
    );
    expect(params).not.toBeNull();
    expect(params!.width).toBe(3840);
  });

  it("parseImageParams validates against allowedWidths when provided", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const allowedWidths = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
    // Allowed width passes
    const params = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=1080&q=75"),
      allowedWidths,
    );
    expect(params).not.toBeNull();
    expect(params!.width).toBe(1080);
    // Non-allowed width is rejected
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=999&q=75"),
        allowedWidths,
      ),
    ).toBeNull();
    expect(
      parseImageParams(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=0&q=75"),
        allowedWidths,
      ),
    ).toBeNull();
  });

  it("parseImageParams allows any quality 1-100 when qualities is unset", async () => {
    // Matches Next.js: an unset `images.qualities` does not restrict the quality
    // to a single value — any integer from 1-100 is accepted.
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75")),
    ).not.toBeNull();
    expect(
      parseImageParams(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=80")),
    ).not.toBeNull();
  });

  it("parseImageParams accepts configured qualities", async () => {
    const { parseImageParams, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const params = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=80"),
      [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES],
      [75, 80],
    );
    expect(params?.quality).toBe(80);
  });

  it("resolveDevImageRedirect applies validation before passthrough", async () => {
    const { resolveDevImageRedirect } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      resolveDevImageRedirect(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640junk&q=75"),
      ),
    ).toBeNull();
    expect(
      resolveDevImageRedirect(
        new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75&extra=1"),
      ),
    ).toBeNull();
    expect(
      resolveDevImageRedirect(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75")),
    ).toBe("/img.jpg");
  });

  it("resolveDevImageRedirect preserves Next.js blur requests", async () => {
    const { resolveDevImageRedirect } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(
      resolveDevImageRedirect(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=8&q=70")),
    ).toBe("/img.jpg");
    expect(
      resolveDevImageRedirect(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=8&q=75")),
    ).toBe("/img.jpg");
    expect(
      resolveDevImageRedirect(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=70")),
    ).toBe("/img.jpg");
    expect(
      resolveDevImageRedirect(new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=08&q=70")),
    ).toBeNull();
  });

  it("parseImageParams allows imageSizes (small widths) in allowedWidths", async () => {
    const { parseImageParams } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const allowedWidths = [
      16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840,
    ];
    const params = parseImageParams(
      new URL("http://localhost/_next/image?url=%2Fimg.jpg&w=64&q=75"),
      allowedWidths,
    );
    expect(params).not.toBeNull();
    expect(params!.width).toBe(64);
  });

  it("negotiateImageFormat prefers AVIF over WebP", async () => {
    const { negotiateImageFormat } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(negotiateImageFormat("image/avif,image/webp,image/jpeg")).toBe("image/avif");
  });

  it("negotiateImageFormat selects WebP when no AVIF", async () => {
    const { negotiateImageFormat } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(negotiateImageFormat("image/webp,image/jpeg")).toBe("image/webp");
  });

  it("negotiateImageFormat falls back to JPEG", async () => {
    const { negotiateImageFormat } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(negotiateImageFormat("image/png,image/jpeg")).toBe("image/jpeg");
    expect(negotiateImageFormat(null)).toBe("image/jpeg");
  });

  it("IMAGE_OPTIMIZATION_PATH is /_next/image", async () => {
    const { IMAGE_OPTIMIZATION_PATH } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(IMAGE_OPTIMIZATION_PATH).toBe("/_next/image");
  });

  it("VINEXT_IMAGE_OPTIMIZATION_PATH is /_vinext/image", async () => {
    const { VINEXT_IMAGE_OPTIMIZATION_PATH } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(VINEXT_IMAGE_OPTIMIZATION_PATH).toBe("/_vinext/image");
  });

  it("isImageOptimizationPath accepts both supported endpoints", async () => {
    const { isImageOptimizationPath } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isImageOptimizationPath("/_next/image")).toBe(true);
    expect(isImageOptimizationPath("/_vinext/image")).toBe(true);
    expect(isImageOptimizationPath("/_next/image/")).toBe(false);
    expect(isImageOptimizationPath("/_next/image.png")).toBe(false);
    expect(isImageOptimizationPath("/_next/data")).toBe(false);
    expect(isImageOptimizationPath("/")).toBe(false);
  });

  it("exports DEFAULT_DEVICE_SIZES and DEFAULT_IMAGE_SIZES matching Next.js defaults", async () => {
    const { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(DEFAULT_DEVICE_SIZES).toEqual([640, 750, 828, 1080, 1200, 1920, 2048, 3840]);
    expect(DEFAULT_IMAGE_SIZES).toEqual([16, 32, 48, 64, 96, 128, 256, 384]);
  });
});

describe("isSafeImageContentType", () => {
  it("accepts safe image content types", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("image/jpeg")).toBe(true);
    expect(isSafeImageContentType("image/png")).toBe(true);
    expect(isSafeImageContentType("image/gif")).toBe(true);
    expect(isSafeImageContentType("image/webp")).toBe(true);
    expect(isSafeImageContentType("image/avif")).toBe(true);
    expect(isSafeImageContentType("image/x-icon")).toBe(true);
    expect(isSafeImageContentType("image/bmp")).toBe(true);
    expect(isSafeImageContentType("image/tiff")).toBe(true);
  });

  it("rejects SVG content type", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("image/svg+xml")).toBe(false);
  });

  it("rejects non-image content types", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("text/html")).toBe(false);
    expect(isSafeImageContentType("application/javascript")).toBe(false);
    expect(isSafeImageContentType("text/xml")).toBe(false);
    expect(isSafeImageContentType("application/octet-stream")).toBe(false);
  });

  it("rejects null content type", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType(null)).toBe(false);
  });

  it("handles content type with parameters (charset, etc.)", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("image/jpeg; charset=utf-8")).toBe(true);
    expect(isSafeImageContentType("image/svg+xml; charset=utf-8")).toBe(false);
  });

  it("is case-insensitive", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("Image/JPEG")).toBe(true);
    expect(isSafeImageContentType("IMAGE/SVG+XML")).toBe(false);
  });

  it("allows SVG when dangerouslyAllowSVG is true", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("image/svg+xml", true)).toBe(true);
  });

  it("allows SVG with parameters when dangerouslyAllowSVG is true", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("image/svg+xml; charset=utf-8", true)).toBe(true);
  });

  it("still rejects non-image types when dangerouslyAllowSVG is true", async () => {
    const { isSafeImageContentType } =
      await import("../packages/vinext/src/server/image-optimization.js");
    expect(isSafeImageContentType("text/html", true)).toBe(false);
    expect(isSafeImageContentType("application/javascript", true)).toBe(false);
    expect(isSafeImageContentType(null, true)).toBe(false);
  });
});

describe("handleImageOptimization", () => {
  it("returns 400 for invalid params", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image");
    const handlers = {
      fetchAsset: async () => new Response("", { status: 200 }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
  });

  it("returns 404 when fetchAsset fails", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () => new Response("", { status: 404 }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(404);
  });

  it("returns original image when no transformImage handler", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("original-image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(await response.text()).toBe("original-image-data");
    expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
    expect(response.headers.get("Vary")).toBe("Accept");
  });

  it("calls transformImage when provided", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=90", {
      headers: { Accept: "image/webp" },
    });
    let capturedOptions: { width: number; format: string; quality: number } | null = null;
    const handlers = {
      fetchAsset: async () =>
        new Response("original", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
      transformImage: async (
        _body: ReadableStream,
        options: { width: number; format: string; quality: number },
      ) => {
        capturedOptions = options;
        return new Response("transformed", { headers: { "Content-Type": options.format } });
      },
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      qualities: [90],
    });
    expect(response.status).toBe(200);
    expect(await response.text()).toBe("transformed");
    expect(capturedOptions).toEqual({ width: 640, format: "image/webp", quality: 90 });
  });

  it("falls back to original on transform error", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    let fetchCount = 0;
    const handlers = {
      fetchAsset: async () => {
        fetchCount += 1;
        return new Response("original", {
          status: 200,
          headers: { "Content-Type": "image/png" },
        });
      },
      transformImage: async () => {
        throw new Error("transform failed");
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(await response.text()).toBe("original");
    expect(fetchCount).toBe(1);
  });

  it("refetches the source when transform consumes the stream before failing", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    let fetchCount = 0;
    const handlers = {
      fetchAsset: async () => {
        fetchCount += 1;
        return new Response(fetchCount === 1 ? "original" : "refetched", {
          status: 200,
          headers: { "Content-Type": "image/png" },
        });
      },
      transformImage: async (body: ReadableStream) => {
        await new Response(body).arrayBuffer();
        throw new Error("transform failed after consuming stream");
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(await response.text()).toBe("refetched");
    expect(fetchCount).toBe(2);
  });

  it("uses refetched source headers when consumed transform falls back", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    let fetchCount = 0;
    const handlers = {
      fetchAsset: async () => {
        fetchCount += 1;
        return new Response(fetchCount === 1 ? "original" : "refetched", {
          status: 200,
          headers: {
            "Content-Type": fetchCount === 1 ? "image/png" : "image/jpeg",
            ETag: fetchCount === 1 ? '"source-etag"' : '"refetched-etag"',
          },
        });
      },
      transformImage: async (body: ReadableStream) => {
        await new Response(body).arrayBuffer();
        throw new Error("transform failed after consuming stream");
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(fetchCount).toBe(2);
    expect(response.headers.get("Content-Type")).toBe("image/jpeg");
    expect(response.headers.get("ETag")).toBe('"refetched-etag"');
    expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
    expect(response.headers.get("Vary")).toBe("Accept");
  });

  it("returns 404 when refetch fallback cannot reload the source image", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    let fetchCount = 0;
    const handlers = {
      fetchAsset: async () => {
        fetchCount += 1;
        if (fetchCount === 1) {
          return new Response("original", {
            status: 200,
            headers: { "Content-Type": "image/png" },
          });
        }
        return new Response("", { status: 404 });
      },
      transformImage: async (body: ReadableStream) => {
        await new Response(body).arrayBuffer();
        throw new Error("transform failed after consuming stream");
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(fetchCount).toBe(2);
    expect(response.status).toBe(404);
  });

  it("returns 400 when refetch fallback reloads an unsafe content type", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    let fetchCount = 0;
    const handlers = {
      fetchAsset: async () => {
        fetchCount += 1;
        return new Response(fetchCount === 1 ? "original" : "<html>bad</html>", {
          status: 200,
          headers: {
            "Content-Type": fetchCount === 1 ? "image/png" : "text/html",
          },
        });
      },
      transformImage: async (body: ReadableStream) => {
        await new Response(body).arrayBuffer();
        throw new Error("transform failed after consuming stream");
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(fetchCount).toBe(2);
    expect(response.status).toBe(400);
    expect(await response.text()).toBe("The requested resource is not an allowed image type");
  });

  it("returns 400 for backslash open redirect (/\\evil.com)", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2F%5Cevil.com&w=640&q=75");
    const handlers = {
      fetchAsset: async () => new Response("should not be called", { status: 200 }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
  });

  it("does not call fetchAsset for backslash URLs", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request(
      "http://localhost/_next/image?url=%2F%5Cgoogle.com%2Fimg.jpg&w=640&q=75",
    );
    let fetchCalled = false;
    const handlers = {
      fetchAsset: async () => {
        fetchCalled = true;
        return new Response("", { status: 200 });
      },
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
    expect(fetchCalled).toBe(false);
  });

  it("blocks SVG content type", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fmalicious.svg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("<svg><script>alert(1)</script></svg>", {
          status: 200,
          headers: { "Content-Type": "image/svg+xml" },
        }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
    expect(await response.text()).toBe("The requested resource is not an allowed image type");
  });

  it("blocks text/html content type", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Ffake.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("<html><script>alert(1)</script></html>", {
          status: 200,
          headers: { "Content-Type": "text/html" },
        }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
  });

  it("blocks responses with no Content-Type", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () => new Response("data", { status: 200 }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(400);
  });

  it("sets Content-Security-Policy header on fallback responses", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Security-Policy")).toBe(
      "script-src 'none'; frame-src 'none'; sandbox;",
    );
    expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
    expect(response.headers.get("Content-Disposition")).toBe("inline");
  });

  it("sets Content-Security-Policy header on transformed responses", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=90", {
      headers: { Accept: "image/webp" },
    });
    const handlers = {
      fetchAsset: async () =>
        new Response("original", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
      transformImage: async (
        _body: ReadableStream,
        options: { width: number; format: string; quality: number },
      ) => new Response("transformed", { headers: { "Content-Type": options.format } }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      qualities: [90],
    });
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Security-Policy")).toBe(
      "script-src 'none'; frame-src 'none'; sandbox;",
    );
    expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
    expect(response.headers.get("Content-Disposition")).toBe("inline");
  });

  it("overrides unsafe Content-Type from transform handler", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=90", {
      headers: { Accept: "image/webp" },
    });
    const handlers = {
      fetchAsset: async () =>
        new Response("original", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
      transformImage: async () =>
        new Response("transformed", { headers: { "Content-Type": "text/html" } }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      qualities: [90],
    });
    expect(response.status).toBe(200);
    // Should override to the negotiated format, not pass through text/html
    expect(response.headers.get("Content-Type")).toBe("image/webp");
  });

  it("allows SVG passthrough with dangerouslyAllowSVG: true", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Flogo.svg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response('<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>', {
          status: 200,
          headers: { "Content-Type": "image/svg+xml" },
        }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      dangerouslyAllowSVG: true,
    });
    expect(response.status).toBe(200);
    expect(await response.text()).toBe('<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>');
    expect(response.headers.get("Content-Type")).toBe("image/svg+xml");
  });

  it("still blocks SVG when dangerouslyAllowSVG is false", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Flogo.svg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("<svg></svg>", {
          status: 200,
          headers: { "Content-Type": "image/svg+xml" },
        }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      dangerouslyAllowSVG: false,
    });
    expect(response.status).toBe(400);
  });

  it("SVG passthrough skips transformImage", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Flogo.svg&w=640&q=75");
    let transformCalled = false;
    const handlers = {
      fetchAsset: async () =>
        new Response("<svg></svg>", {
          status: 200,
          headers: { "Content-Type": "image/svg+xml" },
        }),
      transformImage: async () => {
        transformCalled = true;
        return new Response("transformed");
      },
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      dangerouslyAllowSVG: true,
    });
    expect(response.status).toBe(200);
    expect(transformCalled).toBe(false);
    expect(await response.text()).toBe("<svg></svg>");
  });

  it("applies security headers on SVG passthrough", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Flogo.svg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("<svg></svg>", {
          status: 200,
          headers: { "Content-Type": "image/svg+xml" },
        }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      dangerouslyAllowSVG: true,
    });
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Security-Policy")).toBe(
      "script-src 'none'; frame-src 'none'; sandbox;",
    );
    expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
    expect(response.headers.get("Content-Disposition")).toBe("inline");
  });

  it("applies custom contentDispositionType", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      contentDispositionType: "attachment",
    });
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Disposition")).toBe("attachment");
  });

  it("defaults Content-Disposition to inline when contentDispositionType is invalid", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const response = await handleImageOptimization(request, handlers, undefined, {
      contentDispositionType: "bogus" as "inline",
    });
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Disposition")).toBe("inline");
  });

  it("applies custom contentSecurityPolicy", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const customCSP = "default-src 'self'; script-src 'none';";
    const response = await handleImageOptimization(request, handlers, undefined, {
      contentSecurityPolicy: customCSP,
    });
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Security-Policy")).toBe(customCSP);
  });

  it("default behavior unchanged when no imageConfig provided", async () => {
    const { handleImageOptimization } =
      await import("../packages/vinext/src/server/image-optimization.js");
    const request = new Request("http://localhost/_next/image?url=%2Fimg.jpg&w=640&q=75");
    const handlers = {
      fetchAsset: async () =>
        new Response("image-data", {
          status: 200,
          headers: { "Content-Type": "image/jpeg" },
        }),
    };
    const response = await handleImageOptimization(request, handlers);
    expect(response.status).toBe(200);
    expect(response.headers.get("Content-Security-Policy")).toBe(
      "script-src 'none'; frame-src 'none'; sandbox;",
    );
    expect(response.headers.get("Content-Disposition")).toBe("inline");
  });
});

describe("next/navigation enhancements", () => {
  it("exports ReadonlyURLSearchParams runtime class", async () => {
    const nav = await import("../packages/vinext/src/shims/navigation.js");
    expect(typeof nav.ReadonlyURLSearchParams).toBe("function");
    expect(typeof nav.useServerInsertedHTML).toBe("function");
  });

  it("ReadonlyURLSearchParams preserves reads and blocks mutation methods", async () => {
    const { ReadonlyURLSearchParams } = await import("../packages/vinext/src/shims/navigation.js");

    const searchParams = new ReadonlyURLSearchParams("foo=bar&foo=baz&zap=zazzle");

    expect(searchParams).toBeInstanceOf(URLSearchParams);
    expect(searchParams).toBeInstanceOf(ReadonlyURLSearchParams);
    expect(searchParams.get("foo")).toBe("bar");
    expect(searchParams.getAll("foo")).toEqual(["bar", "baz"]);
    expect(searchParams.toString()).toBe("foo=bar&foo=baz&zap=zazzle");
    expect(() => searchParams.append("x", "1")).toThrow(
      "Method unavailable on `ReadonlyURLSearchParams`.",
    );
    expect(() => searchParams.delete("foo")).toThrow(
      "Method unavailable on `ReadonlyURLSearchParams`.",
    );
    expect(() => searchParams.set("foo", "qux")).toThrow(
      "Method unavailable on `ReadonlyURLSearchParams`.",
    );
    expect(() => searchParams.sort()).toThrow("Method unavailable on `ReadonlyURLSearchParams`.");
    expect(searchParams.toString()).toBe("foo=bar&foo=baz&zap=zazzle");
  });

  it("useSearchParams returns a readonly wrapper on the server path", async () => {
    const { ReadonlyURLSearchParams, setNavigationContext, useSearchParams } =
      await import("../packages/vinext/src/shims/navigation.js");

    try {
      setNavigationContext({
        pathname: "/readonly-test",
        searchParams: new URLSearchParams("foo=bar&foo=baz"),
        params: {},
      });

      const searchParams = useSearchParams();

      expect(searchParams).toBeInstanceOf(ReadonlyURLSearchParams);
      expect(searchParams.getAll("foo")).toEqual(["bar", "baz"]);
      expect(() => searchParams.set("foo", "qux")).toThrow(
        "Method unavailable on `ReadonlyURLSearchParams`.",
      );
    } finally {
      setNavigationContext(null);
    }
  });

  it("useSearchParams reuses the same readonly wrapper for the same server context", async () => {
    const { setNavigationContext, useSearchParams } =
      await import("../packages/vinext/src/shims/navigation.js");

    try {
      const ctx = {
        pathname: "/readonly-test",
        searchParams: new URLSearchParams("foo=bar"),
        params: {},
      };

      setNavigationContext(ctx);

      const first = useSearchParams();
      const second = useSearchParams();

      expect(first).toBe(second);
    } finally {
      setNavigationContext(null);
    }
  });

  it("useSearchParams keeps wrapper identity stable across concurrent ALS-scoped requests", async () => {
    const { runWithNavigationContext } =
      await import("../packages/vinext/src/shims/navigation-state.js");
    const { setNavigationContext, useSearchParams } =
      await import("../packages/vinext/src/shims/navigation.js");

    let releaseInterleave!: () => void;
    const waitForInterleave = new Promise<void>((resolve) => {
      releaseInterleave = resolve;
    });

    async function runRequest(query: string, pathname: string) {
      return runWithNavigationContext(async () => {
        setNavigationContext({
          pathname,
          searchParams: new URLSearchParams(query),
          params: {},
        });

        const first = useSearchParams();
        await waitForInterleave;
        const second = useSearchParams();

        return {
          first,
          second,
          value: first.toString(),
        };
      });
    }

    const requestA = runRequest("a=1", "/request-a");
    const requestB = runRequest("b=2", "/request-b");

    await Promise.resolve();
    releaseInterleave();

    const [a, b] = await Promise.all([requestA, requestB]);

    expect(a.first).toBe(a.second);
    expect(b.first).toBe(b.second);
    expect(a.value).toBe("a=1");
    expect(b.value).toBe("b=2");
    expect(a.first).not.toBe(b.first);
  });

  it("useServerInsertedHTML is a no-op function", async () => {
    const { useServerInsertedHTML } = await import("../packages/vinext/src/shims/navigation.js");
    // Should not throw
    expect(() => useServerInsertedHTML(() => null)).not.toThrow();
  });
});

describe("next/legacy/image shim", () => {
  it("renders LegacyImage with layout=fill as modern Image with fill prop", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const LegacyImage = (await import("../packages/vinext/src/shims/legacy-image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(LegacyImage, {
        src: "/photo.jpg",
        alt: "Test",
        layout: "fill",
        objectFit: "cover",
        objectPosition: "center",
      }),
    );
    expect(html).toContain("photo.jpg");
    expect(html).toContain("alt");
    // fill mode should produce absolute positioning styles
    expect(html).toContain("position:absolute");
  });

  it("renders LegacyImage with layout=intrinsic using width/height", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const LegacyImage = (await import("../packages/vinext/src/shims/legacy-image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(LegacyImage, {
        src: "/photo.jpg",
        alt: "Test",
        layout: "intrinsic",
        width: 640,
        height: 480,
      }),
    );
    expect(html).toContain('width="640"');
    expect(html).toContain('height="480"');
  });

  it("renders LegacyImage with string width/height (converts to number)", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const LegacyImage = (await import("../packages/vinext/src/shims/legacy-image.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(LegacyImage, {
        src: "/photo.jpg",
        alt: "Test",
        width: "200",
        height: "150",
      }),
    );
    expect(html).toContain('width="200"');
    expect(html).toContain('height="150"');
  });
});

describe("next/error shim", () => {
  it("renders 404 error page", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const ErrorComponent = (await import("../packages/vinext/src/shims/error.js")).default;

    const html = renderToStaticMarkup(React.createElement(ErrorComponent, { statusCode: 404 }));
    expect(html).toContain("404");
    expect(html).toContain("could not be found");
  });

  it("renders 500 error page", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const ErrorComponent = (await import("../packages/vinext/src/shims/error.js")).default;

    const html = renderToStaticMarkup(React.createElement(ErrorComponent, { statusCode: 500 }));
    expect(html).toContain("500");
    expect(html).toContain("Internal Server Error");
  });

  it("renders custom title", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const ErrorComponent = (await import("../packages/vinext/src/shims/error.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(ErrorComponent, { statusCode: 403, title: "Forbidden" }),
    );
    expect(html).toContain("403");
    expect(html).toContain("Forbidden");
  });
});

// next/app default export
//
// Ported from Next.js: packages/next/src/pages/_app.tsx
// https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx
//
// Pages Router fixtures very commonly do:
//   import App from "next/app";
//   export default class MyApp extends App { ... }
// or call App.getInitialProps(appContext) from a custom getInitialProps.
// Without a runtime default export, every such fixture fails to build with
// "[MISSING_EXPORT] 'default' is not exported by ...shims/app.js".
describe("next/app shim", () => {
  it("exports a React class component as default", async () => {
    const React = await import("react");
    const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
    expect(typeof AppDefault).toBe("function");
    // Class component: prototype must have a render method, instances must
    // be instances of React.Component.
    expect(typeof AppDefault.prototype.render).toBe("function");
    const instance = new AppDefault({ Component: () => null, pageProps: {} });
    expect(instance).toBeInstanceOf(React.Component);
  });

  it("default App.render() returns <Component {...pageProps} />", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;

    function Page(props: { greeting: string }) {
      return React.createElement("p", null, props.greeting);
    }

    // Use explicit generics so React.createElement's prop type inference
    // lines up with `AppProps<{ greeting: string }>`.
    const html = renderToStaticMarkup(
      React.createElement(AppDefault<unknown, { greeting: string }>, {
        Component: Page,
        pageProps: { greeting: "hello world" },
      }),
    );
    expect(html).toBe("<p>hello world</p>");
  });

  it("App.getInitialProps is a function and forwards Component.getInitialProps result as pageProps", async () => {
    const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
    expect(typeof AppDefault.getInitialProps).toBe("function");
    // origGetInitialProps is preserved for userland code that introspects it.
    expect(typeof AppDefault.origGetInitialProps).toBe("function");

    const pageCtx = { req: { url: "/test" } };
    const Component = Object.assign(() => null, {
      getInitialProps: async (ctx: unknown) => {
        expect(ctx).toBe(pageCtx);
        return { foo: "bar" };
      },
    });

    const result = await AppDefault.getInitialProps({
      Component,
      AppTree: () => null,
      ctx: pageCtx,
      router: {},
    });
    expect(result).toEqual({ pageProps: { foo: "bar" } });
  });

  it("App.getInitialProps returns { pageProps: {} } when Component has no getInitialProps", async () => {
    const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
    const result = await AppDefault.getInitialProps({
      Component: () => null,
      AppTree: () => null,
      ctx: {},
      router: {},
    });
    expect(result).toEqual({ pageProps: {} });
  });
});

describe("next/constants shim", () => {
  it("exports all phase constants", async () => {
    const constants = await import("../packages/vinext/src/shims/constants.js");
    const constantsFromNext = await import("next/constants");
    const normalizeConstants = (mod: Record<string, unknown>) => {
      const {
        __esModule: _esModule,
        default: _default,
        CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL,
        ...rest
      } = mod;
      return {
        ...rest,
        CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL:
          typeof CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL === "symbol"
            ? CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL.description
            : CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL,
      };
    };
    expect(normalizeConstants(constants)).toEqual(normalizeConstants(constantsFromNext));
  });
});

describe("next/script SSR rendering", () => {
  it("beforeInteractive renders <script> tag in SSR", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        src: "https://example.com/analytics.js",
        strategy: "beforeInteractive",
        id: "analytics",
      }),
    );
    expect(html).toContain("<script");
    expect(html).toContain('src="https://example.com/analytics.js"');
    expect(html).toContain('id="analytics"');
  });

  it("afterInteractive emits a preload <link> (no <script> tag) in SSR", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        src: "https://example.com/chat.js",
        strategy: "afterInteractive",
      }),
    );
    // afterInteractive does not render a <script> tag during SSR,
    // but it does emit <link rel="preload" as="script"> via React Float
    // so the script is fetched while HTML streams.
    expect(html).toContain('<link rel="preload"');
    expect(html).toContain('href="https://example.com/chat.js"');
    expect(html).toContain('as="script"');
    expect(html).not.toContain("<script");
  });

  it("lazyOnload returns null (no preload) in SSR", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        src: "https://example.com/tracking.js",
        strategy: "lazyOnload",
      }),
    );
    expect(html).toBe("");
  });

  it("default strategy (no strategy prop) emits a preload <link> in SSR", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        src: "https://example.com/default.js",
      }),
    );
    // Default is afterInteractive → preload link only, no <script> tag.
    expect(html).toContain('<link rel="preload"');
    expect(html).toContain('href="https://example.com/default.js"');
    expect(html).toContain('as="script"');
    expect(html).not.toContain("<script");
  });

  it("beforeInteractive with dangerouslySetInnerHTML renders inline script", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        strategy: "beforeInteractive",
        id: "inline-script",
        dangerouslySetInnerHTML: { __html: "console.log('hello')" },
      }),
    );
    expect(html).toContain("<script");
    expect(html).toContain('id="inline-script"');
    expect(html).toContain("console.log('hello')");
  });

  it("beforeInteractive escapes </script> in dangerouslySetInnerHTML", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        strategy: "beforeInteractive",
        id: "escape-test",
        dangerouslySetInnerHTML: {
          __html: 'var x = "</script><img src=x onerror=alert(1)>";',
        },
      }),
    );
    // The raw </script> must NOT appear — it would break the tag boundary
    expect(html).not.toContain("</script><img");
    // The escaped form should be present instead
    expect(html).toContain("<\\/script>");
  });

  it("beforeInteractive escapes </script> case-insensitively", async () => {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const Script = (await import("../packages/vinext/src/shims/script.js")).default;

    const html = renderToStaticMarkup(
      React.createElement(Script, {
        strategy: "beforeInteractive",
        id: "case-test",
        dangerouslySetInnerHTML: {
          __html: 'var x = "</SCRIPT><img src=x onerror=alert(1)>";',
        },
      }),
    );
    // Mixed-case </SCRIPT> must also be escaped
    expect(html).not.toContain("</SCRIPT>");
    expect(html).toContain("<\\/SCRIPT>");
  });

  it("exports handleClientScriptLoad and initScriptLoader", async () => {
    const scriptModule = await import("../packages/vinext/src/shims/script.js");
    expect(typeof scriptModule.handleClientScriptLoad).toBe("function");
    expect(typeof scriptModule.initScriptLoader).toBe("function");
  });
});

describe("next/dist/* internal import shims", () => {
  it("app-router-context exports AppRouterContext and types", async () => {
    const mod = await import("../packages/vinext/src/shims/internal/app-router-context.js");
    expect(mod.AppRouterContext).toBeDefined();
    expect(mod.GlobalLayoutRouterContext).toBeDefined();
    expect(mod.LayoutRouterContext).toBeDefined();
    expect(mod.MissingSlotContext).toBeDefined();
    expect(mod.TemplateContext).toBeDefined();
  });

  it("app-router-context imports when React.createContext is unavailable", async () => {
    vi.resetModules();
    vi.doMock("react", () => ({ createContext: undefined }));

    try {
      const mod = await import("../packages/vinext/src/shims/internal/app-router-context.js");
      expect(mod.AppRouterContext).toBeNull();
      expect(mod.GlobalLayoutRouterContext).toBeNull();
      expect(mod.LayoutRouterContext).toBeNull();
      expect(mod.MissingSlotContext).toBeNull();
      expect(mod.TemplateContext).toBeNull();
    } finally {
      vi.doUnmock("react");
      vi.resetModules();
    }
  });

  it("utils exports NEXT_DATA type helpers", async () => {
    const mod = await import("../packages/vinext/src/shims/internal/utils.js");
    expect(typeof mod.execOnce).toBe("function");
    expect(typeof mod.getLocationOrigin).toBe("function");
    expect(typeof mod.getURL).toBe("function");

    // execOnce should only call the function once
    let count = 0;
    const fn = mod.execOnce(() => ++count);
    fn();
    fn();
    fn();
    expect(count).toBe(1);
  });

  it("api-utils exports NextApiRequestCookies type", async () => {
    // This module is primarily type-only, but should resolve without errors
    const mod = await import("../packages/vinext/src/shims/internal/api-utils.js");
    expect(mod).toBeDefined();
  });

  it("cookies shim re-exports RequestCookies and ResponseCookies", async () => {
    const mod = await import("../packages/vinext/src/shims/internal/cookies.js");
    expect(mod.RequestCookies).toBeDefined();
    expect(mod.ResponseCookies).toBeDefined();
  });

  it("work-unit-async-storage exports AsyncLocalStorage instances", async () => {
    const mod = await import("../packages/vinext/src/shims/internal/work-unit-async-storage.js");
    expect(mod.workUnitAsyncStorage).toBeDefined();
    expect(mod.requestAsyncStorage).toBeDefined();
    // Both should be the same AsyncLocalStorage instance
    expect(mod.workUnitAsyncStorage).toBe(mod.requestAsyncStorage);
  });

  it("router-context exports RouterContext as a React context", async () => {
    const mod = await import("../packages/vinext/src/shims/internal/router-context.js");
    expect(mod.RouterContext).toBeDefined();
    // Must be a real React context object (has Provider and Consumer)
    expect(mod.RouterContext.Provider).toBeDefined();
    expect(mod.RouterContext.Consumer).toBeDefined();
  });
});

// ---------------------------------------------------------------------------
// Cloudflare KV CacheHandler
// ---------------------------------------------------------------------------

describe("KVCacheHandler", () => {
  // In-memory mock of Cloudflare KV namespace
  function createMockKV() {
    const store = new Map<string, { value: string; expirationTtl?: number }>();
    return {
      store,
      async get(key: string): Promise<string | null> {
        return store.get(key)?.value ?? null;
      },
      async put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void> {
        store.set(key, { value, expirationTtl: options?.expirationTtl });
      },
      async delete(key: string): Promise<void> {
        store.delete(key);
      },
      async list(options?: { prefix?: string; limit?: number; cursor?: string }) {
        const prefix = options?.prefix ?? "";
        const keys: Array<{ name: string }> = [];
        for (const k of store.keys()) {
          if (k.startsWith(prefix)) keys.push({ name: k });
        }
        return { keys, list_complete: true };
      },
    };
  }

  it("stores and retrieves a cache entry", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    await handler.set("test-key", {
      kind: "PAGES",
      html: "<h1>Hello</h1>",
      pageData: { props: {} },
      headers: undefined,
      status: 200,
    });

    const result = await handler.get("test-key");
    expect(result).not.toBeNull();
    expect(result!.value).not.toBeNull();
    expect(result!.value!.kind).toBe("PAGES");
    if (result!.value!.kind === "PAGES") {
      expect(result!.value!.html).toBe("<h1>Hello</h1>");
      expect(result!.value!.status).toBe(200);
    }
  });

  it("returns null for missing keys", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    const result = await handler.get("nonexistent");
    expect(result).toBeNull();
  });

  it("handles tag-based invalidation", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    await handler.set(
      "tagged-entry",
      {
        kind: "FETCH",
        data: { headers: {}, body: '{"result":1}', url: "test" },
        tags: ["my-tag"],
        revalidate: 60,
      },
      { revalidate: 60, tags: ["my-tag"] },
    );

    // Before invalidation — entry exists
    const before = await handler.get("tagged-entry");
    expect(before).not.toBeNull();

    // Invalidate the tag
    await handler.revalidateTag("my-tag");

    // After invalidation — entry should be treated as miss
    const after = await handler.get("tagged-entry");
    expect(after).toBeNull();
  });

  it("returns stale entry when past revalidation time", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    // Set with very short revalidation (already expired)
    await handler.set(
      "stale-key",
      {
        kind: "PAGES",
        html: "<h1>Stale</h1>",
        pageData: {},
        headers: undefined,
        status: 200,
      },
      { revalidate: -1 }, // already past
    );

    // Manually fix the revalidateAt to be in the past
    const raw = await kv.get("cache:stale-key");
    const entry = JSON.parse(raw!);
    entry.revalidateAt = Date.now() - 1000;
    await kv.put("cache:stale-key", JSON.stringify(entry));

    const result = await handler.get("stale-key");
    expect(result).not.toBeNull();
    expect(result!.cacheState).toBe("stale");
    expect(result!.value!.kind).toBe("PAGES");
  });

  it("serializes and restores APP_PAGE with rscData ArrayBuffer", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    const originalData = new TextEncoder().encode("RSC payload data");

    await handler.set("app-page-key", {
      kind: "APP_PAGE",
      html: "<div>App page</div>",
      rscData: originalData.buffer as ArrayBuffer,
      headers: { "x-custom": "value" },
      postponed: undefined,
      status: 200,
    });

    const result = await handler.get("app-page-key");
    expect(result).not.toBeNull();
    expect(result!.value!.kind).toBe("APP_PAGE");
    if (result!.value!.kind === "APP_PAGE") {
      expect(result!.value!.html).toBe("<div>App page</div>");
      // rscData should be restored as ArrayBuffer
      expect(result!.value!.rscData).toBeInstanceOf(ArrayBuffer);
      const restored = new TextDecoder().decode(result!.value!.rscData!);
      expect(restored).toBe("RSC payload data");
    }
  });

  it("serializes and restores APP_ROUTE with body ArrayBuffer", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    const body = new TextEncoder().encode('{"ok":true}');

    await handler.set("route-key", {
      kind: "APP_ROUTE",
      body: body.buffer as ArrayBuffer,
      status: 200,
      headers: { "content-type": "application/json" },
    });

    const result = await handler.get("route-key");
    expect(result).not.toBeNull();
    if (result!.value!.kind === "APP_ROUTE") {
      const restored = new TextDecoder().decode(result!.value!.body);
      expect(restored).toBe('{"ok":true}');
    }
  });

  it("sets KV expiration TTL based on revalidation period", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    await handler.set(
      "ttl-key",
      {
        kind: "PAGES",
        html: "<h1>TTL</h1>",
        pageData: {},
        headers: undefined,
        status: 200,
      },
      { revalidate: 60 }, // 60 seconds
    );

    // The KV entry should have an expiration TTL set
    const stored = kv.store.get("cache:ttl-key");
    expect(stored).toBeDefined();
    expect(stored!.expirationTtl).toBeDefined();
    // KV TTL is always 30 days (2592000s) regardless of revalidation period.
    // Staleness is tracked via revalidateAt in the stored JSON, not KV eviction.
    // Tying TTL to revalidation period would cause frequently-revalidated pages
    // (e.g. revalidate=5) to be evicted quickly under low traffic, forcing a
    // blocking fresh render on the next request instead of serving stale content.
    expect(stored!.expirationTtl).toBe(30 * 24 * 3600);
  });

  it("handles multiple tag invalidation in parallel", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    await handler.set(
      "multi-tag",
      {
        kind: "FETCH",
        data: { headers: {}, body: "{}", url: "test" },
        tags: ["tag-a", "tag-b"],
        revalidate: 0,
      },
      { tags: ["tag-a", "tag-b"] },
    );

    // Invalidate both tags at once
    await handler.revalidateTag(["tag-a", "tag-b"]);

    const result = await handler.get("multi-tag");
    expect(result).toBeNull();
  });

  it("handles corrupted KV entries gracefully", async () => {
    const { KVCacheHandler } =
      await import("../packages/cloudflare/src/cache/kv-data-adapter.runtime.js");
    const kv = createMockKV();
    const handler = new KVCacheHandler(kv as any);

    // Put corrupted data directly
    await kv.put("cache:corrupt-key", "not valid json {{{");

    const result = await handler.get("corrupt-key");
    expect(result).toBeNull();
    // The corrupted entry should be cleaned up
    expect(await kv.get("cache:corrupt-key")).toBeNull();
  });
});

// ─── server-only / client-only shims ─────────────────────────────────────────

describe("server-only shim", () => {
  it("can be imported without error", async () => {
    const mod = await import("../packages/vinext/src/shims/server-only.js");
    expect(mod).toBeDefined();
  });

  it("exports nothing (empty marker module)", async () => {
    const mod = await import("../packages/vinext/src/shims/server-only.js");
    // The module should have no named exports (just the default module namespace)
    const keys = Object.keys(mod).filter((k) => k !== "__esModule" && k !== "default");
    expect(keys).toHaveLength(0);
  });
});

describe("client-only shim", () => {
  it("can be imported without error", async () => {
    const mod = await import("../packages/vinext/src/shims/client-only.js");
    expect(mod).toBeDefined();
  });

  it("exports nothing (empty marker module)", async () => {
    const mod = await import("../packages/vinext/src/shims/client-only.js");
    const keys = Object.keys(mod).filter((k) => k !== "__esModule" && k !== "default");
    expect(keys).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// next/link — onNavigate / NavigateEvent (View Transitions support, Issue #38)
// ---------------------------------------------------------------------------

describe("next/link onNavigate / NavigateEvent", () => {
  it("exports Link as default and useLinkStatus as named export", async () => {
    const mod = await import("../packages/vinext/src/shims/link.js");
    expect(typeof mod.default).toBe("object"); // forwardRef returns an object
    expect(typeof mod.useLinkStatus).toBe("function");
  });

  it("NavigateEvent.preventDefault() sets defaultPrevented to true", () => {
    // Mirrors the NavigateEvent construction in the Link click handler
    let prevented = false;
    const navEvent = {
      url: new URL("/about", "http://localhost"),
      preventDefault() {
        prevented = true;
      },
      get defaultPrevented() {
        return prevented;
      },
    };

    expect(navEvent.defaultPrevented).toBe(false);
    navEvent.preventDefault();
    expect(navEvent.defaultPrevented).toBe(true);
  });

  it("NavigateEvent.defaultPrevented is false when preventDefault is not called", () => {
    let prevented = false;
    const navEvent = {
      url: new URL("/products/1", "http://localhost"),
      preventDefault() {
        prevented = true;
      },
      get defaultPrevented() {
        return prevented;
      },
    };

    expect(navEvent.defaultPrevented).toBe(false);
    expect(navEvent.url.pathname).toBe("/products/1");
  });

  it("onNavigate callback receives event with correct url", () => {
    // Simulate what the Link component does in its click handler
    const resolvedHref = "/view-transitions/posts/42";
    const navUrl = new URL(resolvedHref, "http://localhost:3000");

    let prevented = false;
    const navEvent = {
      url: navUrl,
      preventDefault() {
        prevented = true;
      },
      get defaultPrevented() {
        return prevented;
      },
    };

    // Simulated TransitionLink-style callback
    const onNavigate = (event: typeof navEvent) => {
      event.preventDefault();
    };

    onNavigate(navEvent);
    expect(navEvent.defaultPrevented).toBe(true);
    expect(navEvent.url.pathname).toBe("/view-transitions/posts/42");
  });

  it("multiple preventDefault() calls are idempotent", () => {
    let prevented = false;
    const navEvent = {
      url: new URL("/", "http://localhost"),
      preventDefault() {
        prevented = true;
      },
      get defaultPrevented() {
        return prevented;
      },
    };

    navEvent.preventDefault();
    navEvent.preventDefault();
    navEvent.preventDefault();
    expect(navEvent.defaultPrevented).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// vinext:react-canary — ViewTransition & addTransitionType polyfills (Issue #42)
// ---------------------------------------------------------------------------

describe("vinext:react-canary transform logic", () => {
  // These tests verify the regex patterns used by the vinext:react-canary plugin
  // to detect and rewrite imports of React canary APIs.

  const canaryImportRegex =
    /import\s*\{[^}]*(ViewTransition|addTransitionType)[^}]*\}\s*from\s*['"]react['"]/;

  it("detects ViewTransition import from react", () => {
    const code = `import { ViewTransition } from "react";`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });

  it("detects addTransitionType import from react", () => {
    const code = `import { addTransitionType } from "react";`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });

  it("detects ViewTransition alongside other React imports", () => {
    const code = `import { useState, ViewTransition, useEffect } from "react";`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });

  it("detects addTransitionType alongside other React imports", () => {
    const code = `import { useRef, addTransitionType } from 'react';`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });

  it("detects both canary APIs in a single import", () => {
    const code = `import { ViewTransition, addTransitionType } from "react";`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });

  it("does NOT match imports from other modules", () => {
    const code = `import { ViewTransition } from "some-other-lib";`;
    expect(canaryImportRegex.test(code)).toBe(false);
  });

  it("does NOT match non-canary React imports", () => {
    const code = `import { useState, useEffect } from "react";`;
    expect(canaryImportRegex.test(code)).toBe(false);
  });

  it("does NOT match default React import", () => {
    const code = `import React from "react";`;
    expect(canaryImportRegex.test(code)).toBe(false);
  });

  it("does NOT match namespace React import", () => {
    const code = `import * as React from "react";`;
    expect(canaryImportRegex.test(code)).toBe(false);
  });

  it("rewrites all 'from react' occurrences in a file with canary imports", () => {
    const code = [
      `import { useState } from "react";`,
      `import { ViewTransition } from "react";`,
      ``,
      `export default function Template({ children }) {`,
      `  return <ViewTransition>{children}</ViewTransition>;`,
      `}`,
    ].join("\n");

    // The transform replaces all `from "react"` in the file
    const result = code.replace(/from\s*['"]react['"]/g, 'from "virtual:vinext-react-canary"');

    expect(result).toContain('from "virtual:vinext-react-canary"');
    expect(result).not.toContain('from "react"');
    // Both import lines should be rewritten
    expect(result.match(/virtual:vinext-react-canary/g)?.length).toBe(2);
  });

  it("handles single-quoted imports", () => {
    const code = `import { ViewTransition } from 'react';`;
    expect(canaryImportRegex.test(code)).toBe(true);

    const result = code.replace(/from\s*['"]react['"]/g, 'from "virtual:vinext-react-canary"');
    expect(result).toBe(`import { ViewTransition } from "virtual:vinext-react-canary";`);
  });

  it("handles multiline imports", () => {
    const code = `import {\n  ViewTransition,\n  useState,\n} from "react";`;
    expect(canaryImportRegex.test(code)).toBe(true);
  });
});

describe("ViewTransition polyfill behavior", () => {
  it("provides a passthrough component when React lacks ViewTransition", () => {
    // Simulate the polyfill logic from the virtual module
    const React = { ViewTransition: undefined };
    const ViewTransition =
      React.ViewTransition ||
      function ViewTransition({ children }: { children: any }) {
        return children;
      };

    // ViewTransition should be a function
    expect(typeof ViewTransition).toBe("function");

    // It should pass through children unchanged
    const children = { type: "div", props: {} };
    const result = ViewTransition({ children });
    expect(result).toBe(children);
  });

  it("uses native ViewTransition when React exports it", () => {
    // Simulate React canary that HAS ViewTransition
    const nativeViewTransition = function NativeViewTransition({ children }: { children: any }) {
      return { wrapped: children };
    };
    const React = { ViewTransition: nativeViewTransition };
    const ViewTransition =
      React.ViewTransition ||
      function ViewTransition({ children }: { children: any }) {
        return children;
      };

    expect(ViewTransition).toBe(nativeViewTransition);
  });

  it("provides a no-op addTransitionType when React lacks it", () => {
    const React = { addTransitionType: undefined };
    const addTransitionType = React.addTransitionType || function addTransitionType() {};

    expect(typeof addTransitionType).toBe("function");
    // Should not throw when called
    expect(() => addTransitionType()).not.toThrow();
  });

  it("uses native addTransitionType when React exports it", () => {
    const nativeAddTransitionType = function nativeAddTransitionType(type: string) {
      return type;
    };
    const React = { addTransitionType: nativeAddTransitionType };
    const addTransitionType = React.addTransitionType || function addTransitionType() {};

    expect(addTransitionType).toBe(nativeAddTransitionType);
  });
});

// ---------------------------------------------------------------------------
// next/head SSR security tests
// ---------------------------------------------------------------------------

describe("next/head SSR security", () => {
  async function collectHeadHTML(children: React.ReactElement[]) {
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");
    const {
      default: Head,
      resetSSRHead,
      getSSRHeadHTML,
    } = await import("../packages/vinext/src/shims/head.js");

    resetSSRHead();
    // Render Head with children — SSR path collects elements
    renderToStaticMarkup(React.createElement(Head, null, ...children));
    return getSSRHeadHTML();
  }

  it("escapes HTML special characters in title children", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("title", null, '</title><script>alert("xss")</script>'),
    ]);

    // The injected script tag must be escaped, not raw
    expect(html).not.toContain("<script>");
    expect(html).not.toContain("</title><script>");
    expect(html).toContain("&lt;/title&gt;&lt;script&gt;");
  });

  it("escapes ampersands and angle brackets in children", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("title", null, "Tom & Jerry < Friends > Foes"),
    ]);

    expect(html).toContain("Tom &amp; Jerry &lt; Friends &gt; Foes");
    expect(html).not.toContain("Tom & Jerry < Friends > Foes");
  });

  it("still allows dangerouslySetInnerHTML (intentionally raw)", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("style", {
        dangerouslySetInnerHTML: { __html: "body { color: red; }" },
      }),
    ]);

    expect(html).toContain("body { color: red; }");
  });

  it("escapes </script> in dangerouslySetInnerHTML for script tags", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("script", {
        dangerouslySetInnerHTML: {
          __html: 'var x = "</script><img src=x onerror=alert(1)>";',
        },
      }),
    ]);

    // The raw </script> must NOT appear in the output
    expect(html).not.toContain("</script><img");
    // The escaped form preserves the JS string content
    expect(html).toContain("<\\/script>");
  });

  it("escapes </style> in dangerouslySetInnerHTML for style tags", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("style", {
        dangerouslySetInnerHTML: {
          __html: "body::after { content: '</style><img src=x onerror=alert(1)>'; }",
        },
      }),
    ]);

    // The raw </style> must NOT appear
    expect(html).not.toContain("</style><img");
    expect(html).toContain("<\\/style>");
  });

  it("escapes case-insensitive closing tags in dangerouslySetInnerHTML", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("script", {
        dangerouslySetInnerHTML: {
          __html: 'var x = "</SCRIPT><img src=x onerror=alert(1)>";',
        },
      }),
    ]);

    expect(html).not.toContain("</SCRIPT>");
    expect(html).toContain("<\\/SCRIPT>");
  });

  it("attributes are still properly escaped", async () => {
    const React = await import("react");
    const html = await collectHeadHTML([
      React.createElement("meta", {
        name: "description",
        content: 'He said "hello" & <goodbye>',
      }),
    ]);

    expect(html).toContain("&quot;hello&quot;");
    expect(html).toContain("&amp;");
    expect(html).toContain("&lt;goodbye&gt;");
  });

  it("rejects disallowed tag types (iframe)", async () => {
    const React = await import("react");
    const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});

    const html = await collectHeadHTML([
      React.createElement("iframe" as any, { src: "https://evil.com" }),
    ]);

    // After issue #1569, the default head (charset + viewport) is always
    // emitted, so the output is never an empty string. The disallowed tag
    // must still not appear.
    expect(html).not.toContain("<iframe");
    expect(html).not.toContain("evil.com");
    expect(consoleWarn).toHaveBeenCalledWith(
      expect.stringContaining("ignoring disallowed tag <iframe>"),
    );
    consoleWarn.mockRestore();
  });

  it("rejects disallowed tag types (object, embed, form)", async () => {
    const React = await import("react");
    const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});

    const html = await collectHeadHTML([
      React.createElement("object" as any, { data: "https://evil.com" }),
      React.createElement("embed" as any, { src: "https://evil.com" }),
      React.createElement("form" as any, { action: "https://evil.com" }),
    ]);

    expect(html).not.toContain("<object");
    expect(html).not.toContain("<embed");
    expect(html).not.toContain("<form");
    expect(html).not.toContain("evil.com");
    consoleWarn.mockRestore();
  });

  it("allows all valid head tags", async () => {
    const React = await import("react");
    const { resetSSRHead } = await import("../packages/vinext/src/shims/head.js");

    const allowedTags = ["title", "meta", "link", "style", "script", "base", "noscript"];

    for (const tag of allowedTags) {
      resetSSRHead();
      const selfClosing = ["meta", "link", "base"].includes(tag);
      const el = selfClosing
        ? React.createElement(tag, { name: "test", content: "test" })
        : React.createElement(tag, null, "test content");

      const html = await collectHeadHTML([el]);

      expect(html).toContain(`<${tag}`);
      expect(html).toContain('data-next-head=""');
    }
  });
});

describe("escapeInlineContent", () => {
  it("escapes </script> within script content", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    const input = 'var x = "</script><img src=x onerror=alert(1)>";';
    const result = escapeInlineContent(input, "script");
    expect(result).toBe('var x = "<\\/script><img src=x onerror=alert(1)>";');
    expect(result).not.toContain("</script>");
  });

  it("escapes </style> within style content", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    const input = "body::after { content: '</style><div>'; }";
    const result = escapeInlineContent(input, "style");
    expect(result).toBe("body::after { content: '<\\/style><div>'; }");
    expect(result).not.toContain("</style>");
  });

  it("handles case-insensitive closing tags", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    expect(escapeInlineContent("</Script>", "script")).toBe("<\\/Script>");
    expect(escapeInlineContent("</SCRIPT>", "script")).toBe("<\\/SCRIPT>");
    expect(escapeInlineContent("</sCrIpT>", "script")).toBe("<\\/sCrIpT>");
  });

  it("handles multiple occurrences", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    const input = "</script></script></SCRIPT>";
    const result = escapeInlineContent(input, "script");
    expect(result).toBe("<\\/script><\\/script><\\/SCRIPT>");
    expect(result).not.toContain("</script>");
    expect(result).not.toContain("</SCRIPT>");
  });

  it("does not escape unrelated closing tags", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    // Escaping for "script" should not touch </style>
    const input = "</style></div>";
    const result = escapeInlineContent(input, "script");
    expect(result).toBe("</style></div>");
  });

  it("passes through content with no closing tags", async () => {
    const { escapeInlineContent } = await import("../packages/vinext/src/shims/head.js");
    const input = "console.log('hello world');";
    expect(escapeInlineContent(input, "script")).toBe(input);
  });
});

describe("isValidModulePath", () => {
  it("accepts valid absolute paths", () => {
    expect(isValidModulePath("/src/pages/index.tsx")).toBe(true);
    expect(isValidModulePath("/pages/about.js")).toBe(true);
    expect(isValidModulePath("/src/pages/posts/[id].tsx")).toBe(true);
  });

  it("accepts valid relative paths starting with ./", () => {
    expect(isValidModulePath("./src/pages/index.tsx")).toBe(true);
    expect(isValidModulePath("./pages/about.js")).toBe(true);
  });

  it("rejects external https:// URLs", () => {
    expect(isValidModulePath("https://evil.com/steal-cookies.js")).toBe(false);
  });

  it("rejects external http:// URLs", () => {
    expect(isValidModulePath("http://evil.com/steal-cookies.js")).toBe(false);
  });

  it("rejects protocol-relative URLs (//)", () => {
    expect(isValidModulePath("//evil.com/steal-cookies.js")).toBe(false);
    expect(isValidModulePath("//cdn.example.com/script.js")).toBe(false);
  });

  it("rejects directory traversal", () => {
    expect(isValidModulePath("/src/../../../etc/passwd")).toBe(false);
    expect(isValidModulePath("./../../secret.js")).toBe(false);
    expect(isValidModulePath("/pages/..%2F..%2Fsecret.js")).toBe(false);
  });

  it("rejects data: URLs", () => {
    expect(isValidModulePath("data:text/javascript,alert(1)")).toBe(false);
  });

  it("rejects blob: URLs", () => {
    expect(isValidModulePath("blob:http://localhost/abc")).toBe(false);
  });

  it("rejects bare specifiers", () => {
    expect(isValidModulePath("evil-package")).toBe(false);
    expect(isValidModulePath("@evil/package")).toBe(false);
  });

  it("rejects non-string values", () => {
    expect(isValidModulePath(null)).toBe(false);
    expect(isValidModulePath(undefined)).toBe(false);
    expect(isValidModulePath(42)).toBe(false);
    expect(isValidModulePath({})).toBe(false);
    expect(isValidModulePath("")).toBe(false);
  });

  it("rejects javascript: protocol", () => {
    expect(isValidModulePath("javascript:alert(1)")).toBe(false);
  });

  it("rejects ftp:// protocol", () => {
    expect(isValidModulePath("ftp://evil.com/script.js")).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Cache scope guards — headers()/cookies()/connection() must throw inside
// "use cache" and unstable_cache() scopes (matches Next.js behavior).
// Ported from Next.js: test/e2e/app-dir/use-cache/use-cache.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache/use-cache.test.ts

describe("cache scope guards for dynamic APIs", () => {
  it('headers() throws inside "use cache" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    // Set up a valid headers context so the "no context" error doesn't fire
    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    // Run inside a "use cache" ALS scope
    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        await expect(headers()).rejects.toThrow(/cannot be called inside "use cache"/);
      },
    );

    setHeadersContext(null);
  });

  it('headers() sync access throws the "use cache" error instead of a TypeError', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers({ "x-test": "blocked" }),
      cookies: new Map(),
    });

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        expect(() => headers().get("x-test")).toThrow(/cannot be called inside "use cache"/);
      },
    );

    setHeadersContext(null);
  });

  it('cookies() throws inside "use cache" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        await expect(cookies()).rejects.toThrow(/cannot be called inside "use cache"/);
      },
    );

    setHeadersContext(null);
  });

  it('headers() is allowed inside "use cache: private" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    // Next.js private-cache stores carry request headers.
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/work-unit-async-storage.external.ts
    try {
      setHeadersContext({
        headers: new Headers({ "x-private": "allowed" }),
        cookies: new Map(),
      });

      await cacheContextStorage.run(
        {
          tags: [],
          lifeConfigs: [],
          variant: "private",
          hasExplicitRevalidate: false,
          hasExplicitExpire: false,
          dynamicNestedCacheError: undefined,
        },
        async () => {
          expect((await headers()).get("x-private")).toBe("allowed");
        },
      );
    } finally {
      setHeadersContext(null);
    }
  });

  it('cookies() is allowed inside "use cache: private" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    // Ported from Next.js: test/e2e/app-dir/use-cache-private/use-cache-private.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-private/use-cache-private.test.ts
    try {
      setHeadersContext({
        headers: new Headers({ cookie: "test-cookie=allowed" }),
        cookies: new Map([["test-cookie", "allowed"]]),
      });

      await cacheContextStorage.run(
        {
          tags: [],
          lifeConfigs: [],
          variant: "private",
          hasExplicitRevalidate: false,
          hasExplicitExpire: false,
          dynamicNestedCacheError: undefined,
        },
        async () => {
          await expect(cookies()).resolves.toMatchObject({
            get: expect.any(Function),
          });
          expect((await cookies()).get("test-cookie")?.value).toBe("allowed");
        },
      );
    } finally {
      setHeadersContext(null);
    }
  });

  it('cookies() sync access throws the "use cache" error instead of a TypeError', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "blocked"]]),
    });

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        expect(() => cookies().get("session")).toThrow(/cannot be called inside "use cache"/);
      },
    );

    setHeadersContext(null);
  });

  it('connection() throws inside "use cache" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { connection } = await import("../packages/vinext/src/shims/server.js");

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        await expect(connection()).rejects.toThrow(/cannot be called inside "use cache"/);
      },
    );
  });

  it("headers() throws inside unstable_cache() scope", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());
    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    const cached = unstable_cache(async () => {
      // This should throw because we're inside an unstable_cache scope
      const h = await headers();
      return h.get("x-test");
    }, ["test-headers-in-cache"]);

    await expect(cached()).rejects.toThrow(/unstable_cache/);

    setHeadersContext(null);
    setCacheHandler(new MemoryCacheHandler());
  });

  it("cookies() throws inside unstable_cache() scope", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());
    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["session", "abc"]]),
    });

    const cached = unstable_cache(async () => {
      const c = await cookies();
      return c.get("session");
    }, ["test-cookies-in-cache"]);

    await expect(cached()).rejects.toThrow(/unstable_cache/);

    setHeadersContext(null);
    setCacheHandler(new MemoryCacheHandler());
  });

  it("connection() throws inside unstable_cache() scope", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { connection } = await import("../packages/vinext/src/shims/server.js");

    setCacheHandler(new MemoryCacheHandler());

    const cached = unstable_cache(async () => {
      await connection();
    }, ["test-connection-in-cache"]);

    await expect(cached()).rejects.toThrow(/unstable_cache/);

    setCacheHandler(new MemoryCacheHandler());
  });

  it("headers() works normally outside cache scopes", async () => {
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers({ "x-test": "works" }),
      cookies: new Map(),
    });

    // Should not throw outside any cache scope
    const h = await headers();
    expect(h.get("x-test")).toBe("works");

    setHeadersContext(null);
  });

  it("cookies() works normally outside cache scopes", async () => {
    const { cookies, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers(),
      cookies: new Map([["token", "abc"]]),
    });

    // Should not throw outside any cache scope
    const c = await cookies();
    expect(c.get("token")).toEqual({ name: "token", value: "abc" });

    setHeadersContext(null);
  });

  // Ported from Next.js: packages/next/src/server/request/draft-mode.ts
  // (`getDraftModeProviderForCacheScope` + `trackDynamicDraftMode`).
  // Reading `draftMode()` / `.isEnabled` is allowed inside cache scopes;
  // only the mutating `enable()` / `disable()` methods throw. Issue #1489.
  it('draftMode().isEnabled reads inside "use cache" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { draftMode, setHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({
      headers: new Headers({ cookie: "__prerender_bypass=test-secret" }),
      cookies: new Map([["__prerender_bypass", "test-secret"]]),
      draftModeSecret: "test-secret",
    });

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        const dm = await draftMode();
        expect(dm.isEnabled).toBe(true);
      },
    );

    setHeadersContext(null);
  });

  it("draftMode().isEnabled reads inside unstable_cache() scope", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { draftMode, setHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());
    setHeadersContext({
      headers: new Headers({ cookie: "__prerender_bypass=test-secret" }),
      cookies: new Map([["__prerender_bypass", "test-secret"]]),
      draftModeSecret: "test-secret",
    });

    let observed: boolean | undefined;
    const cached = unstable_cache(async () => {
      const dm = await draftMode();
      observed = dm.isEnabled;
      return observed;
    }, ["test-draftmode-in-cache"]);

    await expect(cached()).resolves.toBe(true);
    expect(observed).toBe(true);

    setHeadersContext(null);
    setCacheHandler(new MemoryCacheHandler());
  });

  it('draftMode().enable() throws inside "use cache" scope', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { draftMode, setHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");

    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        const dm = await draftMode();
        expect(() => dm.enable()).toThrow(/cannot be called inside "use cache"/);
        expect(() => dm.disable()).toThrow(/cannot be called inside "use cache"/);
      },
    );

    setHeadersContext(null);
  });

  it("draftMode().enable() throws inside unstable_cache() scope", async () => {
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { draftMode, setHeadersContext } =
      await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());
    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    const cached = unstable_cache(async () => {
      const dm = await draftMode();
      dm.enable();
    }, ["test-draftmode-enable-in-cache"]);

    await expect(cached()).rejects.toThrow(/unstable_cache/);

    setHeadersContext(null);
    setCacheHandler(new MemoryCacheHandler());
  });

  it('headers() throws inside nested scopes (unstable_cache inside "use cache")', async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");
    const { headers, setHeadersContext } = await import("../packages/vinext/src/shims/headers.js");

    setCacheHandler(new MemoryCacheHandler());
    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    // Nest unstable_cache inside a "use cache" scope: the outermost
    // scope ("use cache") should be detected first.
    await cacheContextStorage.run(
      {
        tags: [],
        lifeConfigs: [],
        variant: "default",
        hasExplicitRevalidate: false,
        hasExplicitExpire: false,
        dynamicNestedCacheError: undefined,
      },
      async () => {
        const cached = unstable_cache(async () => {
          const h = await headers();
          return h.get("x-test");
        }, ["test-nested-scopes"]);

        // Either scope's guard triggers (the "use cache" check runs first)
        await expect(cached()).rejects.toThrow(/cannot be called inside/);
      },
    );

    setHeadersContext(null);
    setCacheHandler(new MemoryCacheHandler());
  });

  it("existing unstable_cache tests still pass (cache miss executes callback)", async () => {
    // Verify that the ALS wrapping in unstable_cache doesn't break normal caching
    const { unstable_cache, setCacheHandler, MemoryCacheHandler } =
      await import("../packages/vinext/src/shims/cache.js");

    setCacheHandler(new MemoryCacheHandler());

    let callCount = 0;
    const cached = unstable_cache(
      async (x: number) => {
        callCount++;
        return x * 2;
      },
      ["regression-test"],
    );

    const r1 = await cached(5);
    expect(r1).toBe(10);
    expect(callCount).toBe(1);

    const r2 = await cached(5);
    expect(r2).toBe(10);
    expect(callCount).toBe(1); // Cached, not called again

    setCacheHandler(new MemoryCacheHandler());
  });

  // Ported from Next.js: workStore.invalidDynamicUsageError in
  // packages/next/src/server/app-render/app-render.tsx
  // https://github.com/vercel/next.js/commit/f5e54c06726b571a042fce67417e40a29f6b8689
  it("records invalid dynamic usage error on request context (survives try/catch)", async () => {
    const { cacheContextStorage } = await import("../packages/vinext/src/shims/cache-runtime.js");
    const { setHeadersContext, throwIfInsideCacheScope, consumeInvalidDynamicUsageError } =
      await import("../packages/vinext/src/shims/headers.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    setHeadersContext({ headers: new Headers(), cookies: new Map() });

    const ctx = createRequestContext({
      headersContext: { headers: new Headers(), cookies: new Map() },
    });
    let recordedError: unknown = null;

    await runWithRequestContext(ctx, async () => {
      try {
        await cacheContextStorage.run(
          {
            tags: [],
            lifeConfigs: [],
            variant: "default",
            hasExplicitRevalidate: false,
            hasExplicitExpire: false,
            dynamicNestedCacheError: undefined,
          },
          async () => {
            throwIfInsideCacheScope("cookies()");
          },
        );
      } catch {
        // User try/catch — the error should still be recorded on the context
      }
      recordedError = consumeInvalidDynamicUsageError();
    });

    expect(recordedError).toBeInstanceOf(Error);
    expect((recordedError as Error).message).toContain('cannot be called inside "use cache"');

    // After consumption, the error is cleared
    expect(consumeInvalidDynamicUsageError()).toBeNull();

    setHeadersContext(null);
  });

  it("consumeInvalidDynamicUsageError returns null when no error was recorded", async () => {
    const { consumeInvalidDynamicUsageError } =
      await import("../packages/vinext/src/shims/headers.js");
    const { createRequestContext, runWithRequestContext } =
      await import("../packages/vinext/src/shims/unified-request-context.js");

    const ctx = createRequestContext();
    let result: unknown;

    await runWithRequestContext(ctx, async () => {
      result = consumeInvalidDynamicUsageError();
    });

    expect(result!).toBeNull();
  });

  it("consumeInvalidDynamicUsageError works outside unified request scope", async () => {
    const { consumeInvalidDynamicUsageError } =
      await import("../packages/vinext/src/shims/headers.js");
    // Should return null without throwing when no unified scope is active
    expect(consumeInvalidDynamicUsageError()).toBeNull();
  });
});

describe("shim alias map .js variants", () => {
  it("every top-level next/* alias has a corresponding .js variant", async () => {
    const plugins = vinext() as Plugin[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    if (!configPlugin?.config) throw new Error("vinext:config plugin not found");

    const hookFn = (
      typeof configPlugin.config === "function" ? configPlugin.config : configPlugin.config.handler
    ) as (config: { root: string }, env: { mode: string; command: string }) => Promise<any>;

    const result = await hookFn(
      { root: PAGES_FIXTURE_DIR },
      { mode: "development", command: "serve" },
    );

    const aliases = result?.resolve?.alias as Record<string, string> | undefined;
    expect(aliases).toBeDefined();

    // Collect top-level next/<name> keys (exclude next/dist/*, next/font/*, next/compat/*, next/legacy/*)
    const topLevel = Object.keys(aliases!).filter((key) => {
      if (!key.startsWith("next/")) return false;
      if (key.endsWith(".js")) return false;
      const segment = key.slice("next/".length);
      if (segment.startsWith("dist/")) return false;
      if (segment.startsWith("font/")) return false;
      if (segment.startsWith("compat/")) return false;
      if (segment.startsWith("legacy/")) return false;
      return true;
    });

    expect(topLevel.length).toBeGreaterThan(0);

    const missing = topLevel.filter((key) => !(key + ".js" in aliases!));
    expect(missing, `Missing .js aliases for: ${missing.join(", ")}`).toEqual([]);
  });

  it("resolves next/error to the react-server shim only in the RSC environment", () => {
    const plugins = vinext() as Plugin[];
    const configPlugin = plugins.find((plugin) => plugin.name === "vinext:config");
    if (!configPlugin?.resolveId || typeof configPlugin.resolveId === "function") {
      throw new Error("vinext:config resolveId hook not found");
    }

    const hook = configPlugin.resolveId as {
      handler: (this: { environment?: { name?: string } }, id: string) => string | undefined;
    };
    const clientShim = path.resolve(import.meta.dirname, "../packages/vinext/src/shims/error.tsx");
    const reactServerShim = path.resolve(
      import.meta.dirname,
      "../packages/vinext/src/shims/error.react-server.ts",
    );

    expect(hook.handler.call({ environment: { name: "rsc" } }, "next/error")).toBe(reactServerShim);
    expect(hook.handler.call({ environment: { name: "ssr" } }, "next/error")).toBe(clientShim);
    expect(hook.handler.call({ environment: { name: "rsc" } }, "next/error.js")).toBe(
      reactServerShim,
    );
  });
});

describe("@vercel/og compatibility resolution", () => {
  type ResolveIdHook = {
    filter: { id: RegExp };
    handler: (
      this: { environment?: { name?: string } },
      id: string,
      importer?: string,
    ) => string | undefined;
  };

  function getResolveIdHook(): ResolveIdHook {
    const plugins = vinext() as Plugin[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    if (!configPlugin?.resolveId || typeof configPlugin.resolveId === "function") {
      throw new Error("vinext:config resolveId hook not found");
    }
    return configPlugin.resolveId as ResolveIdHook;
  }

  it("routes direct @vercel/og app imports through the Next-compatible ImageResponse shim", () => {
    const hook = getResolveIdHook();
    // resolveId returns Vite-style ids: forward slashes on every platform.
    const expectedShim = normalizePathSeparators(
      path.resolve(import.meta.dirname, "../packages/vinext/src/shims/og.tsx"),
    );

    expect(hook.filter.id.test("@vercel/og")).toBe(true);
    expect(hook.filter.id.test("@vercel/og.js")).toBe(true);
    expect(
      hook.handler.call({}, "@vercel/og", path.join(FIXTURE_DIR, "app/opengraph-image.tsx")),
    ).toBe(expectedShim);
    expect(
      hook.handler.call({}, "@vercel/og.js", path.join(FIXTURE_DIR, "app/twitter-image.tsx")),
    ).toBe(expectedShim);
  });

  it("lets the ImageResponse shim delegate to the real @vercel/og package", () => {
    const hook = getResolveIdHook();

    expect(
      hook.handler.call({}, "@vercel/og", "/repo/packages/vinext/src/shims/og.tsx"),
    ).toBeUndefined();
    expect(
      hook.handler.call({}, "@vercel/og", "/repo/node_modules/vinext/dist/shims/og.js"),
    ).toBeUndefined();
  });
});

describe("vinext shim package-subpath resolution", () => {
  type ResolveIdHook = {
    filter: { id: RegExp };
    handler: (
      this: { environment?: { name?: string } },
      id: string,
      importer?: string,
    ) => string | undefined;
  };

  function getResolveIdHook(): ResolveIdHook {
    const plugins = vinext() as Plugin[];
    const configPlugin = plugins.find((plugin) => plugin.name === "vinext:config");
    if (!configPlugin?.resolveId || typeof configPlugin.resolveId === "function") {
      throw new Error("vinext:config resolveId hook not found");
    }
    return configPlugin.resolveId as ResolveIdHook;
  }

  it("strips JavaScript extensions and Vite queries from package subpaths", () => {
    const hook = getResolveIdHook();
    const expectedShim = path.resolve(
      import.meta.dirname,
      "../packages/vinext/src/shims/navigation.ts",
    );
    const expectedReactServerShim = path.resolve(
      import.meta.dirname,
      "../packages/vinext/src/shims/navigation.react-server.ts",
    );
    const expectedOgShim = path.resolve(import.meta.dirname, "../packages/vinext/src/shims/og.tsx");

    expect(hook.filter.id.test("vinext/shims/navigation.js?v=123")).toBe(true);
    expect(hook.filter.id.test("\0vinext/shims/navigation.js?v=123")).toBe(true);
    expect(hook.filter.id.test("\0next/navigation")).toBe(true);
    expect(hook.filter.id.test("\0@vercel/og")).toBe(true);
    expect(hook.handler.call({}, "unrelated-next/navigation")).toBeUndefined();
    expect(hook.handler.call({}, "vinext/shims/navigation.js?v=123")).toBe(expectedShim);
    expect(hook.handler.call({}, "\0vinext/shims/navigation.js?v=123")).toBe(expectedShim);
    expect(hook.handler.call({ environment: { name: "rsc" } }, "\0next/navigation")).toBe(
      expectedReactServerShim,
    );
    expect(
      hook.handler.call({}, "\0@vercel/og", path.join(FIXTURE_DIR, "app/opengraph-image.tsx")),
    ).toBe(expectedOgShim);
  });
});

// ── next/head attribute name validation ─────────────────────────────────────

describe("isSafeAttrName", () => {
  let isSafeAttrName: (name: string) => boolean;

  beforeEach(async () => {
    const mod = await import("../packages/vinext/src/shims/head.js");
    isSafeAttrName = mod.isSafeAttrName;
  });

  it("allows standard HTML attribute names", () => {
    expect(isSafeAttrName("name")).toBe(true);
    expect(isSafeAttrName("content")).toBe(true);
    expect(isSafeAttrName("charset")).toBe(true);
    expect(isSafeAttrName("http-equiv")).toBe(true);
    expect(isSafeAttrName("data-testid")).toBe(true);
    expect(isSafeAttrName("property")).toBe(true);
    expect(isSafeAttrName("rel")).toBe(true);
    expect(isSafeAttrName("href")).toBe(true);
    expect(isSafeAttrName("crossOrigin")).toBe(true);
  });

  it("allows xml-namespaced attributes", () => {
    expect(isSafeAttrName("xml:lang")).toBe(true);
    expect(isSafeAttrName("xlink:href")).toBe(true);
  });

  it("rejects attribute names containing quotes", () => {
    expect(isSafeAttrName('x"')).toBe(false);
    expect(isSafeAttrName("x'")).toBe(false);
  });

  it("rejects attribute names containing angle brackets", () => {
    expect(isSafeAttrName("x>")).toBe(false);
    expect(isSafeAttrName("x<script")).toBe(false);
  });

  it("rejects attribute names containing slashes", () => {
    expect(isSafeAttrName("x/")).toBe(false);
    expect(isSafeAttrName('x"/><script>alert(1)</script><meta a="')).toBe(false);
  });

  it("rejects attribute names containing spaces", () => {
    expect(isSafeAttrName("x y")).toBe(false);
    expect(isSafeAttrName("x\ty")).toBe(false);
  });

  it("rejects attribute names containing equals", () => {
    expect(isSafeAttrName("x=y")).toBe(false);
  });

  it("rejects inline event handler attributes", () => {
    expect(isSafeAttrName("onclick")).toBe(false);
    expect(isSafeAttrName("onerror")).toBe(false);
    expect(isSafeAttrName("onload")).toBe(false);
    expect(isSafeAttrName("onmouseover")).toBe(false);
  });

  it("allows attributes starting with 'o' that are not event handlers", () => {
    expect(isSafeAttrName("open")).toBe(true);
    expect(isSafeAttrName("og:title")).toBe(true);
  });

  it("rejects empty or non-alpha-starting names", () => {
    expect(isSafeAttrName("")).toBe(false);
    expect(isSafeAttrName("123")).toBe(false);
    expect(isSafeAttrName("-foo")).toBe(false);
  });
});

// ── has/missing condition value matching (anchored regex) ──────────────────

describe("checkHasConditions value anchoring", () => {
  let checkHasConditions: Function;
  let requestContextFromRequest: Function;

  beforeEach(async () => {
    const mod = await import("../packages/vinext/src/config/config-matchers.js");
    checkHasConditions = mod.checkHasConditions;
    requestContextFromRequest = mod.requestContextFromRequest;
  });

  it("exact value matches fully", () => {
    const ctx = requestContextFromRequest(
      new Request("http://localhost/", { headers: { cookie: "role=admin" } }),
    );
    const result = checkHasConditions(
      [{ type: "cookie", key: "role", value: "admin" }],
      undefined,
      ctx,
    );
    expect(result).toBe(true);
  });

  it("does not match substring (anchored regex prevents partial match)", () => {
    const ctx = requestContextFromRequest(
      new Request("http://localhost/", { headers: { cookie: "role=not-admin" } }),
    );
    const result = checkHasConditions(
      [{ type: "cookie", key: "role", value: "admin" }],
      undefined,
      ctx,
    );
    expect(result).toBe(false);
  });

  it("does not match superstring", () => {
    const ctx = requestContextFromRequest(
      new Request("http://localhost/", { headers: { cookie: "role=admin-temp" } }),
    );
    const result = checkHasConditions(
      [{ type: "cookie", key: "role", value: "admin" }],
      undefined,
      ctx,
    );
    expect(result).toBe(false);
  });

  it("regex patterns still work with anchoring", () => {
    const ctx = requestContextFromRequest(
      new Request("http://localhost/", { headers: { "x-lang": "en-US" } }),
    );
    const result = checkHasConditions(
      [{ type: "header", key: "x-lang", value: "en.*" }],
      undefined,
      ctx,
    );
    expect(result).toBe(true);
  });
});

// ── CSRF origin wildcard matching ─────────────────────────────────────────
// Ported from Next.js: packages/next/src/server/app-render/csrf-protection.test.ts
// https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/csrf-protection.test.ts

describe("isOriginAllowed", () => {
  let isOriginAllowed: (origin: string, allowed: string[]) => boolean;

  beforeEach(async () => {
    const mod = await import("../packages/vinext/src/server/request-pipeline.js");
    isOriginAllowed = mod.isOriginAllowed;
  });

  it("exact match", () => {
    expect(isOriginAllowed("vercel.com", ["vercel.com"])).toBe(true);
    expect(isOriginAllowed("www.vercel.com", ["www.vercel.com"])).toBe(true);
  });

  it("single-level wildcard matches one subdomain", () => {
    expect(isOriginAllowed("asdf.vercel.com", ["*.vercel.com"])).toBe(true);
  });

  it("single-level wildcard does NOT match multiple subdomains", () => {
    expect(isOriginAllowed("asdf.jkl.vercel.com", ["*.vercel.com"])).toBe(false);
  });

  it("double wildcard matches one or more subdomains", () => {
    expect(isOriginAllowed("asdf.vercel.com", ["**.vercel.com"])).toBe(true);
    expect(isOriginAllowed("asdf.jkl.vercel.com", ["**.vercel.com"])).toBe(true);
  });

  it("does not match different TLD", () => {
    expect(isOriginAllowed("asdf.vercel.com", ["*.vercel.app"])).toBe(false);
    expect(isOriginAllowed("asdf.jkl.vercel.app", ["**.vercel.com"])).toBe(false);
  });

  it("does not match unrelated domain", () => {
    expect(isOriginAllowed("vercel.com", ["nextjs.org"])).toBe(false);
  });

  it("returns false for undefined/empty allowed list", () => {
    expect(isOriginAllowed("vercel.com", [])).toBe(false);
  });

  it("returns false for empty string pattern", () => {
    expect(isOriginAllowed("vercel.com", [""])).toBe(false);
  });

  it("wildcards only match below the domain level", () => {
    expect(isOriginAllowed("vercel.com", ["*"])).toBe(false);
    expect(isOriginAllowed("vercel.com", ["**"])).toBe(false);
  });

  it("matches case-insensitively (RFC 1035)", () => {
    expect(isOriginAllowed("sub.VERCEL.com", ["*.vercel.com"])).toBe(true);
    expect(isOriginAllowed("SUB.vercel.COM", ["*.vercel.com"])).toBe(true);
    expect(isOriginAllowed("VERCEL.COM", ["vercel.com"])).toBe(true);
    expect(isOriginAllowed("vercel.com", ["VERCEL.COM"])).toBe(true);
  });

  it("localhost patterns", () => {
    expect(isOriginAllowed("subdomain.localhost", ["*.localhost"])).toBe(true);
    expect(isOriginAllowed("localhost", ["*.localhost"])).toBe(false);
    expect(isOriginAllowed("subdomain.localhost", ["**.localhost"])).toBe(true);
    expect(isOriginAllowed("a.b.localhost", ["**.localhost"])).toBe(true);
    expect(isOriginAllowed("localhost", ["**.localhost"])).toBe(false);
    expect(isOriginAllowed("localhost", ["localhost"])).toBe(true);
  });

  it("does NOT match attacker-controlled suffix domains", () => {
    // This was the original vulnerability: endsWith(".example.com") matching
    // evil.example.com.attacker.com
    expect(isOriginAllowed("evil.example.com.attacker.com", ["*.example.com"])).toBe(false);
    expect(isOriginAllowed("evil.example.com.attacker.com", ["**.example.com"])).toBe(false);
  });
});

// Reference: vercel/next.js#92012 — useOffline() hook
describe("next/offline shim", () => {
  it("exports useOffline", async () => {
    const offline = await import("../packages/vinext/src/shims/offline.js");
    expect(offline.useOffline).toBeDefined();
    expect(typeof offline.useOffline).toBe("function");
  });

  it("useOffline returns false (no-op stub)", async () => {
    const offline = await import("../packages/vinext/src/shims/offline.js");
    expect(offline.useOffline()).toBe(false);
  });
});

// Regression for cloudflare/vinext#1353. The Pages Router navigation shim
// referenced `window` unconditionally inside `performNavigation()`, so any
// component calling `router.push()` during SSR or prerendering
// (VINEXT_PRERENDER=1) crashed the render pipeline with
// `ReferenceError: window is not defined`. Next.js's `ServerRouter`
// (packages/next/src/server/render.tsx) instead throws a documented
// "No router instance found" error from every navigation method. We mirror
// that here so the failure mode is a recoverable render error rather than
// a global ReferenceError.
//
// Ported from Next.js: test/e2e/with-router/pages/router-method-ssr.js +
// test/e2e/with-router/index.test.ts (the "router-method-ssr" SSR case).
describe("next/router SSR guard (issue #1353)", () => {
  let previousWindow: unknown;

  beforeEach(() => {
    previousWindow = (globalThis as any).window;
    delete (globalThis as any).window;
    vi.resetModules();
  });

  afterEach(() => {
    if (previousWindow === undefined) {
      delete (globalThis as any).window;
    } else {
      (globalThis as any).window = previousWindow;
    }
    vi.resetModules();
  });

  it("Router.push throws the documented 'no router instance' error during SSR (no ReferenceError)", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.push("/a")).toThrow(/No router instance found/);
  });

  it("Router.replace throws the documented 'no router instance' error during SSR", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.replace("/a")).toThrow(/No router instance found/);
  });

  it("Router.back throws during SSR instead of touching window.history", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.back()).toThrow(/No router instance found/);
  });

  it("Router.reload throws during SSR instead of touching window.location", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.reload()).toThrow(/No router instance found/);
  });

  it("Router.prefetch throws during SSR instead of touching document", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.prefetch("/a")).toThrow(/No router instance found/);
  });

  it("Router.beforePopState throws during SSR", async () => {
    const Router = (await import("../packages/vinext/src/shims/router.js")).default;
    expect(() => Router.beforePopState(() => true)).toThrow(/No router instance found/);
  });

  // Render a `withRouter`-wrapped page that calls `router.push()` during SSR
  // (the exact pattern from Next.js's `router-method-ssr.js` fixture).
  // Before the fix this crashed with `ReferenceError: window is not defined`
  // from `performNavigation`. After the fix the render throws the documented
  // "No router instance found" error, which is what Next.js does too.
  it("withRouter component that calls router.push() during SSR throws a render error, not a ReferenceError", async () => {
    const { withRouter, wrapWithRouterContext } =
      await import("../packages/vinext/src/shims/router.js");
    const React = await import("react");
    const { renderToStaticMarkup } = await import("react-dom/server");

    const RouterMethodSSR = ({ router }: { router: NextRouter }) => {
      // Mirrors Next.js's `router-method-ssr.js` fixture, which calls
      // `router.push('/a')` synchronously during SSR. The push must throw
      // synchronously for the test to observe the error; we ignore the
      // returned Promise type via `void` to satisfy the lint rule (the
      // Promise is never produced because the sync throw happens first).
      void router.push("/a");
      return React.createElement("p", null, "Navigating...");
    };
    const Wrapped = withRouter(RouterMethodSSR as React.ComponentType<{ router: NextRouter }>);

    let caught: unknown = null;
    try {
      renderToStaticMarkup(wrapWithRouterContext(React.createElement(Wrapped)));
    } catch (err) {
      caught = err;
    }

    expect(caught).toBeInstanceOf(Error);
    expect((caught as Error).message).toMatch(/No router instance found/);
    expect((caught as Error).message).not.toMatch(/window is not defined/);
    expect(caught as Error).not.toBeInstanceOf(ReferenceError);
  });
});

// ---------------------------------------------------------------------------
// i18n `locale: false` on rewrites/redirects (issue #1336, item 1).
// Ported from Next.js: test/e2e/i18n-ignore-rewrite-source-locale/rewrites.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-ignore-rewrite-source-locale/rewrites.test.ts
//
// When a rewrite/redirect source carries `locale: false`, Next.js leaves the
// source pattern untouched (no internal locale prefix is prepended). So a
// source like `/:locale/rewrite-files/:path*` matches the raw locale-prefixed
// path with `:locale` capturing the actual locale segment.
//
// Without `locale: false`, Next.js prepends `/:nextInternalLocale(en|sv|nl)?`
// to the source so that the rule matches both prefixed and unprefixed paths
// (and the user-supplied source pattern matches against the de-localised
// remainder).

describe("locale: false on rewrites/redirects (issue #1336)", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  it("matches multi-param source with trailing catch-all (regression)", async () => {
    // Even before the locale: false logic, the underlying matcher must handle
    // a source like `/:locale/rewrite-files/:path*` where there is another
    // named param before the trailing catch-all. Previously this fell into
    // the simple catch-all branch which only handled the trailing param.
    const { matchConfigPattern } = await import("../packages/vinext/src/config/config-matchers.js");
    const pattern = "/:locale/rewrite-files/:path*";
    const result = matchConfigPattern("/en/rewrite-files/file.txt", pattern);
    expect(result).toEqual({ locale: "en", path: "file.txt" });

    const nested = matchConfigPattern("/sv/rewrite-files/sub/dir/file.txt", pattern);
    expect(nested).toEqual({ locale: "sv", path: "sub/dir/file.txt" });
  });

  it("processRoutes: with locale:false leaves source untouched", async () => {
    const { applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = [
      {
        source: "/:locale/rewrite-files/:path*",
        destination: "/:path*",
        locale: false as const,
      },
    ];
    const out = applyLocaleToRoutes(rules, i18n, "rewrite");
    expect(out).toHaveLength(1);
    // locale:false → no internal locale prefix is injected.
    expect(out[0].source).toBe("/:locale/rewrite-files/:path*");
    expect(out[0].destination).toBe("/:path*");
  });

  it("processRoutes: without locale:false prepends the locale alternation", async () => {
    const { applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = [{ source: "/old", destination: "/new", permanent: true as const }];
    const out = applyLocaleToRoutes(rules, i18n, "redirect");
    // Next.js produces two source-variants per default-locale (and per domain
    // default locale for redirects): one default-prefixed and one with the
    // internal locale capture group. We only require that one of the produced
    // rules captures the locale alternation so the original source matches
    // localised URLs.
    const hasInternalLocaleVariant = out.some((r) => r.source.startsWith("/:nextInternalLocale("));
    expect(hasInternalLocaleVariant).toBe(true);
  });

  it("matchRewrite honours locale:false against locale-prefixed paths", async () => {
    const { matchRewrite, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes(
      [
        {
          source: "/:locale/rewrite-files/:path*",
          destination: "/:path*",
          locale: false as const,
        },
        {
          source: "/:locale/rewrite-api/:path*",
          destination: "/api/:path*",
          locale: false as const,
        },
      ],
      i18n,
      "rewrite",
    );

    expect(matchRewrite("/en/rewrite-files/file.txt", rules, emptyCtx)).toBe("/file.txt");
    expect(matchRewrite("/sv/rewrite-files/file.txt", rules, emptyCtx)).toBe("/file.txt");
    expect(matchRewrite("/nl/rewrite-files/file.txt", rules, emptyCtx)).toBe("/file.txt");
    expect(matchRewrite("/en/rewrite-api/hello", rules, emptyCtx)).toBe("/api/hello");
  });

  it("matchRewrite without locale:false matches all locale-prefixed forms", async () => {
    // When the user writes a plain source like `/old` and i18n is configured,
    // vinext emits a `/:nextInternalLocale(en|sv|nl)` variant so the rule
    // matches any locale-prefixed URL while retaining the original `/old`
    // source for default-locale requests that arrive without a prefix.
    const { matchRewrite, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes([{ source: "/old", destination: "/new" }], i18n, "rewrite");

    // Locale-prefixed paths match and the destination is also locale-prefixed.
    expect(matchRewrite("/en/old", rules, emptyCtx)).toBe("/en/new");
    expect(matchRewrite("/sv/old", rules, emptyCtx)).toBe("/sv/new");
    expect(matchRewrite("/nl/old", rules, emptyCtx)).toBe("/nl/new");
    // Unprefixed default-locale paths still match the original source.
    expect(matchRewrite("/old", rules, emptyCtx)).toBe("/new");
  });

  it("matchRedirect uses the locale-static fast path for nextInternalLocale variants", async () => {
    // The `_LOCALE_STATIC_RE` detection regex must accept both
    //   `/:locale(en|fr)?/foo` (optional, user-written)
    // and
    //   `/:nextInternalLocale(en|fr)/foo` (mandatory, emitted by
    //                                       applyLocaleToRoutes)
    // — otherwise the locale-capture variants emitted by this PR would fall
    // into the linear scan, regressing the O(1) lookup performance of i18n
    // apps with many redirects.
    const { matchRedirect, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes(
      [{ source: "/security", destination: "/security-dest", permanent: false as const }],
      i18n,
      "redirect",
    );
    // Locale-capture (mandatory) variant must hit the fast path AND substitute
    // the captured locale into the destination.
    expect(matchRedirect("/sv/security", rules, emptyCtx)?.destination).toBe("/sv/security-dest");
    // Default-locale-literal variant strips the prefix.
    expect(matchRedirect("/en/security", rules, emptyCtx)?.destination).toBe("/security-dest");
    // No-locale-prefix path must not be matched by the mandatory variant —
    // the original (unprefixed) source matches and produces the unprefixed
    // destination.
    expect(matchRedirect("/security", rules, emptyCtx)?.destination).toBe("/security-dest");
  });

  it("applyLocaleToRoutes preserves trailing slash when trailingSlash is true", async () => {
    const { applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "fr"], defaultLocale: "en" };

    // trailingSlash: false (default) — root source collapses to "".
    const noTrailing = applyLocaleToRoutes(
      [{ source: "/", destination: "/home" }],
      i18n,
      "rewrite",
      { trailingSlash: false },
    );
    const internalNoTrailing = noTrailing.find((r) => r.source.startsWith("/:nextInternalLocale("));
    expect(internalNoTrailing?.source).toBe("/:nextInternalLocale(en|fr)");

    // trailingSlash: true — root source is preserved.
    const withTrailing = applyLocaleToRoutes(
      [{ source: "/", destination: "/home" }],
      i18n,
      "rewrite",
      { trailingSlash: true },
    );
    const internalWithTrailing = withTrailing.find((r) =>
      r.source.startsWith("/:nextInternalLocale("),
    );
    expect(internalWithTrailing?.source).toBe("/:nextInternalLocale(en|fr)/");
  });

  it("matchRedirect emits both default-locale-literal and locale-capture variants", async () => {
    // For redirects, Next.js emits two source variants per rule:
    //   1. `/${defaultLocale}/old` with destination `/new` (no locale prefix
    //      in destination — the default-locale variant strips the prefix).
    //   2. `/:nextInternalLocale(en|sv|nl)/old` with destination
    //      `/:nextInternalLocale/new` so non-default locales keep their prefix.
    // This matches packages/next/src/lib/load-custom-routes.ts processRoutes.
    const { matchRedirect, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes(
      [{ source: "/old", destination: "/new", permanent: false as const }],
      i18n,
      "redirect",
    );
    // `/en/old` matches the default-locale-literal variant first → strips the prefix.
    expect(matchRedirect("/en/old", rules, emptyCtx)?.destination).toBe("/new");
    // Non-default locales match the `:nextInternalLocale` variant.
    expect(matchRedirect("/sv/old", rules, emptyCtx)?.destination).toBe("/sv/new");
    expect(matchRedirect("/nl/old", rules, emptyCtx)?.destination).toBe("/nl/new");
  });
});

// ---------------------------------------------------------------------------
// Default-locale path normalisation before route matching (issue #1336, item 4).
//
// Next.js normalises every request that arrives without a locale prefix by
// prepending the (domain-aware) defaultLocale before any redirect, rewrite,
// header, or filesystem route match runs. The issue body claims the desired
// behaviour is a 308 from `/en/page` → `/page`, but Next.js does NOT redirect:
// it serves the same content under both URLs and lets the application normalise
// the path internally.
//
// Ported from Next.js:
//   - test/e2e/i18n-default-locale-redirect/i18n-default-locale-redirect.test.ts
//     https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-default-locale-redirect/i18n-default-locale-redirect.test.ts
//   - test/e2e/i18n-ignore-redirect-source-locale/redirects.test.ts
//     https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-ignore-redirect-source-locale/redirects.test.ts
//   - packages/next/src/server/lib/router-utils/resolve-routes.ts (lines ~250-263)
//     https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
//
describe("default-locale path normalisation (issue #1336, item 4)", () => {
  const emptyCtx = {
    headers: new Headers(),
    cookies: {},
    query: new URLSearchParams(),
    host: "localhost",
  };

  it("normalizeDefaultLocalePathname prepends the default locale when path has none", async () => {
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    expect(normalizeDefaultLocalePathname("/about", i18n)).toBe("/en/about");
    expect(normalizeDefaultLocalePathname("/", i18n)).toBe("/en");
    expect(normalizeDefaultLocalePathname("/api/health", i18n)).toBe("/en/api/health");
  });

  it("normalizeDefaultLocalePathname leaves locale-prefixed paths untouched", async () => {
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    expect(normalizeDefaultLocalePathname("/en/about", i18n)).toBe("/en/about");
    expect(normalizeDefaultLocalePathname("/sv/about", i18n)).toBe("/sv/about");
    expect(normalizeDefaultLocalePathname("/nl", i18n)).toBe("/nl");
  });

  it("normalizeDefaultLocalePathname is a no-op when i18n is not configured", async () => {
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    expect(normalizeDefaultLocalePathname("/about", null)).toBe("/about");
    expect(normalizeDefaultLocalePathname("/", undefined)).toBe("/");
    expect(normalizeDefaultLocalePathname("/about", null, { hostname: "example.com" })).toBe(
      "/about",
    );
  });

  it("normalizeDefaultLocalePathname skips internal Next.js / vinext paths", async () => {
    // Next.js's resolve-routes.ts explicitly excludes /_next/* from
    // default-locale normalisation. Build assets and prerender manifests must
    // not get a locale prefix.
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    expect(normalizeDefaultLocalePathname("/_next/static/chunks/main.js", i18n)).toBe(
      "/_next/static/chunks/main.js",
    );
    expect(normalizeDefaultLocalePathname("/_next/data/build-id/index.json", i18n)).toBe(
      "/_next/data/build-id/index.json",
    );
    // vinext-internal endpoints (prerender, image optimisation) likewise.
    expect(normalizeDefaultLocalePathname("/__vinext/prerender/pages-static-paths", i18n)).toBe(
      "/__vinext/prerender/pages-static-paths",
    );
  });

  it("normalizeDefaultLocalePathname prefers the domain-mapped default locale", async () => {
    // Next.js's resolve-routes.ts picks `domainLocale.defaultLocale` over the
    // global default when the inbound host matches an i18n.domains entry.
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = {
      locales: ["en", "sv", "nl"],
      defaultLocale: "en",
      domains: [{ domain: "example.nl", defaultLocale: "nl" }],
    };
    expect(normalizeDefaultLocalePathname("/about", i18n, { hostname: "example.nl" })).toBe(
      "/nl/about",
    );
    // Unknown host falls back to the global default.
    expect(normalizeDefaultLocalePathname("/about", i18n, { hostname: "example.com" })).toBe(
      "/en/about",
    );
    // Domain-mapped path that already carries a locale stays untouched.
    expect(normalizeDefaultLocalePathname("/sv/about", i18n, { hostname: "example.nl" })).toBe(
      "/sv/about",
    );
  });

  it("matchRedirect honours :locale capture on locale:false rules after normalisation", async () => {
    // Ported from Next.js: test/e2e/i18n-ignore-redirect-source-locale/redirects.test.ts
    // A user-supplied source like `/:locale/to-sv` with `locale: false` is
    // matched against the POST-normalisation pathname, so requests that
    // arrive without a locale prefix get the default locale spliced in and
    // the `:locale` segment captures it.
    const { matchRedirect, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes(
      [
        {
          source: "/:locale/to-sv",
          destination: "/sv/newpage",
          permanent: false as const,
          locale: false as const,
        },
        {
          source: "/:locale/to-en",
          destination: "/en/newpage",
          permanent: false as const,
          locale: false as const,
        },
        {
          source: "/:locale/to-slash",
          destination: "/newpage",
          permanent: false as const,
          locale: false as const,
        },
        {
          source: "/:locale/to-same",
          destination: "/:locale/newpage",
          permanent: false as const,
          locale: false as const,
        },
      ],
      i18n,
      "redirect",
    );

    // Unprefixed paths get normalised to the default locale first.
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/to-sv", i18n), rules, emptyCtx)?.destination,
    ).toBe("/sv/newpage");
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/to-en", i18n), rules, emptyCtx)?.destination,
    ).toBe("/en/newpage");
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/to-slash", i18n), rules, emptyCtx)
        ?.destination,
    ).toBe("/newpage");
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/to-same", i18n), rules, emptyCtx)?.destination,
    ).toBe("/en/newpage");

    // Locale-prefixed paths are passed through unchanged.
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/sv/to-sv", i18n), rules, emptyCtx)
        ?.destination,
    ).toBe("/sv/newpage");
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/nl/to-same", i18n), rules, emptyCtx)
        ?.destination,
    ).toBe("/nl/newpage");
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/en/to-en", i18n), rules, emptyCtx)
        ?.destination,
    ).toBe("/en/newpage");
  });

  it("matchRedirect on default-locale paths matches the literal default-locale variant", async () => {
    // For redirects WITHOUT `locale: false`, applyLocaleToRoutes already emits
    // a literal `/${defaultLocale}/...` variant. Normalisation routes unprefixed
    // requests through that variant.
    const { matchRedirect, applyLocaleToRoutes } =
      await import("../packages/vinext/src/config/config-matchers.js");
    const { normalizeDefaultLocalePathname } =
      await import("../packages/vinext/src/server/pages-i18n.js");
    const i18n = { locales: ["en", "sv", "nl"], defaultLocale: "en" };
    const rules = applyLocaleToRoutes(
      [{ source: "/old", destination: "/new", permanent: false as const }],
      i18n,
      "redirect",
    );

    // `/old` (unprefixed) → normalised to `/en/old` → matches default-locale variant.
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/old", i18n), rules, emptyCtx)?.destination,
    ).toBe("/new");
    // `/sv/old` already prefixed → matches the locale-capture variant.
    expect(
      matchRedirect(normalizeDefaultLocalePathname("/sv/old", i18n), rules, emptyCtx)?.destination,
    ).toBe("/sv/new");
  });
});

// ---------------------------------------------------------------------------
// Pages Router runtime state sharing
//
// Vite can evaluate `next/router` in both the client entry and in page chunks.
// State that must be consistent across those instances (events, history keys,
// abort/cancel state, and router readiness) must live on a window-scoped
// runtime object rather than in module-local variables.
// ---------------------------------------------------------------------------
describe("Pages Router runtime state sharing", () => {
  it("shares RouterContext across duplicated next/router module instances", async () => {
    vi.resetModules();
    const contextA = await import("../packages/vinext/src/shims/internal/router-context.js");

    vi.resetModules();
    const contextB = await import("../packages/vinext/src/shims/internal/router-context.js");

    expect(contextB.RouterContext).toBe(contextA.RouterContext);
  });

  it("shares router readiness across duplicated next/router module instances", async () => {
    const previousWindow = (globalThis as any).window;
    const win: any = {
      location: {
        pathname: "/1",
        search: "",
        hash: "",
        href: "http://localhost/1",
        hostname: "localhost",
      },
      history: { state: null, pushState() {}, replaceState() {} },
      addEventListener() {},
      dispatchEvent() {},
      scrollTo() {},
      __NEXT_DATA__: {
        page: "/[id]",
        query: {},
        autoExport: true,
        isFallback: false,
        props: { pageProps: {} },
      },
      __VINEXT_PAGE_LOADERS__: {},
    };
    (globalThis as any).window = win;

    try {
      vi.resetModules();
      const React = await import("react");
      const { renderToStaticMarkup } = await import("react-dom/server");

      // First `next/router` instance: the generated Pages client entry.
      const routerModuleA = await import("../packages/vinext/src/shims/router.js");
      const RouterA = routerModuleA.default;
      const markReadyA = routerModuleA._markPagesRouterReady;

      // Second `next/router` instance: a later page chunk that re-evaluates the
      // module and overwrites window.next.router.
      vi.resetModules();
      const routerModuleB = await import("../packages/vinext/src/shims/router.js");
      const RouterB = routerModuleB.default;
      const useRouterB = routerModuleB.useRouter;

      // Both singletons start with the same deferred-ready state.
      expect(win.next.router).toBe(RouterB);
      expect(RouterA.isReady).toBe(false);
      expect(RouterB.isReady).toBe(false);

      // After hydration, the readiness bit still stays false until the provider
      // publishes it.
      win.__NEXT_HYDRATED = true;
      expect(RouterA.isReady).toBe(false);
      expect(RouterB.isReady).toBe(false);

      // Simulate the provider mounting and flipping the shared ready bit.
      expect(markReadyA()).toBe(true);
      expect(markReadyA()).toBe(false);

      // The readiness transition is visible from every module instance and from
      // the global singleton.
      expect(RouterA.isReady).toBe(true);
      expect(RouterB.isReady).toBe(true);
      expect(win.next.router.isReady).toBe(true);

      // useRouter() backed by the second instance also converges to ready.
      function ProbeB() {
        const router = useRouterB();
        return React.createElement("span", null, router.isReady ? "ready" : "not-ready");
      }
      const htmlB = renderToStaticMarkup(
        routerModuleB.wrapWithRouterContext(React.createElement(ProbeB)),
      );
      expect(htmlB).toBe("<span>ready</span>");

      // And a provider-mounted hook from the first instance sees it too.
      function ProbeA() {
        const router = routerModuleA.useRouter();
        return React.createElement("span", null, router.isReady ? "ready" : "not-ready");
      }
      const htmlA = renderToStaticMarkup(
        routerModuleA.wrapWithRouterContext(React.createElement(ProbeA)),
      );
      expect(htmlA).toBe("<span>ready</span>");
    } finally {
      vi.resetModules();
      if (previousWindow === undefined) {
        delete (globalThis as any).window;
      } else {
        (globalThis as any).window = previousWindow;
      }
    }
  });
});
