import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
import { createServer, build, type ViteDevServer } from "vite";
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 vinext from "../packages/vinext/src/index.js";
import { PAGES_FIXTURE_DIR, startFixtureServer } from "./helpers.js";

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

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 });
}
`,
  );
}

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("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.");
  });

  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:");
  });

  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");
  });

  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");
    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");
  });

  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("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("keeps ISR cache-fill rerenders isolated from the streamed 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__");
  });

  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("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" } });
  });

  // 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);
  });

  // --- 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("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");
  });

  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");
  });

  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("SSR renders unlisted path with getStaticPaths fallback: true (on-demand)", async () => {
    // In dev/SSR mode, fallback: true still renders fully (same as blocking)
    // because data is always available via on-demand SSR.
    const res = await fetch(`${baseUrl}/products/unknown`);
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toMatch(/Product\s*(<!-- -->)?\s*unknown/);
    expect(html).toMatch(/Product ID:.*unknown/);
    // isFallback should be false since we always SSR fully
    expect(html).toMatch(/isFallback:.*false/);
  });

  it("includes isFallback: false in __NEXT_DATA__", async () => {
    const res = await fetch(`${baseUrl}/products/widget`);
    const html = await res.text();
    const match = html.match(/__NEXT_DATA__\s*=\s*(\{.*?\})\s*[;<]/);
    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);
  });
});

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}/_vinext/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}/_vinext/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}/_vinext/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 () => {
    await server?.close();
    await fsp.rm(tmpDir, { recursive: true, force: true });
  });

  it("allows cross-origin requests from allowedDevOrigins", async () => {
    const res = await fetch(`${baseUrl}/`, {
      headers: { Origin: "http://allowed.example.com" },
    });
    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" },
    });
    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]+["']/);
      expect(code).not.toContain(":slug+");
      expect(code).not.toContain(":slug*");
    } finally {
      await testServer.close();
    }
  });
});

describe("Plugin config", () => {
  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();
    expect(result.build.rollupOptions).toBeDefined();
    expect(result.build.rollupOptions.onwarn).toBeDefined();

    const defaultHandler = vi.fn();

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

    // "use server" MODULE_LEVEL_DIRECTIVE warnings should be silenced
    result.build.rollupOptions.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',
    };
    result.build.rollupOptions.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" };
    result.build.rollupOptions.onwarn(otherWarning, defaultHandler);
    expect(defaultHandler).toHaveBeenCalledWith(otherWarning);
  });

  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 defaultHandler = vi.fn();

    // "use client" should still be suppressed (user handler NOT called)
    result.build.rollupOptions.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" };
    result.build.rollupOptions.onwarn(otherWarning, defaultHandler);
    expect(userOnwarn).toHaveBeenCalledWith(otherWarning, defaultHandler);
    expect(defaultHandler).not.toHaveBeenCalled();
  });

  it("registers vinext:mdx proxy plugin with enforce pre for correct ordering", () => {
    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();
    expect(mdxProxy.transform("code", "./foo.ts", {})).toBeUndefined();
  });

  it("vinext:mdx transform skips ids that contain a query string (regression: ?raw)", () => {
    // @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
    expect(mdxProxy.transform("# hello", "/app/content.mdx?raw", {})).toBeUndefined();
    expect(mdxProxy.transform("# hello", "/app/page.mdx?url", {})).toBeUndefined();
    expect(mdxProxy.transform("# hello", "/app/page.mdx?inline", {})).toBeUndefined();
  });

  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 output exists
    const assetsDir = path.join(outDir, "client", "assets");
    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,
      unknown
    >;
    const counterBuildManifestEntries = Object.keys(buildManifest).filter(
      (key) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
    );
    expect(counterBuildManifestEntries).toEqual([]);

    // 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(20 * 1024); // < 20 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,
        unknown
      >;
      const counterBuildManifestEntries = Object.keys(buildManifest).filter(
        (key) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
      );
      expect(counterBuildManifestEntries).toEqual([]);

      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();
      expect(counterManifestEntry?.[1].every((file: string) => file.startsWith("docs/"))).toBe(
        true,
      );

      const { startProdServer } = await import("../packages/vinext/src/server/prod-server.js");
      const prodServer = 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}/docs/counter`);
        expect(res.status).toBe(200);
        const html = await res.text();
        expect(html).toContain('href="/docs/assets/');
        expect(html).toContain('src="/docs/assets/');
      } 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,
        unknown
      >;
      const counterBuildManifestEntries = Object.keys(buildManifest).filter(
        (key) => key.endsWith("/pages/counter.tsx") || key === "pages/counter.tsx",
      );
      expect(counterBuildManifestEntries).toEqual([]);

      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 = 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(async (req, res) => {
      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);
    });

    // 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);
      const ssrHtml = await ssrRes.text();
      expect(ssrHtml).toContain("Server-Side Rendered");

      // 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: 404 for unknown route
      const notFoundRes = await fetch(`${prodUrl}/nonexistent`);
      expect(notFoundRes.status).toBe(404);
    } 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 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 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;
  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 = 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");
  });

  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 = 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("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/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");
  });

  // 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("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);
  });
});

describe("Production server next.config.js features (Pages Router)", () => {
  const outDir = path.resolve(FIXTURE_DIR, "dist");
  let prodServer: import("node:http").Server;
  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 = 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("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 server: ViteDevServer;
  const exportDir = path.resolve(FIXTURE_DIR, "out");

  beforeAll(async () => {
    server = await createServer({
      root: FIXTURE_DIR,
      configFile: false,
      plugins: [vinext()],
      server: { port: 0 },
      logLevel: "silent",
    });
    // Don't need to listen — just need the SSR module loader
  });

  afterAll(async () => {
    await server.close();
    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({
      server,
      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({
        server,
        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({
        server,
        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 = 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("router __NEXT_DATA__ correctness (Pages Router)", () => {
  let routerServer: ViteDevServer;
  let routerBaseUrl: string;

  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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    expect(match).toBeTruthy();
    const nextData = JSON.parse(match![1]);
    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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    const nextData = JSON.parse(match![1]);
    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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    const nextData = JSON.parse(match![1]);
    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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    const nextData = JSON.parse(match![1]);
    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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    const nextData = JSON.parse(match![1]);
    expect(nextData.page).toBe("/about");
  });

  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 match = html.match(/<script>window\.__NEXT_DATA__\s*=\s*({.*?})<\/script>/);
    const nextData = JSON.parse(match![1]);
    expect(nextData.page).toBe("/shallow-test");
    expect(nextData.props.pageProps.gsspCallId).toBeGreaterThan(0);
  });
});

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;
      const outerExecutionContext = {
        waitUntil() {},
      };

      const routeFile = path.join(FIXTURE_DIR, "pages", "isr-test.tsx");
      const server = {
        transformIndexHtml: vi.fn(async (_url: string, html: string) => html),
        ssrLoadModule: vi.fn(async (id: string) => {
          if (id === "next/router") {
            return {
              setSSRContext() {
                getRequestContext().currentRequestTags.push("outer-tag");
                parentRequestTags = [...getRequestContext().currentRequestTags];
              },
              wrapWithRouterContext(element: unknown) {
                return element;
              },
            };
          }

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

          throw new Error(`Unexpected module load: ${id}`);
        }),
      } as unknown as ViteDevServer;

      const handler = createSSRHandler(
        server,
        [
          {
            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",
        }),
      );

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

      await regenPromise;

      expect(regenSawUnifiedScope).toBe(true);
      expect(regenTags).toEqual([]);
      expect(regenExecutionContext).toBeNull();
      expect(regenUnifiedExecutionContext).toBeNull();
      expect(isrSetSpy).toHaveBeenCalledOnce();
    } finally {
      vi.doUnmock("../packages/vinext/src/server/isr-cache.js");
      vi.resetModules();
      vi.restoreAllMocks();
    }
  });
});
