import { describe, it, expect, beforeAll, afterAll, vi } from "vite-plus/test";
import { createServer, build, type ViteDevServer } from "vite-plus";
import { request as httpRequest, type IncomingHttpHeaders } from "node:http";
import path from "node:path";
import fs from "node:fs";
import fsp from "node:fs/promises";
import os from "node:os";
import { Readable } from "node:stream";
import { pathToFileURL } from "node:url";
import zlib from "node:zlib";
import vinext from "../packages/vinext/src/index.js";
import {
  PHASE_DEVELOPMENT_SERVER,
  PHASE_PRODUCTION_BUILD,
} from "../packages/vinext/src/shims/constants.js";
import { PAGES_FIXTURE_DIR, buildPagesFixture, startFixtureServer } from "./helpers.js";

const FIXTURE_DIR = PAGES_FIXTURE_DIR;
const PAGES_APP_COMPONENT = `export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}
`;

type ClientBuildManifestEntry = {
  file?: string;
  css?: string[];
  assets?: string[];
};

function getBuildBundlerOptions(result: any) {
  return result.build?.rolldownOptions ?? result.build?.rollupOptions;
}

/**
 * Fixture: a Pages Router app with both `pages/index.tsx` (static) and
 * `pages/[id].tsx` (dynamic root catch). Models the
 * `test/e2e/middleware-trailing-slash` Next.js fixture: a static `ssr-page`
 * route, a `[id]` dynamic root, plus next.config.js afterFiles rewrites
 * (`/rewrite-1` → `/ssr-page?from=config`) and a middleware that rewrites
 * `/rewrite-me` to `/`. After any rewrite the rewrite target must go
 * through full route resolution — static routes must beat the `[id]`
 * dynamic root.
 */
function writeMiddlewareRewritePriorityFixture(rootDir: string): void {
  fs.mkdirSync(path.join(rootDir, "pages"), { recursive: true });
  const nmLink = path.join(rootDir, "node_modules");
  if (!fs.existsSync(nmLink)) {
    fs.symlinkSync(path.join(process.cwd(), "node_modules"), nmLink);
  }
  fs.writeFileSync(path.join(rootDir, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
  fs.writeFileSync(
    path.join(rootDir, "pages", "index.tsx"),
    `export default function Home() {
  return <p id="home">Hello World</p>;
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "[id].tsx"),
    `export const getServerSideProps = ({ params, query }) => ({
  props: { id: params.id ?? null, q: query.id ?? null },
});
export default function Dynamic({ id, q }: { id: string | null; q: string | null }) {
  return (
    <div>
      <p id="dynamic">Dynamic route</p>
      <p id="id">{id}</p>
      <p id="q">{q}</p>
    </div>
  );
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "about.tsx"),
    `export default function About() {
  return <p id="about">About Page</p>;
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "ssr-page.tsx"),
    `export const getServerSideProps = ({ query }) => ({
  props: { from: query.from ?? null },
});
export default function SsrPage({ from }: { from: string | null }) {
  return (
    <div>
      <p id="ssr">Hello World</p>
      <p id="from">{from ?? ""}</p>
    </div>
  );
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "next.config.js"),
    `module.exports = {
  trailingSlash: true,
  rewrites() {
    return [
      { source: "/rewrite-1", destination: "/ssr-page?from=config" },
    ];
  },
};
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "middleware.ts"),
    `import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export default function middleware(request: NextRequest) {
  const url = new URL(request.url);
  if (url.pathname === "/rewrite-me" || url.pathname === "/rewrite-me/") {
    return NextResponse.rewrite(new URL("/", request.url));
  }
  if (url.pathname === "/rewrite-to-about" || url.pathname === "/rewrite-to-about/") {
    return NextResponse.rewrite(new URL("/about", request.url));
  }
  return NextResponse.next();
}
`,
  );
}

function writeEncodedSlashPagesFixture(rootDir: string): void {
  fs.mkdirSync(path.join(rootDir, "pages", "a"), { recursive: true });
  const nmLink = path.join(rootDir, "node_modules");
  if (!fs.existsSync(nmLink)) {
    fs.symlinkSync(path.join(process.cwd(), "node_modules"), nmLink);
  }
  fs.writeFileSync(path.join(rootDir, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
  fs.writeFileSync(
    path.join(rootDir, "pages", "a", "b.tsx"),
    "export default function Page() { return <div>nested pages route</div>; }\n",
  );
  fs.writeFileSync(
    path.join(rootDir, "middleware.ts"),
    `export const config = { matcher: "/a/b" };
export default function middleware() {
  return new Response("nested blocked", { status: 418 });
}
`,
  );
}

function writeGsspAppInitialPropsContextFixture(rootDir: string): void {
  fs.mkdirSync(path.join(rootDir, "pages", "blog", "[post]"), { recursive: true });
  fs.mkdirSync(path.join(rootDir, "pages", "rewrite-target"), { recursive: true });
  const nmLink = path.join(rootDir, "node_modules");
  if (!fs.existsSync(nmLink)) {
    fs.symlinkSync(path.join(process.cwd(), "node_modules"), nmLink);
  }
  fs.writeFileSync(
    path.join(rootDir, "next.config.js"),
    `module.exports = {
  generateBuildId: () => "test-build-id",
  async rewrites() {
    return [
      { source: "/blog-post-1", destination: "/blog/post-1" },
      { source: "/blog-post-2", destination: "/blog/post-2?hello=world" },
      { source: "/blog-:param", destination: "/blog/post-3" },
      { source: "/rewrite-source/:path+", destination: "/rewrite-target" },
    ];
  },
};
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "_app.jsx"),
    `import App from "next/app";

class MyApp extends App {
  static async getInitialProps(ctx) {
    const { req, query, pathname, asPath } = ctx.ctx;
    let pageProps = {};

    if (ctx.Component.getInitialProps) {
      pageProps = await ctx.Component.getInitialProps(ctx.ctx);
    }

    return {
      appProps: {
        url: (req || {}).url,
        query,
        pathname,
        asPath,
      },
      pageProps,
    };
  }

  render() {
    const { Component, pageProps, appProps, router } = this.props;
    return <Component {...pageProps} appProps={appProps} appRouter={router} />;
  }
}

export default MyApp;
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "blog", "[post]", "index.jsx"),
    `import { useRouter } from "next/router";

export async function getServerSideProps({ params, resolvedUrl }) {
  return {
    props: {
      params,
      resolvedUrl,
      post: params.post,
    },
  };
}

export default function BlogPost({ post, params, appProps, appRouter, resolvedUrl }) {
  const router = useRouter();

  return (
    <>
      <p>Post: {post}</p>
      <div id="params">{JSON.stringify(params)}</div>
      <div id="query">{JSON.stringify(router.query)}</div>
      <div id="app-query">{JSON.stringify(appProps.query)}</div>
      <div id="app-url">{appProps.url}</div>
      <div id="app-router-pathname">{appRouter.pathname}</div>
      <div id="resolved-url">{resolvedUrl}</div>
      <div id="as-path">{router.asPath}</div>
    </>
  );
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "something.jsx"),
    `import { useRouter } from "next/router";

export async function getServerSideProps({ params, query, resolvedUrl }) {
  return {
    props: {
      resolvedUrl,
      world: "world",
      query: query || {},
      params: params || {},
    },
  };
}

export default function Something({ world, params, query, appProps, resolvedUrl }) {
  const router = useRouter();

  return (
    <>
      <p>hello: {world}</p>
      <div id="params">{JSON.stringify(params)}</div>
      <div id="initial-query">{JSON.stringify(query)}</div>
      <div id="query">{JSON.stringify(router.query)}</div>
      <div id="app-query">{JSON.stringify(appProps.query)}</div>
      <div id="app-url">{appProps.url}</div>
      <div id="resolved-url">{resolvedUrl}</div>
      <div id="as-path">{router.asPath}</div>
    </>
  );
}
`,
  );
  fs.writeFileSync(
    path.join(rootDir, "pages", "rewrite-target", "index.jsx"),
    `import { useRouter } from "next/router";

export async function getServerSideProps({ req }) {
  return { props: { url: req.url } };
}

export default function RewriteTarget({ url }) {
  const router = useRouter();

  return (
    <>
      <h1>rewrite-target</h1>
      <p id="as-path">{router.asPath}</p>
      <p id="req-url">{url}</p>
    </>
  );
}
`,
  );
}

async function buildPagesFixtureToOutDir(rootDir: string, outDir: string): Promise<void> {
  await build({
    root: rootDir,
    configFile: false,
    plugins: [vinext({ disableAppRouter: true })],
    logLevel: "silent",
    build: {
      outDir: path.join(outDir, "server"),
      ssr: "virtual:vinext-server-entry",
      rollupOptions: { output: { entryFileNames: "entry.js" } },
    },
  });

  await build({
    root: rootDir,
    configFile: false,
    plugins: [vinext({ disableAppRouter: true })],
    logLevel: "silent",
    build: {
      outDir: path.join(outDir, "client"),
      manifest: true,
      ssrManifest: true,
      rollupOptions: { input: "virtual:vinext-client-entry" },
    },
  });
}

function unwrapStartedProdServer(
  result: import("node:http").Server | { server: import("node:http").Server },
): import("node:http").Server {
  return "server" in result ? result.server : result;
}

type CapturedStreamResponse = {
  body: Buffer;
  headers: IncomingHttpHeaders;
  statusCode: number;
  firstChunkMs: number;
  endMs: number;
  snapshot: Buffer;
  rawBody: Buffer;
  rawSnapshot: Buffer;
};

function createResponseDecoder(
  contentEncoding: string | string[] | undefined,
): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | null {
  const encoding = Array.isArray(contentEncoding) ? contentEncoding[0] : contentEncoding;
  switch (encoding) {
    case undefined:
      return null;
    case "br":
      return zlib.createBrotliDecompress();
    case "gzip":
      return zlib.createGunzip();
    case "deflate":
      return zlib.createInflate();
    default:
      return null;
  }
}

async function captureStreamedResponse(
  url: string,
  options: { headers?: Record<string, string>; snapshotDelayMs?: number } = {},
): Promise<CapturedStreamResponse> {
  const { headers = {}, snapshotDelayMs = 120 } = options;

  return await new Promise<CapturedStreamResponse>((resolve, reject) => {
    const startedAt = Date.now();
    const req = httpRequest(url, { headers }, (res) => {
      const rawChunks: Buffer[] = [];
      const decodedChunks: Buffer[] = [];
      let firstChunkMs = -1;
      let snapshot = Buffer.alloc(0);
      let rawSnapshot = Buffer.alloc(0);
      let snapshotCaptured = false;
      let snapshotTimer: ReturnType<typeof setTimeout> | undefined;
      const decoder = createResponseDecoder(res.headers["content-encoding"]);

      const captureSnapshot = () => {
        if (snapshotCaptured) return;
        snapshotCaptured = true;
        rawSnapshot = Buffer.concat(rawChunks);
        snapshot = Buffer.concat(decodedChunks);
      };

      const observeDecodedChunk = (chunk: Buffer) => {
        decodedChunks.push(Buffer.from(chunk));
        if (firstChunkMs !== -1) return;
        firstChunkMs = Date.now() - startedAt;
        snapshotTimer = setTimeout(captureSnapshot, snapshotDelayMs);
      };

      res.on("data", (chunk: Buffer) => {
        const rawChunk = Buffer.from(chunk);
        rawChunks.push(rawChunk);
        if (decoder) {
          decoder.write(rawChunk);
        } else {
          observeDecodedChunk(rawChunk);
        }
      });

      res.on("error", reject);

      if (decoder) {
        decoder.on("data", (chunk: Buffer) => {
          observeDecodedChunk(Buffer.from(chunk));
        });
        decoder.on("error", reject);
      }

      const finishResponse = async () => {
        try {
          if (decoder) {
            decoder.end();
            await new Promise<void>((resolveDecoder, rejectDecoder) => {
              decoder.once("end", () => resolveDecoder());
              decoder.once("error", rejectDecoder);
            });
          }
          if (snapshotTimer) clearTimeout(snapshotTimer);
          captureSnapshot();
          resolve({
            body: Buffer.concat(decodedChunks),
            headers: res.headers,
            statusCode: res.statusCode ?? 0,
            firstChunkMs,
            endMs: Date.now() - startedAt,
            snapshot,
            rawBody: Buffer.concat(rawChunks),
            rawSnapshot,
          });
        } catch (error) {
          reject(error);
        }
      };

      res.on("end", () => {
        void finishResponse();
      });
    });

    req.on("error", reject);
    req.end();
  });
}

function findBuildManifestEntries(
  buildManifest: Record<string, ClientBuildManifestEntry>,
  moduleId: string,
): Array<[string, ClientBuildManifestEntry]> {
  return Object.entries(buildManifest).filter(
    ([key]) => key === moduleId || key.endsWith(`/${moduleId}`),
  );
}

describe("Pages Router integration", () => {
  let server: ViteDevServer;
  let baseUrl: string;

  beforeAll(async () => {
    ({ server, baseUrl } = await startFixtureServer(FIXTURE_DIR));
  });

  afterAll(async () => {
    await server?.close();
  });

  it("renders the index page with correct HTML", async () => {
    const res = await fetch(`${baseUrl}/`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("text/html");

    const html = await res.text();
    expect(html).toContain("Hello, vinext!");
    expect(html).toContain("This is a Pages Router app running on Vite.");
    expect(html).toContain("Go to About");
  });

  it("sets optimizeDeps.entries for pages and instrumentation hooks so deps are discovered at startup", () => {
    const entries = server.config.optimizeDeps?.entries;

    expect(entries).toBeDefined();
    expect(Array.isArray(entries)).toBe(true);

    const glob = (entries as string[]).join(",");
    expect(glob).toMatch(/pages\/\*\*\/\*\.\{tsx,ts,jsx,js\}/);
    expect(glob).toContain("instrumentation.ts");
    expect(glob).toContain("instrumentation-client.ts");
  });

  it("resolves tsconfig path aliases (@/ imports)", async () => {
    const res = await fetch(`${baseUrl}/alias-test`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Pages Alias Test");
    // Component imported via @/components/heavy
    expect(html).toContain("Loaded via alias");
  });

  it("renders the about page", async () => {
    const res = await fetch(`${baseUrl}/about`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("About");
    expect(html).toContain("This is the about page.");
  });

  // Refs #1463: Pages Router should reject non-GET/HEAD methods to static
  // (no `getServerSideProps`) pages with a 405 + `Allow: GET, HEAD`.
  // Ported from Next.js: test/e2e/prerender.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/prerender.test.ts
  // ('should respond with 405 for POST to static page').
  it("returns 405 with Allow: GET, HEAD on POST to a static Pages page", async () => {
    const res = await fetch(`${baseUrl}/about`, { method: "POST" });
    expect(res.status).toBe(405);
    expect(res.headers.get("allow")).toBe("GET, HEAD");
    expect(await res.text()).toContain("Method Not Allowed");
  });

  // Refs #1463: GSP (getStaticProps) pages are also "static" from the
  // routing perspective; POST should produce 405. Mirrors the Next.js
  // condition `(typeof components.Component === 'string' || isSSG)` in
  // `.nextjs-ref/packages/next/src/server/base-server.ts` around L2287.
  it("returns 405 with Allow: GET, HEAD on POST to a GSP page", async () => {
    const res = await fetch(`${baseUrl}/isr-test`, { method: "POST" });
    expect(res.status).toBe(405);
    expect(res.headers.get("allow")).toBe("GET, HEAD");
  });

  // GET/HEAD must continue to work — guards against an over-broad fix.
  it("HEAD on a static Pages page still returns 200", async () => {
    const res = await fetch(`${baseUrl}/about`, { method: "HEAD" });
    expect(res.status).toBe(200);
  });

  // SSR pages (those with getServerSideProps) must NOT be blocked: the
  // page may legitimately read req.method inside getServerSideProps.
  // Next.js gates 405 on `(typeof components.Component === 'string' || isSSG)`
  // — gSSP routes are neither.
  it("does not return 405 on POST to a getServerSideProps page", async () => {
    const res = await fetch(`${baseUrl}/ssr`, { method: "POST" });
    expect(res.status).not.toBe(405);
  });

  // Tests that React 19 SSR preserves literal string action attributes.
  // Note: This is NOT testing server action invocation (unlike the upstream
  // Next.js test action-in-pages-router.test.ts which tests "use server" functions).
  // Regression test for issue #1476.
  it("preserves literal action:foo in form action attribute", async () => {
    const res = await fetch(`${baseUrl}/action-string-test`);
    expect(res.status).toBe(200);

    const html = await res.text();
    // React 19 may strip action: strings if it mistakes them for server action IDs.
    expect(html).toContain('action="action:foo"');
  });

  // Ported from Next.js: test/e2e/async-modules/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts
  it("renders pages that use top-level await (async modules)", async () => {
    const res = await fetch(`${baseUrl}/async-modules-test`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain('<div id="app-value">hello</div>');
    expect(html).toContain('<div id="page-value">42</div>');
  });

  it("adds middleware CSP nonces to Pages Router next data", async () => {
    const res = await fetch(`${baseUrl}/dynamic-page?mw-csp-nonce=pages-response`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-security-policy")).toBe(
      "script-src 'nonce-pages-response' 'strict-dynamic';",
    );

    const html = await res.text();
    expect(html).toContain(
      '<script id="__NEXT_DATA__" type="application/json" nonce="pages-response">',
    );
  });

  it("does not serve cached Pages ISR HTML to CSP nonce requests", async () => {
    const first = await fetch(`${baseUrl}/isr-test`);
    expect(first.status).toBe(200);
    expect(first.headers.get("x-vinext-cache")).toBe("MISS");
    expect(first.headers.get("x-nextjs-cache")).toBe("MISS");
    const firstHtml = await first.text();
    expect(firstHtml).not.toContain("nonce=");

    const cached = await fetch(`${baseUrl}/isr-test`);
    expect(cached.status).toBe(200);
    expect(cached.headers.get("x-vinext-cache")).toBe("HIT");
    expect(cached.headers.get("x-nextjs-cache")).toBe("HIT");
    await cached.text();

    const second = await fetch(`${baseUrl}/isr-test?mw-csp-nonce=pages-isr`);
    expect(second.status).toBe(200);
    expect(second.headers.get("content-security-policy")).toBe(
      "script-src 'nonce-pages-isr' 'strict-dynamic';",
    );
    expect(second.headers.get("cache-control")).toBe("no-store, must-revalidate");
    expect(second.headers.get("x-vinext-cache")).toBeNull();
    const secondHtml = await second.text();
    expect(secondHtml).toContain(
      '<script id="__NEXT_DATA__" type="application/json" nonce="pages-isr">',
    );
  });

  it("renders the SSR page with getServerSideProps data", async () => {
    const res = await fetch(`${baseUrl}/ssr`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Server-Side Rendered");
    expect(html).toContain("Hello from getServerSideProps");
    // Should have a timestamp
    expect(html).toContain("Rendered at:");
  });

  // Regression test for #1459: Next.js explicitly supports a Promise value
  // for `getServerSideProps` `props`. vinext must `await` the value before
  // serialising — otherwise pageProps end up as a Promise and the rendered
  // page shows empty values.
  it("awaits Promise-shaped getServerSideProps props", async () => {
    const res = await fetch(`${baseUrl}/ssr-promise-props`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("SSR Promise Props");
    expect(html).toContain("world");
    // React SSR inserts a `<!-- -->` comment between text and expressions.
    expect(html).toMatch(/count:\s*(<!--\s*-->)?\s*42/);
    // The serialized __NEXT_DATA__ payload must contain the resolved values
    // (not an empty pageProps object).
    expect(html).toMatch(/"pageProps":\s*\{[^}]*"hello":\s*"world"/);
  });

  // Regression test for #1354: when a page declares `getServerSideProps` as
  // a local `const` and exports it via `export { getServerSideProps }`, the
  // client-bundle transform must strip the export specifier without
  // redeclaring the identifier. Prior to the fix, the build failed with
  // `Identifier 'getServerSideProps' has already been declared` under OXC.
  it("renders a page that exports gSSP via `export { ... }` named re-export", async () => {
    const res = await fetch(`${baseUrl}/gssp-named-export`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("gSSP via named export");
    expect(html).toContain("Hello from named-export gSSP");
  });

  it("getServerSideProps headers and status are applied to the response", async () => {
    const res = await fetch(`${baseUrl}/ssr-headers`);
    // gSSP sets statusCode = 201
    expect(res.status).toBe(201);
    const html = await res.text();
    expect(html).toContain("Headers were set");
    // Custom header set via res.setHeader
    expect(res.headers.get("x-custom-header")).toBe("hello-from-gssp");
    // Cookie set via res.setHeader("set-cookie", ...)
    const setCookie = res.headers.get("set-cookie");
    expect(setCookie).toContain("gssp_token=abc123");
  });

  // Regression for #1461: gSSP responses must carry the default Cache-Control
  // header that Next.js applies for getServerSideProps pages so CDNs and
  // browsers do not cache the per-request payload.
  it("sets the default Cache-Control header on getServerSideProps responses", async () => {
    const res = await fetch(`${baseUrl}/ssr`);
    expect(res.status).toBe(200);
    expect(res.headers.get("cache-control")).toBe(
      "private, no-cache, no-store, max-age=0, must-revalidate",
    );
  });

  // Regression for #1461: when getServerSideProps overrides Cache-Control via
  // res.setHeader, the user-provided value must reach the final HTTP response
  // instead of being clobbered by the default.
  it("preserves res.setHeader Cache-Control overrides set in getServerSideProps", async () => {
    const res = await fetch(`${baseUrl}/ssr-cache-control`);
    expect(res.status).toBe(200);
    expect(res.headers.get("cache-control")).toBe("public, max-age=42");
  });

  it("getServerSideProps calling res.end() short-circuits the response", async () => {
    const res = await fetch(`${baseUrl}/ssr-res-end`);
    // gSSP calls res.end() with a JSON body and status 202
    expect(res.status).toBe(202);
    expect(res.headers.get("content-type")).toBe("application/json");
    expect(res.headers.get("content-length")).toBe("35");
    const body = await res.json();
    expect(body).toEqual({ ok: true, source: "gssp-res-end" });
  });

  it("getServerSideProps returning notFound renders custom 404 page", async () => {
    const res = await fetch(`${baseUrl}/posts/missing`);
    expect(res.status).toBe(404);
    const html = await res.text();
    // Should render the custom 404 page (pages/404.tsx), not plain text
    expect(html).toContain("Page Not Found");
    // Should be wrapped in the _app layout
    expect(html).toContain("app-wrapper");
  });

  // Regression for #1465: a getServerSideProps `{ redirect }` on an HTML
  // request emits a real HTTP redirect (so a hard navigation lands on the
  // destination).
  it("getServerSideProps returning redirect emits an HTTP redirect on HTML requests", async () => {
    const res = await fetch(`${baseUrl}/gssp-redirect`, { redirect: "manual" });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toBe("/gssp-redirect-target");
  });

  // Regression for #1465: a getServerSideProps `{ redirect }` on a `_next/data`
  // request must NOT emit an HTTP redirect (fetch would follow it to non-JSON
  // HTML). Instead Next.js returns a 200 JSON envelope carrying `__N_REDIRECT`
  // / `__N_REDIRECT_STATUS` in pageProps, which the client router uses to
  // re-dispatch a fresh navigation (superseding the in-flight one). See
  // packages/next/src/server/render.tsx (`__N_REDIRECT`).
  it("getServerSideProps redirect returns __N_REDIRECT JSON on _next/data requests", async () => {
    // The dev `_next/data` endpoint matches the build id
    // `nextConfig.buildId ?? __VINEXT_BUILD_ID ?? "development"`; this fixture's
    // next.config.mjs sets `generateBuildId: () => "test-build-id"` (see
    // index.ts `_next/data` normalization).
    const res = await fetch(`${baseUrl}/_next/data/test-build-id/gssp-redirect.json`, {
      headers: { Accept: "application/json", "x-nextjs-data": "1" },
      redirect: "manual",
    });

    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/json");
    expect(res.headers.get("location")).toBeNull();

    const body = (await res.json()) as {
      __N_SSP?: boolean;
      appProps?: Record<string, unknown>;
      pageProps?: Record<string, unknown>;
    };
    expect((body as { __N_SSP?: boolean }).__N_SSP).toBe(true);
    expect(body.pageProps?.__N_REDIRECT).toBe("/gssp-redirect-target");
    expect(body.pageProps?.__N_REDIRECT_STATUS).toBe(307);
  });

  // The data envelope must carry an EXTERNAL redirect destination verbatim so
  // the client router hard-navigates to the right place (the client must not
  // fall back to the originating page URL — that would loop). See
  // handleDataRedirect() in shims/router.ts.
  it("preserves an external destination in __N_REDIRECT on _next/data requests", async () => {
    const res = await fetch(`${baseUrl}/_next/data/test-build-id/gssp-redirect-external.json`, {
      headers: { Accept: "application/json", "x-nextjs-data": "1" },
      redirect: "manual",
    });

    expect(res.status).toBe(200);
    const body = (await res.json()) as { pageProps?: Record<string, unknown> };
    expect(body.pageProps?.__N_REDIRECT).toBe("https://example.com/landing");
  });

  // Regression for #1458: when getServerSideProps throws, dev (and prod) must
  // render the user's custom pages/500.tsx with status 500 rather than the
  // plain "Internal Server Error" text. Mirrors Next.js test/e2e/getserversideprops
  // "should handle throw ENOENT correctly".
  it("getServerSideProps throwing renders custom 500 page (dev)", async () => {
    const res = await fetch(`${baseUrl}/gssp-throw`);
    expect(res.status).toBe(500);
    const html = await res.text();
    expect(html).toContain("custom pages/500");
    expect(html).not.toBe("Internal Server Error");
  });

  it("renders dynamic routes with params", async () => {
    const res = await fetch(`${baseUrl}/posts/42`);
    expect(res.status).toBe(200);

    const html = await res.text();
    // React SSR inserts comment nodes between text and expressions:
    // "Post: <!-- -->42" — so we match with a regex instead
    expect(html).toMatch(/Post:\s*(<!--\s*-->)?\s*42/);
    expect(html).toContain("post-title");
    // Router should have correct pathname and query during SSR
    expect(html).toMatch(/Pathname:\s*(<!--\s*-->)?\s*\/posts\/\[id\]/);
    expect(html).toMatch(/Query ID:\s*(<!--\s*-->)?\s*42/);
  });

  it("keeps dynamic route params ahead of same-key search params during SSR", async () => {
    const res = await fetch(`${baseUrl}/posts/42?id=evil`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toMatch(/Post:\s*(<!--\s*-->)?\s*42/);
    expect(html).toMatch(/Query ID:\s*(<!--\s*-->)?\s*42/);
    expect(html).not.toMatch(/Query ID:\s*(<!--\s*-->)?\s*evil/);
  });

  it("next/compat/router: useRouter returns router object in Pages Router context", async () => {
    const res = await fetch(`${baseUrl}/compat-router-test`);
    expect(res.status).toBe(200);

    const html = await res.text();
    // The shared component detects Pages Router context (router !== null)
    expect(html).toContain('data-testid="router-context"');
    expect(html).toContain("pages-router");
    // The router pathname should reflect the current page
    expect(html).toContain('data-testid="router-pathname"');
    expect(html).toContain("/compat-router-test");
  });

  // Ported from Next.js: test/e2e/app-dir/params-hooks-compat/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/params-hooks-compat/index.test.ts
  // Under a static Pages Router SSR render, `next/navigation` sees the same
  // pre-ready Pages router state that the client uses for hydration. The ready
  // browser transition is covered by the app-router/pages-router-use-params e2e.
  it("next/navigation useParams is null for a pre-ready static Pages Router render", async () => {
    const res = await fetch(`${baseUrl}/nav-compat/foobar?a=pages`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const paramsMatch = html.match(/<pre id="use-params">([^<]*)<\/pre>/);
    expect(paramsMatch).not.toBeNull();
    expect(paramsMatch![1]).toBe("null");
  });

  it("next/navigation useSearchParams is empty for a pre-ready static Pages Router render", async () => {
    const res = await fetch(`${baseUrl}/nav-compat/foobar?q=pages`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const searchMatch = html.match(/<pre id="use-search-params">([^<]*)<\/pre>/);
    expect(searchMatch).not.toBeNull();
    const search = JSON.parse(searchMatch![1].replaceAll("&quot;", '"'));
    expect(search).toEqual({});
  });

  it("next/navigation defers a dynamic getStaticProps Pages route when rewrites are configured", async () => {
    const res = await fetch(`${baseUrl}/nav-compat-gsp/foobar`);
    expect(res.status).toBe(200);
    const html = await res.text();

    const paramsMatch = html.match(/<pre id="use-params">([^<]*)<\/pre>/);
    expect(paramsMatch).not.toBeNull();
    expect(paramsMatch![1]).toBe("null");

    const searchMatch = html.match(/<pre id="use-search-params">([^<]*)<\/pre>/);
    expect(searchMatch).not.toBeNull();
    const search = JSON.parse(searchMatch![1].replaceAll("&quot;", '"'));
    expect(search).toEqual({});
  });

  it("next/navigation treats Page.getInitialProps Pages routes as ready during SSR", async () => {
    const res = await fetch(`${baseUrl}/nav-compat-gip/foobar?q=pages`);
    expect(res.status).toBe(200);
    const html = await res.text();

    const paramsMatch = html.match(/<pre id="use-params">([^<]*)<\/pre>/);
    expect(paramsMatch).not.toBeNull();
    expect(JSON.parse(paramsMatch![1].replaceAll("&quot;", '"'))).toEqual({ slug: "foobar" });

    const searchMatch = html.match(/<pre id="use-search-params">([^<]*)<\/pre>/);
    expect(searchMatch).not.toBeNull();
    const search = JSON.parse(searchMatch![1].replaceAll("&quot;", '"'));
    expect(search).toEqual({ q: "pages" });
  });

  it("does not collapse encoded slashes onto nested routes in dev", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-pages-encoded-dev-"));
    writeEncodedSlashPagesFixture(tmpDir);

    let tempServer: ViteDevServer | undefined;
    try {
      const started = await startFixtureServer(tmpDir);
      tempServer = started.server;

      const encodedRes = await fetch(`${started.baseUrl}/a%2Fb`);
      expect(encodedRes.status).toBe(404);
      expect(await encodedRes.text()).not.toContain("nested blocked");

      const nestedRes = await fetch(`${started.baseUrl}/a/b`);
      expect(nestedRes.status).toBe(418);
      expect(await nestedRes.text()).toBe("nested blocked");
    } finally {
      await tempServer?.close();
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("returns 404 with custom 404 page for non-existent routes", async () => {
    const res = await fetch(`${baseUrl}/nonexistent`);
    expect(res.status).toBe(404);
    const html = await res.text();
    // Should render the custom 404 page
    expect(html).toContain("404 - Page Not Found");
    expect(html).toContain("does not exist");
  });

  it("renders next/head tags in SSR HTML <head>", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    // Index page has <Head><title>Hello vinext</title></Head>
    // This should appear in the actual <head> of the HTML
    expect(html).toContain("<title");
    expect(html).toContain("Hello vinext");
    // The title tag should be in <head>, not in <body>
    const headSection = html.split("</head>")[0];
    expect(headSection).toContain("Hello vinext");
  });

  it("caches the streamed ISR render without carrying prior render state", async () => {
    const firstRes = await fetch(`${baseUrl}/isr-second-render-state`);
    expect(firstRes.status).toBe(200);
    expect(firstRes.headers.get("x-vinext-cache")).toBe("MISS");
    const firstHtml = await firstRes.text();
    expect(firstHtml).toContain('data-testid="head-before">0<');
    expect(firstHtml).toContain('data-testid="private-cache-before">0<');
    expect(firstHtml).toContain('data-testid="inserted-html-before">0<');

    const secondRes = await fetch(`${baseUrl}/isr-second-render-state`);
    expect(secondRes.status).toBe(200);
    expect(secondRes.headers.get("x-vinext-cache")).toBe("HIT");
    const secondHtml = await secondRes.text();
    expect(secondHtml).toContain('data-testid="head-before">0<');
    expect(secondHtml).toContain('data-testid="private-cache-before">0<');
    expect(secondHtml).toContain('data-testid="inserted-html-before">0<');
  });

  it("includes __NEXT_DATA__ script tag", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    expect(html).toContain("__NEXT_DATA__");
  });

  // Dev/prod parity: the production client entry exposes
  // `window.__VINEXT_PAGE_PATTERNS__` so the next/navigation compat hooks can
  // resolve a dynamic route pattern from a resolved path. Dev must expose the
  // same global (in Next.js bracket format, including dynamic patterns) so the
  // hooks behave identically in both runtimes.
  it("exposes __VINEXT_PAGE_PATTERNS__ in dev for next/navigation compat", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    // Route patterns contain `]` (e.g. "/posts/[slug]"), so anchor the capture
    // on the closing `</script>` rather than the first `]`.
    const match = html.match(/window\.__VINEXT_PAGE_PATTERNS__=(\[.*?\])<\/script>/);
    expect(match).toBeTruthy();
    const patterns = JSON.parse(match![1]!) as string[];
    expect(Array.isArray(patterns)).toBe(true);
    // pages-basic has dynamic routes — they must be serialized in bracket form.
    expect(patterns.some((p) => p.includes("["))).toBe(true);
  });

  it("includes the Vite client script for HMR", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    expect(html).toContain("@vite/client");
  });

  it("installs the vinext dev error overlay in the hydration script", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    const hydrationProxyPath = html.match(
      /<script type="module" src="([^"]*html-proxy[^"]*)"><\/script>/,
    )?.[1];
    expect(hydrationProxyPath).toBeDefined();

    const hydrationProxy = await fetch(new URL(hydrationProxyPath!, baseUrl)).then((response) =>
      response.text(),
    );
    expect(hydrationProxy).toContain("dev-error-overlay");
    expect(hydrationProxy).toContain("overlay.installDevErrorOverlay()");
    expect(hydrationProxy).toContain("overlay.installViteHmrErrorHandler(import.meta.hot)");
    expect(hydrationProxy).toContain("overlay.reportInitialDevServerErrors()");
    expect(hydrationProxy).toContain(
      'hydrateRoot(document.getElementById("__next"), element, hydrateRootOptions)',
    );
  });

  it("wraps pages with custom _app.tsx", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    // _app.tsx wraps with an #app-wrapper div and a global nav
    expect(html).toContain("app-wrapper");
    expect(html).toContain("My App");
  });

  it("_app.tsx wrapping works on all pages", async () => {
    const res = await fetch(`${baseUrl}/about`);
    const html = await res.text();
    expect(html).toContain("app-wrapper");
    expect(html).toContain("About");
  });

  it("uses custom _document.tsx for HTML shell", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    // Custom _document sets lang="en" on <html>
    expect(html).toContain('lang="en"');
    // Custom _document adds a meta description
    expect(html).toContain("A vinext test app");
    // Custom _document sets className on body
    expect(html).toContain("custom-body");
  });

  // --- API Routes ---

  it("handles API routes returning JSON", async () => {
    const res = await fetch(`${baseUrl}/api/hello`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/json");

    const data = await res.json();
    expect(data).toEqual({ message: "Hello from API!" });
  });

  it("handles dynamic API routes with query params", async () => {
    const res = await fetch(`${baseUrl}/api/users/123`);
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data).toEqual({ user: { id: "123", name: "User 123" } });
  });

  it("keeps dynamic API route params ahead of same-key query params", async () => {
    const res = await fetch(`${baseUrl}/api/users/123?id=evil`);
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data).toEqual({ user: { id: "123", name: "User 123" } });
  });

  // Ported from Next.js: test/integration/api-support/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/integration/api-support/test/index.test.ts
  it("returns 400 for invalid JSON bodies on Pages API routes", async () => {
    const res = await fetch(`${baseUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: `{"message":Invalid"}`,
    });

    expect(res.status).toBe(400);
    expect(res.statusText).toBe("Invalid JSON");
    expect(await res.text()).toBe("Invalid JSON");
  });

  it("parses empty JSON bodies on Pages API routes as {}", async () => {
    const res = await fetch(`${baseUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: "",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({});
  });

  it("preserves duplicate urlencoded body keys on Pages API routes", async () => {
    const res = await fetch(`${baseUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "tag=a&tag=b&tag=c",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ tag: ["a", "b", "c"] });
  });

  it("parses empty urlencoded bodies on Pages API routes as {}", async () => {
    const res = await fetch(`${baseUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({});
  });

  it("parses application/ld+json bodies on Pages API routes", async () => {
    const res = await fetch(`${baseUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/ld+json; charset=utf-8" },
      body: JSON.stringify({ title: "doc" }),
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ title: "doc" });
  });

  it("sends Buffer payloads from res.send() as raw bytes", async () => {
    const res = await fetch(`${baseUrl}/api/send-buffer`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/octet-stream");
    expect(res.headers.get("content-length")).toBe("3");

    const body = Buffer.from(await res.arrayBuffer());
    expect(body.equals(Buffer.from([1, 2, 3]))).toBe(true);
  });

  it("returns 404 for non-existent API routes", async () => {
    const res = await fetch(`${baseUrl}/api/nonexistent`);
    expect(res.status).toBe(404);
  });

  // Regression coverage for cloudflare/vinext#1338 — Pages Router edge
  // runtime API routes (`export const config = { runtime: 'edge' }`) must
  // execute and return the user's Web Response (200), not 500.
  //
  // Ported from Next.js: test/e2e/edge-pages-support/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts
  it("serves Pages Router edge runtime API routes (export const config = { runtime: 'edge' })", async () => {
    const res = await fetch(`${baseUrl}/api/edge-hello?a=b`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/json");

    const data = await res.json();
    expect(data).toEqual({
      hello: "world",
      query: { a: "b" },
    });
  });

  // 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("passes middleware rewrite search params to Pages Router edge API nextUrl", async () => {
    const res = await fetch(`${baseUrl}/api/edge-search-params?a=b`);
    expect(res.status).toBe(200);
    await expect(res.json()).resolves.toEqual({
      a: "b",
      foo: "bar",
    });
  });

  // Ported from Next.js: test/e2e/edge-pages-support/index.test.ts and
  // packages/next/src/server/next-server.ts (`runEdgeFunction`).
  it("preserves the original pathname and adds route params for rewritten edge APIs", async () => {
    const res = await fetch(`${baseUrl}/edge-api-rewrite/id-1?a=b`);
    expect(res.status).toBe(200);
    await expect(res.json()).resolves.toEqual({
      pathname: "/edge-api-rewrite/id-1",
      query: {
        a: "b",
        foo: "bar",
        id: "id-1",
      },
    });
  });

  // Regression coverage for cloudflare/vinext#1338 — Pages Router OG image
  // routes using `next/og` ImageResponse with `runtime: 'edge'` must execute
  // and return image/png, not 404.
  //
  // Ported from Next.js: test/e2e/og-api/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/og-api/index.test.ts
  it("serves Pages Router OG image routes (next/og + edge runtime)", async () => {
    const res = await fetch(`${baseUrl}/api/og`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("image/png");
    const body = await res.blob();
    expect(body.size).toBeGreaterThan(0);
  });

  // --- Client Hydration ---

  it("includes hydration script for client-side rendering", async () => {
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    // Vite extracts inline module scripts into html-proxy modules.
    // The hydration script becomes a <script type="module" src="...html-proxy...">
    expect(html).toMatch(/html-proxy.*\.js/);
  });

  // --- Catch-all Routes ---

  it("renders catch-all routes with multiple segments", async () => {
    const res = await fetch(`${baseUrl}/docs/getting-started/install`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Docs");
    expect(html).toMatch(/Path:\s*(<!--\s*-->)?\s*getting-started\/install/);
  });

  it("renders catch-all routes with single segment", async () => {
    const res = await fetch(`${baseUrl}/docs/intro`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toMatch(/Path:\s*(<!--\s*-->)?\s*intro/);
  });

  // --- Hyphenated param names (issue #71) ---

  it("renders optional catch-all with hyphenated param name [[...sign-up]]", async () => {
    const res = await fetch(`${baseUrl}/sign-up`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Sign Up");
    expect(html).toContain('data-testid="sign-up-page"');
    expect(html).toMatch(/Segments:.*0/);
    expect(html).toContain("(root)");
  });

  it("renders hyphenated optional catch-all with segments", async () => {
    const res = await fetch(`${baseUrl}/sign-up/step/2`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Sign Up");
    expect(html).toMatch(/Segments:.*2/);
  });

  // --- Hydration ---

  // --- next.config.js ---

  it("applies redirects from next.config.js", async () => {
    const res = await fetch(`${baseUrl}/old-about`, { redirect: "manual" });
    expect(res.status).toBe(308);
    expect(res.headers.get("location")).toBe("/about");
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // and
  // 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("applies next.config.js headers using the pre-middleware pathname after a rewrite in dev", async () => {
    const res = await fetch(`${baseUrl}/headers-before-middleware-rewrite`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-rewrite-source-header")).toBe("1");
    const html = await res.text();
    expect(html).toContain("Server-Side Rendered");
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // and
  // 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("applies next.config.js redirects before middleware rewrites in dev", async () => {
    const res = await fetch(`${baseUrl}/redirect-before-middleware-rewrite`, {
      redirect: "manual",
    });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  it("applies next.config.js redirects before middleware responses in dev", async () => {
    const res = await fetch(`${baseUrl}/redirect-before-middleware-response`, {
      redirect: "manual",
    });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  it("applies redirects with repeated dynamic params in the destination", async () => {
    const res = await fetch(`${baseUrl}/repeat-redirect/hello`, { redirect: "manual" });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toBe("/docs/hello/hello");
  });

  it("applies custom headers from next.config.js", async () => {
    const res = await fetch(`${baseUrl}/api/hello`);
    expect(res.headers.get("x-custom-header")).toBe("vinext");
  });

  // Ported from PR #47 by @ibruno
  it("applies has/missing conditions for next.config.js headers", async () => {
    const guestRes = await fetch(`${baseUrl}/about`);
    expect(guestRes.status).toBe(200);
    expect(guestRes.headers.get("x-guest-only-header")).toBe("1");
    expect(guestRes.headers.get("x-auth-only-header")).toBeNull();

    const authRes = await fetch(`${baseUrl}/about`, {
      headers: { Cookie: "logged-in=1" },
    });
    expect(authRes.status).toBe(200);
    expect(authRes.headers.get("x-auth-only-header")).toBe("1");
    expect(authRes.headers.get("x-guest-only-header")).toBeNull();
  });

  it("applies beforeFiles rewrites from next.config.js", async () => {
    const res = await fetch(`${baseUrl}/before-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  it("applies rewrites with repeated dynamic params in the destination", async () => {
    const res = await fetch(`${baseUrl}/repeat-rewrite/hello`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("hello/hello");
  });

  it("applies afterFiles rewrites from next.config.js", async () => {
    const res = await fetch(`${baseUrl}/after-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  it("does not let afterFiles rewrites override static page routes in dev", async () => {
    const res = await fetch(`${baseUrl}/nav-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Navigation Test");
    expect(html).not.toContain("This is the about page.");
  });

  it("applies fallback rewrites from next.config.js", async () => {
    const res = await fetch(`${baseUrl}/fallback-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  // ── Percent-encoded paths should be decoded before config matching ──

  it("percent-encoded redirect path is decoded before config matching (dev)", async () => {
    // /%6Fld-%61bout decodes to /old-about → /about (permanent redirect)
    const res = await fetch(`${baseUrl}/%6Fld-%61bout`, { redirect: "manual" });
    expect(res.status).toBe(308);
    expect(res.headers.get("location")).toBe("/about");
  });

  it("percent-encoded header path is decoded before config matching (dev)", async () => {
    // /%61pi/hello decodes to /api/hello → X-Custom-Header: vinext
    const res = await fetch(`${baseUrl}/%61pi/hello`);
    expect(res.headers.get("x-custom-header")).toBe("vinext");
  });

  it("percent-encoded rewrite path is decoded before config matching (dev)", async () => {
    // /%62efore-rewrite decodes to /before-rewrite → /about
    const res = await fetch(`${baseUrl}/%62efore-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  // --- getStaticPaths ---

  it("renders pages with getStaticPaths + getStaticProps", async () => {
    const res = await fetch(`${baseUrl}/blog/hello-world`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Hello World");
    expect(html).toContain("Blog post slug:");
    expect(html).toMatch(/slug:\s*(<!--\s*-->)?\s*hello-world/);
  });

  it("returns 404 for paths not in getStaticPaths when fallback is false", async () => {
    const res = await fetch(`${baseUrl}/blog/nonexistent-post`);
    expect(res.status).toBe(404);
  });

  it("renders pre-listed paths with getStaticPaths fallback: blocking", async () => {
    const res = await fetch(`${baseUrl}/articles/1`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("First Article");
    expect(html).toMatch(/Article ID:\s*(<!--\s*-->)?\s*1/);
  });

  it("renders unlisted paths with getStaticPaths fallback: blocking (on-demand SSR)", async () => {
    // Article 99 is not in getStaticPaths but fallback: blocking allows rendering
    const res = await fetch(`${baseUrl}/articles/99`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Article 99");
    expect(html).toMatch(/Article ID:\s*(<!--\s*-->)?\s*99/);
  });

  // --- next/dynamic ---

  it("renders dynamically imported components during SSR", async () => {
    const res = await fetch(`${baseUrl}/dynamic-page`);
    expect(res.status).toBe(200);

    const html = await res.text();
    expect(html).toContain("Dynamic Import Page");
    // The heavy component should be rendered server-side (ssr: true by default)
    expect(html).toContain("Heavy Component");
    expect(html).toContain("Loaded dynamically");
  });

  // --- Hydration ---

  // --- next/config ---

  it("renders pages that use next/config getConfig()", async () => {
    const res = await fetch(`${baseUrl}/config-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Config Test");
    // publicRuntimeConfig is empty by default, so it should show the fallback
    // React SSR inserts <!-- --> between text and expressions
    expect(html).toMatch(/App:.*default-app/);
  });

  // --- next/script ---

  it("renders Script with beforeInteractive strategy as <script> tag in SSR", async () => {
    const res = await fetch(`${baseUrl}/script-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Script Test");
    expect(html).toContain("Page with scripts");
    // beforeInteractive should render a <script> tag in the SSR output
    expect(html).toContain('src="https://example.com/analytics.js"');
  });

  // --- next/server ---

  it("resolves next/server imports in API routes", async () => {
    const res = await fetch(`${baseUrl}/api/middleware-test`);
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toEqual({ ok: true, message: "middleware-test works" });
  });

  // --- Middleware ---

  it("middleware adds custom headers to responses", async () => {
    const res = await fetch(`${baseUrl}/`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-custom-middleware")).toBe("active");
  });

  it("middleware redirects /old-page to /about", async () => {
    const res = await fetch(`${baseUrl}/old-page`, { redirect: "manual" });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  // Regression for #1331: after a middleware rewrite, the rewrite target
  // must go through full route resolution where static routes win over
  // dynamic catch-alls. Without the fix the `[id]` dynamic page captures
  // the rewrite target and renders "Dynamic route" with id="rewrite-me".
  it("middleware rewrite to / resolves to static index over [id] dynamic route (dev)", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-mw-rewrite-priority-dev-"));
    writeMiddlewareRewritePriorityFixture(tmpDir);

    let tempServer: ViteDevServer | undefined;
    try {
      const started = await startFixtureServer(tmpDir);
      tempServer = started.server;

      const res = await fetch(`${started.baseUrl}/rewrite-me/`);
      expect(res.status).toBe(200);
      const html = await res.text();
      // `id="home"` is unique to `pages/index.tsx`; ssr-page also says
      // "Hello World" so this disambiguates that the index rendered.
      expect(html).toContain('id="home"');
      expect(html).toContain("Hello World");
      expect(html).not.toContain("Dynamic route");
    } finally {
      await tempServer?.close();
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("middleware rewrite to /about resolves to static about over [id] dynamic route (dev)", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-mw-rewrite-priority-dev-about-"));
    writeMiddlewareRewritePriorityFixture(tmpDir);

    let tempServer: ViteDevServer | undefined;
    try {
      const started = await startFixtureServer(tmpDir);
      tempServer = started.server;

      const res = await fetch(`${started.baseUrl}/rewrite-to-about/`);
      expect(res.status).toBe(200);
      const html = await res.text();
      expect(html).toContain("About Page");
      expect(html).not.toContain("Dynamic route");
    } finally {
      await tempServer?.close();
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  // Regression for #1331: next.config.js rewrites with `trailingSlash: true`
  // and a `[id].tsx` dynamic root catch — the `[id]` route is also matched
  // by the rewrite source, so afterFiles rewrites must still be considered
  // (the matched route is dynamic), and the rewrite target must resolve to
  // the static page, not back into `[id]`.
  it("config afterFiles rewrite target resolves static page over [id] dynamic root (dev)", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-mw-rewrite-priority-dev-cfg-"));
    writeMiddlewareRewritePriorityFixture(tmpDir);

    let tempServer: ViteDevServer | undefined;
    try {
      const started = await startFixtureServer(tmpDir);
      tempServer = started.server;

      const res = await fetch(`${started.baseUrl}/rewrite-1/`);
      expect(res.status).toBe(200);
      const html = await res.text();
      // `id="ssr"` only lives on the rewrite target (`pages/ssr-page.tsx`) —
      // `pages/index.tsx` also says "Hello World" so this disambiguates that
      // the rewrite target is what rendered.
      expect(html).toContain('id="ssr"');
      expect(html).toContain("Hello World");
      expect(html).not.toContain("Dynamic route");
    } finally {
      await tempServer?.close();
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("middleware rewrites /rewritten to /ssr", async () => {
    const res = await fetch(`${baseUrl}/rewritten`);
    expect(res.status).toBe(200);
    const html = await res.text();
    // Should get the SSR page content (rewritten from /rewritten to /ssr)
    expect(html).toContain("Server-Side Rendered");
  });

  // Ported from Next.js:
  // test/e2e/getserversideprops/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/getserversideprops/test/index.test.ts
  it("passes original req.url, query, asPath, and resolvedUrl through _app.getInitialProps on GSSP pages", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-gssp-app-context-dev-"));
    writeGsspAppInitialPropsContextFixture(tmpDir);

    let tempServer: Awaited<ReturnType<typeof startFixtureServer>>["server"] | undefined;
    try {
      const started = await startFixtureServer(tmpDir);
      tempServer = started.server;
      const fixtureUrl = started.baseUrl;

      const dynamicRes = await fetch(`${fixtureUrl}/blog/post-1`);
      expect(dynamicRes.status).toBe(200);
      const dynamicHtml = await dynamicRes.text();
      const elementText = (html: string, id: string) => {
        const match = html.match(new RegExp(`<[^>]+id="${id}"[^>]*>(.*?)</[^>]+>`));
        expect(match).not.toBeNull();
        return match?.[1]?.replaceAll("&quot;", '"') ?? "";
      };
      const expectElementText = (html: string, id: string, expected: string) => {
        expect(elementText(html, id)).toBe(expected);
      };
      const expectElementJson = (html: string, id: string, expected: unknown) => {
        expect(JSON.parse(elementText(html, id))).toEqual(expected);
      };
      expect(dynamicHtml).toMatch(/Post:\s*(<!--\s*-->)?\s*post-1/);
      expectElementJson(dynamicHtml, "params", { post: "post-1" });
      expectElementJson(dynamicHtml, "query", { post: "post-1" });
      expectElementJson(dynamicHtml, "app-query", { post: "post-1" });
      expectElementText(dynamicHtml, "app-url", "/blog/post-1");
      expectElementText(dynamicHtml, "app-router-pathname", "/blog/[post]");
      expectElementText(dynamicHtml, "resolved-url", "/blog/post-1");
      expectElementText(dynamicHtml, "as-path", "/blog/post-1");
      const dynamicNextDataMatch = dynamicHtml.match(
        /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
      );
      expect(dynamicNextDataMatch).toBeTruthy();
      const dynamicNextData = JSON.parse(dynamicNextDataMatch![1]!);
      expect(dynamicNextData.props.__N_SSP).toBe(true);
      expect(dynamicNextData.props.appProps).toEqual({
        url: "/blog/post-1",
        query: { post: "post-1" },
        asPath: "/blog/post-1",
        pathname: "/blog/[post]",
      });

      const dataRes = await fetch(
        `${fixtureUrl}/_next/data/test-build-id/blog/post-1.json?hello=world`,
      );
      expect(dataRes.status).toBe(200);
      const data = await dataRes.json();
      expect(data.pageProps.resolvedUrl).toEqual("/blog/post-1?hello=world");
      expect(data.__N_SSP).toBe(true);
      expect(data.appProps).toEqual({
        url: "/_next/data/test-build-id/blog/post-1.json?hello=world",
        query: { post: "post-1", hello: "world" },
        asPath: "/blog/post-1?hello=world",
        pathname: "/blog/[post]",
      });

      const queryRes = await fetch(`${fixtureUrl}/something?hello=world`);
      expect(queryRes.status).toBe(200);
      const queryHtml = await queryRes.text();
      expect(queryHtml).toMatch(/hello:\s*(<!--\s*-->)?\s*world/);
      expectElementJson(queryHtml, "params", {});
      expectElementJson(queryHtml, "initial-query", { hello: "world" });
      expectElementJson(queryHtml, "query", { hello: "world" });
      expectElementJson(queryHtml, "app-query", { hello: "world" });
      expectElementText(queryHtml, "app-url", "/something?hello=world");
      expectElementText(queryHtml, "resolved-url", "/something?hello=world");
      expectElementText(queryHtml, "as-path", "/something?hello=world");

      const rewriteRes = await fetch(`${fixtureUrl}/blog-post-2`);
      expect(rewriteRes.status).toBe(200);
      const rewriteHtml = await rewriteRes.text();
      expectElementText(rewriteHtml, "app-url", "/blog-post-2");
      expectElementJson(rewriteHtml, "app-query", { post: "post-2", hello: "world" });
      expectElementText(rewriteHtml, "resolved-url", "/blog/post-2");
      expectElementText(rewriteHtml, "as-path", "/blog-post-2");

      const rewriteParamRes = await fetch(`${fixtureUrl}/blog-post-3`);
      expect(rewriteParamRes.status).toBe(200);
      const rewriteParamHtml = await rewriteParamRes.text();
      expectElementText(rewriteParamHtml, "app-url", "/blog-post-3");
      expectElementJson(rewriteParamHtml, "app-query", {
        post: "post-3",
        param: "post-3",
      });
      expectElementText(rewriteParamHtml, "resolved-url", "/blog/post-3");
      expectElementText(rewriteParamHtml, "as-path", "/blog-post-3");

      const sourceRewriteRes = await fetch(`${fixtureUrl}/rewrite-source/foo`);
      expect(sourceRewriteRes.status).toBe(200);
      const sourceRewriteHtml = await sourceRewriteRes.text();
      expect(sourceRewriteHtml).toContain("<h1>rewrite-target</h1>");
      expect(sourceRewriteHtml).toContain('<p id="as-path">/rewrite-source/foo</p>');
      expect(sourceRewriteHtml).toContain('<p id="req-url">/rewrite-source/foo</p>');
    } finally {
      await tempServer?.close();
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  // Regression for cloudflare/vinext#1471: when a query value itself contains
  // a query string (e.g. `?href=/about?hello=world`), the embedded `?hello=world`
  // is part of the `href` value per RFC 3986 — only the first `?` separates the
  // path from the query string. `getServerSideProps({ query })` must surface
  // the full value so `<Link href={query.href}>` renders the complete target.
  // Mirrors `test/e2e/trailing-slashes/pages/linker.js` from the Next.js suite.
  it("Pages Router Link preserves an embedded query string in the href prop", async () => {
    const res = await fetch(`${baseUrl}/linker?href=/about?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    // The rendered link target must include the embedded `?hello=world`. The
    // anchor uses `id="link"` to match Next.js's linker fixture; the literal
    // anchor href is what `<Link>` resolves through normalizePathTrailingSlash
    // and withBasePath. With trailingSlash:false and no basePath this is the
    // exact source string.
    expect(html).toContain('href="/about?hello=world"');
  });

  it("Pages Router Link strips trailing slash before an embedded query string", async () => {
    const res = await fetch(`${baseUrl}/linker?href=/about/?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    // trailingSlash defaults to false — `/about/?hello=world` collapses to
    // `/about?hello=world` while preserving the query.
    expect(html).toContain('href="/about?hello=world"');
  });

  // Ported from Next.js: test/e2e/edge-pages-support/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts
  // Closes cloudflare/vinext#1342: original query params must survive a
  // middleware rewrite. Next.js merges via
  // Object.assign(parsedUrl.query, rewrittenParsedUrl.query) — original first,
  // rewrite-target overrides on key conflicts.
  it("middleware rewrite preserves original query params to getServerSideProps", async () => {
    const res = await fetch(`${baseUrl}/mw-rewrite-query?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("SSR Query");
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toMatchObject({ hello: "world" });
  });

  it("middleware rewrite to a dynamic route merges original query with route params", async () => {
    const res = await fetch(`${baseUrl}/mw-rewrite-dynamic-query?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toMatch(/Post:\s*(<!--\s*-->)?\s*first/);
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toMatchObject({ id: "first", hello: "world" });
  });

  it("middleware rewrite with target-side query lets rewrite-target win on key conflicts", async () => {
    // Original ?hello=world, rewrite target is /ssr-query?hello=from-rewrite —
    // rewrite-target query should win, matching Next.js Object.assign semantics.
    const res = await fetch(`${baseUrl}/mw-rewrite-merge-query?hello=world&other=keep`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toMatchObject({
      hello: "from-rewrite",
      other: "keep",
    });
  });

  it("middleware rewrite without any original query still renders correctly", async () => {
    const res = await fetch(`${baseUrl}/mw-rewrite-query`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("SSR Query");
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toEqual({});
  });

  // Regression for cloudflare/vinext#1342: middleware that explicitly deletes
  // search params from `request.nextUrl` and rewrites to it must observe only
  // the keys it kept — vinext must NOT silently re-merge the original query.
  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ("should clear query parameters")
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-rewrites/test/index.test.ts
  it("middleware rewrite respects searchParams.delete on the rewrite-target URL", async () => {
    const res = await fetch(`${baseUrl}/mw-clear-query-params?a=1&b=2&foo=bar&allowed=kept`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toEqual({ allowed: "kept" });
  });

  it("middleware blocks /blocked with 403", async () => {
    const res = await fetch(`${baseUrl}/blocked`);
    expect(res.status).toBe(403);
    const text = await res.text();
    expect(text).toContain("Access Denied");
  });

  it("middleware custom response preserves binary body", async () => {
    const res = await fetch(`${baseUrl}/binary-response`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toBe("image/png");
    const buf = new Uint8Array(await res.arrayBuffer());
    // PNG magic bytes
    expect(buf[0]).toBe(0x89);
    expect(buf[1]).toBe(0x50); // P
    expect(buf[2]).toBe(0x4e); // N
    expect(buf[3]).toBe(0x47); // G
  });

  it("middleware custom response preserves multiple Set-Cookie headers", async () => {
    const res = await fetch(`${baseUrl}/multi-cookie-response`);
    expect(res.status).toBe(200);
    const setCookies = res.headers.getSetCookie();
    expect(setCookies).toContain("a=1; Path=/");
    expect(setCookies).toContain("b=2; Path=/");
    expect(setCookies).toContain("c=3; Path=/");
  });

  it("object-form matcher requires has and missing conditions", async () => {
    const noHeaderRes = await fetch(`${baseUrl}/mw-object-gated`);
    expect(noHeaderRes.status).toBe(200);
    expect(noHeaderRes.headers.get("x-custom-middleware")).toBeNull();

    const blockedRes = await fetch(`${baseUrl}/mw-object-gated`, {
      headers: {
        "x-mw-allow": "1",
        Cookie: "mw-blocked=1",
      },
    });
    expect(blockedRes.status).toBe(200);
    expect(blockedRes.headers.get("x-custom-middleware")).toBeNull();

    const allowedRes = await fetch(`${baseUrl}/mw-object-gated`, {
      headers: { "x-mw-allow": "1" },
    });
    expect(allowedRes.status).toBe(200);
    expect(allowedRes.headers.get("x-custom-middleware")).toBe("active");
  });

  it("middleware request header overrides can delete credential headers before page handling", async () => {
    // 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
    const res = await fetch(`${baseUrl}/header-override-delete`, {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
      },
    });

    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('id="authorization">null<');
    expect(html).toContain('id="cookie">null<');
    expect(html).toContain('id="middleware-header">hello-from-middleware<');
  });

  // --- Hydration ---

  it("hydration proxy script is fetchable", async () => {
    // Fetch the index page, find the proxy script URL, fetch it,
    // and verify it contains our hydration code
    const res = await fetch(`${baseUrl}/`);
    const html = await res.text();
    const proxyMatch = html.match(/src="([^"]*html-proxy[^"]*)"/);
    expect(proxyMatch).toBeTruthy();

    const scriptRes = await fetch(`${baseUrl}${proxyMatch![1]}`);
    expect(scriptRes.status).toBe(200);
    const scriptContent = await scriptRes.text();
    // The proxy module should contain our hydration imports
    expect(scriptContent).toContain("hydrateRoot");
    expect(scriptContent).toContain("__NEXT_DATA__");
  });

  it("renders Suspense + React.lazy content via streaming SSR", async () => {
    // With progressive streaming SSR (onShellReady), if the Suspense
    // content resolves before the shell finishes, React inlines it
    // directly (no fallback in the wire HTML). If it resolves after,
    // the fallback appears with streaming replacement scripts.
    // Our lazy component resolves synchronously in tests.
    const res = await fetch(`${baseUrl}/suspense-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Suspense Test");
    // The lazy component's content should be in the response
    expect(html).toContain("Hello from lazy component");
  });

  // --- getStaticPaths tests ---

  it("renders blog post with getStaticPaths fallback: false for listed path", async () => {
    const res = await fetch(`${baseUrl}/blog/hello-world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Hello World");
    expect(html).toMatch(/Blog post slug:.*hello-world/);
  });

  it("returns 404 for unlisted path with getStaticPaths fallback: false", async () => {
    const res = await fetch(`${baseUrl}/blog/nonexistent`);
    expect(res.status).toBe(404);
  });

  it("renders article with getStaticPaths fallback: 'blocking' for listed path", async () => {
    const res = await fetch(`${baseUrl}/articles/1`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("First Article");
    expect(html).toMatch(/Article ID:.*1/);
  });

  it("SSR renders unlisted path with getStaticPaths fallback: 'blocking'", async () => {
    const res = await fetch(`${baseUrl}/articles/99`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toMatch(/Article\s*(<!-- -->)?\s*99/);
    expect(html).toMatch(/Article ID:.*99/);
  });

  it("renders product with getStaticPaths fallback: true for listed path", async () => {
    const res = await fetch(`${baseUrl}/products/widget`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Super Widget");
    expect(html).toMatch(/Product ID:.*widget/);
    expect(html).toMatch(/isFallback:.*false/);
  });

  it("renders fallback shell for unlisted path with getStaticPaths fallback: true", async () => {
    // Next.js parity: when `fallback: true` and the path isn't pre-rendered,
    // skip getStaticProps, render with `useRouter().isFallback === true`, and
    // ship a loading shell that the client later swaps for the full data.
    // See: .nextjs-ref/packages/next/src/server/render.tsx — `if (isSSG && !isFallback)`.
    const res = await fetch(`${baseUrl}/products/unknown`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Loading product...");
    // The full-content branch must NOT render — getStaticProps was skipped.
    expect(html).not.toMatch(/Product ID:.*unknown/);
    const match = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(match).toBeTruthy();
    const nextData = JSON.parse(match![1]);
    expect(nextData.isFallback).toBe(true);
    // Empty pageProps on the fallback shell — client fetches them later.
    expect(nextData.props).toEqual({ pageProps: {} });
  });

  it("resolves real props for the data URL of an unlisted fallback: true path", async () => {
    // Counterpart to the fallback-shell test: the page HTML ships empty props,
    // but the client follows up with `/_next/data/<buildId>/products/unknown.json`
    // to fetch the actual props. That request must invoke getStaticProps.
    const res = await fetch(`${baseUrl}/_next/data/test-build-id/products/unknown.json`, {
      headers: { "x-nextjs-data": "1" },
    });
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json.pageProps).toMatchObject({ pid: "unknown" });
  });

  it("serves generated content after fallback data hydration", async () => {
    const slug = `hydrated-${Math.random().toString(36).slice(2)}`;
    const initialRes = await fetch(`${baseUrl}/products/${slug}`);
    expect(await initialRes.text()).toContain("Loading product...");

    const dataRes = await fetch(`${baseUrl}/_next/data/test-build-id/products/${slug}.json`, {
      headers: { "x-nextjs-data": "1" },
    });
    expect(dataRes.status).toBe(200);

    const finalRes = await fetch(`${baseUrl}/products/${slug}`);
    const finalHtml = await finalRes.text();
    expect(finalHtml).not.toContain("Loading product...");
    expect(finalHtml).toMatch(new RegExp(`Product ID:.*${slug}`));
  });

  // Refs #1543: bot/crawler requests must bypass the `fallback: true` loading
  // shell and synchronously render real content so crawlers index the page,
  // not `Loading...`. Mirrors Next.js's bot check in
  // `.nextjs-ref/packages/next/src/server/route-modules/pages/pages-handler.ts`
  // and the Next.js e2e regression test
  // `.nextjs-ref/test/e2e/prerender-crawler.test.ts`.
  it("renders synchronously (not the fallback shell) for crawler UAs on unlisted fallback: true paths", async () => {
    const userAgents = [
      "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
      "Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)",
      "DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)",
      "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)",
      "facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)",
    ];
    for (const userAgent of userAgents) {
      const slug = `bot-slug-${Math.random().toString(36).slice(2)}`;
      const res = await fetch(`${baseUrl}/products/${slug}`, {
        headers: { "user-agent": userAgent },
      });
      expect(res.status, `UA: ${userAgent}`).toBe(200);
      const html = await res.text();
      // Bot should see the real rendered page, not the loading shell.
      expect(html, `UA: ${userAgent}`).not.toContain("Loading product...");
      expect(html, `UA: ${userAgent}`).toMatch(new RegExp(`Product ID:.*${slug}`));
      const match = html.match(
        /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
      );
      expect(match, `UA: ${userAgent}`).toBeTruthy();
      const nextData = JSON.parse(match![1]);
      expect(nextData.isFallback, `UA: ${userAgent}`).toBe(false);
      expect(nextData.props.pageProps).toMatchObject({ pid: slug });
    }
  });

  it("still ships the fallback shell for normal browser UAs on unlisted fallback: true paths", async () => {
    // Counterpart of the crawler test — the bot-flip must not catch real
    // browsers. Plain Chrome UA should still receive the loading shell.
    const res = await fetch(`${baseUrl}/products/non-bot-slug`, {
      headers: {
        "user-agent":
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",
      },
    });
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Loading product...");
    const match = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(match).toBeTruthy();
    const nextData = JSON.parse(match![1]);
    expect(nextData.isFallback).toBe(true);
  });

  it("includes isFallback: false in __NEXT_DATA__", async () => {
    const res = await fetch(`${baseUrl}/products/widget`);
    const html = await res.text();
    const match = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(match).toBeTruthy();
    const nextData = JSON.parse(match![1]);
    expect(nextData.isFallback).toBe(false);
  });

  // ── Cross-origin request protection ─────────────────────────────────
  it("blocks page requests with cross-origin Origin header", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: {
        Origin: "https://evil.com",
        Host: new URL(baseUrl).host,
      },
    });
    expect(res.status).toBe(403);
    const text = await res.text();
    expect(text).toBe("Forbidden");
  });

  it("blocks API requests with cross-origin Origin header", async () => {
    const res = await fetch(`${baseUrl}/api/hello`, {
      headers: {
        Origin: "https://external.io",
        Host: new URL(baseUrl).host,
      },
    });
    expect(res.status).toBe(403);
  });

  it("blocks requests with cross-site Sec-Fetch headers", async () => {
    // Node.js fetch overrides Sec-Fetch-* headers (they're forbidden headers
    // in the Fetch spec). Use raw HTTP to simulate browser behavior.
    const http = await import("node:http");
    const url = new URL(baseUrl);
    const status = await new Promise<number>((resolve, reject) => {
      const req = http.request(
        {
          hostname: url.hostname,
          port: url.port,
          path: "/",
          method: "GET",
          headers: {
            "sec-fetch-site": "cross-site",
            "sec-fetch-mode": "no-cors",
          },
        },
        (res) => resolve(res.statusCode ?? 0),
      );
      req.on("error", reject);
      req.end();
    });
    expect(status).toBe(403);
  });

  it("allows page requests from localhost origin", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: {
        Origin: baseUrl,
        Host: new URL(baseUrl).host,
      },
    });
    expect(res.status).toBe(200);
  });

  it("allows page requests without Origin header", async () => {
    const res = await fetch(`${baseUrl}/`);
    expect(res.status).toBe(200);
  });

  // ── /_next/data JSON endpoint (issue #1330) ──────────────────────
  // Ported from Next.js: test/e2e/middleware-general/test/index.test.ts
  // ("should trigger middleware for data requests").
  describe("/_next/data JSON endpoint", () => {
    // pages-basic's next.config.mjs pins the build id to "test-build-id".
    // In dev the plugin now reads this from the resolved config so the
    // value matches the prod-server's embedded buildId.
    const BUILD_ID = "test-build-id";

    // 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("does not treat a normal URL as a data request from x-nextjs-data alone", async () => {
      const res = await fetch(`${baseUrl}/old-page`, {
        redirect: "manual",
        headers: { "x-nextjs-data": "1" },
      });
      expect(res.status).toBe(307);
      expect(res.headers.get("location")).toContain("/about");
      expect(res.headers.get("x-nextjs-redirect")).toBeNull();
    });

    it("adds x-nextjs-rewrite for a real data URL rewritten by middleware", async () => {
      const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/rewritten.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("x-nextjs-rewrite")).toBe("/ssr");
      expect(res.headers.get("x-middleware-rewrite")).toBeNull();
    });

    it("returns { pageProps } JSON for a getServerSideProps page", async () => {
      const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/ssr.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("content-type")).toContain("application/json");
      const json = (await res.json()) as { pageProps: { message: string } };
      expect(json.pageProps.message).toBe("Hello from getServerSideProps");
    });

    it("returns { pageProps } JSON for a getStaticProps page", async () => {
      // /isr-test uses getStaticProps with revalidate; the data endpoint
      // must bypass the HTML ISR cache and surface the props as JSON
      // (mirroring Next.js' `isNextDataRequest` cache-bypass path).
      const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/isr-test.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("content-type")).toContain("application/json");
      const json = (await res.json()) as { pageProps: Record<string, unknown> };
      expect(json).toHaveProperty("pageProps");
      expect(typeof json.pageProps).toBe("object");
    });

    it("normalizes the URL to /<page> BEFORE middleware runs", async () => {
      const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/ssr.json`);
      expect(res.status).toBe(200);
      // Middleware exposes the pathname it observed via `x-mw-pathname`.
      // The raw `/_next/data/...` should never reach the middleware function —
      // Next.js normalizes it to `/ssr` first.
      expect(res.headers.get("x-mw-pathname")).toBe("/ssr");
      // The middleware also sets `x-custom-middleware: active` on every match,
      // proving the middleware actually executed for this request.
      expect(res.headers.get("x-custom-middleware")).toBe("active");
    });

    it("returns 404 JSON for an unknown page", async () => {
      const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/totally-missing-page.json`);
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type")).toContain("application/json");
      // Body must still be valid JSON so naive clients calling `.json()` do
      // not throw before checking the status code.
      expect(await res.json()).toEqual({});
    });

    it("returns JSON 404 when getStaticPaths fallback:false rejects the path", async () => {
      // /blog/[slug] has `fallback: false` and only allows the slugs listed
      // in getStaticPaths. An unlisted slug must produce a JSON 404 for
      // data requests (not the HTML 404 page) so the client router can
      // hard-navigate instead of failing to parse HTML as JSON.
      const res = await fetch(
        `${baseUrl}/_next/data/${BUILD_ID}/blog/this-slug-does-not-exist.json`,
      );
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type")).toContain("application/json");
      expect(await res.json()).toEqual({});
    });

    it("returns JSON 404 for a stale buildId (dev)", async () => {
      // Mirrors the prod-server path: when the buildId in the URL doesn't
      // match the resolved buildId we surface a JSON 404 right away so the
      // client can hard-navigate (instead of parsing Vite's HTML 404).
      const res = await fetch(`${baseUrl}/_next/data/wrong-build-id/ssr.json`);
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type")).toContain("application/json");
      expect(await res.json()).toEqual({});
    });

    // ── x-nextjs-deployment-id on dev _next/data exits (issue #1829) ──
    // The fixture server runs in-process, so the dev middleware (index.ts)
    // and SSR handler (dev-server.ts) read the real `process.env` at request
    // time. Set NEXT_DEPLOYMENT_ID per-test to exercise the deployment-skew
    // header on the dev-only exits that have no prod/worker equivalent test.
    describe("x-nextjs-deployment-id (dev)", () => {
      const DEPLOYMENT_ID = "dev-deploy-abc";

      /** Run `fn` with NEXT_DEPLOYMENT_ID set, restoring the env after. */
      async function withDeploymentId(fn: () => Promise<void>): Promise<void> {
        const saved = process.env.NEXT_DEPLOYMENT_ID;
        process.env.NEXT_DEPLOYMENT_ID = DEPLOYMENT_ID;
        try {
          await fn();
        } finally {
          if (saved === undefined) {
            delete process.env.NEXT_DEPLOYMENT_ID;
          } else {
            process.env.NEXT_DEPLOYMENT_ID = saved;
          }
        }
      }

      it("sets the header on the stale-buildId JSON 404", async () => {
        // Exercises the wrong-buildId data 404 in the plugin middleware
        // (index.ts `_next/data` normalization) — the primary skew trigger:
        // a stale client whose buildId no longer matches the server.
        await withDeploymentId(async () => {
          const res = await fetch(`${baseUrl}/_next/data/wrong-build-id/ssr.json`);
          expect(res.status).toBe(404);
          expect(res.headers.get("content-type")).toContain("application/json");
          expect(res.headers.get("x-nextjs-deployment-id")).toBe(DEPLOYMENT_ID);
          expect(await res.json()).toEqual({});
        });
      });

      it("sets the header on the route-miss JSON 404", async () => {
        // Exercises createSSRHandler's `!match` data exit (dev-server.ts):
        // the page was removed under a new deployment, so a stale client's
        // data fetch must still see the header to hard-navigate.
        await withDeploymentId(async () => {
          const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/totally-missing-page.json`);
          expect(res.status).toBe(404);
          expect(res.headers.get("content-type")).toContain("application/json");
          expect(res.headers.get("x-nextjs-deployment-id")).toBe(DEPLOYMENT_ID);
          expect(await res.json()).toEqual({});
        });
      });

      it("sets the header on the success { pageProps } response", async () => {
        // Exercises createSSRHandler's data success short-circuit
        // (dev-server.ts), matching the prod createPagesPageHandler tests.
        await withDeploymentId(async () => {
          const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/ssr.json`);
          expect(res.status).toBe(200);
          expect(res.headers.get("x-nextjs-deployment-id")).toBe(DEPLOYMENT_ID);
          const json = (await res.json()) as { pageProps: { message: string } };
          expect(json.pageProps.message).toBe("Hello from getServerSideProps");
        });
      });

      it("omits the header on the /500 data success response", async () => {
        // Next.js pages-handler.ts guards the success-path header with
        // `!isErrorPage && !is500Page`, so /_error and /500 data responses
        // must not carry it even when a deployment id is configured.
        await withDeploymentId(async () => {
          const res = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/500.json`);
          expect(res.status).toBe(200);
          expect(res.headers.get("x-nextjs-deployment-id")).toBeNull();
        });
      });

      it("omits the header when no deployment id is configured", async () => {
        // Without NEXT_DEPLOYMENT_ID / a configured deploymentId the header
        // must be absent on every exit — mirroring Next.js, which only sets
        // NEXT_NAV_DEPLOYMENT_ID_HEADER when `deploymentId` is configured.
        const staleRes = await fetch(`${baseUrl}/_next/data/wrong-build-id/ssr.json`);
        expect(staleRes.status).toBe(404);
        expect(staleRes.headers.get("x-nextjs-deployment-id")).toBeNull();

        const missRes = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/totally-missing-page.json`);
        expect(missRes.status).toBe(404);
        expect(missRes.headers.get("x-nextjs-deployment-id")).toBeNull();

        const okRes = await fetch(`${baseUrl}/_next/data/${BUILD_ID}/ssr.json`);
        expect(okRes.status).toBe(200);
        expect(okRes.headers.get("x-nextjs-deployment-id")).toBeNull();
      });
    });
  });
});

describe("Pages Router dev dot-path rewrite preflight", () => {
  let server: ViteDevServer;
  let baseUrl: string;
  let tmpDir: string;

  beforeAll(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-dot-path-rewrite-"));
    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.mkdir(path.join(tmpDir, "public"), { recursive: true });
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(tmpDir, "node_modules"),
      "junction",
    );
    await fsp.writeFile(
      path.join(tmpDir, "pages", "about.tsx"),
      `export default function About() { return <div>rewritten download page</div>; }`,
    );
    await fsp.writeFile(path.join(tmpDir, "public", "unrelated.txt"), "unrelated asset");
    await fsp.writeFile(
      path.join(tmpDir, "next.config.mjs"),
      `export default {
  async rewrites() {
    return {
      beforeFiles: [{
        source: "/download.txt",
        has: [{ type: "header", key: "x-download-rewrite", value: "enabled" }],
        destination: "/about",
      }],
      afterFiles: [],
      fallback: [],
    };
  },
};
`,
    );
    await fsp.writeFile(
      path.join(tmpDir, "middleware.ts"),
      `import { NextResponse } from "next/server";

export default function middleware(request) {
  if (new URL(request.url).searchParams.has("rewrite")) {
    const headers = new Headers(request.headers);
    headers.set("x-download-rewrite", "enabled");
    return NextResponse.next({ request: { headers } });
  }
  return NextResponse.next();
}

export const config = { matcher: "/download.txt" };
`,
    );

    ({ server, baseUrl } = await startFixtureServer(tmpDir));
  }, 30000);

  afterAll(async () => {
    await server?.close();
    await fsp.rm(tmpDir, { recursive: true, force: true });
  });

  it("runs middleware before evaluating has conditions for dot-path rewrites", async () => {
    const response = await fetch(`${baseUrl}/download.txt?rewrite`);

    expect(response.status).toBe(200);
    expect(response.headers.get("content-type")).toContain("text/html");
    expect(await response.text()).toContain("rewritten download page");
  });

  it("does not apply the rewrite when the post-middleware condition is false", async () => {
    const response = await fetch(`${baseUrl}/download.txt`);

    expect(response.status).toBe(404);
    expect(await response.text()).not.toContain("rewritten download page");
  });

  it("does not route unrelated dot-path assets through rewrite handling", async () => {
    const response = await fetch(`${baseUrl}/unrelated.txt`);

    expect(response.status).toBe(200);
    expect(await response.text()).toBe("unrelated asset");
  });
});

describe("Pages Router dev server origin check", () => {
  let server: ViteDevServer;
  let baseUrl: string;

  beforeAll(async () => {
    ({ server, baseUrl } = await startFixtureServer(FIXTURE_DIR));
  }, 30000);

  afterAll(async () => {
    await server?.close();
  });

  it("allows requests with no Origin header (direct navigation)", async () => {
    const res = await fetch(`${baseUrl}/`);
    expect(res.status).toBe(200);
  });

  it("allows same-origin requests", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: baseUrl },
    });
    expect(res.status).toBe(200);
  });

  it("blocks cross-origin requests", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: "http://evil.com" },
    });
    expect(res.status).toBe(403);
  });

  it("blocks cross-origin requests to /@* Vite internal paths", async () => {
    const res = await fetch(`${baseUrl}/@fs/etc/passwd`, {
      headers: { Origin: "http://evil.com" },
    });
    expect(res.status).toBe(403);
  });

  it("blocks cross-origin requests to /__vite internal paths", async () => {
    const res = await fetch(`${baseUrl}/__vite_ping`, {
      headers: { Origin: "http://evil.com" },
    });
    expect(res.status).toBe(403);
  });

  it("blocks cross-origin requests to /node_modules paths", async () => {
    const res = await fetch(`${baseUrl}/node_modules/.vite/deps/react.js`, {
      headers: { Origin: "http://evil.com" },
    });
    expect(res.status).toBe(403);
  });

  it("blocks requests with malformed Origin header", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: "not-a-url" },
    });
    expect(res.status).toBe(403);
  });

  it("blocks image endpoint redirect to /@* internal paths", async () => {
    const res = await fetch(`${baseUrl}/_next/image?url=/@fs/etc/passwd&w=100&q=75`, {
      redirect: "manual",
    });
    expect(res.status).toBe(400);
  });

  it("blocks image endpoint redirect to /__vite internal paths", async () => {
    const res = await fetch(`${baseUrl}/_next/image?url=/__vite_hmr&w=100&q=75`, {
      redirect: "manual",
    });
    expect(res.status).toBe(400);
  });

  it("blocks image endpoint redirect to /node_modules paths", async () => {
    const res = await fetch(
      `${baseUrl}/_next/image?url=/node_modules/.vite/manifest.json&w=100&q=75`,
      {
        redirect: "manual",
      },
    );
    expect(res.status).toBe(400);
  });
});

// Ported from Next.js: test/development/basic/allowed-dev-origins.test.ts
// https://github.com/vercel/next.js/blob/canary/test/development/basic/allowed-dev-origins.test.ts
describe("Pages Router allowedDevOrigins config", () => {
  let server: ViteDevServer;
  let baseUrl: string;
  let tmpDir: string;

  beforeAll(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-allowed-dev-origins-"));
    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(tmpDir, "node_modules"),
      "junction",
    );
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <div>allowed-dev-origins-pages</div>; }`,
    );
    await fsp.writeFile(
      path.join(tmpDir, "next.config.mjs"),
      `export default {
  allowedDevOrigins: ["allowed.example.com"],
  experimental: {
    serverActions: {
      allowedOrigins: ["actions.example.com"],
    },
  },
};
`,
    );
    ({ server, baseUrl } = await startFixtureServer(tmpDir));
  }, 30000);

  afterAll(async () => {
    try {
      (
        server?.httpServer as
          | {
              closeAllConnections?: () => void;
            }
          | undefined
      )?.closeAllConnections?.();
      await Promise.race([server?.close(), new Promise((resolve) => setTimeout(resolve, 5000))]);
    } catch {
      // Best-effort cleanup: the temp directory removal below is the durable assertion.
    }
    await fsp.rm(tmpDir, { recursive: true, force: true });
  }, 30000);

  it("allows cross-origin requests from allowedDevOrigins", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: "http://allowed.example.com" },
    });
    await res.text();
    expect(res.status).toBe(200);
  });

  it("does not treat serverActions.allowedOrigins as allowedDevOrigins", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: "http://actions.example.com" },
    });
    await res.text();
    expect(res.status).toBe(403);
  });
});

describe("Virtual server entry generation", () => {
  it("generates valid JavaScript for the server entry", async () => {
    // Create a minimal server just to access the plugin's virtual module
    const testServer = await createServer({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      server: { port: 0 },
      logLevel: "silent",
    });

    try {
      // Load the virtual module through Vite's SSR pipeline
      const entry = await testServer.ssrLoadModule("virtual:vinext-server-entry");

      // Verify it exports the expected functions
      expect(typeof entry.renderPage).toBe("function");
      expect(typeof entry.handleApiRoute).toBe("function");
    } finally {
      await testServer.close();
    }
  });

  it("client entry uses Next.js bracket format for dynamic route keys", async () => {
    // The client entry generates a pageLoaders map keyed by route pattern.
    // These keys MUST match __NEXT_DATA__.page (which uses Next.js bracket
    // format like "/posts/[id]"), not the internal Express-style ":id" format.
    // A mismatch prevents client-side hydration for dynamic route pages.
    const testServer = await createServer({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      server: { port: 0 },
      logLevel: "silent",
    });

    try {
      const resolved = await testServer.pluginContainer.resolveId("virtual:vinext-client-entry");
      expect(resolved).toBeTruthy();
      const loaded = await testServer.pluginContainer.load(resolved!.id);
      expect(loaded).toBeTruthy();
      const code = typeof loaded === "string" ? loaded : ((loaded as any)?.code ?? "");

      // Dynamic routes should use [param] format, not :param
      // The fixture has pages/posts/[id].tsx
      expect(code).toContain('"/posts/[id]"');
      // Catch-all routes: pages/docs/[...slug].tsx
      expect(code).toContain('"/docs/[...slug]"');
      // Should NOT contain Express-style :param patterns for any route
      expect(code).not.toMatch(/["']\/(posts|blog|articles|docs|products)\/:[\w]+["']/);
      // Strip the `__VINEXT_PAGES_LINK_PREFETCH_ROUTES__` manifest before the
      // next two assertions. The manifest is exempt because it carries the
      // internal pattern shape (with `:slug+` / `:slug*`) so the client-side
      // hybrid owner resolver can rebuild a pattern from `patternParts` to
      // feed `routePrecedence`. The pageLoaders map (above) still uses
      // Next.js bracket format for hydration keys.
      const codeWithoutPrefetchManifest = code.replace(
        /__VINEXT_PAGES_LINK_PREFETCH_ROUTES__\s*=\s*(\[[\s\S]*?\]);/,
        "__VINEXT_PAGES_LINK_PREFETCH_ROUTES__ = /* stripped for test */;",
      );
      expect(codeWithoutPrefetchManifest).not.toContain(":slug+");
      expect(codeWithoutPrefetchManifest).not.toContain(":slug*");
    } finally {
      await testServer.close();
    }
  });

  // Issue #1329 — `window.next = { version, router, ... }` must be exposed
  // before the Next.js deploy test suite can run `next.router.push(...)`
  // via `browser.eval()`. The installer (shims/router.ts → installWindowNext)
  // only runs once next/router is imported, so the client entry must
  // statically import next/router at the top, not lazily inside hydrate().
  //
  // Mirrors Next.js: .nextjs-ref/packages/next/src/client/next.ts (line 5),
  // which statically imports the router from './' before initialize/hydrate.
  it("client entry statically imports next/router so window.next.router is set before hydration", async () => {
    const testServer = await createServer({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      server: { port: 0 },
      logLevel: "silent",
    });

    try {
      const resolved = await testServer.pluginContainer.resolveId("virtual:vinext-client-entry");
      expect(resolved).toBeTruthy();
      const loaded = await testServer.pluginContainer.load(resolved!.id);
      expect(loaded).toBeTruthy();
      const code = typeof loaded === "string" ? loaded : ((loaded as any)?.code ?? "");

      // Static import — module-level side effect installs window.next.router.
      expect(code).toMatch(
        /^import\s+Router,\s*\{[^}]*\bwrapWithRouterContext\b[^}]*\}\s+from\s+["']next\/router["']/m,
      );

      // Defense-in-depth: the original lazy `await import("next/router")`
      // inside hydrate() must NOT remain, otherwise the static import is
      // dead-code and the side effect can be tree-shaken or deferred.
      expect(code).not.toMatch(/await\s+import\(\s*["']next\/router["']\s*\)/);
    } finally {
      await testServer.close();
    }
  });
});

describe("Plugin config", () => {
  it("uses inline nextConfig instead of root next.config and warns once", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-inline-config-"));
    const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});

    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <h1>Home</h1>; }`,
    );
    await fsp.writeFile(
      path.join(tmpDir, "next.config.mjs"),
      `export default { basePath: "/disk", env: { CONFIG_SOURCE: "disk" } };`,
    );

    try {
      const plugins = vinext({
        nextConfig: {
          basePath: "/inline",
          env: { CONFIG_SOURCE: "inline" },
        },
      }) as any[];
      const configPlugin = plugins.find((p) => p.name === "vinext:config");
      expect(configPlugin).toBeDefined();

      const result = await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "serve", mode: "development" },
      );

      expect(result.base).toBe("/inline/");
      expect(result.define["process.env.CONFIG_SOURCE"]).toBe(JSON.stringify("inline"));
      expect(consoleWarn).toHaveBeenCalledWith(
        expect.stringContaining("vinext({ nextConfig }) overrides next.config.mjs"),
      );
    } finally {
      consoleWarn.mockRestore();
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("passes the current phase to inline function-form nextConfig", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-inline-phase-"));

    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <h1>Home</h1>; }`,
    );

    try {
      const buildPlugins = vinext({
        nextConfig: async (phase) => ({ env: { RECEIVED_PHASE: phase } }),
      }) as any[];
      const buildConfigPlugin = buildPlugins.find((p) => p.name === "vinext:config");
      expect(buildConfigPlugin).toBeDefined();

      const buildResult = await buildConfigPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "build", mode: "production" },
      );

      expect(buildResult.define["process.env.RECEIVED_PHASE"]).toBe(
        JSON.stringify(PHASE_PRODUCTION_BUILD),
      );

      const servePlugins = vinext({
        nextConfig: (phase) => ({ env: { RECEIVED_PHASE: phase } }),
      }) as any[];
      const serveConfigPlugin = servePlugins.find((p) => p.name === "vinext:config");
      expect(serveConfigPlugin).toBeDefined();

      const serveResult = await serveConfigPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "serve", mode: "development" },
      );

      expect(serveResult.define["process.env.RECEIVED_PHASE"]).toBe(
        JSON.stringify(PHASE_DEVELOPMENT_SERVER),
      );
    } finally {
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("injects an opaque App Router RSC compatibility ID instead of the raw build ID", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-rsc-compat-id-"));
    const buildId = "release-2026-05-15";

    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <h1>Home</h1>; }`,
    );

    try {
      const plugins = vinext({
        nextConfig: {
          generateBuildId: () => buildId,
        },
      }) as any[];
      const configPlugin = plugins.find((p) => p.name === "vinext:config");
      expect(configPlugin).toBeDefined();

      const result = await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "build", mode: "production" },
      );
      const repeatedResult = await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "build", mode: "production" },
      );

      expect(result.define["process.env.__VINEXT_BUILD_ID"]).toBe(JSON.stringify(buildId));
      expect(result.define["process.env.__VINEXT_RSC_COMPATIBILITY_ID"]).not.toBe(
        JSON.stringify(buildId),
      );
      expect(JSON.parse(result.define["process.env.__VINEXT_RSC_COMPATIBILITY_ID"])).toMatch(
        /^[0-9a-f-]{36}$/,
      );
      expect(repeatedResult.define["process.env.__VINEXT_RSC_COMPATIBILITY_ID"]).toBe(
        result.define["process.env.__VINEXT_RSC_COMPATIBILITY_ID"],
      );
    } finally {
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("uses deploymentId as the App Router RSC compatibility ID when configured", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-rsc-deployment-id-"));

    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <h1>Home</h1>; }`,
    );

    try {
      const plugins = vinext({
        nextConfig: {
          deploymentId: "public-deployment-id",
          generateBuildId: () => "release-2026-05-15",
        },
      }) as any[];
      const configPlugin = plugins.find((p) => p.name === "vinext:config");
      expect(configPlugin).toBeDefined();

      const result = await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "build", mode: "production" },
      );

      expect(result.define["process.env.__VINEXT_RSC_COMPATIBILITY_ID"]).toBe(
        JSON.stringify("public-deployment-id"),
      );
    } finally {
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("loads .env before evaluating inline function-form nextConfig", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-inline-env-"));
    const envKey = "VINEXT_INLINE_NEXT_CONFIG_ENV";
    delete process.env[envKey];

    await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpDir, "pages", "index.tsx"),
      `export default function Home() { return <h1>Home</h1>; }`,
    );
    await fsp.writeFile(path.join(tmpDir, ".env"), `${envKey}=loaded-before-inline-config\n`);

    try {
      const plugins = vinext({
        nextConfig: () => ({
          env: {
            INLINE_ENV_VALUE: process.env[envKey] ?? "missing",
          },
        }),
      }) as any[];
      const configPlugin = plugins.find((p) => p.name === "vinext:config");
      expect(configPlugin).toBeDefined();

      const result = await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "serve", mode: "development" },
      );

      expect(result.define["process.env.INLINE_ENV_VALUE"]).toBe(
        JSON.stringify("loaded-before-inline-config"),
      );
    } finally {
      delete process.env[envKey];
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("auto-injects @vitejs/plugin-react as a top-level async plugin", async () => {
    const plugins = vinext() as any[];
    const resolvedPlugins = (
      await Promise.all(
        plugins.map(async (plugin) => {
          if (plugin && typeof plugin.then === "function") {
            return await plugin;
          }
          return plugin;
        }),
      )
    ).flat();

    const hasReactPlugin = resolvedPlugins.some(
      (plugin) => plugin && typeof plugin.name === "string" && plugin.name.startsWith("vite:react"),
    );
    expect(hasReactPlugin).toBe(true);
  });

  it("throws when user double-registers react() alongside auto-registration", async () => {
    const plugins = vinext() as any[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    expect(configPlugin).toBeDefined();

    await configPlugin.config(
      { root: FIXTURE_DIR, plugins: [] },
      { command: "serve", mode: "development" },
    );

    await expect(
      configPlugin.configResolved({
        command: "serve",
        configFile: false,
        plugins: [
          { name: "vite:react-babel" },
          { name: "vite:react-refresh" },
          { name: "vite:react-babel" },
          { name: "vite:react-refresh" },
        ],
      }),
    ).rejects.toThrow("Duplicate @vitejs/plugin-react detected");
  });

  it("adds resolve.dedupe for React packages to prevent dual instance errors", async () => {
    const plugins = vinext() as any[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    expect(configPlugin).toBeDefined();

    // Call the config hook with a minimal config
    const result = await configPlugin.config({ root: FIXTURE_DIR, plugins: [] });

    expect(result.resolve).toBeDefined();
    expect(result.resolve.dedupe).toBeDefined();
    expect(result.resolve.dedupe).toContain("react");
    expect(result.resolve.dedupe).toContain("react-dom");
    expect(result.resolve.dedupe).toContain("react/jsx-runtime");
    expect(result.resolve.dedupe).toContain("react/jsx-dev-runtime");
  });

  it("suppresses MODULE_LEVEL_DIRECTIVE warnings from Rollup", async () => {
    const plugins = vinext() as any[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    expect(configPlugin).toBeDefined();

    const result = await configPlugin.config({ root: FIXTURE_DIR, plugins: [] });

    expect(result.build).toBeDefined();
    const bundlerOptions = getBuildBundlerOptions(result);
    expect(bundlerOptions).toBeDefined();
    expect(bundlerOptions.onwarn).toBeDefined();

    const defaultHandler = vi.fn();

    // "use client" MODULE_LEVEL_DIRECTIVE warnings should be silenced
    bundlerOptions.onwarn(
      { code: "MODULE_LEVEL_DIRECTIVE", message: '"use client" was ignored' },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    // "use server" MODULE_LEVEL_DIRECTIVE warnings should be silenced
    bundlerOptions.onwarn(
      { code: "MODULE_LEVEL_DIRECTIVE", message: '"use server" was ignored' },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    // MODULE_LEVEL_DIRECTIVE warnings for other directives should pass through
    const otherDirectiveWarning = {
      code: "MODULE_LEVEL_DIRECTIVE",
      message: '"use strict" was ignored',
    };
    bundlerOptions.onwarn(otherDirectiveWarning, defaultHandler);
    expect(defaultHandler).toHaveBeenCalledWith(otherDirectiveWarning);

    // Other warning codes should pass through to the default handler
    defaultHandler.mockClear();
    const otherWarning = { code: "CIRCULAR_DEPENDENCY", message: "circular" };
    bundlerOptions.onwarn(otherWarning, defaultHandler);
    expect(defaultHandler).toHaveBeenCalledWith(otherWarning);
  });

  it("suppresses IMPORT_IS_UNDEFINED noise for generated proxy/middleware fallback probes", async () => {
    const plugins = vinext() as any[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    expect(configPlugin).toBeDefined();

    const result = await configPlugin.config({ root: FIXTURE_DIR, plugins: [] });

    expect(result.build).toBeDefined();
    const bundlerOptions = getBuildBundlerOptions(result);
    expect(bundlerOptions).toBeDefined();
    expect(bundlerOptions.onwarn).toBeDefined();

    const defaultHandler = vi.fn();

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `default` will always be undefined because there is no matching export in 'proxy.ts'\\n      ╭─[ \\0virtual:vinext-rsc-entry:2632:34 ]",
      },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `default` will always be undefined because there is no matching export in 'middleware.ts'\\n      ╭─[ \\0virtual:vinext-server-entry:168:34 ]",
      },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `proxy` will always be undefined because there is no matching export in 'proxy.tsx'\\n      ╭─[ \\0virtual:vinext-rsc-entry:2632:34 ]",
      },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `middleware` will always be undefined because there is no matching export in 'middleware.jsx'\\n      ╭─[ \\0virtual:vinext-server-entry:168:34 ]",
      },
      defaultHandler,
    );
    expect(defaultHandler).not.toHaveBeenCalled();

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `default` will always be undefined because there is no matching export in 'some-user-file.ts'",
      },
      defaultHandler,
    );
    expect(defaultHandler).toHaveBeenCalledTimes(1);

    bundlerOptions.onwarn(
      {
        code: "IMPORT_IS_UNDEFINED",
        message:
          "[IMPORT_IS_UNDEFINED] Warning: Import `proxy` will always be undefined because there is no matching export in 'some-user-file.ts'",
      },
      defaultHandler,
    );
    expect(defaultHandler).toHaveBeenCalledTimes(2);
  });

  it("preserves user-supplied build.rollupOptions.onwarn", async () => {
    const plugins = vinext() as any[];
    const configPlugin = plugins.find((p) => p.name === "vinext:config");
    expect(configPlugin).toBeDefined();

    const userOnwarn = vi.fn();
    const result = await configPlugin.config({
      root: FIXTURE_DIR,
      plugins: [],
      build: { rollupOptions: { onwarn: userOnwarn } },
    });

    const bundlerOptions = getBuildBundlerOptions(result);
    const defaultHandler = vi.fn();

    // "use client" should still be suppressed (user handler NOT called)
    bundlerOptions.onwarn(
      { code: "MODULE_LEVEL_DIRECTIVE", message: '"use client" was ignored' },
      defaultHandler,
    );
    expect(userOnwarn).not.toHaveBeenCalled();
    expect(defaultHandler).not.toHaveBeenCalled();

    // Other warnings should be forwarded to the user's handler
    const otherWarning = { code: "CIRCULAR_DEPENDENCY", message: "circular" };
    bundlerOptions.onwarn(otherWarning, defaultHandler);
    expect(userOnwarn).toHaveBeenCalledWith(otherWarning, defaultHandler);
    expect(defaultHandler).not.toHaveBeenCalled();
  });

  it("registers vinext:mdx proxy plugin with enforce pre for correct ordering", async () => {
    const plugins = vinext() as any[];
    const mdxProxy = plugins.find((p) => p.name === "vinext:mdx");
    expect(mdxProxy).toBeDefined();
    expect(mdxProxy.enforce).toBe("pre");
    // Proxy forwards config and transform to the delegate (@mdx-js/rollup)
    expect(typeof mdxProxy.config).toBe("function");
    expect(typeof mdxProxy.transform).toBe("function");
    // Proxy should be inert when no MDX files are detected (mdxDelegate is null)
    expect(mdxProxy.config({}, { command: "build", mode: "production" })).toBeUndefined();
    await expect(mdxProxy.transform("code", "./foo.ts", {})).resolves.toBeUndefined();
  });

  it("vinext:mdx transform skips ids that contain a query string (regression: ?raw)", async () => {
    // @mdx-js/rollup strips the query before matching the file extension, so
    // it would compile "foo.mdx?raw" as MDX and return compiled JSX instead of
    // raw text. The proxy must short-circuit on any id that contains "?".
    const plugins = vinext() as any[];
    const mdxProxy = plugins.find((p: any) => p.name === "vinext:mdx");

    // Common query-param import patterns that must be skipped
    await expect(
      mdxProxy.transform("# hello", "/app/content.mdx?raw", {}),
    ).resolves.toBeUndefined();
    await expect(mdxProxy.transform("# hello", "/app/page.mdx?url", {})).resolves.toBeUndefined();
    await expect(
      mdxProxy.transform("# hello", "/app/page.mdx?inline", {}),
    ).resolves.toBeUndefined();
    // Additional query variations
    await expect(mdxProxy.transform("# hello", "/app/page.mdx?v=123", {})).resolves.toBeUndefined();
    await expect(mdxProxy.transform("# hello", "/app/page.mdx?mdx", {})).resolves.toBeUndefined();
    // Edge case: query value contains .mdx but isn't the extension
    await expect(
      mdxProxy.transform("# hello", "/app/page.mdx?something.mdx", {}),
    ).resolves.toBeUndefined();
  });

  it("vinext:mdx lazily compiles plain .mdx imports that were not pre-detected", async () => {
    const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-mdx-lazy-"));

    try {
      await fsp.writeFile(
        path.join(tmpDir, "package.json"),
        JSON.stringify({ name: "vinext-mdx-lazy", private: true, type: "module" }),
      );

      const plugins = vinext({ appDir: tmpDir }) as any[];
      const configPlugin = plugins.find((p) => p.name === "vinext:config");
      const mdxProxy = plugins.find((p) => p.name === "vinext:mdx");

      await configPlugin.config(
        { root: tmpDir, plugins: [] },
        { command: "build", mode: "production" },
      );

      const result = await mdxProxy.transform(
        `---
title: "Second Post"
---

export const marker = "mdx-evaluated";

# Hello <span>world</span>
`,
        path.join(tmpDir, "content", "post.mdx"),
        {},
      );

      expect(result).toBeDefined();
      expect(result.code).toContain("mdx-evaluated");
      expect(result.code).not.toContain('title: "Second Post"');
    } finally {
      await fsp.rm(tmpDir, { recursive: true, force: true });
    }
  });

  it("vinext:mdx proxy logic — ?raw guard prevents delegate from compiling query imports", () => {
    // Self-contained unit test that exercises the guard independently of whether
    // mdxDelegate is set. Without the guard, @mdx-js/rollup silently compiles
    // ?raw imports into JSX; with it, the proxy returns undefined (pass-through).
    const mockTransformResult = { code: "/* compiled mdx */", map: null };
    const mockDelegate = {
      transform: vi.fn().mockReturnValue(mockTransformResult),
    };

    // Proxy WITHOUT the query guard — reproduces the bug
    function transformWithoutGuard(code: string, id: string) {
      if (!mockDelegate.transform) return;
      return (mockDelegate.transform as any).call({}, code, id, {});
    }

    // Proxy WITH the query guard — the fix
    function transformWithGuard(code: string, id: string) {
      // Skip ?raw and other query imports — @mdx-js/rollup ignores the query
      // and would compile the file as MDX instead of returning raw text.
      if (id.includes("?")) return;
      if (!mockDelegate.transform) return;
      return (mockDelegate.transform as any).call({}, code, id, {});
    }

    // Without the guard: ?raw import is incorrectly handed to the MDX compiler
    expect(transformWithoutGuard("", "/app/content.mdx?raw")).toEqual(mockTransformResult);
    expect(mockDelegate.transform).toHaveBeenCalledWith("", "/app/content.mdx?raw", {});

    mockDelegate.transform.mockClear();

    // With the guard: ?raw import is skipped (undefined = Vite pass-through)
    expect(transformWithGuard("", "/app/content.mdx?raw")).toBeUndefined();
    expect(mockDelegate.transform).not.toHaveBeenCalled();

    // Plain .mdx (no query) still goes through the delegate
    expect(transformWithGuard("", "/app/content.mdx")).toEqual(mockTransformResult);
    expect(mockDelegate.transform).toHaveBeenCalledWith("", "/app/content.mdx", {});
  });
});

describe("Production build", () => {
  const outDir = path.resolve(FIXTURE_DIR, "dist");

  afterAll(() => {
    // Clean up build output
    fs.rmSync(outDir, { recursive: true, force: true });
  });

  it("produces SSR server entry via vite build --ssr", async () => {
    // Build the SSR bundle using the virtual server entry
    await build({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      logLevel: "silent",
      build: {
        outDir: path.join(outDir, "server"),
        ssr: "virtual:vinext-server-entry",
        rollupOptions: {
          output: {
            entryFileNames: "entry.js",
          },
        },
      },
    });

    // Verify the server entry was produced
    const entryPath = path.join(outDir, "server", "entry.js");
    expect(fs.existsSync(entryPath)).toBe(true);

    const entryContent = fs.readFileSync(entryPath, "utf-8");
    // Should export renderPage and handleApiRoute
    expect(entryContent).toContain("renderPage");
    expect(entryContent).toContain("handleApiRoute");
    // Should contain route patterns from our fixture pages
    expect(entryContent).toContain("/about");
    expect(entryContent).toContain("/ssr");
  });

  it("runMiddleware in generated pages prod entry executes named proxy export", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-proxy-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

      await fsp.writeFile(
        path.join(tmpRoot, "pages", "index.tsx"),
        "export default function Page() { return <div>ok</div>; }\n",
      );

      await fsp.writeFile(
        path.join(tmpRoot, "proxy.js"),
        `import { NextResponse } from "next/server";
export function proxy(request) {
  const url = new URL(request.url);
  if (url.pathname === "/protected") {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}
export const config = { matcher: ["/protected"] };
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: {
            output: {
              entryFileNames: "entry.js",
            },
          },
        },
      });

      const entryPath = path.join(fixtureOutDir, "server", "entry.js");
      const entryModule = await import(pathToFileURL(entryPath).href);
      const result = await entryModule.runMiddleware(new Request("http://localhost/protected"));

      expect(result.continue).toBe(false);
      expect(result.redirectStatus).toBe(307);
      expect(result.redirectUrl).toContain("/login");
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("runMiddleware in generated pages prod entry prefers named proxy export over default (matching Next.js)", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-proxy-precedence-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

      await fsp.writeFile(
        path.join(tmpRoot, "pages", "index.tsx"),
        "export default function Page() { return <div>ok</div>; }\n",
      );

      await fsp.writeFile(
        path.join(tmpRoot, "proxy.js"),
        `import { NextResponse } from "next/server";
export default function defaultProxy(request) {
  const url = new URL(request.url);
  if (url.pathname === "/protected") {
    return NextResponse.redirect(new URL("/from-default", request.url));
  }
  return NextResponse.next();
}
export function proxy(request) {
  const url = new URL(request.url);
  if (url.pathname === "/protected") {
    return NextResponse.redirect(new URL("/from-proxy", request.url));
  }
  return NextResponse.next();
}
export function middleware(request) {
  const url = new URL(request.url);
  if (url.pathname === "/protected") {
    return NextResponse.redirect(new URL("/from-middleware", request.url));
  }
  return NextResponse.next();
}
export const config = { matcher: ["/protected"] };
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: {
            output: {
              entryFileNames: "entry.js",
            },
          },
        },
      });

      const entryPath = path.join(fixtureOutDir, "server", "entry.js");
      const entryModule = await import(pathToFileURL(entryPath).href);
      const result = await entryModule.runMiddleware(new Request("http://localhost/protected"));

      expect(result.continue).toBe(false);
      expect(result.redirectStatus).toBe(307);
      expect(result.redirectUrl).toContain("/from-proxy");
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("produces client bundle with page chunks and SSR manifest", async () => {
    // Build the client bundle
    await build({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      logLevel: "silent",
      build: {
        outDir: path.join(outDir, "client"),
        manifest: true,
        ssrManifest: true,
        rollupOptions: {
          input: "virtual:vinext-client-entry",
        },
      },
    });

    // Verify client JS output exists under Next.js's canonical
    // `_next/static/chunks/` directory.
    const assetsDir = path.join(outDir, "client", "_next", "static", "chunks");
    expect(fs.existsSync(assetsDir)).toBe(true);

    // Verify SSR manifest was produced
    const manifestPath = path.join(outDir, "client", ".vite", "ssr-manifest.json");
    expect(fs.existsSync(manifestPath)).toBe(true);

    const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as Record<string, string[]>;
    // Manifest should have entries (module IDs -> asset URLs)
    expect(Object.keys(manifest).length).toBeGreaterThan(0);

    // Verify build manifest was also produced (needed for lazy chunk computation)
    const buildManifestPath = path.join(outDir, "client", ".vite", "manifest.json");
    expect(fs.existsSync(buildManifestPath)).toBe(true);
    const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")) as Record<
      string,
      ClientBuildManifestEntry
    >;
    const counterBuildManifestEntries = findBuildManifestEntries(
      buildManifest,
      "pages/counter.tsx",
    );
    expect(counterBuildManifestEntries.length).toBeGreaterThan(0);
    expect(counterBuildManifestEntries.some(([, entry]) => typeof entry.file === "string")).toBe(
      true,
    );

    // There should be JS files in the assets directory
    const assets = fs.readdirSync(assetsDir);
    const jsFiles = assets.filter((f: string) => f.endsWith(".js"));
    expect(jsFiles.length).toBeGreaterThan(0);

    // Client bundle should be code-split: framework (React/ReactDOM) in its
    // own chunk, vinext runtime in another, and the entry bootstrap should be
    // small (not a monolithic bundle containing all vendor code).
    const frameworkChunk = jsFiles.find((f: string) => f.startsWith("framework-"));
    const vinextChunk = jsFiles.find((f: string) => f.startsWith("vinext-"));
    const entryChunk = jsFiles.find((f: string) => f.includes("vinext-client-entry"));
    expect(frameworkChunk).toBeDefined();
    expect(vinextChunk).toBeDefined();
    expect(entryChunk).toBeDefined();

    // The entry chunk should be small (just the hydration bootstrap, not the
    // entire React framework). Before code-splitting this was ~200KB+.
    if (entryChunk) {
      const entrySize = fs.statSync(path.join(assetsDir, entryChunk)).size;
      expect(entrySize).toBeLessThan(25 * 1024); // < 25 KB
    }

    const counterManifestEntry = Object.entries(manifest).find(
      ([key]) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
    );
    expect(counterManifestEntry).toBeDefined();
    expect(counterManifestEntry?.[1].some((file: string) => file.endsWith(".js"))).toBe(true);
  });

  it("preserves basePath on backfilled SSR manifest entries and emitted asset tags", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-basepath-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default { basePath: "/docs" };\n`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "counter.tsx"),
        `import { useState } from "react";
export default function CounterPage() {
  const [count, setCount] = useState(0);
  return (
    <button data-testid="increment" onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const buildManifestPath = path.join(fixtureOutDir, "client", ".vite", "manifest.json");
      const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")) as Record<
        string,
        ClientBuildManifestEntry
      >;
      const counterBuildManifestEntries = findBuildManifestEntries(
        buildManifest,
        "pages/counter.tsx",
      );
      expect(counterBuildManifestEntries.length).toBeGreaterThan(0);
      expect(counterBuildManifestEntries.some(([, entry]) => typeof entry.file === "string")).toBe(
        true,
      );

      const manifestPath = path.join(fixtureOutDir, "client", ".vite", "ssr-manifest.json");
      const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as Record<
        string,
        string[]
      >;
      const counterManifestEntry = Object.entries(manifest).find(
        ([key]) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
      );
      expect(counterManifestEntry).toBeDefined();
      // Next.js parity: when `basePath` is set and `assetPrefix` is unset,
      // `assetPrefix` falls back to `basePath`. The on-disk layout therefore
      // mirrors `<basePath>/_next/static/...` rather than the legacy
      // `<basePath>/assets/...` Vite default.
      // See packages/next/src/server/config.ts:528-531.
      //
      // Every entry should be anchored under basePath. With the parity
      // fallback in effect, entries land under `<basePath>/_next/static/`
      // (Vite's raw SSR manifest may produce duplicate-prefixed entries
      // alongside the backfilled ones — both forms start with `docs/` so
      // the prod-server's URL→file lookup is unaffected. The
      // user-visible HTML asserts below are the source of truth).
      expect(counterManifestEntry?.[1].every((file: string) => file.startsWith("docs/"))).toBe(
        true,
      );

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;
        const res = await fetch(`${baseUrl}/docs/counter`);
        expect(res.status).toBe(200);
        const html = await res.text();
        // Asset URLs land under `<basePath>/_next/static/` per Next.js
        // parity (basePath→assetPrefix fallback). Stylesheets and scripts
        // both share the same prefix.
        expect(html).toContain('href="/docs/_next/static/');
        expect(html).toContain('src="/docs/_next/static/');

        // Every emitted asset URL must actually resolve to 200 from the
        // prod server. The previous version of this test only asserted
        // the URLs APPEAR in HTML, not that they were served correctly.
        // The Pages Router asset lookup was stripping basePath BEFORE
        // matching against the assetPrefix, so requests for
        // `/docs/_next/static/...` were 404ing when assetPrefix fell
        // back to basePath (round-5 review feedback on #1311).
        const assetUrls = new Set<string>();
        for (const m of html.matchAll(
          /<(?:script|link)[^>]+(?:src|href)="(\/docs\/_next\/[^"]+)"/g,
        )) {
          assetUrls.add(m[1]);
        }
        expect(assetUrls.size).toBeGreaterThan(0);
        for (const url of assetUrls) {
          const assetRes = await fetch(`${baseUrl}${url}`);
          expect(assetRes.status, `expected 200 for ${url}`).toBe(200);
        }
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("emits Pages asset tags under a distinct assetPrefix (not basePath) and serves them 200", async () => {
    // Regression guard for the basePath + distinct path-style assetPrefix bug:
    // collectAssetTags used to emit modulepreload/script hrefs as
    // /<basePath>/<assetPrefix>/_next/... (404) because the SSR-manifest values
    // are base-anchored. assetPrefix REPLACES basePath for asset URLs, so the
    // emitted hrefs must be /<assetPrefix>/_next/... — which is what actually
    // serves. This test fetches every emitted asset URL and asserts 200.
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-baseprefix-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default { basePath: "/docs", assetPrefix: "/cdn" };\n`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "counter.tsx"),
        `import { useState } from "react";
export default function CounterPage() {
  const [count, setCount] = useState(0);
  return (
    <button data-testid="increment" onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({ port: 0, host: "127.0.0.1", outDir: fixtureOutDir }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;
        // Route is under basePath; assets are under assetPrefix.
        const res = await fetch(`${baseUrl}/docs/counter`);
        expect(res.status).toBe(200);
        const html = await res.text();

        // Asset hrefs are anchored under the assetPrefix, NOT basePath, and NOT
        // the buggy base+prefix combination.
        expect(html).toContain('src="/cdn/_next/static/');
        expect(html).not.toContain("/docs/cdn/");
        expect(html).not.toContain('src="/docs/_next/static/');

        // The definitive guard: every emitted asset URL must serve 200.
        const assetUrls = new Set<string>();
        for (const m of html.matchAll(
          /<(?:script|link)[^>]+(?:src|href)="(\/cdn\/_next\/[^"]+)"/g,
        )) {
          assetUrls.add(m[1]);
        }
        expect(assetUrls.size).toBeGreaterThan(0);
        for (const url of assetUrls) {
          const assetRes = await fetch(`${baseUrl}${url}`);
          expect(assetRes.status, `expected 200 for ${url}`).toBe(200);
        }
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("renders pages/404 for basePath route misses after stripping one basePath segment", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-basepath-404-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default { basePath: "/docs" };\n`,
      );
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "404.tsx"),
        `export default function Custom404() {
  return <main id="custom-404">This page could not be found</main>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "hello.tsx"),
        `export default function Hello() {
  return <main id="hello">Hello World</main>;
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const res = await fetch(`${baseUrl}/docs/docs/other-page`);
        expect(res.status).toBe(404);
        const html = await res.text();
        expect(html).toContain('id="custom-404"');
        expect(html).toContain("This page could not be found");
        expect(html).toContain('"page":"/404"');
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("applies fallback rewrites before rendering custom 404 pages", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-fallback-before-404-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default {
  basePath: "/docs",
  async rewrites() {
    return {
      fallback: [{ source: "/:path*", destination: "/fallback" }],
    };
  },
};
`,
      );
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "404.tsx"),
        `export default function Custom404() {
  const shouldThrow = Boolean(
    (globalThis as { __VINEXT_FALLBACK_REWRITE_TEST_RUNTIME?: boolean })
      .__VINEXT_FALLBACK_REWRITE_TEST_RUNTIME,
  );
  if (shouldThrow) {
    throw new Error("pages/404 should not execute before fallback rewrites");
  }
  return <main id="custom-404">This page could not be found</main>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "fallback.tsx"),
        `export default function Fallback() {
  return <main id="fallback">Fallback rewrite</main>;
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const explicitNotFoundRes = await fetch(`${baseUrl}/docs/404`);
        expect(explicitNotFoundRes.status).toBe(404);
        const explicitNotFoundHtml = await explicitNotFoundRes.text();
        expect(explicitNotFoundHtml).toContain('id="custom-404"');
        expect(explicitNotFoundHtml).toContain("This page could not be found");
        expect(explicitNotFoundHtml).toContain('"page":"/404"');
        expect(explicitNotFoundHtml).not.toContain('id="fallback"');

        (
          globalThis as { __VINEXT_FALLBACK_REWRITE_TEST_RUNTIME?: boolean }
        ).__VINEXT_FALLBACK_REWRITE_TEST_RUNTIME = true;
        const res = await fetch(`${baseUrl}/docs/missing`);
        expect(res.status).toBe(200);
        const html = await res.text();
        expect(html).toContain('id="fallback"');
        expect(html).toContain("Fallback rewrite");
        expect(html).toContain('"page":"/fallback"');
        expect(html).not.toContain("pages/404 should not execute before fallback rewrites");
      } finally {
        delete (globalThis as { __VINEXT_FALLBACK_REWRITE_TEST_RUNTIME?: boolean })
          .__VINEXT_FALLBACK_REWRITE_TEST_RUNTIME;
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("falls back to pages/_error for route misses when pages/404 is absent", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-basepath-error-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default { basePath: "/docs" };\n`,
      );
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "_error.tsx"),
        `export default function ErrorPage({ statusCode }: { statusCode?: number }) {
  return <main id="custom-error">Error status: {statusCode}</main>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "hello.tsx"),
        `export default function Hello() {
  return <main id="hello">Hello World</main>;
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const res = await fetch(`${baseUrl}/docs/docs/other-page`);
        expect(res.status).toBe(404);
        const html = await res.text();
        expect(html).toContain('id="custom-error"');
        expect(html).toContain("Error status:");
        expect(html).toContain("404</main>");
        expect(html).toContain('"page":"/_error"');
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  // Ported from Next.js: test/e2e/error-handler-not-found-req-url/error-handler-not-found-req-url.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/error-handler-not-found-req-url/error-handler-not-found-req-url.test.ts
  it("passes the original request URL and asPath to _error.getInitialProps for getStaticProps notFound", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-gsp-notfound-error-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "_error.tsx"),
        `import type { NextPageContext } from "next";

type ErrorProps = { reqUrl?: string; asPath?: string };

ErrorPage.getInitialProps = (ctx: NextPageContext): ErrorProps => {
  return {
    reqUrl: ctx.req?.url,
    asPath: ctx.asPath,
  };
};

export default function ErrorPage({ reqUrl, asPath }: ErrorProps) {
  return <p>reqUrl: {reqUrl}, asPath: {asPath}</p>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "[slug].tsx"),
        `export default function Page() {
  return <p>hello world</p>;
}

export async function getStaticProps() {
  return {
    notFound: true,
  };
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: "blocking",
  };
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const res = await fetch(`${baseUrl}/3`);
        expect(res.status).toBe(404);
        const html = await res.text();
        const visibleText = html
          .replace(/<!--[\s\S]*?-->/g, "")
          .replace(/<[^>]*>/g, "")
          .replace(/\s+/g, " ")
          .trim();

        expect(visibleText).toContain("reqUrl: /3, asPath: /3");
        expect(html).toContain('"page":"/_error"');
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("renders explicit pages/404 over pages/_error when getStaticProps returns notFound", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-gsp-notfound-404-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "_error.tsx"),
        `export default function ErrorPage() {
  return <p id="error">_error page</p>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "404.tsx"),
        `export default function Custom404() {
  return <p id="custom-404">custom 404</p>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "[slug].tsx"),
        `export default function Page() {
  return <p>hello world</p>;
}

export async function getStaticProps() {
  return {
    notFound: true,
  };
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: "blocking",
  };
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const res = await fetch(`${baseUrl}/3`);
        expect(res.status).toBe(404);
        const html = await res.text();
        expect(html).toContain('id="custom-404"');
        expect(html).toContain("custom 404");
        expect(html).not.toContain('id="error"');
        expect(html).toContain('"page":"/404"');
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("passes the original error to _error.getInitialProps when getServerSideProps throws", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-gssp-throw-error-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "_error.tsx"),
        `import type { NextPageContext } from "next";

type ErrorProps = { errMessage?: string };

ErrorPage.getInitialProps = (ctx: NextPageContext): ErrorProps => {
  return {
    errMessage: ctx.err instanceof Error ? ctx.err.message : String(ctx.err),
  };
};

export default function ErrorPage({ errMessage }: ErrorProps) {
  return <p>errMessage: {errMessage}</p>;
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "index.tsx"),
        `export default function Page() {
  return <p>hello world</p>;
}

export async function getServerSideProps() {
  throw new Error("intentional gssp throw");
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;

        const res = await fetch(`${baseUrl}/`);
        expect(res.status).toBe(500);
        const html = await res.text();
        const visibleText = html
          .replace(/<!--[\s\S]*?-->/g, "")
          .replace(/<[^>]*>/g, "")
          .replace(/\s+/g, " ")
          .trim();

        expect(visibleText).toContain("errMessage: intentional gssp throw");
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("preserves 404 status for cached ISR custom 404 route misses", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-isr-404-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(path.join(tmpRoot, "pages", "_app.tsx"), PAGES_APP_COMPONENT);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "404.tsx"),
        `export async function getStaticProps() {
  return { props: { marker: "custom ISR 404" }, revalidate: 60 };
}

export default function Custom404({ marker }: { marker: string }) {
  return <main id="custom-404">{marker}</main>;
}
`,
      );

      await buildPagesFixtureToOutDir(tmpRoot, fixtureOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const baseUrl = `http://127.0.0.1:${addr.port}`;
        const missingUrl = `${baseUrl}/cached-custom-404-miss`;

        const first = await fetch(missingUrl);
        expect(first.status).toBe(404);
        expect(first.headers.get("x-vinext-cache")).toBe("MISS");
        const firstHtml = await first.text();
        expect(firstHtml).toContain('id="custom-404"');
        expect(firstHtml).toContain("custom ISR 404");

        const second = await fetch(missingUrl);
        expect(second.status).toBe(404);
        expect(second.headers.get("x-vinext-cache")).toBe("HIT");
        const secondHtml = await second.text();
        expect(secondHtml).toContain('id="custom-404"');
        expect(secondHtml).toContain("custom ISR 404");
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("emits stylesheet and static asset URLs for backfilled inlined pages", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-inline-assets-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const fixtureOutDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "counter.module.css"),
        `.button { color: red; background-image: url("./dot.svg"); }\n`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "dot.svg"),
        `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5"/></svg>\n`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "counter.tsx"),
        `import { useState } from "react";
import styles from "./counter.module.css";
export default function CounterPage() {
  const [count, setCount] = useState(0);
  return (
    <button className={styles.button} data-testid="increment" onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  );
}
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(fixtureOutDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const buildManifestPath = path.join(fixtureOutDir, "client", ".vite", "manifest.json");
      const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")) as Record<
        string,
        ClientBuildManifestEntry
      >;
      const counterBuildManifestEntries = findBuildManifestEntries(
        buildManifest,
        "pages/counter.tsx",
      );
      expect(counterBuildManifestEntries.length).toBeGreaterThan(0);
      expect(
        counterBuildManifestEntries.some(
          ([, entry]) =>
            typeof entry.file === "string" ||
            (Array.isArray(entry.css) && entry.css.length > 0) ||
            (Array.isArray(entry.assets) && entry.assets.length > 0),
        ),
      ).toBe(true);

      const manifestPath = path.join(fixtureOutDir, "client", ".vite", "ssr-manifest.json");
      const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as Record<
        string,
        string[]
      >;
      const counterManifestEntries = Object.entries(manifest).filter(
        ([key]) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
      );
      expect(counterManifestEntries.length).toBeGreaterThan(0);
      const populatedCounterManifestEntry = counterManifestEntries.find(([, files]) =>
        files.some((file: string) => file.endsWith(".css")),
      );
      expect(populatedCounterManifestEntry).toBeDefined();
      const cssFile = populatedCounterManifestEntry?.[1].find((file: string) =>
        file.endsWith(".css"),
      );
      expect(cssFile).toBeDefined();
      const cssContent = fs.readFileSync(path.join(fixtureOutDir, "client", cssFile!), "utf-8");
      expect(cssContent).toContain("url(");
      expect(cssContent).toMatch(/data:image\/svg\+xml|\.svg/);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: fixtureOutDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const res = await fetch(`http://127.0.0.1:${addr.port}/counter`);
        expect(res.status).toBe(200);
        const html = await res.text();
        expect(html).toContain('rel="stylesheet"');
        expect(html).toContain(".css");
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("serves pages from production build end-to-end", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const manifestPath = path.join(outDir, "client", ".vite", "ssr-manifest.json");

    // Both should exist from prior tests
    if (!fs.existsSync(serverEntryPath) || !fs.existsSync(manifestPath)) {
      // Build if needed (tests may run in isolation)
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });
    }

    // Import the server entry
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));

    // Create a minimal HTTP server using the built entry.
    // The server entry uses Web-standard Request/Response, so we bridge
    // from Node.js HTTP objects.
    const { createServer: createHttpServer } = await import("node:http");
    const httpServer = createHttpServer((req, res) => {
      void (async () => {
        const url = req.url ?? "/";
        const pathname = url.split("?")[0];

        // Convert Node.js req to Web Request
        const headers = new Headers();
        for (const [k, v] of Object.entries(req.headers)) {
          if (v) headers.set(k, Array.isArray(v) ? v.join(", ") : v);
        }
        const host = req.headers.host ?? "localhost";
        const method = req.method ?? "GET";
        const init: RequestInit & { duplex?: "half" } = {
          method,
          headers,
        };
        if (method !== "GET" && method !== "HEAD") {
          init.body = Readable.toWeb(req) as ReadableStream;
          init.duplex = "half";
        }
        const webRequest = new Request(`http://${host}${url}`, init);

        let response: Response;
        if (pathname.startsWith("/api/") || pathname === "/api") {
          response = await serverEntry.handleApiRoute(webRequest, url);
        } else {
          response = await serverEntry.renderPage(webRequest, url, manifest);
        }

        // Pipe Web Response back to Node.js res
        const body = await response.text();
        const resHeaders: Record<string, string> = {};
        response.headers.forEach((v: string, k: string) => {
          resHeaders[k] = v;
        });
        res.writeHead(response.status, response.statusText || undefined, resHeaders);
        res.end(body);
      })().catch((error: unknown) => {
        res.destroy(error instanceof Error ? error : new Error(String(error)));
      });
    });

    // Start on a random port
    await new Promise<void>((resolve) => httpServer.listen(0, resolve));
    const addr = httpServer.address() as { port: number };
    const prodUrl = `http://localhost:${addr.port}`;

    try {
      // Test: index page renders
      const indexRes = await fetch(`${prodUrl}/`);
      expect(indexRes.status).toBe(200);
      const indexHtml = await indexRes.text();
      expect(indexHtml).toContain("Hello, vinext!");
      expect(indexHtml).toContain("__NEXT_DATA__");

      // Test: about page renders
      const aboutRes = await fetch(`${prodUrl}/about`);
      expect(aboutRes.status).toBe(200);
      const aboutHtml = await aboutRes.text();
      expect(aboutHtml).toContain("About");

      const isrFirstRes = await fetch(`${prodUrl}/isr-second-render-state`);
      expect(isrFirstRes.status).toBe(200);
      expect(isrFirstRes.headers.get("x-vinext-cache")).toBe("MISS");
      const isrFirstHtml = await isrFirstRes.text();
      expect(isrFirstHtml).toContain('data-testid="head-before">0<');
      expect(isrFirstHtml).toContain('data-testid="private-cache-before">0<');
      expect(isrFirstHtml).toContain('data-testid="inserted-html-before">0<');

      const isrSecondRes = await fetch(`${prodUrl}/isr-second-render-state`);
      expect(isrSecondRes.status).toBe(200);
      expect(isrSecondRes.headers.get("x-vinext-cache")).toBe("HIT");
      const isrSecondHtml = await isrSecondRes.text();
      expect(isrSecondHtml).toContain('data-testid="head-before">0<');
      expect(isrSecondHtml).toContain('data-testid="private-cache-before">0<');
      expect(isrSecondHtml).toContain('data-testid="inserted-html-before">0<');

      // Test: SSR page with getServerSideProps
      const ssrRes = await fetch(`${prodUrl}/ssr`);
      expect(ssrRes.status).toBe(200);
      // Regression for #1461: gssp pages get the default Cache-Control header.
      expect(ssrRes.headers.get("cache-control")).toBe(
        "private, no-cache, no-store, max-age=0, must-revalidate",
      );
      const ssrHtml = await ssrRes.text();
      expect(ssrHtml).toContain("Server-Side Rendered");

      // Regression for #1461: user-set Cache-Control via res.setHeader sticks.
      const ssrCcRes = await fetch(`${prodUrl}/ssr-cache-control`);
      expect(ssrCcRes.status).toBe(200);
      expect(ssrCcRes.headers.get("cache-control")).toBe("public, max-age=42");
      await ssrCcRes.text();

      // Regression test for #1354: a page that exports `getServerSideProps`
      // via a separate `export { getServerSideProps }` re-export must build
      // and render in production. Previously, the client bundle transform
      // emitted a stub `export const getServerSideProps = undefined;` that
      // collided with the user's local `const getServerSideProps = ...`
      // binding and broke the Rolldown/OXC parse step.
      const gsspNamedRes = await fetch(`${prodUrl}/gssp-named-export`);
      expect(gsspNamedRes.status).toBe(200);
      const gsspNamedHtml = await gsspNamedRes.text();
      expect(gsspNamedHtml).toContain("gSSP via named export");
      expect(gsspNamedHtml).toContain("Hello from named-export gSSP");

      // Test: API route
      const apiRes = await fetch(`${prodUrl}/api/hello`);
      expect(apiRes.status).toBe(200);
      const apiData = await apiRes.json();
      expect(apiData).toEqual({ message: "Hello from API!" });

      const invalidJsonRes = await fetch(`${prodUrl}/api/parse`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: `{"message":Invalid"}`,
      });
      expect(invalidJsonRes.status).toBe(400);
      expect(invalidJsonRes.statusText).toBe("Invalid JSON");
      expect(await invalidJsonRes.text()).toBe("Invalid JSON");

      const duplicateFormRes = await fetch(`${prodUrl}/api/parse`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: "tag=a&tag=b&tag=c",
      });
      expect(duplicateFormRes.status).toBe(200);
      expect(await duplicateFormRes.json()).toEqual({ tag: ["a", "b", "c"] });

      const emptyJsonRes = await fetch(`${prodUrl}/api/parse`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: "",
      });
      expect(emptyJsonRes.status).toBe(200);
      expect(await emptyJsonRes.json()).toEqual({});

      const ldJsonRes = await fetch(`${prodUrl}/api/parse`, {
        method: "POST",
        headers: { "Content-Type": "application/ld+json; charset=utf-8" },
        body: JSON.stringify({ title: "doc" }),
      });
      expect(ldJsonRes.status).toBe(200);
      expect(await ldJsonRes.json()).toEqual({ title: "doc" });

      // Test: Pages Router edge runtime API route. Regression coverage for
      // cloudflare/vinext#1338 — edge runtime API routes were reported as
      // returning 500 against the Next.js deploy suite. Verifies the
      // production server entry correctly dispatches edge handlers.
      const edgeApiRes = await fetch(`${prodUrl}/api/edge-hello?a=b`);
      expect(edgeApiRes.status).toBe(200);
      expect(edgeApiRes.headers.get("content-type")).toContain("application/json");
      expect(await edgeApiRes.json()).toEqual({ hello: "world", query: { a: "b" } });

      // Test: Pages Router edge runtime OG image route. Regression coverage
      // for cloudflare/vinext#1338 — OG routes were reported as returning
      // 404 against the Next.js deploy suite.
      const ogRes = await fetch(`${prodUrl}/api/og`);
      expect(ogRes.status).toBe(200);
      expect(ogRes.headers.get("content-type")).toContain("image/png");
      expect((await ogRes.blob()).size).toBeGreaterThan(0);

      // Test: 404 for unknown route
      const notFoundRes = await fetch(`${prodUrl}/nonexistent`);
      expect(notFoundRes.status).toBe(404);

      // Test: page using top-level await (async module).
      // Ported from Next.js: test/e2e/async-modules/index.test.ts
      // https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts
      const asyncModRes = await fetch(`${prodUrl}/async-modules-test`);
      expect(asyncModRes.status).toBe(200);
      const asyncModHtml = await asyncModRes.text();
      expect(asyncModHtml).toContain('<div id="app-value">hello</div>');
      expect(asyncModHtml).toContain('<div id="page-value">42</div>');

      // Regression for #1458: when getServerSideProps throws, the production
      // server must render the user's custom pages/500.tsx with status 500
      // instead of returning a plain "Internal Server Error" text response.
      // Mirrors Next.js test/e2e/getserversideprops "should handle throw
      // ENOENT correctly" (.nextjs-ref/test/e2e/getserversideprops/test/index.test.ts:377).
      const gsspThrowRes = await fetch(`${prodUrl}/gssp-throw`);
      expect(gsspThrowRes.status).toBe(500);
      const gsspThrowHtml = await gsspThrowRes.text();
      expect(gsspThrowHtml).toContain("custom pages/500");
      expect(gsspThrowHtml).not.toBe("Internal Server Error");
    } finally {
      httpServer.close();
    }
  });

  it("server entry exports runMiddleware function", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    expect(typeof serverEntry.runMiddleware).toBe("function");
  });

  it("runMiddleware skips non-matching paths", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    // The middleware matcher is /((?!api|_next|favicon\.ico).*) so /api should not match
    const request = new Request("http://localhost/api/hello");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(true);
    expect(result.redirectUrl).toBeUndefined();
  });

  it("runMiddleware handles redirect (/old-page -> /about)", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/old-page");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(false);
    expect(result.redirectUrl).toContain("/about");
    expect(result.redirectStatus).toBe(307);
  });

  it("runMiddleware preserves responseHeaders on redirect (/redirect-with-cookies)", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/redirect-with-cookies");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(false);
    expect(result.redirectUrl).toContain("/about");
    expect(result.redirectStatus).toBe(307);
    // The inline runMiddleware codegen must collect non-internal headers
    // (e.g. Set-Cookie) on redirect responses, just like it does for
    // next() and rewrite() responses.
    expect(result.responseHeaders).toBeDefined();
    const cookies = [...result.responseHeaders.entries()]
      .filter(([k]: [string, string]) => k === "set-cookie")
      .map(([, v]: [string, string]) => v);
    expect(cookies.some((c: string) => c.includes("mw-session=abc123"))).toBe(true);
    expect(cookies.some((c: string) => c.includes("mw-theme=dark"))).toBe(true);
  });

  it("runMiddleware handles rewrite (/rewritten -> /ssr)", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/rewritten");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(true);
    expect(result.rewriteUrl).toContain("/ssr");
  });

  it("runMiddleware preserves internal middleware cookie headers on rewrites", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/rewrite-with-cookie");
    const result = await serverEntry.runMiddleware(request);

    expect(result.continue).toBe(true);
    expect(result.rewriteUrl).toContain("/ssr");
    expect(result.responseHeaders.get("x-middleware-set-cookie")).toContain(
      "rewrite-cookie=visible",
    );
  });

  // 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("runMiddleware preserves external middleware rewrite destinations", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    if (!fs.existsSync(serverEntryPath)) {
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: {
            output: {
              entryFileNames: "entry.js",
            },
          },
        },
      });
    }
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/external-middleware-rewrite");
    const result = await serverEntry.runMiddleware(request);

    expect(result.continue).toBe(true);
    expect(result.rewriteUrl).toBe("https://api.example.com/from-middleware?ok=1");
  });

  it("runMiddleware handles block (/blocked -> 403)", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/blocked");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(false);
    expect(result.response).toBeInstanceOf(Response);
    expect(result.response.status).toBe(403);
  });

  it("runMiddleware strips internal cookie headers from custom responses", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/blocked-with-cookie");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(false);
    expect(result.response).toBeInstanceOf(Response);
    expect(result.response.status).toBe(403);
    expect(result.response.headers.get("x-middleware-set-cookie")).toBeNull();
    expect(result.response.headers.get("set-cookie")).toContain("blocked=1");
  });

  it("runMiddleware sets x-custom-middleware header on matched paths", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    // /about matches the middleware but doesn't redirect/rewrite/block
    const request = new Request("http://localhost/about");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(true);
    expect(result.responseHeaders).toBeDefined();
    expect(result.responseHeaders.get("x-custom-middleware")).toBe("active");
  });

  it("runMiddleware preserves x-middleware-request-* headers from NextResponse.next({ request: { headers } })", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    // /header-override triggers NextResponse.next({ request: { headers } }) which sets
    // x-middleware-request-x-custom-injected header. The runMiddleware codegen must
    // preserve these so the downstream consumer can unpack them into actual request headers.
    const request = new Request("http://localhost/header-override");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(true);
    expect(result.responseHeaders).toBeDefined();
    // x-middleware-request-* headers must be preserved (the fix)
    expect(result.responseHeaders.get("x-middleware-request-x-custom-injected")).toBe(
      "from-middleware",
    );
    // Other x-middleware-* internal headers must be stripped
    expect(result.responseHeaders.get("x-middleware-next")).toBeNull();
  });

  it("runMiddleware returns 500 when middleware throws", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    const request = new Request("http://localhost/middleware-throw");
    const result = await serverEntry.runMiddleware(request);
    expect(result.continue).toBe(false);
    expect(result.response).toBeInstanceOf(Response);
    expect(result.response.status).toBe(500);
  });
});

describe("Production server middleware (Pages Router)", () => {
  const outDir = path.resolve(FIXTURE_DIR, "dist");
  let prodServer: import("node:http").Server | undefined;
  let prodUrl: string;

  beforeAll(async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const manifestPath = path.join(outDir, "client", ".vite", "ssr-manifest.json");

    // Build if needed (tests may run in isolation)
    if (!fs.existsSync(serverEntryPath) || !fs.existsSync(manifestPath)) {
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });
    }

    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  });

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer!.close(() => resolve()));
    }
  });

  it("redirects /old-page to /about via middleware", async () => {
    const res = await fetch(`${prodUrl}/old-page`, { redirect: "manual" });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  // 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("passes middleware rewrite search params to Pages Router edge API nextUrl in production", async () => {
    const res = await fetch(`${prodUrl}/api/edge-search-params?a=b`);
    expect(res.status).toBe(200);
    await expect(res.json()).resolves.toEqual({
      a: "b",
      foo: "bar",
    });
  });

  it("preserves the original pathname and adds route params for rewritten edge APIs in production", async () => {
    const res = await fetch(`${prodUrl}/edge-api-rewrite/id-1?a=b`);
    expect(res.status).toBe(200);
    await expect(res.json()).resolves.toEqual({
      pathname: "/edge-api-rewrite/id-1",
      query: {
        a: "b",
        foo: "bar",
        id: "id-1",
      },
    });
  });

  // Refs #1463: prod-server parity for the dev-server 405 check. POST to a
  // static Pages Router page must return 405 + Allow: GET, HEAD.
  // Ported from Next.js: test/e2e/prerender.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/prerender.test.ts
  // ('should respond with 405 for POST to static page').
  it("returns 405 with Allow: GET, HEAD on POST to a static Pages page (prod)", async () => {
    const res = await fetch(`${prodUrl}/about`, { method: "POST" });
    expect(res.status).toBe(405);
    expect(res.headers.get("allow")).toBe("GET, HEAD");
    expect(await res.text()).toContain("Method Not Allowed");
  });

  // Regression for #1331: after a middleware rewrite, the rewrite target
  // must go through full route resolution where static routes win over
  // dynamic catch-alls. Without the fix the `[id]` dynamic page captures
  // the rewrite target and renders "Dynamic route" with id="rewrite-me".
  it("middleware rewrite resolves static index over [id] dynamic route in production", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-mw-rewrite-priority-prod-"));
    writeMiddlewareRewritePriorityFixture(tmpDir);

    let prodServer: import("node:http").Server | undefined;
    try {
      await build({
        root: tmpDir,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(tmpDir, "dist", "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: tmpDir,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(tmpDir, "dist", "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: path.join(tmpDir, "dist"),
        }),
      );
      const addr = prodServer.address() as { port: number };
      const tempProdUrl = `http://127.0.0.1:${addr.port}`;

      const indexRes = await fetch(`${tempProdUrl}/rewrite-me/`);
      expect(indexRes.status).toBe(200);
      const indexHtml = await indexRes.text();
      // `id="home"` is unique to `pages/index.tsx`; ssr-page also says
      // "Hello World" so this disambiguates that the index rendered.
      expect(indexHtml).toContain('id="home"');
      expect(indexHtml).toContain("Hello World");
      expect(indexHtml).not.toContain("Dynamic route");

      const aboutRes = await fetch(`${tempProdUrl}/rewrite-to-about/`);
      expect(aboutRes.status).toBe(200);
      const aboutHtml = await aboutRes.text();
      expect(aboutHtml).toContain("About Page");
      expect(aboutHtml).not.toContain("Dynamic route");

      // Next.js parity: with trailingSlash: true and a [id] dynamic root,
      // `/rewrite-1/` matches `[id]` but afterFiles config rewrites must
      // still rewrite it to /ssr-page, and the rewrite target must resolve
      // to the static ssr-page rather than back into [id].
      const cfgRes = await fetch(`${tempProdUrl}/rewrite-1/`);
      expect(cfgRes.status).toBe(200);
      const cfgHtml = await cfgRes.text();
      // `id="ssr"` is unique to `pages/ssr-page.tsx`; `pages/index.tsx`
      // also says "Hello World" so this disambiguates that the rewrite
      // target rendered (not the index, not the dynamic [id]).
      expect(cfgHtml).toContain('id="ssr"');
      expect(cfgHtml).toContain("Hello World");
      expect(cfgHtml).not.toContain("Dynamic route");
    } finally {
      await new Promise<void>((resolve) => prodServer?.close(() => resolve()) ?? resolve());
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("does not collapse encoded slashes onto nested routes in production", async () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-pages-encoded-prod-"));
    writeEncodedSlashPagesFixture(tmpDir);

    let prodServer: import("node:http").Server | undefined;
    try {
      await build({
        root: tmpDir,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(tmpDir, "dist", "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: tmpDir,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(tmpDir, "dist", "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: path.join(tmpDir, "dist"),
        }),
      );
      const addr = prodServer.address() as { port: number };
      const tempProdUrl = `http://127.0.0.1:${addr.port}`;

      const encodedRes = await fetch(`${tempProdUrl}/a%2Fb`);
      expect(encodedRes.status).toBe(404);
      expect(await encodedRes.text()).not.toContain("nested blocked");

      const nestedRes = await fetch(`${tempProdUrl}/a/b`);
      expect(nestedRes.status).toBe(418);
      expect(await nestedRes.text()).toBe("nested blocked");
    } finally {
      await new Promise<void>((resolve) => prodServer?.close(() => resolve()) ?? resolve());
      fs.rmSync(tmpDir, { recursive: true, force: true });
    }
  });

  it("preserves Set-Cookie headers on middleware redirect", async () => {
    const res = await fetch(`${prodUrl}/redirect-with-cookies`, {
      redirect: "manual",
    });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
    // Middleware sets mw-session and mw-theme cookies on this redirect.
    // These must survive into the production response — not be dropped.
    const cookies = res.headers.getSetCookie();
    expect(cookies.some((c: string) => c.includes("mw-session=abc123"))).toBe(true);
    expect(cookies.some((c: string) => c.includes("mw-theme=dark"))).toBe(true);
  });

  it("adds middleware CSP nonces to production Pages Router scripts and preloads", async () => {
    const res = await fetch(`${prodUrl}/dynamic-page?mw-csp-nonce=pages-prod`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-security-policy")).toBe(
      "script-src 'nonce-pages-prod' 'strict-dynamic';",
    );

    const html = await res.text();
    expect(html).toContain(
      '<script id="__NEXT_DATA__" type="application/json" nonce="pages-prod">',
    );
    expect(html).toMatch(/<script type="module" defer nonce="pages-prod" src="\/[^"]+"/);
    expect(html).toMatch(/<link rel="modulepreload" nonce="pages-prod" href="\/[^"]+"/);
  });

  // Ported from Next.js: test/e2e/optimized-loading/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/optimized-loading/test/index.test.ts
  //
  // Regression for #1519: optimized loading is enabled by default in Next.js
  // (`experimental.disableOptimizedLoading: false`). Page scripts must be
  // emitted with `defer` in <head>, not as plain scripts at the end of <body>
  // (and never as `async`). The Next.js E2E asserts both `script[async]
  // .length === 0` and `head script[defer].length > 0`.
  it("emits page scripts with defer in <head> by default (optimized loading)", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();

    // No async script tags (matches `script[async].length === 0` upstream).
    expect(html).not.toMatch(/<script[^>]*\sasync(\s|>|=)/);

    // Locate the head so we can search just that slice for defer scripts.
    const headEnd = html.indexOf("</head>");
    expect(headEnd).toBeGreaterThan(-1);
    const head = html.slice(0, headEnd);

    // Matches `head script[defer].length > 0` upstream.
    const deferInHead = head.match(/<script[^>]*\sdefer(\s|>|=)[^>]*>/g) ?? [];
    expect(deferInHead.length).toBeGreaterThan(0);
  });

  it("does not serve cached production Pages ISR HTML to CSP nonce requests", async () => {
    const first = await fetch(`${prodUrl}/isr-test`);
    expect(first.status).toBe(200);
    expect(first.headers.get("x-vinext-cache")).toBe("MISS");
    const firstHtml = await first.text();
    expect(firstHtml).not.toContain("nonce=");

    const second = await fetch(`${prodUrl}/isr-test?mw-csp-nonce=pages-prod-isr`);
    expect(second.status).toBe(200);
    expect(second.headers.get("content-security-policy")).toBe(
      "script-src 'nonce-pages-prod-isr' 'strict-dynamic';",
    );
    expect(second.headers.get("cache-control")).toBe("no-store, must-revalidate");
    expect(second.headers.get("x-vinext-cache")).toBeNull();
    const secondHtml = await second.text();
    expect(secondHtml).toContain(
      '<script id="__NEXT_DATA__" type="application/json" nonce="pages-prod-isr">',
    );
  });

  it("rewrites /rewritten to render /ssr content", async () => {
    const res = await fetch(`${prodUrl}/rewritten`);
    expect(res.status).toBe(200);
    const html = await res.text();
    // /rewritten should serve the content of /ssr page
    expect(html).toContain("Server-Side Rendered");
  });

  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ('should rewrite to fallback: true page successfully').
  // Refs #1331: post-rewrite fallback: true must render the loading shell.
  it("renders the loading shell when middleware/route targets an unlisted fallback: true path", async () => {
    const res = await fetch(`${prodUrl}/products/never-built`);
    expect(res.status).toBe(200);
    const html = await res.text();
    // Page renders its fallback branch (the slug is not in getStaticPaths).
    expect(html).toContain("Loading product...");
    // Full-data branch must not have rendered — getStaticProps was skipped.
    expect(html).not.toMatch(/Product ID:.*never-built/);
    const match = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(match).toBeTruthy();
    const nextData = JSON.parse(match![1]);
    expect(nextData.isFallback).toBe(true);
    expect(nextData.props).toEqual({ pageProps: {} });
  });

  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ('should handle middleware rewrite with body correctly').
  // Refs #1331: POST bodies must reach the upstream when middleware
  // externally rewrites the request.
  it("forwards the POST body to the upstream on external middleware rewrites", async () => {
    const { createServer: createHttpServer } = await import("node:http");
    const upstream = createHttpServer((req, res) => {
      const chunks: Buffer[] = [];
      req.on("data", (chunk: Buffer) => chunks.push(chunk));
      req.on("end", () => {
        const received = Buffer.concat(chunks);
        res.writeHead(200, { "content-type": "application/json" });
        res.end(received);
      });
    });

    try {
      await new Promise<void>((resolve) => upstream.listen(0, "127.0.0.1", resolve));
      const addr = upstream.address();
      if (typeof addr === "string" || addr === null) throw new Error("Expected upstream port");

      const body = JSON.stringify({ hello: "world" });
      const res = await fetch(`${prodUrl}/external-middleware-rewrite-body`, {
        method: "POST",
        body,
        headers: {
          "content-type": "application/json",
          "x-middleware-test-rewrite-target": `http://127.0.0.1:${addr.port}/echo-body`,
        },
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe(body);
    } finally {
      await new Promise<void>((resolve) => upstream.close(() => resolve()));
    }
  });

  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ('should handle middleware rewrite with body and headers correctly').
  // Refs #1331: `NextResponse.rewrite(url, { request: { headers } })` request
  // header overrides must propagate to the proxied upstream request.
  it("forwards middleware-overridden request headers on external middleware rewrites", async () => {
    const { createServer: createHttpServer } = await import("node:http");
    const upstream = createHttpServer((req, res) => {
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ headers: req.headers }));
    });

    try {
      await new Promise<void>((resolve) => upstream.listen(0, "127.0.0.1", resolve));
      const addr = upstream.address();
      if (typeof addr === "string" || addr === null) throw new Error("Expected upstream port");

      const res = await fetch(`${prodUrl}/external-middleware-rewrite-with-headers`, {
        headers: {
          "x-middleware-test-rewrite-target": `http://127.0.0.1:${addr.port}/echo-headers`,
        },
      });
      expect(res.status).toBe(200);
      const json = (await res.json()) as { headers: Record<string, string> };
      expect(json.headers["x-hello-from-middleware1"]).toBe("hello");
    } finally {
      await new Promise<void>((resolve) => upstream.close(() => resolve()));
    }
  });

  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ('should rewrite to the external url for incoming data request
  //  externally rewritten'). Refs #1331: a `_next/data/<buildId>/<page>.json`
  // request whose middleware rewrites to an external URL must proxy through
  // — the data-request path is not allowed to short-circuit external rewrites.
  it("proxies through to upstream when an external middleware rewrite hits a data request", async () => {
    const { createServer: createHttpServer } = await import("node:http");
    const upstream = createHttpServer((_, res) => {
      res.writeHead(200, { "content-type": "text/html" });
      res.end("<!doctype html><html><body>External Domain</body></html>");
    });

    try {
      await new Promise<void>((resolve) => upstream.listen(0, "127.0.0.1", resolve));
      const addr = upstream.address();
      if (typeof addr === "string" || addr === null) throw new Error("Expected upstream port");

      const res = await fetch(`${prodUrl}/_next/data/test-build-id/data-external-rewrite.json`, {
        headers: {
          "x-nextjs-data": "1",
          "x-middleware-test-rewrite-target": `http://127.0.0.1:${addr.port}/data`,
        },
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toContain("External Domain");
    } finally {
      await new Promise<void>((resolve) => upstream.close(() => resolve()));
    }
  });

  // 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 upstream status for external middleware rewrites in production", async () => {
    const { createServer: createHttpServer } = await import("node:http");
    const upstream = createHttpServer((_, res) => {
      res.writeHead(418, { "content-type": "text/plain" });
      res.end("upstream status");
    });

    try {
      await new Promise<void>((resolve) => upstream.listen(0, "127.0.0.1", resolve));
      const addr = upstream.address();
      if (typeof addr === "string" || addr === null) throw new Error("Expected upstream port");

      const res = await fetch(`${prodUrl}/external-middleware-rewrite-status`, {
        headers: {
          "x-middleware-test-rewrite-target": `http://127.0.0.1:${addr.port}/external`,
        },
      });
      expect(res.status).toBe(418);
      expect(await res.text()).toBe("upstream status");
    } finally {
      await new Promise<void>((resolve) => upstream.close(() => resolve()));
    }
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // and
  // 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("applies next.config.js headers using the pre-middleware pathname after a rewrite", async () => {
    const res = await fetch(`${prodUrl}/headers-before-middleware-rewrite`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-rewrite-source-header")).toBe("1");
    const html = await res.text();
    expect(html).toContain("Server-Side Rendered");
  });

  // Regression for cloudflare/vinext#1342: original request query params must
  // survive a middleware rewrite into the rewrite target's getServerSideProps.
  // Mirrors the dev-server coverage so the production prod-server is exercised.
  // Ported from Next.js: test/e2e/edge-pages-support/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts
  it("middleware rewrite preserves original query params into getServerSideProps in production", async () => {
    const res = await fetch(`${prodUrl}/mw-rewrite-query?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toMatchObject({ hello: "world" });
  });

  it("middleware rewrite to a dynamic route merges original query with route params in production", async () => {
    const res = await fetch(`${prodUrl}/mw-rewrite-dynamic-query?hello=world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toMatchObject({ id: "first", hello: "world" });
  });

  // Regression for cloudflare/vinext#1342 (production): middleware that
  // explicitly deletes search params from `request.nextUrl` and rewrites to
  // it must observe only the keys it kept. Dev coverage of the same shared
  // code path exists in the integration describe above; this proves the
  // prod-server path agrees.
  // Ported from Next.js: test/e2e/middleware-rewrites/test/index.test.ts
  // ("should clear query parameters")
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-rewrites/test/index.test.ts
  it("middleware rewrite respects searchParams.delete on the rewrite-target URL in production", async () => {
    const res = await fetch(`${prodUrl}/mw-clear-query-params?a=1&b=2&foo=bar&allowed=kept`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextDataMatch = html.match(
      /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
    );
    expect(nextDataMatch).toBeTruthy();
    const nextData = JSON.parse(nextDataMatch![1]!);
    expect(nextData.props.pageProps.query).toEqual({ allowed: "kept" });
  });

  // /_next/data fetch for a middleware-rewritten page must also surface the
  // original request query params in the JSON props envelope. Client-side
  // navigations go through this code path, so a regression here would silently
  // break query state after a rewrite even when the HTML render is correct.
  it("middleware rewrite preserves original query params on _next/data JSON in production", async () => {
    const res = await fetch(
      `${prodUrl}/_next/data/test-build-id/mw-rewrite-query.json?hello=world`,
      { headers: { "x-nextjs-data": "1" } },
    );
    expect(res.status).toBe(200);
    const data = (await res.json()) as {
      pageProps: { query: Record<string, string | string[]> };
    };
    expect(data.pageProps.query).toMatchObject({ hello: "world" });
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // and
  // 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("applies next.config.js redirects before middleware rewrites in production", async () => {
    const res = await fetch(`${prodUrl}/redirect-before-middleware-rewrite`, {
      redirect: "manual",
    });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  // Ported from Next.js:
  // test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/rewrites-redirects/rewrites-redirects.test.ts
  it("applies next.config.js redirects before middleware responses in production", async () => {
    const res = await fetch(`${prodUrl}/redirect-before-middleware-response`, {
      redirect: "manual",
    });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toContain("/about");
  });

  it("blocks /blocked with 403 via middleware", async () => {
    const res = await fetch(`${prodUrl}/blocked`);
    expect(res.status).toBe(403);
    expect(res.statusText).toBe("Blocked by Middleware");
    const text = await res.text();
    expect(text).toContain("Access Denied");
  });

  it("returns 500 when middleware throws", async () => {
    const res = await fetch(`${prodUrl}/middleware-throw`);
    expect(res.status).toBe(500);
  });

  it("sets x-custom-middleware header on matched requests", async () => {
    const res = await fetch(`${prodUrl}/about`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-custom-middleware")).toBe("active");
  });

  it("middleware request header overrides can delete credential headers before page handling", async () => {
    const res = await fetch(`${prodUrl}/header-override-delete`, {
      headers: {
        authorization: "Bearer secret",
        cookie: "a=1; b=2",
      },
    });

    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('id="authorization">null<');
    expect(html).toContain('id="cookie">null<');
    expect(html).toContain('id="middleware-header">hello-from-middleware<');
  });

  it("does not run middleware on /api routes", async () => {
    const res = await fetch(`${prodUrl}/api/hello`);
    expect(res.status).toBe(200);
    // Middleware matcher excludes /api, so no x-custom-middleware header
    expect(res.headers.get("x-custom-middleware")).toBeNull();
  });

  it("preserves invalid JSON failures for Pages API routes in production", async () => {
    const res = await fetch(`${prodUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: `{"message":Invalid"}`,
    });

    expect(res.status).toBe(400);
    expect(res.statusText).toBe("Invalid JSON");
    expect(await res.text()).toBe("Invalid JSON");
  });

  it("preserves duplicate urlencoded body keys for Pages API routes in production", async () => {
    const res = await fetch(`${prodUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "tag=a&tag=b&tag=c",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ tag: ["a", "b", "c"] });
  });

  it("parses empty urlencoded bodies for Pages API routes in production as {}", async () => {
    const res = await fetch(`${prodUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({});
  });

  it("parses empty JSON bodies for Pages API routes in production as {}", async () => {
    const res = await fetch(`${prodUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: "",
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({});
  });

  it("parses application/ld+json bodies for Pages API routes in production", async () => {
    const res = await fetch(`${prodUrl}/api/parse`, {
      method: "POST",
      headers: { "Content-Type": "application/ld+json; charset=utf-8" },
      body: JSON.stringify({ title: "doc" }),
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ title: "doc" });
  });

  it("production object-form matcher requires has and missing conditions", async () => {
    const noHeaderRes = await fetch(`${prodUrl}/mw-object-gated`);
    expect(noHeaderRes.status).toBe(200);
    expect(noHeaderRes.headers.get("x-custom-middleware")).toBeNull();

    const blockedRes = await fetch(`${prodUrl}/mw-object-gated`, {
      headers: {
        "x-mw-allow": "1",
        Cookie: "mw-blocked=1",
      },
    });
    expect(blockedRes.status).toBe(200);
    expect(blockedRes.headers.get("x-custom-middleware")).toBeNull();

    const allowedRes = await fetch(`${prodUrl}/mw-object-gated`, {
      headers: { "x-mw-allow": "1" },
    });
    expect(allowedRes.status).toBe(200);
    expect(allowedRes.headers.get("x-custom-middleware")).toBe("active");
  });

  it("preserves binary API response bytes", async () => {
    const res = await fetch(`${prodUrl}/api/binary`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/octet-stream");

    const body = Buffer.from(await res.arrayBuffer());
    // Must match exactly: invalid UTF-8-leading bytes + null + ASCII tail.
    // This catches any accidental text() decode/re-encode in prod-server.
    expect(body.equals(Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x61, 0x62, 0x63]))).toBe(true);
  });

  it("preserves repeated urlencoded API body keys in production", async () => {
    const res = await fetch(`${prodUrl}/api/echo-body`, {
      method: "POST",
      headers: {
        "content-type": "application/x-www-form-urlencoded",
      },
      body: "a=1&a=2&b=3",
    });
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data).toEqual({ body: { a: ["1", "2"], b: "3" } });
  });

  it("returns 400 for malformed JSON API bodies in production", async () => {
    const res = await fetch(`${prodUrl}/api/echo-body`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
      },
      body: "{invalid json",
    });

    expect(res.status).toBe(400);
    expect(await res.text()).toBe("Invalid JSON");
  });

  it("sends Buffer payloads from res.send() as raw bytes in production", async () => {
    const res = await fetch(`${prodUrl}/api/send-buffer`);
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/octet-stream");
    expect(res.headers.get("content-length")).toBe("3");

    const body = Buffer.from(await res.arrayBuffer());
    expect(body.equals(Buffer.from([1, 2, 3]))).toBe(true);
  });

  it("defaults to application/octet-stream for API routes without Content-Type", async () => {
    const res = await fetch(`${prodUrl}/api/no-content-type`);
    expect(res.status).toBe(200);
    const ct = res.headers.get("content-type") ?? "";
    // Must NOT default to text/html, which would cause browsers to render
    // the response body as HTML. When the handler passes a string to
    // res.end(), the Response constructor sets text/plain automatically,
    // so we verify the dangerous text/html default is gone.
    expect(ct).not.toContain("text/html");
  });

  it("serves normal pages without middleware interference", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Hello, vinext!");
  });

  it("preserves content-length for getServerSideProps res.end() short-circuit responses in production", async () => {
    const res = await fetch(`${prodUrl}/ssr-res-end`);
    expect(res.status).toBe(202);
    expect(res.headers.get("content-type")).toBe("application/json");
    expect(res.headers.get("content-length")).toBe("35");
    expect(await res.json()).toEqual({ ok: true, source: "gssp-res-end" });
  });

  // Regression test for #1459: Next.js supports a Promise value for `props`
  // returned from getServerSideProps. The prod worker entry must await it
  // before serialising into __NEXT_DATA__ / pageProps.
  it("awaits Promise-shaped getServerSideProps props in production", async () => {
    const res = await fetch(`${prodUrl}/ssr-promise-props`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("SSR Promise Props");
    expect(html).toContain("world");
    // React SSR inserts a `<!-- -->` comment between text and expressions.
    expect(html).toMatch(/count:\s*(<!--\s*-->)?\s*42/);
    expect(html).toMatch(/"pageProps":\s*\{[^}]*"hello":\s*"world"/);
  });

  it("returns 400 for malformed percent-encoded path (not crash)", async () => {
    const res = await fetch(`${prodUrl}/%E0%A4%A`);
    expect(res.status).toBe(400);
    const body = await res.text();
    expect(body).toContain("Bad Request");
  });

  it("returns 400 for bare percent sign in path (not crash)", async () => {
    const res = await fetch(`${prodUrl}/%`);
    expect(res.status).toBe(400);
    const body = await res.text();
    expect(body).toContain("Bad Request");
  });

  it("blocks access to .vite/ build metadata directory", async () => {
    // The .vite/ directory contains build manifests (ssr-manifest.json,
    // manifest.json) that should not be publicly accessible.
    const res = await fetch(`${prodUrl}/.vite/ssr-manifest.json`);
    expect(res.status).toBe(404);
  });

  it("blocks access to .vite/ with percent-encoded dot", async () => {
    // Ensure encoded variants like /%2Evite/ are also blocked
    const res = await fetch(`${prodUrl}/%2Evite/ssr-manifest.json`);
    expect(res.status).toBe(404);
  });

  // ── /_next/data JSON endpoint in production (issue #1330) ─────────
  // Ported from Next.js: test/e2e/middleware-general/test/index.test.ts
  // ("should trigger middleware for data requests", "should normalize data
  // requests into page requests").
  describe("/_next/data JSON endpoint", () => {
    // pages-basic's next.config.mjs pins the build id to "test-build-id".
    const BUILD_ID = "test-build-id";

    // 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("does not treat a normal URL as a data request from x-nextjs-data alone", async () => {
      const res = await fetch(`${prodUrl}/old-page`, {
        redirect: "manual",
        headers: { "x-nextjs-data": "1" },
      });
      expect(res.status).toBe(307);
      expect(res.headers.get("location")).toContain("/about");
      expect(res.headers.get("x-nextjs-redirect")).toBeNull();
    });

    it("adds x-nextjs-rewrite for a real data URL rewritten by middleware", async () => {
      const res = await fetch(`${prodUrl}/_next/data/${BUILD_ID}/rewritten.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("x-nextjs-rewrite")).toBe("/ssr");
      expect(res.headers.get("x-middleware-rewrite")).toBeNull();
    });

    it("returns { pageProps } JSON for a getServerSideProps page", async () => {
      const res = await fetch(`${prodUrl}/_next/data/${BUILD_ID}/ssr.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("content-type")).toContain("application/json");
      const json = (await res.json()) as { pageProps: { message: string } };
      expect(json.pageProps.message).toBe("Hello from getServerSideProps");
    });

    it("returns { pageProps } JSON for a getStaticProps page (bypasses HTML cache)", async () => {
      // /isr-test uses getStaticProps with revalidate. The data endpoint
      // must bypass the cached HTML body and surface pageProps as JSON —
      // mirrors Next.js' `isNextDataRequest` cache-bypass logic in
      // base-server.ts.
      const res = await fetch(`${prodUrl}/_next/data/${BUILD_ID}/isr-test.json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("content-type")).toContain("application/json");
      const json = (await res.json()) as { pageProps: Record<string, unknown> };
      expect(json).toHaveProperty("pageProps");
      expect(typeof json.pageProps).toBe("object");
    });

    it("normalizes the URL to /<page> BEFORE middleware runs", async () => {
      const res = await fetch(`${prodUrl}/_next/data/${BUILD_ID}/ssr.json`);
      expect(res.status).toBe(200);
      // The middleware fixture sets `x-mw-pathname` to whatever pathname it
      // observed. If `_next/data` is not normalized first, middleware sees
      // the raw `/_next/data/.../ssr.json` URL — which is the failure mode
      // tracked in issue #1330 and surfaced by `middleware-general` tests
      // in the deploy suite.
      expect(res.headers.get("x-mw-pathname")).toBe("/ssr");
      expect(res.headers.get("x-custom-middleware")).toBe("active");
    });

    it("returns JSON 404 for an unknown page", async () => {
      const res = await fetch(`${prodUrl}/_next/data/${BUILD_ID}/totally-missing-page.json`);
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type")).toContain("application/json");
      expect(await res.json()).toEqual({});
    });

    it("returns JSON 404 for a stale buildId", async () => {
      const res = await fetch(`${prodUrl}/_next/data/wrong-build-id/ssr.json`);
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type")).toContain("application/json");
      expect(await res.json()).toEqual({});
    });
  });
});

describe("Pages _document renderPage enhancers", () => {
  let fixtureRoot: string;
  let devServer: ViteDevServer;
  let devUrl: string;
  let prodServer: import("node:http").Server;
  let prodUrl: string;
  let outDir: string;

  const enhancerCases = [
    ["withEnhancer=true", ["render-page-enhance-component"]],
    ["withEnhanceComponent=true", ["render-page-enhance-component"]],
    ["withEnhanceApp=true", ["render-page-enhance-app"]],
    [
      "withEnhanceComponent=true&withEnhanceApp=true",
      ["render-page-enhance-component", "render-page-enhance-app"],
    ],
  ] as const;

  function expectErrorDocument(
    html: string,
    expectedMessage: string | RegExp,
    expectedQueryKey: string,
  ): void {
    expect(html).toContain('id="error-page"');
    if (typeof expectedMessage === "string") {
      expect(html).toContain(`id="error-message">${expectedMessage}`);
    } else {
      expect(html).toMatch(expectedMessage);
    }
    expect(html).toContain(`id="document-error-context">/_error|${expectedQueryKey}|`);
    expect(html.match(/id="document-error-enhancer"/g)).toHaveLength(1);
    expect(html.match(/data-error-document-style/g)).toHaveLength(1);
    expect(html).toContain(".error-document{color:red}");
    expect(html).toContain('id="error-render-count">1');
    expect(html).not.toContain('id="page-content"');
  }

  beforeAll(async () => {
    fixtureRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-document-enhancers-"));
    outDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-document-enhancers-out-"));
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(fixtureRoot, "node_modules"),
      "junction",
    );
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(outDir, "node_modules"),
      "junction",
    );
    await fsp.mkdir(path.join(fixtureRoot, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(fixtureRoot, "package.json"),
      JSON.stringify({ private: true, dependencies: { next: "*", react: "*", "react-dom": "*" } }),
    );
    await fsp.writeFile(
      path.join(fixtureRoot, "pages", "_app.tsx"),
      `export default function App({ Component, pageProps }: any) {
  return <main id="app-shell"><Component {...pageProps} /></main>;
}
`,
    );
    await fsp.writeFile(
      path.join(fixtureRoot, "render-counts.ts"),
      `export const renderCounts = { enhancer: 0, page: 0, error: 0 };
`,
    );
    await fsp.writeFile(
      path.join(fixtureRoot, "pages", "index.tsx"),
      `import { renderCounts } from "../render-counts";
export function getServerSideProps({ query }: any) {
  renderCounts.enhancer = 0;
  renderCounts.page = 0;
  renderCounts.error = 0;
  return { props: { throwPage: query.throwPage === "true" } };
}
export default function Page({ throwPage }: { throwPage: boolean }) {
  if (throwPage) {
    renderCounts.page += 1;
    throw new Error("page render failed");
  }
  return <p id="page-content">PAGE</p>;
}
`,
    );
    await fsp.writeFile(
      path.join(fixtureRoot, "pages", "_error.tsx"),
      `import { renderCounts } from "../render-counts";
function ErrorPage({ message }: { message: string }) {
  renderCounts.error += 1;
  return (
    <div id="error-page">
      <p id="error-message">{message}</p>
      <p id="enhancer-render-count">{renderCounts.enhancer}</p>
      <p id="page-render-count">{renderCounts.page}</p>
      <p id="error-render-count">{renderCounts.error}</p>
    </div>
  );
}
ErrorPage.getInitialProps = ({ err }: any) => ({
  message: err instanceof Error ? err.message : String(err),
});
export default ErrorPage;
`,
    );
    await fsp.writeFile(
      path.join(fixtureRoot, "pages", "_document.tsx"),
      `import Document, { Html, Head, Main, NextScript } from "next/document";
import { renderCounts } from "../render-counts";
export default class CustomDocument extends Document {
  static async getInitialProps(ctx: any) {
    const enhanceComponent = (Component: any) => (props: any) => (
      <div><span id="render-page-enhance-component">RENDERED</span><Component {...props} /></div>
    );
    const enhanceApp = (App: any) => (props: any) => (
      <div><span id="render-page-enhance-app">RENDERED</span><App {...props} /></div>
    );
    const throwEnhancer = (_Component: any) => {
      renderCounts.enhancer += 1;
      throw new Error("enhancer render failed");
    };
    const ThrowingStyle = () => {
      throw new Error("style serialization failed");
    };
    const enhanceErrorComponent = (Component: any) => (props: any) => (
      <div id="document-error-enhancer"><Component {...props} /></div>
    );
    let options;
    if (ctx.pathname !== "/_error" && ctx.query?.throwEnhancer) {
      options = throwEnhancer;
    } else if (ctx.query?.withEnhancer) {
      options = enhanceComponent;
    } else if (ctx.query?.withEnhanceComponent || ctx.query?.withEnhanceApp) {
      options = {
        enhanceComponent: ctx.query.withEnhanceComponent ? enhanceComponent : undefined,
        enhanceApp: ctx.query.withEnhanceApp ? enhanceApp : undefined,
      };
    }
    if (ctx.pathname !== "/_error" && ctx.query?.invalidDocumentHtml) return { html: null };
    if (ctx.query?.manualDocumentHtml) {
      return {
        html: '<article id="manual-document-html">MANUAL</article>',
        styles: <style data-manual-document-style>{".manual{color:blue}"}</style>,
        documentProp: "DOCUMENT",
        documentErrorContext: "",
      };
    }
    const originalRenderPage = ctx.renderPage;
    ctx.renderPage = () =>
      originalRenderPage(
        ctx.pathname === "/_error" ? { enhanceComponent: enhanceErrorComponent } : options,
      );
    const initialProps = await Document.getInitialProps(ctx);
    return {
      ...initialProps,
      styles:
        ctx.pathname === "/_error"
          ? <style data-error-document-style>{".error-document{color:red}"}</style>
          : ctx.query?.throwStyles
            ? <ThrowingStyle />
            : initialProps.styles,
      documentProp: "DOCUMENT",
      documentErrorContext:
        ctx.pathname === "/_error"
          ? [ctx.pathname, Object.keys(ctx.query ?? {})[0] ?? "", ctx.err?.message ?? ""].join("|")
          : "",
    };
  }
  render() {
    return (
      <Html><Head /><body><p id="document-prop">{(this.props as any).documentProp}</p><p id="document-error-context">{(this.props as any).documentErrorContext}</p><Main /><NextScript /></body></Html>
    );
  }
}
`,
    );

    const dev = await startFixtureServer(fixtureRoot);
    devServer = dev.server;
    devUrl = dev.baseUrl;

    await buildPagesFixtureToOutDir(fixtureRoot, outDir);
    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({ port: 0, host: "127.0.0.1", outDir }),
    );
    const address = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${address.port}`;
  }, 120000);

  afterAll(async () => {
    await devServer?.close();
    if (prodServer) await new Promise<void>((resolve) => prodServer.close(() => resolve()));
    if (fixtureRoot) fs.rmSync(fixtureRoot, { recursive: true, force: true });
    if (outDir) fs.rmSync(outDir, { recursive: true, force: true });
  });

  // Ported from Next.js: test/e2e/app-document/rendering.test.ts
  // https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-document/rendering.test.ts
  it.each(["dev", "prod"] as const)("applies all renderPage enhancer forms in %s", async (mode) => {
    const url = mode === "dev" ? devUrl : prodUrl;
    for (const [query, expectedIds] of enhancerCases) {
      const response = await fetch(`${url}/?${query}`);
      expect(response.status).toBe(200);
      const html = await response.text();
      expect(html).toContain('id="document-prop">DOCUMENT');
      expect(html).toContain('id="page-content">PAGE');
      expect(html.match(/id="page-content"/g)).toHaveLength(1);
      for (const id of expectedIds) {
        expect(html).toContain(`id="${id}">RENDERED`);
        expect(html.match(new RegExp(`id="${id}"`, "g"))).toHaveLength(1);
      }
    }
  });

  it.each(["dev", "prod"] as const)(
    "uses direct document html and styles without rendering the page in %s",
    async (mode) => {
      const url = mode === "dev" ? devUrl : prodUrl;
      const response = await fetch(`${url}/?manualDocumentHtml=true`);
      expect(response.status).toBe(200);
      const html = await response.text();
      expect(html).toContain('id="manual-document-html">MANUAL');
      expect(html).toContain("data-manual-document-style");
      expect(html).toContain(".manual{color:blue}");
      expect(html).not.toContain('id="page-content"');
      expect(html).not.toContain('id="app-shell"');
    },
  );

  it.each(["dev", "prod"] as const)(
    "routes throwing renderPage enhancers through the error page once in %s",
    async (mode) => {
      const url = mode === "dev" ? devUrl : prodUrl;
      const response = await fetch(`${url}/?throwEnhancer=true`);
      expect(response.status).toBe(500);
      const html = await response.text();
      expectErrorDocument(html, "enhancer render failed", "throwEnhancer");
      expect(html).toContain('id="enhancer-render-count">1');
      expect(html).toContain('id="page-render-count">0');
    },
  );

  it.each(["dev", "prod"] as const)(
    "routes throwing page renders through the error page once in %s",
    async (mode) => {
      const url = mode === "dev" ? devUrl : prodUrl;
      const response = await fetch(`${url}/?throwPage=true`);
      expect(response.status).toBe(500);
      const html = await response.text();
      expectErrorDocument(html, "page render failed", "throwPage");
      expect(html).toContain('id="enhancer-render-count">0');
    },
  );

  it.each(["dev", "prod"] as const)(
    "routes invalid document html through the error page in %s",
    async (mode) => {
      const url = mode === "dev" ? devUrl : prodUrl;
      const response = await fetch(`${url}/?invalidDocumentHtml=true`);
      expect(response.status).toBe(500);
      const html = await response.text();
      expectErrorDocument(
        html,
        /id="error-message">(?:&quot;|").+?\.getInitialProps\(\)(?:&quot;|") should resolve to an object with a (?:&quot;|")html(?:&quot;|") prop set with a valid html string/,
        "invalidDocumentHtml",
      );
    },
  );

  it.each(["dev", "prod"] as const)(
    "routes document style serialization failures through the error page in %s",
    async (mode) => {
      const url = mode === "dev" ? devUrl : prodUrl;
      const response = await fetch(`${url}/?throwStyles=true`);
      expect(response.status).toBe(500);
      const html = await response.text();
      expectErrorDocument(html, "style serialization failed", "throwStyles");
    },
  );
});

describe("Production Pages Router SSR streaming", () => {
  let outDir: string;
  let prodServer: import("node:http").Server;
  let prodUrl: string;

  async function withFreshStreamingProdServer<T>(
    run: (freshProdUrl: string) => Promise<T>,
  ): Promise<T> {
    const freshOutDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-streaming-fresh-"));
    let freshServer: import("node:http").Server | undefined;

    try {
      await fsp.symlink(
        path.resolve(import.meta.dirname, "../node_modules"),
        path.join(freshOutDir, "node_modules"),
        "junction",
      );
      await buildPagesFixtureToOutDir(FIXTURE_DIR, freshOutDir);

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      freshServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir: freshOutDir,
        }),
      );
      const addr = freshServer.address() as { port: number };
      return await run(`http://127.0.0.1:${addr.port}`);
    } finally {
      const serverToClose = freshServer;
      if (serverToClose) {
        await new Promise<void>((resolve) => serverToClose.close(() => resolve()));
      }
      fs.rmSync(freshOutDir, { recursive: true, force: true });
    }
  }

  beforeAll(async () => {
    outDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-streaming-prod-"));
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(outDir, "node_modules"),
      "junction",
    );
    await buildPagesFixtureToOutDir(FIXTURE_DIR, outDir);

    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  }, 60000);

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer.close(() => resolve()));
    }
    if (outDir) {
      fs.rmSync(outDir, { recursive: true, force: true });
    }
  });

  it("streams Pages SSR responses incrementally in production with br compression", async () => {
    // Parity target: Next.js streams Node responses via sendResponse() ->
    // pipeToNodeResponse() instead of buffering the full HTML first, while
    // still leaving compression enabled under next start.
    // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/send-response.ts
    // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/pipe-readable.ts
    const response = await captureStreamedResponse(`${prodUrl}/streaming-ssr`, {
      headers: { "accept-encoding": "br" },
    });
    const partialHtml = response.snapshot.toString("utf8");
    const finalHtml = response.body.toString("utf8");
    const contentType = response.headers["content-type"];
    const contentEncoding = response.headers["content-encoding"];
    const middlewareHeader = response.headers["x-custom-middleware"];
    const transferEncoding = response.headers["transfer-encoding"];

    expect(response.statusCode).toBe(200);
    expect(String(contentType)).toContain("text/html");
    expect(String(contentEncoding)).toBe("br");
    expect(String(middlewareHeader)).toBe("active");
    expect(response.headers["content-length"]).toBeUndefined();
    expect(String(transferEncoding)).toBe("chunked");
    expect(response.firstChunkMs).toBeGreaterThanOrEqual(0);
    expect(response.firstChunkMs).toBeLessThan(400);
    expect(response.endMs).toBeGreaterThanOrEqual(400);
    expect(response.rawBody.byteLength).toBeGreaterThan(0);
    expect(response.rawSnapshot.byteLength).toBeGreaterThan(0);

    expect(partialHtml).toContain("Streaming SSR Test");
    expect(partialHtml).toContain("Loading delayed chunk...");
    expect(partialHtml).not.toContain("Delayed stream content loaded");

    expect(finalHtml).toContain("Streaming SSR Test");
    expect(finalHtml).toContain("Delayed stream content loaded");
    expect(finalHtml).toContain("__NEXT_DATA__");
  });

  it("streams Pages SSR responses incrementally in production with gzip compression", async () => {
    const response = await withFreshStreamingProdServer((freshProdUrl) =>
      captureStreamedResponse(`${freshProdUrl}/streaming-ssr`, {
        headers: { "accept-encoding": "gzip" },
      }),
    );
    const partialHtml = response.snapshot.toString("utf8");
    const finalHtml = response.body.toString("utf8");

    expect(response.statusCode).toBe(200);
    expect(String(response.headers["content-encoding"])).toBe("gzip");
    expect(response.headers["content-length"]).toBeUndefined();
    expect(String(response.headers["transfer-encoding"])).toBe("chunked");
    expect(response.firstChunkMs).toBeGreaterThanOrEqual(0);
    expect(response.firstChunkMs).toBeLessThan(400);
    expect(response.endMs).toBeGreaterThanOrEqual(400);
    expect(partialHtml).toContain("Loading delayed chunk...");
    expect(partialHtml).not.toContain("Delayed stream content loaded");
    expect(finalHtml).toContain("Delayed stream content loaded");
  });

  it("preserves streamed SSR bodies when middleware rewrites are merged into the response", async () => {
    const res = await fetch(`${prodUrl}/streaming-ssr`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-custom-middleware")).toBe("active");

    const html = await res.text();
    expect(html).toContain("Delayed stream content loaded");
  });

  it("serves streamed Pages SSR HEAD requests as headers-only responses in production", async () => {
    const startedAt = Date.now();
    const res = await fetch(`${prodUrl}/streaming-ssr`, {
      method: "HEAD",
      headers: { "accept-encoding": "br" },
    });

    expect(res.status).toBe(200);
    expect(res.headers.get("x-custom-middleware")).toBe("active");
    expect(res.headers.get("content-length")).toBeNull();
    expect(await res.text()).toBe("");
    expect(Date.now() - startedAt).toBeLessThan(400);
  });

  it("strips stale content-length from streamed Pages SSR responses when gSSP sets one", async () => {
    // Parity target: Next.js only sets Content-Length for unchunked render
    // payloads; streamed HTML is sent without one.
    // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/send-payload.ts
    const response = await captureStreamedResponse(`${prodUrl}/streaming-gssp-content-length`, {
      headers: { "accept-encoding": "br" },
    });
    const partialHtml = response.snapshot.toString("utf8");
    const finalHtml = response.body.toString("utf8");

    expect(response.statusCode).toBe(200);
    expect(String(response.headers["content-encoding"])).toBe("br");
    expect(response.headers["content-length"]).toBeUndefined();
    expect(String(response.headers["transfer-encoding"])).toBe("chunked");
    expect(response.firstChunkMs).toBeGreaterThanOrEqual(0);
    expect(response.firstChunkMs).toBeLessThan(400);
    expect(partialHtml).toContain("Loading delayed gSSP chunk...");
    expect(partialHtml).not.toContain("Delayed gSSP stream content loaded");
    expect(finalHtml).toContain("Streaming gSSP Content-Length Test");
    expect(finalHtml).toContain("Delayed gSSP stream content loaded");
  });

  it("strips middleware-provided content-length when rewriting to a streamed Pages SSR response", async () => {
    // Parity target: Next.js route resolution explicitly skips forwarding
    // middleware content-length headers.
    // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
    const response = await withFreshStreamingProdServer((freshProdUrl) =>
      captureStreamedResponse(`${freshProdUrl}/middleware-bad-content-length`, {
        headers: { "accept-encoding": "br" },
      }),
    );
    const partialHtml = response.snapshot.toString("utf8");
    const finalHtml = response.body.toString("utf8");

    expect(response.statusCode).toBe(200);
    expect(String(response.headers["content-encoding"])).toBe("br");
    expect(response.headers["content-length"]).toBeUndefined();
    expect(String(response.headers["transfer-encoding"])).toBe("chunked");
    expect(partialHtml).toContain("Loading delayed chunk...");
    expect(partialHtml).not.toContain("Delayed stream content loaded");
    expect(finalHtml).toContain("Streaming SSR Test");
    expect(finalHtml).toContain("Delayed stream content loaded");
  });
});

describe("Production server next.config.js features (Pages Router)", () => {
  const outDir = path.resolve(FIXTURE_DIR, "dist");
  let prodServer: import("node:http").Server | undefined;
  let prodUrl: string;

  beforeAll(async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const manifestPath = path.join(outDir, "client", ".vite", "ssr-manifest.json");

    // Build if needed (tests may run in isolation)
    if (!fs.existsSync(serverEntryPath) || !fs.existsSync(manifestPath)) {
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: FIXTURE_DIR,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });
    }

    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  });

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer!.close(() => resolve()));
    }
  });

  it("server entry exports vinextConfig with correct shape", async () => {
    const serverEntryPath = path.join(outDir, "server", "entry.js");
    const serverEntry = await import(pathToFileURL(serverEntryPath).href);
    expect(serverEntry.vinextConfig).toBeDefined();
    expect(serverEntry.vinextConfig.redirects).toBeInstanceOf(Array);
    expect(serverEntry.vinextConfig.rewrites).toBeDefined();
    expect(serverEntry.vinextConfig.headers).toBeInstanceOf(Array);
    expect(typeof serverEntry.vinextConfig.basePath).toBe("string");
    expect(typeof serverEntry.vinextConfig.trailingSlash).toBe("boolean");
  });

  it("applies redirects from next.config.js (/old-about -> /about)", async () => {
    const res = await fetch(`${prodUrl}/old-about`, { redirect: "manual" });
    expect(res.status).toBe(308); // permanent redirect
    expect(res.headers.get("location")).toContain("/about");
  });

  it("applies redirects with repeated dynamic params in production", async () => {
    const res = await fetch(`${prodUrl}/repeat-redirect/hello`, { redirect: "manual" });
    expect(res.status).toBe(307);
    expect(res.headers.get("location")).toBe("/docs/hello/hello");
  });

  it("applies beforeFiles rewrites from next.config.js (/before-rewrite -> /about)", async () => {
    const res = await fetch(`${prodUrl}/before-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  it("applies rewrites with repeated dynamic params in production", async () => {
    const res = await fetch(`${prodUrl}/repeat-rewrite/hello`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("hello/hello");
  });

  it("applies afterFiles rewrites from next.config.js (/after-rewrite -> /about)", async () => {
    const res = await fetch(`${prodUrl}/after-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });

  it("does not let afterFiles rewrites override static page routes in production", async () => {
    const res = await fetch(`${prodUrl}/nav-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Navigation Test");
    expect(html).not.toContain("This is the about page.");
  });

  it("applies custom headers from next.config.js on /api routes", async () => {
    const res = await fetch(`${prodUrl}/api/hello`);
    expect(res.status).toBe(200);
    expect(res.headers.get("x-custom-header")).toBe("vinext");
  });

  // Ported from PR #47 by @ibruno
  it("applies has/missing conditions for next.config.js headers", async () => {
    const guestRes = await fetch(`${prodUrl}/about`);
    expect(guestRes.status).toBe(200);
    expect(guestRes.headers.get("x-guest-only-header")).toBe("1");
    expect(guestRes.headers.get("x-auth-only-header")).toBeNull();

    const authRes = await fetch(`${prodUrl}/about`, {
      headers: { Cookie: "logged-in=1" },
    });
    expect(authRes.status).toBe(200);
    expect(authRes.headers.get("x-auth-only-header")).toBe("1");
    expect(authRes.headers.get("x-guest-only-header")).toBeNull();
  });

  it("has/missing conditions do not see middleware-injected cookies", async () => {
    // When ?inject-login is present, middleware injects logged-in=1 cookie
    // into the request headers. The config has/missing conditions should
    // evaluate against the updated request, not the original.
    const res = await fetch(`${prodUrl}/about?inject-login`);
    expect(res.status).toBe(200);
    // The has:[cookie:logged-in] condition should match
    expect(res.headers.get("x-auth-only-header")).toBeNull();
    // The missing:[cookie:logged-in] condition should NOT match
    expect(res.headers.get("x-guest-only-header")).toBe("1");
  });

  it("config Vary header appends instead of replacing existing values", async () => {
    // The /ssr page has config headers: [{ key: "Vary", value: "Accept-Language" }].
    // If the response already has a Vary header (e.g. from compression),
    // the config value should be appended, not replace it.
    const res = await fetch(`${prodUrl}/ssr`);
    expect(res.status).toBe(200);
    const vary = res.headers.get("vary") ?? "";
    expect(vary).toContain("Accept-Language");
  });

  // afterFiles rewrites run after middleware in the App Router execution order.
  // has/missing conditions on afterFiles rules should evaluate against
  // middleware-modified headers, not the original pre-middleware request.
  it("afterFiles rewrite has/missing conditions see middleware-injected cookies", async () => {
    // Without ?mw-auth, middleware does NOT inject mw-user=1.
    // The has:[cookie:mw-user] afterFiles rule should NOT match → no rewrite.
    const noAuthRes = await fetch(`${prodUrl}/mw-gated-rewrite`);
    expect(noAuthRes.status).toBe(404);

    // With ?mw-auth, middleware injects mw-user=1 into request cookies.
    // The has:[cookie:mw-user] afterFiles rule SHOULD match → rewrite to /about.
    const authRes = await fetch(`${prodUrl}/mw-gated-rewrite?mw-auth`);
    expect(authRes.status).toBe(200);
    const html = await authRes.text();
    expect(html).toContain("About");
  });

  // beforeFiles rewrites run after middleware per the Next.js execution order:
  // headers → redirects → Middleware → beforeFiles → filesystem → afterFiles → fallback.
  // has/missing conditions on beforeFiles rules should evaluate against
  // middleware-modified headers, not the original pre-middleware request.
  it("beforeFiles rewrite has/missing conditions see middleware-injected cookies", async () => {
    // Without ?mw-auth, middleware does NOT inject mw-before-user=1.
    // The has:[cookie:mw-before-user] beforeFiles rule should NOT match → 404.
    const noAuthRes = await fetch(`${prodUrl}/mw-gated-before`);
    expect(noAuthRes.status).toBe(404);

    // With ?mw-auth, middleware injects mw-before-user=1 into request cookies.
    // The has:[cookie:mw-before-user] beforeFiles rule SHOULD match → rewrite to /about.
    const authRes = await fetch(`${prodUrl}/mw-gated-before?mw-auth`);
    expect(authRes.status).toBe(200);
    const html = await authRes.text();
    expect(html).toContain("About");
  });

  it("serves normal pages unaffected by config rules", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Hello, vinext!");
  });

  // ── Percent-encoded paths should be decoded before config matching ──
  // Config matchers must receive decoded paths so that encoded variants
  // like /%6Fld-%61bout still match the /old-about redirect rule.

  it("percent-encoded redirect path is decoded before config matching (prod)", async () => {
    // /old-about → /about (permanent redirect). /%6Fld-%61bout decodes to /old-about.
    const res = await fetch(`${prodUrl}/%6Fld-%61bout`, { redirect: "manual" });
    expect(res.status).toBe(308);
    expect(res.headers.get("location")).toContain("/about");
  });

  it("percent-encoded header path is decoded before config matching (prod)", async () => {
    // /api/(.*) should receive X-Custom-Header: vinext.
    // /%61pi/hello decodes to /api/hello.
    const res = await fetch(`${prodUrl}/%61pi/hello`);
    expect(res.headers.get("x-custom-header")).toBe("vinext");
  });

  it("percent-encoded rewrite path is decoded before config matching (prod)", async () => {
    // /before-rewrite → /about (beforeFiles rewrite).
    // /%62efore-rewrite decodes to /before-rewrite.
    const res = await fetch(`${prodUrl}/%62efore-rewrite`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("About");
  });
});

describe("Static export (Pages Router)", () => {
  let pagesBundlePath: string;
  const exportDir = path.resolve(FIXTURE_DIR, "out");

  beforeAll(async () => {
    pagesBundlePath = await buildPagesFixture(FIXTURE_DIR);
  }, 60_000);

  afterAll(() => {
    fs.rmSync(exportDir, { recursive: true, force: true });
  });

  it("exports static pages to HTML files", async () => {
    const { staticExportPages } = await import("../packages/vinext/src/build/static-export.js");
    const { pagesRouter, apiRouter } =
      await import("../packages/vinext/src/routing/pages-router.js");
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    const pagesDir = path.resolve(FIXTURE_DIR, "pages");
    const routes = await pagesRouter(pagesDir);
    const apiRoutes = await apiRouter(pagesDir);
    const config = await resolveNextConfig({ output: "export" });

    const result = await staticExportPages({
      pagesBundlePath,
      routes,
      apiRoutes,
      pagesDir,
      outDir: exportDir,
      config,
    });

    // Should have generated HTML files
    expect(result.pageCount).toBeGreaterThan(0);

    // Index page
    expect(result.files).toContain("index.html");
    const indexHtml = fs.readFileSync(path.join(exportDir, "index.html"), "utf-8");
    expect(indexHtml).toContain("<!DOCTYPE html>");
    expect(indexHtml).toContain("Hello, vinext!");

    // About page
    expect(result.files).toContain("about.html");
    const aboutHtml = fs.readFileSync(path.join(exportDir, "about.html"), "utf-8");
    expect(aboutHtml).toContain("About");
  });

  it("pre-renders dynamic routes from getStaticPaths", async () => {
    // blog/[slug] has getStaticPaths returning hello-world and getting-started
    expect(fs.existsSync(path.join(exportDir, "blog", "hello-world.html"))).toBe(true);
    expect(fs.existsSync(path.join(exportDir, "blog", "getting-started.html"))).toBe(true);

    const blogHtml = fs.readFileSync(path.join(exportDir, "blog", "hello-world.html"), "utf-8");
    expect(blogHtml).toContain("Hello World");
    expect(blogHtml).toContain("hello-world");
  });

  it("generates 404.html", async () => {
    expect(fs.existsSync(path.join(exportDir, "404.html"))).toBe(true);
    const html404 = fs.readFileSync(path.join(exportDir, "404.html"), "utf-8");
    expect(html404).toContain("404");
  });

  it("escapes meta refresh URL to prevent HTML injection", async () => {
    expect(fs.existsSync(path.join(exportDir, "redirect-xss.html"))).toBe(true);
    const html = fs.readFileSync(path.join(exportDir, "redirect-xss.html"), "utf-8");
    expect(html).toContain(
      'content="0;url=foo&quot; /&gt;&lt;script&gt;alert(1)&lt;/script&gt;&lt;meta x=&quot;"',
    );
    expect(html).not.toContain("<script>alert(1)</script>");
  });

  it("reports errors for pages using getServerSideProps", async () => {
    // The result from the first test should have errors for SSR-only pages
    const { staticExportPages } = await import("../packages/vinext/src/build/static-export.js");
    const { pagesRouter, apiRouter } =
      await import("../packages/vinext/src/routing/pages-router.js");
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    const pagesDir = path.resolve(FIXTURE_DIR, "pages");
    const routes = await pagesRouter(pagesDir);
    const apiRoutes = await apiRouter(pagesDir);
    const config = await resolveNextConfig({ output: "export" });

    const tempDir = path.resolve(FIXTURE_DIR, "out-temp");
    try {
      const result = await staticExportPages({
        pagesBundlePath,
        routes,
        apiRoutes,
        pagesDir,
        outDir: tempDir,
        config,
      });

      // Should report errors for getServerSideProps pages
      const ssrErrors = result.errors.filter((e) => e.error.includes("getServerSideProps"));
      expect(ssrErrors.length).toBeGreaterThan(0);

      // Should warn about API routes
      expect(result.warnings.some((w) => w.includes("API route"))).toBe(true);
    } finally {
      fs.rmSync(tempDir, { recursive: true, force: true });
    }
  });

  it("includes __NEXT_DATA__ in exported HTML", async () => {
    const indexHtml = fs.readFileSync(path.join(exportDir, "index.html"), "utf-8");
    expect(indexHtml).toContain("__NEXT_DATA__");
  });

  it("respects trailingSlash config", async () => {
    const { staticExportPages } = await import("../packages/vinext/src/build/static-export.js");
    const { pagesRouter, apiRouter } =
      await import("../packages/vinext/src/routing/pages-router.js");
    const { resolveNextConfig } = await import("../packages/vinext/src/config/next-config.js");

    const pagesDir = path.resolve(FIXTURE_DIR, "pages");
    const routes = await pagesRouter(pagesDir);
    const apiRoutes = await apiRouter(pagesDir);
    const config = await resolveNextConfig({
      output: "export",
      trailingSlash: true,
    });

    const trailingDir = path.resolve(FIXTURE_DIR, "out-trailing");
    try {
      const result = await staticExportPages({
        pagesBundlePath,
        routes,
        apiRoutes,
        pagesDir,
        outDir: trailingDir,
        config,
      });

      // With trailingSlash, about → about/index.html
      expect(result.files).toContain("about/index.html");
      expect(fs.existsSync(path.join(trailingDir, "about", "index.html"))).toBe(true);
    } finally {
      fs.rmSync(trailingDir, { recursive: true, force: true });
    }
  });
});

describe("Pages Router production rewrite status reason phrases", () => {
  it("drops stale statusText when middleware rewrite status overrides an API response status", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-rewrite-status-text-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
    const outDir = path.join(tmpRoot, "dist");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages", "api"), { recursive: true });

      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
      await fsp.writeFile(
        path.join(tmpRoot, "middleware.ts"),
        `import { NextResponse } from "next/server";
export function middleware(request) {
  const url = new URL(request.url);
  if (url.pathname === "/blocked") {
    return NextResponse.rewrite(new URL("/api/parse", request.url), { status: 403 });
  }
  return NextResponse.next();
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "api", "parse.ts"),
        `export default function handler(req, res) {
  res.status(200).json(req.body ?? null);
}
`,
      );

      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "server"),
          ssr: "virtual:vinext-server-entry",
          rollupOptions: { output: { entryFileNames: "entry.js" } },
        },
      });
      await build({
        root: tmpRoot,
        configFile: false,
        plugins: [vinext()],
        logLevel: "silent",
        build: {
          outDir: path.join(outDir, "client"),
          manifest: true,
          ssrManifest: true,
          rollupOptions: { input: "virtual:vinext-client-entry" },
        },
      });

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = unwrapStartedProdServer(
        await startProdServer({
          port: 0,
          host: "127.0.0.1",
          outDir,
        }),
      );

      try {
        const addr = prodServer.address() as { port: number };
        const res = await fetch(`http://127.0.0.1:${addr.port}/blocked`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: `{"message":Invalid"}`,
        });

        expect(res.status).toBe(403);
        expect(res.statusText).toBe("Forbidden");
        expect(await res.text()).toBe("Invalid JSON");
      } finally {
        await new Promise<void>((resolve) => prodServer.close(() => resolve()));
      }
    } finally {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });
});

describe("Pages Router production no-body rewrite statuses", () => {
  let tmpRoot: string;
  let outDir: string;
  let prodServer: import("node:http").Server;
  let prodUrl: string;

  beforeAll(async () => {
    tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-no-body-rewrite-"));
    outDir = path.join(tmpRoot, "dist");

    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(tmpRoot, "node_modules"),
      "junction",
    );
    await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

    await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
    await fsp.writeFile(path.join(tmpRoot, "next.config.mjs"), `export default {};\n`);
    await fsp.writeFile(
      path.join(tmpRoot, "middleware.ts"),
      `import { NextResponse } from "next/server";
export function middleware(request) {
  const url = new URL(request.url);
  const match = url.pathname.match(/^\\/status-(204|205|304)$/);
  if (match) {
    const response = NextResponse.rewrite(new URL("/target", request.url), {
      status: Number(match[1]),
    });
    response.headers.set("x-custom-middleware", "active");
    return response;
  }
  const apiMatch = url.pathname.match(/^\\/api-status-(204|205|304)$/);
  if (!apiMatch) return NextResponse.next();
  const response = NextResponse.rewrite(new URL("/api/target", request.url), {
    status: Number(apiMatch[1]),
  });
  response.headers.set("x-custom-middleware", "active");
  return response;
}
`,
    );
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "index.tsx"),
      `export default function Home() {
  return <div>home</div>;
}
`,
    );
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "target.tsx"),
      `export default function TargetPage() {
  return <div>TARGET PAGE</div>;
}
`,
    );
    await fsp.mkdir(path.join(tmpRoot, "pages", "api"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "api", "target.ts"),
      `export default function handler(req, res) {
  res.status(200).json({ ok: true });
}
`,
    );

    await buildPagesFixtureToOutDir(tmpRoot, outDir);

    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
        noCompression: true,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  }, 60000);

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer.close(() => resolve()));
    }
    if (tmpRoot) {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  for (const statusCode of [204, 205, 304]) {
    it(`preserves middleware rewrite status ${statusCode} for Pages SSR responses in production`, async () => {
      const res = await fetch(`${prodUrl}/status-${statusCode}`);

      expect(res.status).toBe(statusCode);
      expect(res.headers.get("x-custom-middleware")).toBe("active");
      expect(await res.text()).toBe("");
    });
  }

  for (const statusCode of [204, 205, 304]) {
    it(`drops body headers for middleware rewrite status ${statusCode} on Pages API responses in production`, async () => {
      // Parity targets:
      // - Next.js skips forwarding middleware content-length in route resolution.
      // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
      // - Next.js sends bodyless responses by ending the Node response without piping the body.
      // https://raw.githubusercontent.com/vercel/next.js/canary/packages/next/src/server/send-response.ts
      const res = await fetch(`${prodUrl}/api-status-${statusCode}`);

      expect(res.status).toBe(statusCode);
      expect(res.headers.get("x-custom-middleware")).toBe("active");
      expect(res.headers.get("content-type")).toBeNull();
      expect(res.headers.get("content-length")).toBeNull();
      expect(await res.text()).toBe("");
    });
  }
});

// Ported from Next.js: test/e2e/async-modules/index.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts
//
// Verifies that page modules using top-level await (async modules) render
// their resolved data, not empty content. This covers the Pages Router
// production build path where `_app.tsx` and the page module each contain
// `await` at the module top level. Vite/Rolldown must propagate TLA through
// the generated SSR entry's static imports so the entry awaits these modules
// before reading their default exports.
describe("Pages Router top-level await (async modules) in production", () => {
  let tmpRoot: string;
  let outDir: string;
  let prodServer: import("node:http").Server;
  let prodUrl: string;

  beforeAll(async () => {
    tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-async-modules-"));
    outDir = path.join(tmpRoot, "dist");
    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(tmpRoot, "node_modules"),
      "junction",
    );
    await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
    await fsp.mkdir(path.join(tmpRoot, "pages", "api"), { recursive: true });

    await fsp.writeFile(
      path.join(tmpRoot, "pages", "_app.tsx"),
      `const appValue = await Promise.resolve("hello");
export default function MyApp({ Component, pageProps }: any) {
  return <Component {...pageProps} appValue={appValue} />;
}
`,
    );

    await fsp.writeFile(
      path.join(tmpRoot, "pages", "index.tsx"),
      `const value = await Promise.resolve(42);
export default function Index({ appValue }: any) {
  return (
    <main>
      <div id="app-value">{appValue}</div>
      <div id="page-value">{value}</div>
    </main>
  );
}
`,
    );

    await fsp.writeFile(
      path.join(tmpRoot, "pages", "gssp.tsx"),
      `const gsspValue = await Promise.resolve(42);
export async function getServerSideProps() {
  return { props: { gsspValue } };
}
export default function Page({ gsspValue }: any) {
  return <div id="gssp-value">{gsspValue}</div>;
}
`,
    );

    await fsp.writeFile(
      path.join(tmpRoot, "pages", "gsp.tsx"),
      `const gspValue = await Promise.resolve(42);
export async function getStaticProps() {
  return { props: { gspValue } };
}
export default function Page({ gspValue }: any) {
  return <div id="gsp-value">{gspValue}</div>;
}
`,
    );

    await fsp.writeFile(
      path.join(tmpRoot, "pages", "api", "hello.ts"),
      `const value = await Promise.resolve(42);
export default function handler(_req: any, res: any) {
  res.status(200).json({ value });
}
`,
    );

    // Class-based Document. Mirrors the original Next.js async-modules
    // fixture (pages/_document.jsx) which uses `class MyDocument extends
    // Document` and provides `docValue` through a `static async
    // getInitialProps()` override that itself uses top-level `await`. This
    // requires (a) the `next/document` default export to be a class, not a
    // function — otherwise React refuses to construct MyDocument and throws
    // "Class constructor cannot be invoked without 'new'", and (b) the SSR
    // pipeline to invoke `Document.getInitialProps()` and pass the resolved
    // props to the Document element, so `this.props.docValue` is defined at
    // render time (Next.js's render.tsx does this in `documentElement`).
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "_document.tsx"),
      `import Document, { Html, Head, Main, NextScript } from "next/document";
const docValue = await Promise.resolve("doc value");
export default class MyDocument extends Document<{ docValue: string }> {
  static async getInitialProps(ctx: any) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps, docValue };
  }
  render() {
    return (
      <Html>
        <Head />
        <body>
          <div id="doc-value">{(this.props as any).docValue}</div>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
`,
    );

    // Custom 404 page using top-level await. Mirrors Next.js async-modules
    // pages/404.jsx — the page module must resolve its top-level await before
    // the 404 handler renders it.
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "404.tsx"),
      `const content = await Promise.resolve("hi y'all");
export default function Custom404() {
  return <h1 id="content-404">{content}</h1>;
}
`,
    );

    await buildPagesFixtureToOutDir(tmpRoot, outDir);
    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  }, 120000);

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer.close(() => resolve()));
    }
    if (tmpRoot) {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("renders an index page whose _app and page both use top-level await", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('<div id="app-value">hello</div>');
    expect(html).toContain('<div id="page-value">42</div>');
  });

  it("renders a page whose module-level await runs before getServerSideProps", async () => {
    const res = await fetch(`${prodUrl}/gssp`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('<div id="gssp-value">42</div>');
  });

  it("renders a page whose module-level await runs before getStaticProps", async () => {
    const res = await fetch(`${prodUrl}/gsp`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('<div id="gsp-value">42</div>');
  });

  it("serves an API route whose module uses top-level await", async () => {
    const res = await fetch(`${prodUrl}/api/hello`);
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ value: 42 });
  });

  it("renders an async class-based _document.tsx with resolved TLA values", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('<div id="doc-value">doc value</div>');
  });

  // Ported from Next.js: test/e2e/async-modules/index.test.ts
  //   ('can render async 404 pages')
  // The 404 page module uses top-level `await`. When the prod server falls
  // through to the custom 404 it must render that module's resolved content.
  it("renders a custom 404.tsx whose module uses top-level await", async () => {
    const res = await fetch(`${prodUrl}/dhiuhefoiahjeoij`);
    expect(res.status).toBe(404);
    const html = await res.text();
    expect(html).toContain(`<h1 id="content-404">hi y&#x27;all</h1>`);
  });
});

// Ported from Next.js: test/e2e/import-meta/import-meta.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/import-meta/import-meta.test.ts
describe("Pages Router import.meta.url in production", () => {
  let tmpRoot: string;
  let outDir: string;
  let prodServer: import("node:http").Server;
  let prodUrl: string;

  function decodeHtmlText(text: string): string {
    return text.replace(/&amp;/g, "&").replace(/&quot;/g, '"');
  }

  function collectJavaScriptFiles(dir: string): string[] {
    const files: string[] = [];
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        files.push(...collectJavaScriptFiles(fullPath));
      } else if (entry.isFile() && entry.name.endsWith(".js")) {
        files.push(fullPath);
      }
    }
    return files;
  }

  beforeAll(async () => {
    tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-import-meta-url-"));
    outDir = path.join(tmpRoot, "dist");

    await fsp.symlink(
      path.resolve(import.meta.dirname, "../node_modules"),
      path.join(tmpRoot, "node_modules"),
      "junction",
    );
    await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
    await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "_app.tsx"),
      `export default function MyApp({ Component, pageProps }: any) {
  return <Component {...pageProps} />;
}
`,
    );
    await fsp.writeFile(
      path.join(tmpRoot, "pages", "index.tsx"),
      `export default function Page() {
  const data = { url: import.meta.url };
  return <div id="test-data">{JSON.stringify(data)}</div>;
}
`,
    );

    await buildPagesFixtureToOutDir(tmpRoot, outDir);
    const { runPrerender } = await import("../packages/vinext/src/build/run-prerender.js");
    await runPrerender({
      root: tmpRoot,
      pagesBundlePath: path.join(outDir, "server", "entry.js"),
      concurrency: 1,
    });

    const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
    prodServer = unwrapStartedProdServer(
      await startProdServer({
        port: 0,
        host: "127.0.0.1",
        outDir,
      }),
    );
    const addr = prodServer.address() as { port: number };
    prodUrl = `http://127.0.0.1:${addr.port}`;
  }, 120000);

  afterAll(async () => {
    if (prodServer) {
      await new Promise<void>((resolve) => prodServer.close(() => resolve()));
    }
    if (tmpRoot) {
      fs.rmSync(tmpRoot, { recursive: true, force: true });
    }
  });

  it("preserves the page module file URL during server rendering", async () => {
    const res = await fetch(`${prodUrl}/`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const match = html.match(/<div id="test-data">([^<]*)<\/div>/);
    expect(match).not.toBeNull();
    const data = JSON.parse(decodeHtmlText(match![1])) as { url: string };

    expect(data.url).toMatch(/^file:\/\/\//);
    expect(data.url).toMatch(/\/pages\/index\.tsx$/);
    expect(data.url).not.toContain("/dist/server/entry.js");
  });

  it("normalizes the page module file URL in the client page chunk", () => {
    const jsFiles = collectJavaScriptFiles(path.join(outDir, "client"));
    const clientCode = jsFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n");

    expect(clientCode).toContain("file:///ROOT/pages/index.tsx");
    expect(clientCode).not.toContain("/dist/server/entry.js");
  });
});

describe("router __NEXT_DATA__ correctness (Pages Router)", () => {
  let routerServer: ViteDevServer;
  let routerBaseUrl: string;

  function readNextData(html: string) {
    const match = html.match(
      /<script id="__NEXT_DATA__" type="application\/json"(?: nonce="[^"]+")?>([\s\S]*?)<\/script>/,
    );
    expect(match).toBeTruthy();
    return JSON.parse(match![1]);
  }

  beforeAll(async () => {
    ({ server: routerServer, baseUrl: routerBaseUrl } = await startFixtureServer(FIXTURE_DIR));
  });

  afterAll(async () => {
    await routerServer?.close();
  });

  it("dynamic route params are included in __NEXT_DATA__.query", async () => {
    const res = await fetch(`${routerBaseUrl}/blog/hello-world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.query).toEqual({ slug: "hello-world" });
    expect(nextData.page).toBe("/blog/[slug]");
  });

  it("__NEXT_DATA__.page is the route pattern, not the actual path", async () => {
    const res = await fetch(`${routerBaseUrl}/posts/hello-world`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.page).toBe("/posts/[id]");
    expect(nextData.query.id).toBe("hello-world");
  });

  it("catch-all route pattern in __NEXT_DATA__.page", async () => {
    const res = await fetch(`${routerBaseUrl}/docs/a/b/c`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.page).toBe("/docs/[...slug]");
  });

  it("__NEXT_DATA__ includes isFallback: false", async () => {
    const res = await fetch(`${routerBaseUrl}/blog/hello-world`);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.isFallback).toBe(false);
  });

  it("static page __NEXT_DATA__.page is the pathname", async () => {
    const res = await fetch(`${routerBaseUrl}/about`);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.page).toBe("/about");
  });

  // Ported from Next.js: test/e2e/prerender.test.ts
  // https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/prerender.test.ts
  it("omits gsp from __NEXT_DATA__ for non-GSP pages", async () => {
    const res = await fetch(`${routerBaseUrl}/about`);
    const html = await res.text();
    const nextData = readNextData(html);
    expect("gsp" in nextData).toBe(false);
  });

  it("shallow-test page returns correct __NEXT_DATA__ with GSSP props", async () => {
    const res = await fetch(`${routerBaseUrl}/shallow-test`);
    expect(res.status).toBe(200);
    const html = await res.text();
    const nextData = readNextData(html);
    expect(nextData.page).toBe("/shallow-test");
    expect(nextData.props.pageProps.gsspCallId).toBeGreaterThan(0);
  });

  // 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
  // Regression test for GitHub issue #1196 — catch-all + basePath + rewrites + middleware.
  it("catch-all route params are preserved with basePath + rewrites + middleware", async () => {
    const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-pages-catchall-basepath-"));
    const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");

    try {
      await fsp.symlink(rootNodeModules, path.join(tmpRoot, "node_modules"), "junction");
      await fsp.mkdir(path.join(tmpRoot, "pages"), { recursive: true });

      await fsp.writeFile(path.join(tmpRoot, "package.json"), JSON.stringify({ type: "module" }));
      await fsp.writeFile(
        path.join(tmpRoot, "next.config.mjs"),
        `export default {
          basePath: "/docs",
          async rewrites() {
            return {
              beforeFiles: [
                { source: "/before-rewrite", destination: "/about" },
              ],
            };
          },
        };\n`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "middleware.ts"),
        `import { NextResponse } from "next/server";
export const config = { matcher: "/:path*" };
export default function middleware() {
  return NextResponse.next();
}
`,
      );
      await fsp.writeFile(
        path.join(tmpRoot, "pages", "[...path].tsx"),
        `export default function CatchAllPage({ path }: { path: string[] }) {
          return (
            <div>
              <h1 data-testid="page-title">CatchAll</h1>
              <p data-testid="query-path">{JSON.stringify(path)}</p>
            </div>
          );
        }

        export async function getServerSideProps({ params }: { params: { path: string[] } }) {
          return { props: { path: params.path } };
        }
`,
      );

      const { server, baseUrl } = await startFixtureServer(tmpRoot);
      try {
        const res = await fetch(`${baseUrl}/docs/first`);
        expect(res.status).toBe(200);
        const html = await res.text();
        const nextData = readNextData(html);
        expect(nextData.page).toBe("/[...path]");
        expect(nextData.query).toEqual({ path: ["first"] });
        expect(html).toContain("CatchAll");
      } finally {
        await server.close();
      }
    } finally {
      await fsp.rm(tmpRoot, { recursive: true, force: true });
    }
  });
});

describe("Pages Router dev ISR regeneration", () => {
  it("wraps stale regeneration in a fresh unified request context", async () => {
    vi.resetModules();

    let regenPromise: Promise<void> | null = null;
    const isrSetSpy = vi.fn().mockResolvedValue(undefined);

    vi.doMock("../packages/vinext/src/server/isr-cache.js", async () => {
      const actual = await vi.importActual<
        typeof import("../packages/vinext/src/server/isr-cache.js")
      >("../packages/vinext/src/server/isr-cache.js");

      return {
        ...actual,
        getRevalidateDuration: vi.fn(() => 1),
        isrGet: vi.fn().mockResolvedValue({
          isStale: true,
          value: {
            value: actual.buildPagesCacheValue("<html><body>stale</body></html>", {
              timestamp: 1,
              message: "stale",
            }),
            cacheState: "stale",
          },
        }),
        isrSet: isrSetSpy,
        triggerBackgroundRegeneration: vi.fn((_key: string, renderFn: () => Promise<void>) => {
          regenPromise = renderFn();
        }),
      };
    });

    try {
      const [
        { createSSRHandler },
        { getRequestContext, isInsideUnifiedScope },
        { getRequestExecutionContext, runWithExecutionContext },
      ] = await Promise.all([
        import("../packages/vinext/src/server/dev-server.js"),
        import("../packages/vinext/src/shims/unified-request-context.js"),
        import("../packages/vinext/src/shims/request-context.js"),
      ]);

      let parentRequestTags: string[] = [];
      let regenSawUnifiedScope = false;
      let regenTags: string[] = [];
      let regenExecutionContext: unknown;
      let regenUnifiedExecutionContext: unknown;
      let regenSsrContext: unknown;
      let regenI18nContext: unknown;
      let appTreeWrapCount = 0;
      const App = Object.assign(({ Component, pageProps }: any) => Component(pageProps), {
        getInitialProps: vi.fn(async () => ({
          appLevel: "preserved",
          pageProps: { fromApp: true },
        })),
      });
      const outerExecutionContext = {
        waitUntil() {},
      };

      const routeFile = path.join(FIXTURE_DIR, "pages", "isr-test.tsx");
      const loadModule = async (id: string) => {
        // ALS registration side-effects loaded at createSSRHandler startup
        if (id === "vinext/head-state" || id === "vinext/router-state") {
          return {};
        }

        if (id === "next/router") {
          return {
            setSSRContext() {
              getRequestContext().currentRequestTags.push("outer-tag");
              parentRequestTags = [...getRequestContext().currentRequestTags];
            },
            wrapWithRouterContext(element: unknown) {
              appTreeWrapCount += 1;
              return element;
            },
          };
        }

        if (id === path.join(FIXTURE_DIR, "pages", "_app")) {
          return { default: App };
        }

        if (id === routeFile) {
          return {
            default() {
              return null;
            },
            async getStaticProps() {
              regenSawUnifiedScope = isInsideUnifiedScope();
              regenTags = [...getRequestContext().currentRequestTags];
              regenExecutionContext = getRequestExecutionContext();
              regenUnifiedExecutionContext = getRequestContext().executionContext;
              regenSsrContext = getRequestContext().ssrContext;
              regenI18nContext = getRequestContext().i18nContext;
              return {
                props: {
                  timestamp: Date.now(),
                  message: "fresh",
                },
                revalidate: 1,
              };
            },
          };
        }

        throw new Error(`Unexpected module load: ${id}`);
      };
      const server = {
        config: {
          root: FIXTURE_DIR,
          base: "/docs/",
        },
        transformIndexHtml: vi.fn(async (_url: string, html: string) => html),
      } as unknown as ViteDevServer;
      const runner = { import: loadModule };

      const handler = createSSRHandler(
        server,
        runner,
        [
          {
            pattern: "/isr-test",
            patternParts: ["isr-test"],
            filePath: routeFile,
            isDynamic: false,
            params: [],
          },
        ],
        path.join(FIXTURE_DIR, "pages"),
      );

      const finishListeners: Array<() => void> = [];
      const res = {
        statusCode: 200,
        on(event: string, listener: () => void) {
          if (event === "finish") {
            finishListeners.push(listener);
          }
          return this;
        },
        writeHead: vi.fn(function (this: { statusCode: number }, status: number) {
          this.statusCode = status;
          return this;
        }),
        end: vi.fn(() => {
          for (const listener of finishListeners) {
            listener();
          }
        }),
      } as any;

      await runWithExecutionContext(outerExecutionContext, () =>
        handler({ method: "GET", headers: {} } as any, res, "/isr-test"),
      );

      expect(parentRequestTags).toEqual(["outer-tag"]);
      expect(res.writeHead).toHaveBeenCalledWith(
        200,
        expect.objectContaining({
          "X-Vinext-Cache": "STALE",
          "x-nextjs-cache": "STALE",
        }),
      );

      if (!regenPromise) {
        throw new Error("expected stale ISR request to start background regeneration");
      }
      const pendingRegen = regenPromise;

      await Promise.resolve(pendingRegen);

      expect(regenSawUnifiedScope).toBe(true);
      expect(regenTags).toEqual([]);
      expect(regenExecutionContext).toBeNull();
      expect(regenUnifiedExecutionContext).toBeNull();
      expect(regenSsrContext).toMatchObject({
        pathname: "/isr-test",
        query: {},
        asPath: "/isr-test",
      });
      expect(regenI18nContext).toBeNull();
      expect(appTreeWrapCount).toBe(1);
      expect(isrSetSpy).toHaveBeenCalledOnce();
      expect(isrSetSpy.mock.calls[0]?.[1]).toMatchObject({
        kind: "PAGES",
        pageData: {
          appLevel: "preserved",
          pageProps: {
            fromApp: true,
            message: "fresh",
          },
        },
      });
      const regeneratedHtml = isrSetSpy.mock.calls[0]?.[1].html as string;
      expect(regeneratedHtml).toContain('"pageModuleUrl":"/docs/pages/isr-test.tsx"');
      expect(regeneratedHtml).toContain('"appModuleUrl":"/docs/pages/_app"');
    } finally {
      vi.doUnmock("../packages/vinext/src/server/isr-cache.js");
      vi.resetModules();
      vi.restoreAllMocks();
    }
  });
});
