import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { describe, expect, it, vi } from "vite-plus/test";
import {
  computeRscCacheBustingSearchParam,
  createRscRequestHeaders,
  createRscRequestUrl,
  VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM,
  VINEXT_RSC_VARY_HEADER,
} from "../packages/vinext/src/server/app-rsc-cache-busting.js";
import { createAppRscHandler } from "../packages/vinext/src/server/app-rsc-handler.js";
import { createArtifactCompatibilityEnvelope } from "../packages/vinext/src/server/artifact-compatibility.js";
import {
  createClientReuseManifest,
  createClientReusePayloadHash,
} from "../packages/vinext/src/server/client-reuse-manifest.js";
import { VINEXT_CLIENT_REUSE_MANIFEST_HEADER } from "../packages/vinext/src/server/headers.js";
import { makeThenableParams } from "../packages/vinext/src/shims/thenable-params.js";

type TestRoute = {
  isDynamic: boolean;
  page?: { default?: unknown } | null;
  pattern: string;
  rootParamNames?: readonly string[];
  routeHandler?: { GET?: () => Response; runtime?: string } | null;
  routeSegments: readonly string[];
};

type HandlerOptions = Parameters<typeof createAppRscHandler<TestRoute>>[0];
type DispatchMatchedRouteHandler = HandlerOptions["dispatchMatchedRouteHandler"];

function createPageRoute(overrides: Partial<TestRoute> = {}): TestRoute {
  return {
    isDynamic: false,
    page: { default() {} },
    pattern: "/about",
    routeSegments: ["about"],
    ...overrides,
  };
}

function createHandler(overrides: Partial<HandlerOptions> = {}) {
  const route = createPageRoute();

  return createAppRscHandler<TestRoute>({
    basePath: "/docs",
    buildId: overrides.buildId ?? "build-id",
    clearRequestContext: overrides.clearRequestContext ?? (() => {}),
    configHeaders: overrides.configHeaders ?? [
      {
        source: "/about",
        headers: [{ key: "x-test-header", value: "applied" }],
      },
    ],
    configRedirects: overrides.configRedirects ?? [],
    configRewrites: overrides.configRewrites ?? {
      afterFiles: [],
      beforeFiles: [],
      fallback: [],
    },
    draftModeSecret: overrides.draftModeSecret ?? "test-draft-secret",
    dispatchMatchedPage:
      overrides.dispatchMatchedPage ??
      (async () => new Response("page", { status: 200, headers: { "x-from-dispatch": "page" } })),
    dispatchMatchedRouteHandler:
      overrides.dispatchMatchedRouteHandler ?? (async () => new Response("route", { status: 200 })),
    ensureInstrumentation: overrides.ensureInstrumentation,
    handleProgressiveActionRequest: overrides.handleProgressiveActionRequest ?? (async () => null),
    handleServerActionRequest: overrides.handleServerActionRequest ?? (async () => null),
    i18nConfig: overrides.i18nConfig ?? null,
    imageConfig: overrides.imageConfig,
    isDev: overrides.isDev ?? true,
    isMiddlewareProxy: overrides.isMiddlewareProxy ?? false,
    makeThenableParams,
    matchRoute:
      overrides.matchRoute ??
      ((pathname: string) =>
        pathname === "/about"
          ? {
              params: {},
              route,
            }
          : null),
    metadataRoutes: overrides.metadataRoutes ?? [],
    middlewareModule: overrides.middlewareModule ?? null,
    publicFiles: overrides.publicFiles ?? new Set<string>(),
    registerCacheAdapters: () => {},
    renderNotFound: overrides.renderNotFound ?? (async () => null),
    renderPagesFallback: overrides.renderPagesFallback,
    rootParamNamesByPattern: overrides.rootParamNamesByPattern,
    setNavigationContext: overrides.setNavigationContext ?? (() => {}),
    staticParamsMap: overrides.staticParamsMap ?? {},
    trailingSlash: overrides.trailingSlash ?? false,
    validateDevRequestOrigin: overrides.validateDevRequestOrigin ?? (() => null),
  });
}

function prerenderRouteParamsHeader(payload: unknown): string {
  return encodeURIComponent(JSON.stringify(payload));
}

describe("createAppRscHandler", () => {
  it.each([
    "url=%2Fimg.jpg&w=640junk&q=75",
    "url=%2Fimg.jpg&w=640&q=75&extra=1",
    "url=%2Fimg.jpg&w=640&w=640&q=75",
  ])("rejects malformed pure App Router dev image parameters: %s", async (query) => {
    const handler = createHandler();
    const response = await handler(
      new Request(`https://example.test/docs/_next/image?${query}`),
      null,
    );
    expect(response.status).toBe(400);
  });

  it("uses configured image widths and qualities in pure App Router dev", async () => {
    const handler = createHandler({
      imageConfig: { deviceSizes: [320], imageSizes: [16], qualities: [60] },
    });
    const allowed = await handler(
      new Request("https://example.test/docs/_next/image?url=%2Fimg.jpg&w=320&q=60"),
      null,
    );
    expect(allowed.status).toBe(302);
    expect(allowed.headers.get("location")).toBe("https://example.test/img.jpg");

    const defaultOnly = await handler(
      new Request("https://example.test/docs/_next/image?url=%2Fimg.jpg&w=640&q=75"),
      null,
    );
    expect(defaultOnly.status).toBe(400);
  });

  it("allows independent Next.js blur width and quality exceptions in pure App Router dev", async () => {
    // The blur quality exception (q=70) is only observable when `qualities` is
    // configured — with an unset allowlist any quality 1-100 is permitted, so
    // pin it to [75] to exercise the dev-only exception itself.
    const handler = createHandler({ imageConfig: { qualities: [75] } });
    for (const query of ["url=%2Fimg.jpg&w=8&q=75", "url=%2Fimg.jpg&w=640&q=70"]) {
      const response = await handler(
        new Request(`https://example.test/docs/_next/image?${query}`),
        null,
      );
      expect(response.status).toBe(302);
    }
  });

  it("rejects Next.js blur width and quality exceptions in production", async () => {
    const handler = createHandler({ isDev: false, imageConfig: { qualities: [75] } });
    for (const query of ["url=%2Fimg.jpg&w=8&q=75", "url=%2Fimg.jpg&w=640&q=70"]) {
      const response = await handler(
        new Request(`https://example.test/docs/_next/image?${query}`),
        null,
      );
      expect(response.status).toBe(400);
    }
  });

  it("allows any quality 1-100 in production when images.qualities is unset", async () => {
    // Matches Next.js: an unset `qualities` is not restricted to a single value,
    // so q=70 (and any 1-100) is a normal quality even in production.
    const handler = createHandler({ isDev: false });
    const response = await handler(
      new Request("https://example.test/docs/_next/image?url=%2Fimg.jpg&w=640&q=70"),
      null,
    );
    expect(response.status).toBe(302);
  });

  it("wraps dispatch responses with request-scoped finalization", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({ dispatchMatchedPage });

    const response = await handler(new Request("https://example.test/docs/about"), null);

    expect(dispatchMatchedPage).toHaveBeenCalledTimes(1);
    expect(response.status).toBe(200);
    expect(response.headers.get("x-test-header")).toBe("applied");
    expect(response.headers.get("vary")).toBe(VINEXT_RSC_VARY_HEADER);
  });

  it("does not trailing-slash redirect RSC requests built from already-canonical trailingSlash paths", async () => {
    const headers = createRscRequestHeaders();
    const requestPath = await createRscRequestUrl("/about/", headers);
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const route = createPageRoute({ pattern: "/about/", routeSegments: ["about"] });
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      matchRoute(pathname: string) {
        return pathname === "/about/" ? { params: {}, route } : null;
      },
      trailingSlash: true,
    });

    const response = await handler(
      new Request(`https://example.test/docs${requestPath}`, { headers }),
      null,
    );

    expect(response.status).toBe(200);
    expect(response.headers.has("location")).toBe(false);
    expect(dispatchMatchedPage).toHaveBeenCalledTimes(1);
  });

  it("marks progressive action page renders even when decoded form state is null", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      async handleProgressiveActionRequest() {
        return {
          kind: "form-state",
          formState: null,
          pendingCookies: [],
          draftCookie: null,
          revalidationKind: 0,
        };
      },
    });

    const response = await handler(
      new Request("https://example.test/docs/about", {
        method: "POST",
        headers: { "content-type": "multipart/form-data; boundary=vinext" },
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({
        formState: null,
        isProgressiveActionRender: true,
      }),
    );
  });

  // Regression for issue #1483 — `cookies().set(...)` / `cookies().delete(...)`
  // and `draftMode().enable()` invoked inside a no-JS server action must flow
  // through to the page rerender response. Before the fix, those Set-Cookie
  // headers (plus the x-action-revalidated marker) were dropped on the floor
  // because the handler returned the dispatcher's response untouched.
  it("propagates cookies, draft cookie, and revalidation marker from a progressive action to the page response (#1483)", async () => {
    const dispatchMatchedPage = vi.fn(
      async () =>
        new Response("page", {
          status: 200,
          headers: { "content-type": "text/html; charset=utf-8" },
        }),
    );
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      async handleProgressiveActionRequest() {
        return {
          kind: "form-state",
          formState: null,
          pendingCookies: ["session=abc; Path=/", "theme=dark; Path=/"],
          draftCookie: "__prerender_bypass=secret; Path=/",
          revalidationKind: 1,
        };
      },
    });

    const response = await handler(
      new Request("https://example.test/docs/about", {
        method: "POST",
        headers: { "content-type": "multipart/form-data; boundary=vinext" },
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(response.headers.getSetCookie()).toEqual([
      "session=abc; Path=/",
      "theme=dark; Path=/",
      "__prerender_bypass=secret; Path=/",
    ]);
    expect(response.headers.get("x-action-revalidated")).toBe("1");
  });

  // When an action did not mutate cookies and did not request a revalidation,
  // the page response should NOT carry an x-action-revalidated marker — that
  // header tells the client router cache to invalidate, and emitting it
  // spuriously would force unnecessary refetches.
  it("does not add x-action-revalidated when a progressive action made no mutations (#1483)", async () => {
    const dispatchMatchedPage = vi.fn(
      async () =>
        new Response("page", {
          status: 200,
          headers: { "content-type": "text/html; charset=utf-8" },
        }),
    );
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      async handleProgressiveActionRequest() {
        return {
          kind: "form-state",
          formState: null,
          pendingCookies: [],
          draftCookie: null,
          revalidationKind: 0,
        };
      },
    });

    const response = await handler(
      new Request("https://example.test/docs/about", {
        method: "POST",
        headers: { "content-type": "multipart/form-data; boundary=vinext" },
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(response.headers.getSetCookie()).toEqual([]);
    expect(response.headers.has("x-action-revalidated")).toBe(false);
  });

  it("uses encoded prerender route params for rendering while retaining decoded params for static validation", async () => {
    const previousPrerender = process.env.VINEXT_PRERENDER;
    process.env.VINEXT_PRERENDER = "1";
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const prerenderRoute = createPageRoute({
      isDynamic: true,
      pattern: "/prerender-encoding/:id",
      routeSegments: ["prerender-encoding", "[id]"],
    });
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      matchRoute(pathname: string) {
        return pathname === "/prerender-encoding/sticks & stones"
          ? {
              params: { id: "sticks & stones" },
              route: prerenderRoute,
            }
          : null;
      },
    });

    try {
      const response = await handler(
        new Request("https://example.test/docs/prerender-encoding/sticks%20%26%20stones", {
          headers: {
            "x-vinext-prerender-secret": "test-secret",
            "x-vinext-prerender-route-params": prerenderRouteParamsHeader({
              routePattern: "/prerender-encoding/:id",
              params: { id: "sticks%20%26%20stones" },
            }),
          },
        }),
        null,
      );

      expect(response.status).toBe(200);
      expect(dispatchMatchedPage).toHaveBeenCalledWith(
        expect.objectContaining({
          params: { id: "sticks%20%26%20stones" },
          staticParamsValidationParams: { id: "sticks & stones" },
        }),
      );
    } finally {
      if (previousPrerender === undefined) {
        delete process.env.VINEXT_PRERENDER;
      } else {
        process.env.VINEXT_PRERENDER = previousPrerender;
      }
    }
  });

  it("ignores encoded prerender route params from a different rewritten route pattern", async () => {
    const previousPrerender = process.env.VINEXT_PRERENDER;
    process.env.VINEXT_PRERENDER = "1";
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const productRoute = createPageRoute({
      isDynamic: true,
      pattern: "/product/:id",
      routeSegments: ["product", "[id]"],
    });
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/source/:slug", destination: "/product/:slug" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute(pathname: string) {
        return pathname === "/product/sticks & stones"
          ? {
              params: { id: "sticks & stones" },
              route: productRoute,
            }
          : null;
      },
    });

    try {
      const response = await handler(
        new Request("https://example.test/docs/source/sticks%20%26%20stones", {
          headers: {
            "x-vinext-prerender-secret": "test-secret",
            "x-vinext-prerender-route-params": prerenderRouteParamsHeader({
              routePattern: "/source/:slug",
              params: { slug: "sticks%20%26%20stones" },
            }),
          },
        }),
        null,
      );

      expect(response.status).toBe(200);
      expect(dispatchMatchedPage).toHaveBeenCalledWith(
        expect.objectContaining({
          cleanPathname: "/product/sticks & stones",
          params: { id: "sticks & stones" },
          staticParamsValidationParams: undefined,
        }),
      );
    } finally {
      if (previousPrerender === undefined) {
        delete process.env.VINEXT_PRERENDER;
      } else {
        process.env.VINEXT_PRERENDER = previousPrerender;
      }
    }
  });

  it("ignores encoded prerender route params when a same-pattern rewrite changes the matched params", async () => {
    const previousPrerender = process.env.VINEXT_PRERENDER;
    process.env.VINEXT_PRERENDER = "1";
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const productRoute = createPageRoute({
      isDynamic: true,
      pattern: "/product/:id",
      routeSegments: ["product", "[id]"],
    });
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/product/:id", destination: "/product/sticks-and-stones" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute(pathname: string) {
        return pathname === "/product/sticks-and-stones"
          ? {
              params: { id: "sticks-and-stones" },
              route: productRoute,
            }
          : null;
      },
    });

    try {
      const response = await handler(
        new Request("https://example.test/docs/product/sticks%20%26%20stones", {
          headers: {
            "x-vinext-prerender-secret": "test-secret",
            "x-vinext-prerender-route-params": prerenderRouteParamsHeader({
              routePattern: "/product/:id",
              params: { id: "sticks%20%26%20stones" },
            }),
          },
        }),
        null,
      );

      expect(response.status).toBe(200);
      expect(dispatchMatchedPage).toHaveBeenCalledWith(
        expect.objectContaining({
          cleanPathname: "/product/sticks-and-stones",
          params: { id: "sticks-and-stones" },
          staticParamsValidationParams: undefined,
        }),
      );
    } finally {
      if (previousPrerender === undefined) {
        delete process.env.VINEXT_PRERENDER;
      } else {
        process.env.VINEXT_PRERENDER = previousPrerender;
      }
    }
  });

  it("ignores forged prerender route params outside trusted prerender requests", async () => {
    const previousPrerender = process.env.VINEXT_PRERENDER;
    delete process.env.VINEXT_PRERENDER;
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const prerenderRoute = createPageRoute({
      isDynamic: true,
      pattern: "/prerender-encoding/:id",
      routeSegments: ["prerender-encoding", "[id]"],
    });
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      matchRoute(pathname: string) {
        return pathname === "/prerender-encoding/sticks & stones"
          ? {
              params: { id: "sticks & stones" },
              route: prerenderRoute,
            }
          : null;
      },
    });

    try {
      const response = await handler(
        new Request("https://example.test/docs/prerender-encoding/sticks%20%26%20stones", {
          headers: {
            "x-vinext-prerender-secret": "test-secret",
            "x-vinext-prerender-route-params": prerenderRouteParamsHeader({
              routePattern: "/prerender-encoding/:id",
              params: { id: "forged" },
            }),
          },
        }),
        null,
      );

      expect(response.status).toBe(200);
      expect(dispatchMatchedPage).toHaveBeenCalledWith(
        expect.objectContaining({
          params: { id: "sticks & stones" },
        }),
      );
      expect(dispatchMatchedPage).not.toHaveBeenCalledWith(
        expect.objectContaining({
          staticParamsValidationParams: expect.anything(),
        }),
      );
    } finally {
      if (previousPrerender === undefined) {
        delete process.env.VINEXT_PRERENDER;
      } else {
        process.env.VINEXT_PRERENDER = previousPrerender;
      }
    }
  });

  it("returns config redirects before route dispatch and skips finalization", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({
      configRedirects: [{ source: "/old-about", destination: "/about", permanent: true }],
      dispatchMatchedPage,
    });

    const response = await handler(new Request("https://example.test/docs/old-about"), null);

    expect(response.status).toBe(308);
    expect(response.headers.get("location")).toBe("/docs/about");
    expect(response.headers.get("x-test-header")).toBeNull();
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("uses the soft redirect protocol for config redirects on Pages data requests", async () => {
    const handler = createHandler({
      configRedirects: [{ source: "/old-about", destination: "/about", permanent: true }],
      matchRoute: () => null,
      renderPagesFallback: async () => new Response("pages-data"),
    });

    const response = await handler(
      new Request("https://example.test/docs/_next/data/build-id/old-about.json"),
      null,
    );

    expect(response.status).toBe(200);
    expect(response.headers.get("location")).toBeNull();
    expect(response.headers.get("x-nextjs-redirect")).toBe("/docs/about");
  });

  it("lets middleware redirect headers override earlier matching config headers", async () => {
    // Next.js route order reference:
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({
      dispatchMatchedPage,
      middlewareModule: {
        default: () =>
          new Response(null, {
            status: 307,
            headers: {
              Location: "/login",
              "x-test-header": "middleware",
            },
          }),
      },
    });

    const response = await handler(new Request("https://example.test/docs/about"), null);

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toBe("/login");
    expect(response.headers.get("x-test-header")).toBe("middleware");
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("carries config headers on middleware redirects when middleware does not override them", async () => {
    // Next.js route order reference:
    // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({
      dispatchMatchedPage,
      middlewareModule: {
        default: () =>
          new Response(null, {
            status: 307,
            headers: { Location: "/login" },
          }),
      },
    });

    const response = await handler(new Request("https://example.test/docs/about"), null);

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toBe("/login");
    expect(response.headers.get("x-test-header")).toBe("applied");
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("propagates middleware rewrite query parameters to App pages", async () => {
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage: async (options) => {
        pageOptions = options;
        return new Response("page");
      },
      middlewareModule: {
        default: () =>
          new Response(null, {
            headers: {
              "x-middleware-rewrite": "https://example.test/docs/about?destination=2&same=new",
            },
          }),
      },
    });

    await handler(new Request("https://example.test/docs/source?original=1&same=old"), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      destination: "2",
      same: "new",
    });
  });

  it("evaluates config rewrite conditions against middleware rewrite queries", async () => {
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [
          {
            source: "/intermediate",
            destination: "/about?destination=2",
            has: [{ type: "query", key: "stage", value: "1" }],
          },
        ],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage: async (options) => {
        pageOptions = options;
        return new Response("page");
      },
      middlewareModule: {
        default: () =>
          new Response(null, {
            headers: {
              "x-middleware-rewrite": "https://example.test/docs/intermediate?stage=1",
            },
          }),
      },
    });

    await handler(new Request("https://example.test/docs/source"), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      destination: "2",
      stage: "1",
    });
  });

  it("allows middleware-rewritten RSC requests to hand off to Pages HTML", async () => {
    const headers = createRscRequestHeaders();
    const rscUrl = await createRscRequestUrl("/docs/source", headers);
    const renderPagesFallback = vi.fn(async ({ allowRscDocumentFallback, pathname }) =>
      allowRscDocumentFallback && pathname === "/pages"
        ? new Response("pages", { headers: { "content-type": "text/html" } })
        : null,
    );
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      middlewareModule: {
        default: () =>
          new Response(null, {
            headers: { "x-middleware-rewrite": "https://example.test/docs/pages" },
          }),
      },
      renderPagesFallback,
    });

    const response = await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(response.headers.get("content-type")).toBe("text/html");
    expect(await response.text()).toBe("pages");
  });

  it("does not duplicate additive config headers on non-redirect middleware responses", async () => {
    const handler = createHandler({
      configHeaders: [
        {
          source: "/about",
          headers: [{ key: "Vary", value: "X-Config" }],
        },
      ],
      middlewareModule: {
        default: () =>
          new Response("blocked", {
            status: 401,
            headers: { Vary: "User-Agent" },
          }),
      },
    });

    const response = await handler(new Request("https://example.test/docs/about"), null);
    const varyTokens = (response.headers.get("vary") ?? "").split(",").map((token) => token.trim());

    expect(response.status).toBe(401);
    expect(varyTokens).toContain("User-Agent");
    expect(varyTokens).toContain("X-Config");
    expect(varyTokens.filter((token) => token === "X-Config")).toHaveLength(1);
  });

  it("canonicalizes config redirect locations for RSC requests", async () => {
    const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
    const expectedHash = await computeRscCacheBustingSearchParam(headers);
    const handler = createHandler({
      configHeaders: [],
      configRedirects: [{ source: "/old-about", destination: "/about?from=old", permanent: false }],
    });

    const response = await handler(
      new Request("https://example.test/docs/old-about.rsc", { headers }),
      null,
    );

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toBe(
      `https://example.test/docs/about?from=old&_rsc=${expectedHash}`,
    );
  });

  it("preserves the _rsc query on config redirects for .rsc requests without the RSC header (#1529)", async () => {
    // A `.rsc`-suffixed request is an RSC request even when the `RSC: 1`
    // header is absent (e.g. a CDN-style or auto-followed fetch). Without the
    // header the handler can't recompute the cache-busting hash, so the
    // non-header branch carries the original request query onto the Location
    // verbatim (mirroring Next.js resolve-routes.ts) rather than dropping it.
    // (Note: the `.rsc` suffix is not re-applied to the destination, so the
    // followed request isn't re-detected as RSC purely from `_rsc` — the
    // guarantee here is query preservation, not RSC re-detection.)
    const handler = createHandler({
      configHeaders: [],
      configRedirects: [{ source: "/old-about", destination: "/about", permanent: true }],
    });

    const response = await handler(
      new Request("https://example.test/docs/old-about.rsc?_rsc=abc123", {
        headers: { Accept: "text/x-component" },
      }),
      null,
    );

    expect(response.status).toBe(308);
    expect(response.headers.get("location")).toBe("/docs/about?_rsc=abc123");
  });

  it("preserves the original request query on config redirects for document requests (#1529)", async () => {
    // A plain (non-RSC) document request that hits a config redirect must
    // carry its original query onto the Location, matching Next.js
    // resolve-routes.ts. The destination's own query wins on key conflicts.
    const handler = createHandler({
      configHeaders: [],
      configRedirects: [{ source: "/old-about", destination: "/about?from=old", permanent: true }],
    });

    const response = await handler(
      new Request("https://example.test/docs/old-about?foo=bar&from=req"),
      null,
    );

    expect(response.status).toBe(308);
    expect(response.headers.get("location")).toBe("/docs/about?from=old&foo=bar");
  });

  it("redirects invalid RSC cache-busting requests before middleware", async () => {
    const middleware = vi.fn(() => new Response("middleware"));
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
    const expectedHash = await computeRscCacheBustingSearchParam(headers);
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      middlewareModule: { default: middleware },
    });

    const response = await handler(
      new Request("https://example.test/docs/about.rsc?tab=latest", { headers }),
      null,
    );

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toBe(
      `/docs/about.rsc?tab=latest&_rsc=${expectedHash}`,
    );
    expect(middleware).not.toHaveBeenCalled();
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("hides internal RSC cache-busting params from middleware nextUrl", async () => {
    // Ported from Next.js:
    // test/e2e/app-dir/navigation/middleware.js
    // https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/navigation/middleware.js
    const middleware = vi.fn(
      (_: { nextUrl: URL }) => new Response(null, { headers: { "x-middleware-next": "1" } }),
    );
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
    const rscUrl = await createRscRequestUrl("/docs/about?tab=latest", headers);
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      middlewareModule: { default: middleware },
    });

    const response = await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(response.status).toBe(200);
    expect(middleware).toHaveBeenCalledTimes(1);
    const middlewareRequest = middleware.mock.calls[0]?.[0];
    expect(middlewareRequest?.nextUrl.searchParams.has(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM)).toBe(
      false,
    );
    expect(middlewareRequest?.nextUrl.search).toBe("?tab=latest");
    expect(dispatchMatchedPage).toHaveBeenCalledTimes(1);
  });

  it("forwards validated RSC cache-busting params to external rewrite proxies", async () => {
    // Matches Next.js middleware-rsc-external-rewrite: the destination server
    // needs `_rsc` because it cannot validate against the original request URL.
    // The fetch-cache instrumentation captures the real `fetch` at module load
    // and reinstalls a patched copy during request handling, so a global
    // `fetch` mock can't intercept the proxied request. Use a real loopback
    // server as the external rewrite destination and record the URL it
    // receives — that exercises the full handler -> applyRewrite ->
    // proxyExternalRequest path without fighting the instrumentation.
    const receivedUrls: string[] = [];
    const server = createServer((req, res) => {
      receivedUrls.push(req.url ?? "");
      res.writeHead(200, { "content-type": "text/plain" });
      res.end("upstream");
    });
    await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
    const address = server.address() as AddressInfo;
    const upstreamBase = `http://127.0.0.1:${address.port}`;

    try {
      const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
      const rscUrl = await createRscRequestUrl("/docs/proxy?tab=latest", headers);
      const handler = createHandler({
        configHeaders: [],
        configRewrites: {
          beforeFiles: [{ source: "/proxy", destination: `${upstreamBase}/proxy` }],
          afterFiles: [],
          fallback: [],
        },
        matchRoute: () => null,
      });

      const response = await handler(
        new Request(`https://example.test${rscUrl}`, { headers }),
        null,
      );

      expect(response.status).toBe(200);
      expect(receivedUrls).toHaveLength(1);
      const forwardedUrl = new URL(`${upstreamBase}${receivedUrls[0]}`);
      expect(forwardedUrl.searchParams.has(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM)).toBe(true);
      expect(forwardedUrl.searchParams.get("tab")).toBe("latest");
    } finally {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
  });

  it("preserves Node route handler RSC URLs while hiding internal parsed params", async () => {
    // Ported from Next.js:
    // test/e2e/app-dir/front-redirect-issue/front-redirect-issue.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/front-redirect-issue/front-redirect-issue.test.ts
    //
    // The upstream fixture fallback-rewrites a front URL to an App route
    // handler. Next strips `_rsc` from the parsed query in base-server.ts, but
    // its Node request adapter rebuilds request.url from initURL and preserves
    // the original search string.
    const route = createPageRoute({
      isDynamic: true,
      page: null,
      pattern: "/api/app-redirect/:path",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "app-redirect", "[path]"],
    });
    const dispatchMatchedRouteHandler = vi.fn<DispatchMatchedRouteHandler>(
      async () => new Response("route", { status: 200 }),
    );
    const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
    const rscUrl = await createRscRequestUrl("/docs/vercel-user?tab=latest", headers);
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [],
        fallback: [{ source: "/:path*", destination: "/api/app-redirect/:path*" }],
      },
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/app-redirect/vercel-user"
          ? {
              params: { path: "vercel-user" },
              route,
            }
          : null,
    });

    const response = await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(response.status).toBe(200);
    expect(dispatchMatchedRouteHandler).toHaveBeenCalledTimes(1);
    const dispatched = dispatchMatchedRouteHandler.mock.calls[0]?.[0];
    expect(dispatched).toEqual(
      expect.objectContaining({
        cleanPathname: "/api/app-redirect/vercel-user",
        params: { path: "vercel-user" },
        route,
      }),
    );
    const dispatchedUrl = new URL(dispatched?.request.url ?? "");
    expect(dispatchedUrl.pathname).toBe("/docs/vercel-user");
    expect(dispatchedUrl.searchParams.has("_rsc")).toBe(true);
    expect(dispatchedUrl.searchParams.get("tab")).toBe("latest");
    expect(dispatched?.searchParams.has("_rsc")).toBe(false);
  });

  it("normalizes edge route handler RSC URLs and hides internal params", async () => {
    // Next.js normalizes `.rsc` in web/adapter.ts before stripping internal
    // search params from the Edge NextRequest.
    const route = createPageRoute({
      page: null,
      pattern: "/api/inspect",
      routeHandler: { GET: () => new Response("route"), runtime: "edge" },
      routeSegments: ["api", "inspect"],
    });
    const dispatchMatchedRouteHandler = vi.fn<DispatchMatchedRouteHandler>(
      async () => new Response("route", { status: 200 }),
    );
    const headers = createRscRequestHeaders({ mountedSlotsHeader: "slot:modal:/" });
    const rscUrl = await createRscRequestUrl("/docs/api/inspect?tab=latest", headers);
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/inspect" ? { params: {}, route } : null,
    });

    const response = await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(response.status).toBe(200);
    const dispatched = dispatchMatchedRouteHandler.mock.calls[0]?.[0];
    const dispatchedUrl = new URL(dispatched?.request.url ?? "");
    expect(dispatchedUrl.pathname).toBe("/docs/api/inspect");
    expect(dispatchedUrl.search).toBe("?tab=latest");
    expect(dispatched?.searchParams.toString()).toBe("tab=latest");
  });

  it("preserves non-RSC route handler request URLs while hiding internal parsed params", async () => {
    const route = createPageRoute({
      page: null,
      pattern: "/api/inspect",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "inspect"],
    });
    const dispatchMatchedRouteHandler = vi.fn<DispatchMatchedRouteHandler>(
      async () => new Response("route", { status: 200 }),
    );
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/inspect" ? { params: {}, route } : null,
    });

    const response = await handler(
      new Request("https://example.test/docs/api/inspect?tab=latest&_rsc=user-value"),
      null,
    );

    expect(response.status).toBe(200);
    const dispatched = dispatchMatchedRouteHandler.mock.calls[0]?.[0];
    expect(new URL(dispatched?.request.url ?? "").search).toBe("?tab=latest&_rsc=user-value");
    expect(dispatched?.searchParams.toString()).toBe("tab=latest");
  });

  it("hides internal RSC params from non-RSC edge route handler request URLs", async () => {
    const route = createPageRoute({
      page: null,
      pattern: "/api/inspect",
      routeHandler: { GET: () => new Response("route"), runtime: "edge" },
      routeSegments: ["api", "inspect"],
    });
    const dispatchMatchedRouteHandler = vi.fn<DispatchMatchedRouteHandler>(
      async () => new Response("route", { status: 200 }),
    );
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/inspect" ? { params: {}, route } : null,
    });

    const response = await handler(
      new Request("https://example.test/docs/api/inspect?tab=latest&_rsc=user-value"),
      null,
    );

    expect(response.status).toBe(200);
    const dispatched = dispatchMatchedRouteHandler.mock.calls[0]?.[0];
    expect(new URL(dispatched?.request.url ?? "").search).toBe("?tab=latest");
    expect(dispatched?.searchParams.toString()).toBe("tab=latest");
  });

  it("does not render RSC payloads at HTML URLs marked only by RSC headers", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
    });

    const response = await handler(
      new Request("https://example.test/docs/about", {
        headers: createRscRequestHeaders(),
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({
        cleanPathname: "/about",
        isRscRequest: false,
      }),
    );
  });

  it("passes parsed ClientReuseManifest hints from canonical RSC requests to page dispatch", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const manifest = createClientReuseManifest({
      entries: [
        {
          artifactCompatibility: createArtifactCompatibilityEnvelope(),
          id: "layout:/",
          payloadHash: createClientReusePayloadHash("root-layout"),
          privacy: "public",
          variantCacheKey: "cp1:root",
        },
      ],
      visibleCommitVersion: 1,
    });
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
    });

    const response = await handler(
      new Request("https://example.test/docs/about.rsc", {
        headers: {
          [VINEXT_CLIENT_REUSE_MANIFEST_HEADER]: JSON.stringify(manifest),
        },
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({
        clientReuseManifest: expect.objectContaining({
          kind: "parsed",
        }),
        isRscRequest: true,
      }),
    );
  });

  it("strips internal RSC cache-busting params before setting navigation context", async () => {
    const setNavigationContext = vi.fn();
    const headers = createRscRequestHeaders();
    const rscUrl = await createRscRequestUrl("/docs/about?tab=latest", headers);
    const handler = createHandler({
      configHeaders: [],
      setNavigationContext,
    });

    const response = await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(response.status).toBe(200);
    expect(setNavigationContext).toHaveBeenLastCalledWith(
      expect.objectContaining({
        pathname: "/about",
        params: {},
      }),
    );
    const context = setNavigationContext.mock.lastCall?.[0];
    expect(context?.searchParams.get("tab")).toBe("latest");
    expect(context?.searchParams.has("_rsc")).toBe(false);
  });

  it("preserves beforeFiles destination query while stripping the RSC cache key", async () => {
    const headers = createRscRequestHeaders();
    const rscUrl = await createRscRequestUrl("/docs/legacy?original=1", headers);
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/legacy", destination: "/about?destination=2" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage: async (options) => {
        pageOptions = options;
        return new Response("page");
      },
    });

    await handler(new Request(`https://example.test${rscUrl}`, { headers }), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      destination: "2",
      original: "1",
    });
  });

  it("runs beforeFiles rewrites before route matching", async () => {
    const matchRoute = vi.fn((pathname: string) =>
      pathname === "/about"
        ? {
            params: {},
            route: createPageRoute(),
          }
        : null,
    );
    const dispatchMatchedPage = vi.fn(
      async (_options: Parameters<HandlerOptions["dispatchMatchedPage"]>[0]) =>
        new Response("rewritten", { status: 200 }),
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/alias", destination: "/about" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute,
    });

    const response = await handler(new Request("https://example.test/docs/alias"), null);

    expect(response.status).toBe(200);
    expect(await response.text()).toBe("rewritten");
    expect(matchRoute).toHaveBeenLastCalledWith("/about");
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({ cleanPathname: "/about" }),
    );
  });

  it("propagates rewritten query parameters to App pages", async () => {
    const setNavigationContext = vi.fn();
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const dispatchMatchedPage = vi.fn(
      async (options: Parameters<HandlerOptions["dispatchMatchedPage"]>[0]) => {
        pageOptions = options;
        return new Response("rewritten", { status: 200 });
      },
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/legacy", destination: "/about?destination=2&same=new" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage,
      setNavigationContext,
    });

    await handler(new Request("https://example.test/docs/legacy?original=1&same=old"), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      destination: "2",
      original: "1",
      same: "new",
    });
    expect(Object.fromEntries(setNavigationContext.mock.lastCall![0].searchParams)).toEqual({
      destination: "2",
      original: "1",
      same: "new",
    });
  });

  it("applies sequential beforeFiles rewrites with accumulated query conditions", async () => {
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [
          { source: "/source", destination: "/intermediate?preview=1" },
          {
            source: "/intermediate",
            destination: "/about?destination=2",
            has: [{ type: "query", key: "preview", value: "1" }],
          },
        ],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage: async (options) => {
        pageOptions = options;
        return new Response("page");
      },
    });

    await handler(new Request("https://example.test/docs/source?original=1"), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      destination: "2",
      original: "1",
      preview: "1",
    });
  });

  it("exposes unused rewrite source params through App searchParams", async () => {
    let pageOptions: Parameters<HandlerOptions["dispatchMatchedPage"]>[0] | undefined;
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [
          {
            source: "/source/:section/:name",
            destination: "/about?first=:section&second=:name",
          },
        ],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedPage: async (options) => {
        pageOptions = options;
        return new Response("page");
      },
    });

    await handler(new Request("https://example.test/docs/source/hello/world"), null);

    expect(Object.fromEntries(pageOptions!.searchParams)).toEqual({
      first: "hello",
      name: "world",
      second: "world",
      section: "hello",
    });
  });

  it.each(["afterFiles", "fallback"] as const)(
    "continues through unmatched %s rewrite destinations",
    async (rewritePhase) => {
      const handler = createHandler({
        configHeaders: [],
        configRewrites: {
          beforeFiles: [],
          afterFiles:
            rewritePhase === "afterFiles"
              ? [
                  { source: "/source", destination: "/intermediate" },
                  { source: "/intermediate", destination: "/about" },
                ]
              : [],
          fallback:
            rewritePhase === "fallback"
              ? [
                  { source: "/source", destination: "/intermediate" },
                  { source: "/intermediate", destination: "/about" },
                ]
              : [],
        },
        matchRoute: (pathname) =>
          pathname === "/about" ? { params: {}, route: createPageRoute() } : null,
      });

      const response = await handler(new Request("https://example.test/docs/source"), null);

      expect(response.status).toBe(200);
    },
  );

  it("propagates rewritten query parameters to App route handlers", async () => {
    const route = createPageRoute({
      page: null,
      pattern: "/api/static",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "static"],
    });
    const dispatchMatchedRouteHandler = vi.fn(
      async (_options: Parameters<HandlerOptions["dispatchMatchedRouteHandler"]>[0]) =>
        new Response("route"),
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/legacy", destination: "/api/static?destination=2&same=new" }],
        afterFiles: [],
        fallback: [],
      },
      dispatchMatchedRouteHandler,
      matchRoute: (pathname) => (pathname === "/api/static" ? { params: {}, route } : null),
    });

    await handler(new Request("https://example.test/docs/legacy?original=1&same=old"), null);

    const routeHandlerOptions = dispatchMatchedRouteHandler.mock.lastCall?.[0];
    expect(Object.fromEntries(routeHandlerOptions!.searchParams)).toEqual({
      destination: "2",
      original: "1",
      same: "new",
    });
    expect(new URL(routeHandlerOptions!.request.url).pathname).toBe("/docs/legacy");
    expect(Object.fromEntries(new URL(routeHandlerOptions!.request.url).searchParams)).toEqual({
      destination: "2",
      original: "1",
      same: "new",
    });
  });

  it("does not let afterFiles rewrites override non-dynamic app routes", async () => {
    const routes = {
      "/about": createPageRoute({ pattern: "/about", routeSegments: ["about"] }),
      "/nav": createPageRoute({ pattern: "/nav", routeSegments: ["nav"] }),
    };
    const dispatchMatchedPage = vi.fn(
      async ({ route }) => new Response(`page:${route.pattern}`, { status: 200 }),
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/nav", destination: "/about" }],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute: (pathname: string) => {
        if (pathname === "/about") return { params: {}, route: routes["/about"] };
        if (pathname === "/nav") return { params: {}, route: routes["/nav"] };
        return null;
      },
    });

    const response = await handler(new Request("https://example.test/docs/nav"), null);

    expect(response.status).toBe(200);
    expect(await response.text()).toBe("page:/nav");
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({ cleanPathname: "/nav", route: routes["/nav"] }),
    );
  });

  it("runs afterFiles rewrites before dynamic app route matching", async () => {
    const routes = {
      "/about": createPageRoute({ pattern: "/about", routeSegments: ["about"] }),
      dynamicBlog: createPageRoute({
        isDynamic: true,
        pattern: "/blog/:slug",
        routeSegments: ["blog", "[slug]"],
      }),
    };
    const dispatchMatchedPage = vi.fn(
      async ({ route }) => new Response(`page:${route.pattern}`, { status: 200 }),
    );
    const emptyParams: Record<string, string | string[]> = {};
    const legacyParams: Record<string, string | string[]> = { slug: "legacy" };
    const matchRoute: HandlerOptions["matchRoute"] = (pathname) => {
      if (pathname === "/about") return { params: emptyParams, route: routes["/about"] };
      if (pathname === "/blog/legacy") {
        return { params: legacyParams, route: routes.dynamicBlog };
      }
      return null;
    };
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/blog/legacy", destination: "/about" }],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute,
    });

    const response = await handler(new Request("https://example.test/docs/blog/legacy"), null);

    expect(response.status).toBe(200);
    expect(await response.text()).toBe("page:/about");
    expect(dispatchMatchedPage).toHaveBeenCalledWith(
      expect.objectContaining({ cleanPathname: "/about", route: routes["/about"] }),
    );
  });

  it("lets a static Pages route win before afterFiles rewrites", async () => {
    const dynamicRoute = createPageRoute({
      isDynamic: true,
      pattern: "/:path+",
      routeSegments: ["[...path]"],
    });
    const renderPagesFallback = vi.fn(async () => new Response("pages:/about", { status: 200 }));
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/about", destination: "/rewritten" }],
        fallback: [],
      },
      matchRoute: () => ({ params: { path: ["about"] }, route: dynamicRoute }),
      renderPagesFallback,
    });

    const response = await handler(new Request("https://example.test/docs/about"), null);

    expect(await response.text()).toBe("pages:/about");
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({
        matchKind: "static",
        pathname: "/about",
        appRouteMatch: expect.objectContaining({ route: dynamicRoute }),
      }),
    );
  });

  it("normalizes hybrid Pages data requests before middleware", async () => {
    let middlewarePathname: string | null = null;
    let middlewareCf: unknown;
    let pagesDataCf: unknown;
    let pagesDataUrl: string | null = null;
    const renderPagesFallback = vi.fn(async (_options: unknown) => new Response("pages-data"));
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      middlewareModule: {
        default: (request: Request) => {
          middlewarePathname = new URL(request.url).pathname;
          middlewareCf = (request as Request & { cf?: unknown }).cf;
          return new Response(null, { headers: { "x-middleware-next": "1" } });
        },
      },
      renderPagesFallback: async (options) => {
        pagesDataCf = (options.pagesDataRequest as (Request & { cf?: unknown }) | null)?.cf;
        pagesDataUrl = options.pagesDataRequest?.url ?? null;
        return renderPagesFallback(options);
      },
    });

    const request = new Request(
      "https://example.test/docs/_next/data/build-id/form-search.json?query=basic",
    );
    const cf = { colo: "LHR" };
    Object.defineProperty(request, "cf", { value: cf, enumerable: true });
    const response = await handler(request, null);

    expect(await response.text()).toBe("pages-data");
    expect(middlewarePathname).toBe("/docs/form-search");
    expect(middlewareCf).toBe(cf);
    expect(pagesDataCf).toBe(cf);
    expect(pagesDataUrl).toBe(
      "https://example.test/_next/data/build-id/form-search.json?query=basic",
    );
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({
        pathname: "/form-search?query=basic",
        pagesDataRequest: expect.any(Request),
      }),
    );
  });

  it("exposes the rewritten route on hybrid Pages data responses", async () => {
    const renderPagesFallback = vi.fn(
      async () =>
        new Response('{"pageProps":{"query":"basic"}}', {
          headers: { "content-type": "application/json" },
        }),
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [{ source: "/form-search", destination: "/rewritten-search" }],
        afterFiles: [],
        fallback: [],
      },
      matchRoute: () => null,
      renderPagesFallback,
    });

    const response = await handler(
      new Request("https://example.test/docs/_next/data/build-id/form-search.json?query=basic"),
      null,
    );

    expect(response.headers.get("x-nextjs-rewrite")).toBe("/rewritten-search?query=basic");
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({
        pathname: "/rewritten-search?query=basic",
        pagesDataRequest: expect.any(Request),
      }),
    );
  });

  it("uses the soft redirect protocol for URL-recognized Pages data requests", async () => {
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      middlewareModule: {
        default: () => new Response(null, { status: 307, headers: { Location: "/login" } }),
      },
      renderPagesFallback: async () => new Response("pages-data"),
    });

    const response = await handler(
      new Request("https://example.test/docs/_next/data/build-id/form-search.json"),
      null,
    );

    expect(response.status).toBe(200);
    expect(response.headers.get("location")).toBeNull();
    expect(response.headers.get("x-nextjs-redirect")).toBe("/login");
  });

  it("returns JSON 404 for stale hybrid Pages data requests before middleware", async () => {
    const middleware = vi.fn(() => new Response(null, { headers: { "x-middleware-next": "1" } }));
    const renderPagesFallback = vi.fn(async () => new Response("pages-data"));
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      middlewareModule: { default: middleware },
      renderPagesFallback,
    });

    const response = await handler(
      new Request("https://example.test/docs/_next/data/stale/form-search.json?query=basic"),
      null,
    );

    expect(response.status).toBe(404);
    expect(response.headers.get("content-type")).toContain("application/json");
    expect(await response.text()).toBe("{}");
    expect(middleware).not.toHaveBeenCalled();
    expect(renderPagesFallback).not.toHaveBeenCalled();
  });

  it("does not normalize hybrid Pages data requests outside basePath", async () => {
    const renderPagesFallback = vi.fn(async () => new Response("pages-data"));
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      renderPagesFallback,
    });

    const response = await handler(
      new Request("https://example.test/_next/data/build-id/form-search.json?query=basic"),
      null,
    );

    expect(response.status).toBe(404);
    expect(response.headers.get("content-type")).not.toContain("application/json");
    expect(renderPagesFallback).not.toHaveBeenCalled();
  });

  it("returns JSON 404 when an App route owns a Pages data URL", async () => {
    const appRoute = createPageRoute({ pattern: "/app-only" });
    const dispatchMatchedPage = vi.fn(async () => new Response("app-html"));
    const renderPagesFallback = vi.fn(async () => null);
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedPage,
      matchRoute: (pathname) => (pathname === "/app-only" ? { route: appRoute, params: {} } : null),
      renderPagesFallback,
    });

    const response = await handler(
      new Request("https://example.test/docs/_next/data/build-id/app-only.json"),
      null,
    );

    expect(response.status).toBe(404);
    expect(response.headers.get("content-type")).toContain("application/json");
    expect(await response.text()).toBe("{}");
    expect(renderPagesFallback).not.toHaveBeenCalled();
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("runs afterFiles rewrites before dynamic Pages route ownership", async () => {
    const appDynamicRoute = createPageRoute({
      isDynamic: true,
      pattern: "/:slug",
      routeSegments: ["[slug]"],
    });
    const appDestinationRoute = createPageRoute({
      pattern: "/destination",
      routeSegments: ["destination"],
    });
    const renderPagesFallback = vi.fn(async ({ matchKind }) =>
      matchKind === "dynamic" ? new Response("pages-dynamic", { status: 200 }) : null,
    );
    const dispatchMatchedPage = vi.fn(
      async ({ route }) => new Response(`app:${route.pattern}`, { status: 200 }),
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/legacy", destination: "/destination" }],
        fallback: [],
      },
      dispatchMatchedPage,
      matchRoute: (pathname): ReturnType<HandlerOptions["matchRoute"]> => {
        if (pathname === "/legacy") {
          return { params: { slug: "legacy" }, route: appDynamicRoute };
        }
        if (pathname === "/destination") return { params: {}, route: appDestinationRoute };
        return null;
      },
      renderPagesFallback,
    });

    const response = await handler(new Request("https://example.test/docs/legacy"), null);

    expect(await response.text()).toBe("app:/destination");
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({ matchKind: "static", pathname: "/legacy" }),
    );
    expect(renderPagesFallback).not.toHaveBeenCalledWith(
      expect.objectContaining({ matchKind: "dynamic", pathname: "/legacy" }),
    );
  });

  it("rechecks static Pages routes after an afterFiles rewrite", async () => {
    const renderPagesFallback = vi.fn(async ({ matchKind, pathname }) =>
      matchKind === "static" && pathname === "/pages-static"
        ? new Response("pages-static", { status: 200 })
        : null,
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/legacy", destination: "/pages-static" }],
        fallback: [],
      },
      matchRoute: () => null,
      renderPagesFallback,
    });

    const response = await handler(new Request("https://example.test/docs/legacy"), null);

    expect(await response.text()).toBe("pages-static");
  });

  it("rechecks static and dynamic Pages routes after a fallback rewrite", async () => {
    const renderPagesFallback = vi.fn(async ({ matchKind, pathname }) =>
      pathname === "/pages-dynamic" && matchKind === "dynamic"
        ? new Response("pages-dynamic", { status: 200 })
        : null,
    );
    const handler = createHandler({
      configHeaders: [],
      configRewrites: {
        beforeFiles: [],
        afterFiles: [],
        fallback: [{ source: "/legacy", destination: "/pages-dynamic" }],
      },
      matchRoute: () => null,
      renderPagesFallback,
    });

    const response = await handler(new Request("https://example.test/docs/legacy"), null);

    expect(await response.text()).toBe("pages-dynamic");
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({ matchKind: "static", pathname: "/pages-dynamic" }),
    );
    expect(renderPagesFallback).toHaveBeenCalledWith(
      expect.objectContaining({ matchKind: "dynamic", pathname: "/pages-dynamic" }),
    );
  });

  it.each(["beforeFiles", "afterFiles", "fallback"] as const)(
    "preserves and overrides query parameters for %s rewrites to Pages routes",
    async (rewritePhase) => {
      const renderPagesFallback = vi.fn(async ({ pathname }) =>
        pathname.startsWith("/pages?") ? new Response("pages", { status: 200 }) : null,
      );
      const handler = createHandler({
        configHeaders: [],
        configRewrites: {
          beforeFiles:
            rewritePhase === "beforeFiles"
              ? [{ source: "/legacy", destination: "/pages?dest=2&same=new" }]
              : [],
          afterFiles:
            rewritePhase === "afterFiles"
              ? [{ source: "/legacy", destination: "/pages?dest=2&same=new" }]
              : [],
          fallback:
            rewritePhase === "fallback"
              ? [{ source: "/legacy", destination: "/pages?dest=2&same=new" }]
              : [],
        },
        matchRoute: () => null,
        renderPagesFallback,
      });

      const response = await handler(
        new Request("https://example.test/docs/legacy?keep=1&same=old"),
        null,
      );

      expect(await response.text()).toBe("pages");
      const rewrittenCall = renderPagesFallback.mock.calls.find(([options]) =>
        options.pathname.startsWith("/pages?"),
      );
      expect(rewrittenCall).toBeDefined();
      const rewrittenUrl = new URL(rewrittenCall![0].pathname, "https://example.test");
      expect(rewrittenUrl.pathname).toBe("/pages");
      expect(Object.fromEntries(rewrittenUrl.searchParams)).toEqual({
        dest: "2",
        keep: "1",
        same: "new",
      });
    },
  );

  it.each(["beforeFiles", "afterFiles", "fallback"] as const)(
    "excludes rewrite fragments from %s route matching",
    async (rewritePhase) => {
      const matchRoute = vi.fn((pathname: string) =>
        pathname === "/about"
          ? {
              params: {},
              route: createPageRoute(),
            }
          : null,
      );
      const handler = createHandler({
        configHeaders: [],
        configRewrites: {
          beforeFiles:
            rewritePhase === "beforeFiles"
              ? [{ source: "/legacy/:code", destination: "/about#:code" }]
              : [],
          afterFiles:
            rewritePhase === "afterFiles"
              ? [{ source: "/legacy/:code", destination: "/about#:code" }]
              : [],
          fallback:
            rewritePhase === "fallback"
              ? [{ source: "/legacy/:code", destination: "/about#:code" }]
              : [],
        },
        matchRoute,
      });

      const response = await handler(new Request("https://example.test/docs/legacy/500"), null);

      expect(response.status).toBe(200);
      expect(matchRoute).toHaveBeenCalledWith("/about");
      expect(matchRoute).not.toHaveBeenCalledWith("/about#500");
    },
  );

  it("serves public files before route matching and clears request context", async () => {
    const clearRequestContext = vi.fn();
    const matchRoute = vi.fn(() => null);
    const handler = createHandler({
      clearRequestContext,
      configHeaders: [],
      matchRoute,
      publicFiles: new Set(["/logo.svg"]),
    });

    const response = await handler(new Request("https://example.test/docs/logo.svg"), null);

    expect(response.status).toBe(200);
    expect(response.headers.get("x-vinext-static-file")).toBe("%2Flogo.svg");
    expect(response.headers.get("vary")).toBeNull();
    expect(clearRequestContext).toHaveBeenCalledTimes(1);
    expect(matchRoute).not.toHaveBeenCalled();
  });

  it("lets middleware Cache-Control override static metadata route defaults", async () => {
    // Ported from Next.js: test/e2e/app-dir/no-duplicate-headers-middleware/no-duplicate-headers-middleware.test.ts
    // https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/no-duplicate-headers-middleware/no-duplicate-headers-middleware.test.ts
    const handler = createHandler({
      configHeaders: [],
      matchRoute: () => null,
      metadataRoutes: [
        {
          type: "favicon",
          isDynamic: false,
          filePath: "/tmp/app/favicon.ico",
          routePrefix: "",
          routeSegments: [],
          servedUrl: "/favicon.ico",
          contentType: "image/x-icon",
          fileDataBase64: btoa("icon-bytes"),
        },
      ],
      middlewareModule: {
        middleware() {
          return new Response(null, {
            headers: {
              "Cache-Control": "max-age=1234",
              "x-middleware-next": "1",
            },
          });
        },
      },
    });

    const response = await handler(new Request("https://example.test/docs/favicon.ico"), null);

    expect(response.status).toBe(200);
    expect(response.headers.get("cache-control")).toBe("max-age=1234");
    expect(response.headers.get("content-type")).toBe("image/x-icon");
    await expect(response.text()).resolves.toBe("icon-bytes");
  });

  it("lets next.config headers override static metadata route defaults", async () => {
    // Ported from Next.js: test/e2e/app-dir/no-duplicate-headers-next-config/no-duplicate-headers-next-config.test.ts
    // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/no-duplicate-headers-next-config/no-duplicate-headers-next-config.test.ts
    const handler = createHandler({
      configHeaders: [
        {
          source: "/favicon.ico",
          headers: [
            { key: "cache-control", value: "max-age=1234" },
            { key: "content-type", value: "text/plain" },
          ],
        },
      ],
      matchRoute: () => null,
      metadataRoutes: [
        {
          type: "favicon",
          isDynamic: false,
          filePath: "/tmp/app/favicon.ico",
          routePrefix: "",
          routeSegments: [],
          servedUrl: "/favicon.ico",
          contentType: "image/x-icon",
          fileDataBase64: btoa("icon-bytes"),
        },
      ],
    });

    const response = await handler(new Request("https://example.test/docs/favicon.ico"), null);

    expect(response.status).toBe(200);
    expect(response.headers.get("cache-control")).toBe("max-age=1234");
    expect(response.headers.get("content-type")).toBe("image/x-icon");
    await expect(response.text()).resolves.toBe("icon-bytes");
  });

  it("keeps middleware Cache-Control above matching config headers for metadata routes", async () => {
    const handler = createHandler({
      configHeaders: [
        {
          source: "/favicon.ico",
          headers: [{ key: "cache-control", value: "max-age=1234" }],
        },
      ],
      matchRoute: () => null,
      metadataRoutes: [
        {
          type: "favicon",
          isDynamic: false,
          filePath: "/tmp/app/favicon.ico",
          routePrefix: "",
          routeSegments: [],
          servedUrl: "/favicon.ico",
          contentType: "image/x-icon",
          fileDataBase64: btoa("icon-bytes"),
        },
      ],
      middlewareModule: {
        middleware() {
          return new Response(null, {
            headers: {
              "Cache-Control": "max-age=5678",
              "x-middleware-next": "1",
            },
          });
        },
      },
    });

    const response = await handler(new Request("https://example.test/docs/favicon.ico"), null);

    expect(response.status).toBe(200);
    expect(response.headers.get("cache-control")).toBe("max-age=5678");
    expect(response.headers.get("content-type")).toBe("image/x-icon");
    await expect(response.text()).resolves.toBe("icon-bytes");
  });

  it("lets server actions short-circuit routing while still applying final headers", async () => {
    const dispatchMatchedPage = vi.fn(async () => new Response("page", { status: 200 }));
    const handleServerActionRequest = vi.fn(
      async () => new Response("action", { status: 200, headers: { "x-action": "done" } }),
    );
    const handler = createHandler({
      configRewrites: {
        beforeFiles: [],
        afterFiles: [{ source: "/about", destination: "/rewritten-action" }],
        fallback: [],
      },
      dispatchMatchedPage,
      handleServerActionRequest,
    });

    const response = await handler(
      new Request("https://example.test/docs/about", {
        method: "POST",
        headers: { "next-action": "abc123" },
      }),
      null,
    );

    expect(response.status).toBe(200);
    expect(await response.text()).toBe("action");
    expect(response.headers.get("x-action")).toBe("done");
    expect(response.headers.get("x-test-header")).toBe("applied");
    expect(handleServerActionRequest).toHaveBeenCalledWith(
      expect.objectContaining({ actionId: "abc123", cleanPathname: "/about" }),
    );
    expect(dispatchMatchedPage).not.toHaveBeenCalled();
  });

  it("accepts the vinext action header name for server actions", async () => {
    const handleServerActionRequest = vi.fn(async () => new Response("action", { status: 200 }));
    const handler = createHandler({ handleServerActionRequest });

    await handler(
      new Request("https://example.test/docs/about", {
        method: "POST",
        headers: { "x-rsc-action": "vinext-action" },
      }),
      null,
    );

    expect(handleServerActionRequest).toHaveBeenCalledWith(
      expect.objectContaining({ actionId: "vinext-action" }),
    );
  });

  it("dispatches route handlers with matched params", async () => {
    const route = createPageRoute({
      isDynamic: true,
      page: null,
      pattern: "/api/:id",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "[id]"],
    });
    const dispatchMatchedRouteHandler = vi.fn(async () => new Response("route", { status: 200 }));
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/123"
          ? {
              params: { id: "123" },
              route,
            }
          : null,
    });

    const response = await handler(new Request("https://example.test/docs/api/123"), null);

    expect(response.status).toBe(200);
    expect(dispatchMatchedRouteHandler).toHaveBeenCalledWith(
      expect.objectContaining({
        cleanPathname: "/api/123",
        params: { id: "123" },
        route,
      }),
    );
  });

  // Matches Next.js behavior: non-dynamic route handlers receive params=null.
  // See test/e2e/app-dir/app-routes/app-custom-routes.test.ts in next.js.
  it("dispatches non-dynamic route handlers with params: null", async () => {
    const route = createPageRoute({
      isDynamic: false,
      page: null,
      pattern: "/api/static",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "static"],
    });
    const dispatchMatchedRouteHandler = vi.fn(async () => new Response("route", { status: 200 }));
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/static"
          ? {
              params: {},
              route,
            }
          : null,
    });

    const response = await handler(new Request("https://example.test/docs/api/static"), null);

    expect(response.status).toBe(200);
    expect(dispatchMatchedRouteHandler).toHaveBeenCalledWith(
      expect.objectContaining({
        cleanPathname: "/api/static",
        params: null,
        route,
      }),
    );
  });

  it("appends App Router RSC vary values to route handler responses", async () => {
    const route = createPageRoute({
      isDynamic: true,
      page: null,
      pattern: "/api/:id",
      routeHandler: { GET: () => new Response("route") },
      routeSegments: ["api", "[id]"],
    });
    const dispatchMatchedRouteHandler = vi.fn(
      async () => new Response("route", { status: 200, headers: { Vary: "User-Agent" } }),
    );
    const handler = createHandler({
      configHeaders: [],
      dispatchMatchedRouteHandler,
      matchRoute: (pathname: string) =>
        pathname === "/api/123"
          ? {
              params: { id: "123" },
              route,
            }
          : null,
    });

    const response = await handler(new Request("https://example.test/docs/api/123"), null);

    expect(response.status).toBe(200);
    expect(response.headers.get("vary")).toBe(`User-Agent, ${VINEXT_RSC_VARY_HEADER}`);
  });

  it("clears request context before returning the plain 404 fallback", async () => {
    const clearRequestContext = vi.fn();
    const handler = createHandler({
      clearRequestContext,
      configHeaders: [],
      matchRoute: () => null,
      renderNotFound: async () => null,
    });

    const response = await handler(new Request("https://example.test/docs/missing"), null);

    expect(response.status).toBe(404);
    expect(await response.text()).toBe("This page could not be found");
    expect(clearRequestContext).toHaveBeenCalledTimes(1);
  });

  // Issue #1452 — root params must be visible to actions/route handlers/use cache,
  // not only to the page render. The handler used to call setRootParams only
  // after the post-action route match, leaving rootParams null during action
  // dispatch and route-handler dispatch. See app-rsc-handler.ts pre-action
  // seeding block.
  describe("root params propagation (issue #1452)", () => {
    it("populates root params before route-handler dispatch", async () => {
      const route = createPageRoute({
        isDynamic: true,
        page: null,
        pattern: "/:lang/:locale/api",
        rootParamNames: ["lang", "locale"],
        routeHandler: { GET: () => new Response("route") },
        routeSegments: ["[lang]", "[locale]", "api"],
      });
      let observedRootParams: Record<string, string | string[] | undefined> | null = null;
      const dispatchMatchedRouteHandler = vi.fn(async () => {
        // Read from the unified request context active at dispatch time.
        const { getRootParam } = await import("../packages/vinext/src/shims/root-params.js");
        observedRootParams = {
          lang: await getRootParam("lang"),
          locale: await getRootParam("locale"),
        };
        return new Response("route", { status: 200 });
      });
      const handler = createHandler({
        configHeaders: [],
        dispatchMatchedRouteHandler,
        matchRoute: (pathname: string) =>
          pathname === "/en/us/api"
            ? {
                params: { lang: "en", locale: "us" },
                route,
              }
            : null,
      });

      const response = await handler(new Request("https://example.test/docs/en/us/api"), null);
      expect(response.status).toBe(200);
      expect(observedRootParams).toEqual({ lang: "en", locale: "us" });
    });

    it("populates root params before server-action dispatch", async () => {
      const route = createPageRoute({
        isDynamic: true,
        pattern: "/:lang/:locale/server-action",
        rootParamNames: ["lang", "locale"],
        routeSegments: ["[lang]", "[locale]", "server-action"],
      });
      let observedRootParams: Record<string, string | string[] | undefined> | null = null;
      const handleServerActionRequest = vi.fn(async () => {
        const { getRootParam } = await import("../packages/vinext/src/shims/root-params.js");
        observedRootParams = {
          lang: await getRootParam("lang"),
          locale: await getRootParam("locale"),
        };
        return new Response("action", { status: 200 });
      });
      const handler = createHandler({
        configHeaders: [],
        handleServerActionRequest,
        matchRoute: (pathname: string) =>
          pathname === "/en/us/server-action"
            ? {
                params: { lang: "en", locale: "us" },
                route,
              }
            : null,
      });

      const response = await handler(
        new Request("https://example.test/docs/en/us/server-action", {
          method: "POST",
          headers: { "next-action": "abc123" },
        }),
        null,
      );
      expect(response.status).toBe(200);
      expect(observedRootParams).toEqual({ lang: "en", locale: "us" });
    });

    it("populates root params before progressive (form) action dispatch", async () => {
      const route = createPageRoute({
        isDynamic: true,
        pattern: "/:lang/:locale/server-action",
        rootParamNames: ["lang", "locale"],
        routeSegments: ["[lang]", "[locale]", "server-action"],
      });
      let observedRootParams: Record<string, string | string[] | undefined> | null = null;
      const handleProgressiveActionRequest = vi.fn(async () => {
        const { getRootParam } = await import("../packages/vinext/src/shims/root-params.js");
        observedRootParams = {
          lang: await getRootParam("lang"),
          locale: await getRootParam("locale"),
        };
        return new Response("progressive-action", { status: 200 });
      });
      const handler = createHandler({
        configHeaders: [],
        handleProgressiveActionRequest,
        matchRoute: (pathname: string) =>
          pathname === "/en/us/server-action"
            ? {
                params: { lang: "en", locale: "us" },
                route,
              }
            : null,
      });

      const response = await handler(
        new Request("https://example.test/docs/en/us/server-action", {
          method: "POST",
          headers: { "content-type": "multipart/form-data; boundary=vinext" },
        }),
        null,
      );
      expect(response.status).toBe(200);
      expect(observedRootParams).toEqual({ lang: "en", locale: "us" });
    });

    it("only picks root params declared on the matched route", async () => {
      // The route has a dynamic [slug] segment but only [lang] is a root param.
      // setRootParams must surface only `lang`, not `slug`.
      const route = createPageRoute({
        isDynamic: true,
        page: null,
        pattern: "/:lang/blog/:slug",
        rootParamNames: ["lang"],
        routeHandler: { GET: () => new Response("route") },
        routeSegments: ["[lang]", "blog", "[slug]"],
      });
      let observedLang: string | string[] | undefined = "<unset>";
      let observedSlug: string | string[] | undefined = "<unset>";
      const dispatchMatchedRouteHandler = vi.fn(async () => {
        const { getRootParam } = await import("../packages/vinext/src/shims/root-params.js");
        observedLang = await getRootParam("lang");
        observedSlug = await getRootParam("slug");
        return new Response("route", { status: 200 });
      });
      const handler = createHandler({
        configHeaders: [],
        dispatchMatchedRouteHandler,
        matchRoute: (pathname: string) =>
          pathname === "/en/blog/hello"
            ? {
                params: { lang: "en", slug: "hello" },
                route,
              }
            : null,
      });

      const response = await handler(new Request("https://example.test/docs/en/blog/hello"), null);
      expect(response.status).toBe(200);
      expect(observedLang).toBe("en");
      expect(observedSlug).toBeUndefined();
    });
  });
});
