import { describe, it, expect, vi } from "vite-plus/test";
import {
  runPagesRequest,
  wrapMiddlewareWithBasePath,
  type PagesPipelineDeps,
  type MiddlewareResult,
  type PagesRenderOptions,
} from "../packages/vinext/src/server/pages-request-pipeline.js";

// Helpers

function makeRequest(pathname: string, headers?: Record<string, string>): Request {
  return new Request(`http://localhost${pathname}`, { headers });
}

function baseDeps(overrides?: Partial<PagesPipelineDeps>): PagesPipelineDeps {
  return {
    basePath: "",
    trailingSlash: false,
    i18nConfig: null,
    configRedirects: [],
    configRewrites: { beforeFiles: [], afterFiles: [], fallback: [] },
    configHeaders: [],
    hadBasePath: true,
    isDataReq: false,
    isDataRequest: false,
    ...overrides,
  };
}

function makeMiddleware(result: Partial<MiddlewareResult>) {
  return vi.fn(async (_req: Request, _ctx: unknown, _opts: { isDataRequest: boolean }) => ({
    continue: true,
    ...result,
  }));
}

function makeRenderPage(status = 200, body = "ok") {
  return vi.fn(
    async (_req: Request, _url: string, _opts?: PagesRenderOptions) =>
      new Response(body, { status }),
  );
}

// 1. Trailing-slash: /foo/ with trailingSlash: false → {type:"response"} with status 308
describe("trailing slash normalization", () => {
  it("redirects /foo/ to /foo when trailingSlash is false", async () => {
    const req = makeRequest("/foo/");
    const result = await runPagesRequest(req, baseDeps({ trailingSlash: false }));
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(308);
    expect(result.response.headers.get("Location")).toBe("/foo");
  });

  it("does not redirect /foo when trailingSlash is false", async () => {
    const req = makeRequest("/foo");
    const result = await runPagesRequest(req, baseDeps({ renderPage: makeRenderPage() }));
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
  });
});

// 2. Config redirect: permanent redirect → status 308 with Location
describe("config redirects", () => {
  it("permanent redirect returns 308", async () => {
    const req = makeRequest("/old");
    const result = await runPagesRequest(
      req,
      baseDeps({
        configRedirects: [{ source: "/old", destination: "/new", permanent: true }],
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(308);
    expect(result.response.headers.get("Location")).toBe("/new");
  });

  // 3. Config redirect: non-permanent → status 307
  it("non-permanent redirect returns 307", async () => {
    const req = makeRequest("/old");
    const result = await runPagesRequest(
      req,
      baseDeps({
        configRedirects: [{ source: "/old", destination: "/new", permanent: false }],
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(307);
    expect(result.response.headers.get("Location")).toBe("/new");
  });

  it("does not match when source does not match", async () => {
    const req = makeRequest("/other");
    const result = await runPagesRequest(
      req,
      baseDeps({
        configRedirects: [{ source: "/old", destination: "/new", permanent: false }],
        renderPage: makeRenderPage(),
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
  });
});

// 4. Middleware redirect short-circuit → {type:"response"} status 307
describe("middleware", () => {
  it("middleware redirect short-circuits with 307", async () => {
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({
          continue: false,
          redirectUrl: "http://localhost/bar",
          redirectStatus: 307,
        }),
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(307);
    expect(result.response.headers.get("Location")).toBe("http://localhost/bar");
  });

  it("middleware redirect uses default 307 when no redirectStatus given", async () => {
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({ continue: false, redirectUrl: "http://localhost/bar" }),
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(307);
  });

  // Ported from Next.js: test/e2e/middleware-general/test/index.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/middleware-general/test/index.test.ts
  it("does not classify a normal request as data from x-nextjs-data alone", async () => {
    const runMiddleware = vi.fn(async () => ({
      continue: false,
      redirectUrl: "http://localhost/somewhere",
      redirectStatus: 307,
    }));

    const result = await runPagesRequest(
      makeRequest("/redirect-to-somewhere", { "x-nextjs-data": "1" }),
      baseDeps({ runMiddleware }),
    );

    expect(runMiddleware).toHaveBeenCalledWith(expect.any(Request), null, {
      isDataRequest: false,
    });
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(307);
    expect(result.response.headers.get("Location")).toBe("http://localhost/somewhere");
    expect(result.response.headers.get("x-nextjs-redirect")).toBeNull();
  });

  // 5. Middleware rewrite: resolvedUrl changes, pipeline continues
  it("middleware rewrite changes resolved URL, pipeline continues to render", async () => {
    const renderPage = makeRenderPage(200, "rewrite target");
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({ continue: true, rewriteUrl: "/bar" }),
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    // renderPage should have been called with the rewritten URL
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/bar",
      undefined,
      expect.any(Headers),
    );
  });

  it.each([
    { i18nConfig: null, requestPath: "/ssr-page", rewritePath: "/ssr-page-2" },
    {
      i18nConfig: { locales: ["en", "fr"], defaultLocale: "en" },
      requestPath: "/en/ssr-page",
      rewritePath: "/en/ssr-page-2",
    },
  ])(
    "exposes the middleware rewrite target on real Pages data responses ($requestPath)",
    async ({ i18nConfig, requestPath, rewritePath }) => {
      const result = await runPagesRequest(
        makeRequest(requestPath),
        baseDeps({
          isDataReq: true,
          isDataRequest: true,
          i18nConfig,
          runMiddleware: makeMiddleware({
            continue: true,
            rewriteUrl: rewritePath,
          }),
          renderPage: makeRenderPage(200, '{"pageProps":{"message":"Bye Cruel World"}}'),
        }),
      );

      expect(result.type).toBe("response");
      if (result.type !== "response") return;
      expect(result.response.headers.get("x-nextjs-rewrite")).toBe(rewritePath);
      expect(result.response.headers.get("x-middleware-rewrite")).toBeNull();
    },
  );

  it("does not expose the middleware rewrite target on HTML responses", async () => {
    const result = await runPagesRequest(
      makeRequest("/to-blog/post"),
      baseDeps({
        runMiddleware: makeMiddleware({
          continue: true,
          rewriteUrl: "/fallback-true-blog/post",
        }),
        renderPage: makeRenderPage(200),
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.headers.get("x-nextjs-rewrite")).toBeNull();
  });

  it("exposes the final config rewrite URL with appended source params", async () => {
    const result = await runPagesRequest(
      makeRequest("/config-rewrite-to-dynamic-static/post-2"),
      baseDeps({
        isDataRequest: true,
        configRewrites: {
          beforeFiles: [
            {
              source: "/config-rewrite-to-dynamic-static/:rewriteSlug",
              destination: "/ssg",
            },
          ],
          afterFiles: [],
          fallback: [],
        },
        renderPage: makeRenderPage(200),
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.headers.get("x-nextjs-rewrite")).toBe("/ssg?rewriteSlug=post-2");
  });

  // 6. Middleware response short-circuit → {type:"response"} with middleware response
  it("middleware response short-circuit returns the middleware response", async () => {
    const middlewareResponse = new Response("blocked", { status: 403 });
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({ continue: false, response: middlewareResponse }),
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response).toBe(middlewareResponse);
    expect(result.response.status).toBe(403);
    // Passthrough response: no content-type default → Node sends it verbatim.
    expect(result.defaultContentType).toBeUndefined();
  });

  // 18. middlewareStatus reconciliation: result.status takes priority over result.rewriteStatus
  it("middlewareStatus: result.status takes priority over result.rewriteStatus", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({ continue: true, status: 404, rewriteStatus: 403 }),
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    // middlewareStatus=404 from result.status should override rewriteStatus=403
    expect(result.response.status).toBe(404);
  });

  // 20. Multi-value Set-Cookie accumulation from middleware response headers
  it("accumulates multiple Set-Cookie headers from middleware", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/foo");
    const responseHeaders = new Headers();
    responseHeaders.append("set-cookie", "a=1");
    responseHeaders.append("set-cookie", "b=2");

    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: vi.fn(async () => ({
          continue: true,
          responseHeaders: [...responseHeaders.entries()],
        })),
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    const cookies = result.response.headers.getSetCookie?.() ?? [];
    expect(cookies).toContain("a=1");
    expect(cookies).toContain("b=2");
  });
});

// 7. Config headers staged
describe("config headers", () => {
  it("stages config headers into middleware headers", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/foo");
    const result = await runPagesRequest(
      req,
      baseDeps({
        configHeaders: [{ source: "/foo", headers: [{ key: "X-Custom", value: "test" }] }],
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.headers.get("X-Custom")).toBe("test");
  });
});

// 8. External proxy after middleware rewrite
describe("external proxy", () => {
  it("proxies to external URL when middleware rewrites to external", async () => {
    const req = makeRequest("/proxy-me");
    // Use fetchMock or just observe isExternalUrl → proxyExternalRequest
    // Since proxyExternalRequest will actually fetch, we mock globalThis.fetch
    const mockFetch = vi
      .spyOn(globalThis, "fetch")
      .mockResolvedValueOnce(new Response("external response", { status: 200 }));

    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({
          continue: true,
          rewriteUrl: "https://example.com/proxied",
        }),
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    mockFetch.mockRestore();
  });
});

// 9. beforeFiles rewrite with external URL → {type:"response"} from proxy
describe("beforeFiles rewrites", () => {
  it("beforeFiles external rewrite proxies the request", async () => {
    const req = makeRequest("/proxy-me");
    const mockFetch = vi
      .spyOn(globalThis, "fetch")
      .mockResolvedValueOnce(new Response("from proxy", { status: 200 }));

    const result = await runPagesRequest(
      req,
      baseDeps({
        configRewrites: {
          beforeFiles: [{ source: "/proxy-me", destination: "https://example.com/proxied" }],
          afterFiles: [],
          fallback: [],
        },
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    mockFetch.mockRestore();
  });

  it("beforeFiles rewrite changes resolved URL", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/from");

    const result = await runPagesRequest(
      req,
      baseDeps({
        configRewrites: {
          beforeFiles: [{ source: "/from", destination: "/to" }],
          afterFiles: [],
          fallback: [],
        },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/to",
      undefined,
      expect.any(Headers),
    );
  });

  it("applies chained beforeFiles rewrites with accumulated query conditions", async () => {
    const renderPage = makeRenderPage(200);
    const result = await runPagesRequest(
      makeRequest("/from?keep=1"),
      baseDeps({
        configRewrites: {
          beforeFiles: [
            { source: "/from", destination: "/middle?stage=1" },
            {
              source: "/middle",
              destination: "/to",
              has: [{ type: "query", key: "stage", value: "1" }],
            },
          ],
          afterFiles: [],
          fallback: [],
        },
        renderPage,
      }),
    );

    expect(result.type).toBe("response");
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/to?keep=1&stage=1",
      undefined,
      expect.any(Headers),
    );
  });

  it("applies every matching beforeFiles rewrite in sequence", async () => {
    const renderPage = makeRenderPage(200);
    const result = await runPagesRequest(
      makeRequest("/start"),
      baseDeps({
        isDataRequest: true,
        configRewrites: {
          beforeFiles: [
            { source: "/start", destination: "/middle?first=1" },
            { source: "/middle", destination: "/destination?second=2" },
          ],
          afterFiles: [],
          fallback: [],
        },
        renderPage,
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/destination?first=1&second=2",
      undefined,
      expect.any(Headers),
    );
    expect(result.response.headers.get("x-nextjs-rewrite")).toBe("/destination?first=1&second=2");
  });

  it("excludes beforeFiles fragments from Pages route matching", async () => {
    const matchPageRoute = vi.fn((pathname: string) =>
      pathname === "/to" ? { route: { isDynamic: false } } : null,
    );
    await runPagesRequest(
      makeRequest("/from"),
      baseDeps({
        configRewrites: {
          beforeFiles: [{ source: "/from", destination: "/to#section" }],
          afterFiles: [],
          fallback: [],
        },
        matchPageRoute,
        renderPage: makeRenderPage(200),
      }),
    );

    expect(matchPageRoute).toHaveBeenCalledWith("/to", expect.any(Request));
    expect(matchPageRoute).not.toHaveBeenCalledWith("/to#section", expect.any(Request));
  });
});

// 10. Out-of-basePath reject when basePath: "/base" and hadBasePath: false and no configRewrite fired
describe("out-of-basePath rejection", () => {
  it("rejects requests outside basePath with 404 when no rewrite fires", async () => {
    const req = makeRequest("/outside/page");
    const result = await runPagesRequest(
      req,
      baseDeps({
        basePath: "/base",
        hadBasePath: false,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(404);
  });

  it("allows requests outside basePath when beforeFiles rewrite fires", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/outside");
    const result = await runPagesRequest(
      req,
      baseDeps({
        basePath: "/base",
        hadBasePath: false,
        configRewrites: {
          beforeFiles: [{ source: "/outside", destination: "/inside", basePath: false }],
          afterFiles: [],
          fallback: [],
        },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
  });
});

// 11. API route with renderPage absent → {type:"api"} intent
describe("API routes", () => {
  it("emits api intent when handleApi absent", async () => {
    const req = makeRequest("/api/users");
    const result = await runPagesRequest(req, baseDeps());
    expect(result.type).toBe("api");
    if (result.type !== "api") return;
    expect(result.apiUrl).toBe("/api/users");
  });

  // 12. API route with handleApi present → {type:"response"}
  it("returns response when handleApi present", async () => {
    const req = makeRequest("/api/users");
    const handleApi = vi.fn(async () => new Response('{"ok":true}', { status: 200 }));
    const result = await runPagesRequest(req, baseDeps({ handleApi }));
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    expect(handleApi).toHaveBeenCalledWith(expect.any(Request), "/api/users", null);
    // API responses default a missing content-type to octet-stream, not text/html.
    expect(result.defaultContentType).toBe("application/octet-stream");
  });

  it("tags page renders with a text/html content-type default", async () => {
    const req = makeRequest("/page");
    const result = await runPagesRequest(req, baseDeps({ renderPage: makeRenderPage(200) }));
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.defaultContentType).toBe("text/html");
  });

  it("matches /api exactly", async () => {
    const req = makeRequest("/api");
    const result = await runPagesRequest(req, baseDeps());
    expect(result.type).toBe("api");
  });
});

// Filesystem/static route serving supplied by each runtime adapter.
describe("serveFilesystemRoute", () => {
  it("returns {type:'handled'} when serveFilesystemRoute serves the request", async () => {
    const renderPage = makeRenderPage(200);
    const serveFilesystemRoute = vi.fn(async () => true);
    const req = makeRequest("/favicon.ico");
    const result = await runPagesRequest(req, baseDeps({ serveFilesystemRoute, renderPage }));
    expect(result.type).toBe("handled");
    // The static file short-circuits — the renderer is never invoked.
    expect(renderPage).not.toHaveBeenCalled();
  });

  it("falls through to render when serveFilesystemRoute returns false", async () => {
    const renderPage = makeRenderPage(200);
    const serveFilesystemRoute = vi.fn(async () => false);
    const req = makeRequest("/page");
    const result = await runPagesRequest(req, baseDeps({ serveFilesystemRoute, renderPage }));
    expect(serveFilesystemRoute).toHaveBeenCalledTimes(1);
    expect(result.type).toBe("response");
    expect(renderPage).toHaveBeenCalled();
  });

  it("passes the original (pre-rewrite) pathname and staged headers", async () => {
    const serveFilesystemRoute = vi.fn(async () => true);
    const middleware = makeMiddleware({ responseHeaders: [["set-cookie", "a=1"]] });
    const req = makeRequest("/robots.txt");
    await runPagesRequest(req, baseDeps({ serveFilesystemRoute, runMiddleware: middleware }));
    expect(serveFilesystemRoute).toHaveBeenCalledWith(
      "/robots.txt",
      { "set-cookie": ["a=1"] },
      "direct",
    );
  });

  it("runs after middleware — a middleware redirect wins over a public file", async () => {
    const serveFilesystemRoute = vi.fn(async () => true);
    const middleware = makeMiddleware({
      continue: false,
      redirectUrl: "/elsewhere",
      redirectStatus: 307,
    });
    const req = makeRequest("/favicon.ico");
    const result = await runPagesRequest(
      req,
      baseDeps({ serveFilesystemRoute, runMiddleware: middleware }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(307);
    expect(result.response.headers.get("Location")).toBe("/elsewhere");
    expect(serveFilesystemRoute).not.toHaveBeenCalled();
  });

  // Ported from Next.js: test/e2e/i18n-ignore-rewrite-source-locale/rewrites.test.ts
  // https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-ignore-rewrite-source-locale/rewrites.test.ts
  it("re-enters filesystem matching after a locale:false beforeFiles rewrite", async () => {
    const serveFilesystemRoute = vi.fn(async (pathname: string) => pathname === "/file.txt");
    const result = await runPagesRequest(
      makeRequest("/sv/rewrite-files/file.txt"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        configRewrites: {
          beforeFiles: [
            {
              source: "/:locale/rewrite-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
          afterFiles: [],
          fallback: [],
        },
        serveFilesystemRoute,
      }),
    );

    expect(result.type).toBe("handled");
    expect(serveFilesystemRoute).toHaveBeenNthCalledWith(
      1,
      "/sv/rewrite-files/file.txt",
      {},
      "direct",
    );
    expect(serveFilesystemRoute).toHaveBeenNthCalledWith(2, "/file.txt", {}, "beforeFiles");
  });

  it("returns a Worker-style asset response after a beforeFiles rewrite", async () => {
    const serveFilesystemRoute = vi.fn(async (pathname: string) =>
      pathname === "/file.txt"
        ? new Response("worker asset", { headers: { "content-type": "text/plain" } })
        : false,
    );
    const result = await runPagesRequest(
      makeRequest("/en/rewrite-files/file.txt"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        configRewrites: {
          beforeFiles: [
            {
              source: "/:locale/rewrite-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
          afterFiles: [],
          fallback: [],
        },
        serveFilesystemRoute,
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    await expect(result.response.text()).resolves.toBe("worker asset");
  });

  it("continues rewritten API and page routing when no filesystem target exists", async () => {
    const serveFilesystemRoute = vi.fn(async () => false);
    const handleApi = vi.fn(async () => new Response("api"));
    const renderPage = makeRenderPage(200, "page");
    const rewrites = {
      beforeFiles: [
        { source: "/rewrite-api/:path*", destination: "/api/:path*" },
        { source: "/rewrite-page", destination: "/about" },
      ],
      afterFiles: [],
      fallback: [],
    };

    const apiResult = await runPagesRequest(
      makeRequest("/rewrite-api/hello"),
      baseDeps({ configRewrites: rewrites, serveFilesystemRoute, handleApi, renderPage }),
    );
    const pageResult = await runPagesRequest(
      makeRequest("/rewrite-page"),
      baseDeps({ configRewrites: rewrites, serveFilesystemRoute, handleApi, renderPage }),
    );

    expect(apiResult.type).toBe("response");
    expect(handleApi).toHaveBeenCalledWith(expect.any(Request), "/api/hello", null);
    expect(pageResult.type).toBe("response");
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/about",
      undefined,
      expect.any(Headers),
    );
  });
});

// 13. afterFiles rewrite: dynamic page match is re-queried
describe("afterFiles rewrites", () => {
  it("re-enters Worker assets after afterFiles rewrites", async () => {
    const serveFilesystemRoute = vi.fn(async (pathname: string, _headers, phase) =>
      pathname === "/file.txt" && phase === "afterFiles"
        ? new Response("worker afterFiles asset")
        : false,
    );
    const result = await runPagesRequest(
      makeRequest("/sv/after-files/file.txt"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        matchPageRoute: vi.fn().mockReturnValue(null),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [
            {
              source: "/:locale/after-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
          fallback: [],
        },
        serveFilesystemRoute,
        renderPage: makeRenderPage(404),
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    await expect(result.response.text()).resolves.toBe("worker afterFiles asset");
    expect(serveFilesystemRoute).toHaveBeenLastCalledWith("/file.txt", {}, "afterFiles");
  });

  it("does not run afterFiles filesystem re-entry when a static page matches", async () => {
    const serveFilesystemRoute = vi.fn(async () => false);
    const renderPage = makeRenderPage(200, "page wins");
    const result = await runPagesRequest(
      makeRequest("/after-control"),
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue({ route: { isDynamic: false } }),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [{ source: "/after-control", destination: "/file.txt" }],
          fallback: [],
        },
        serveFilesystemRoute,
        renderPage,
      }),
    );

    expect(result.type).toBe("response");
    expect(serveFilesystemRoute).toHaveBeenCalledOnce();
    expect(serveFilesystemRoute).toHaveBeenCalledWith("/after-control", {}, "direct");
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/after-control",
      undefined,
      expect.any(Headers),
    );
  });

  it("dispatches rewritten API routes after afterFiles filesystem misses", async () => {
    const serveFilesystemRoute = vi.fn(async () => false);
    const handleApi = vi.fn(async () => new Response("worker afterFiles api"));
    const result = await runPagesRequest(
      makeRequest("/sv/after-files/api/hello"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        matchPageRoute: vi.fn().mockReturnValue(null),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [
            {
              source: "/:locale/after-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
          fallback: [],
        },
        serveFilesystemRoute,
        handleApi,
      }),
    );

    expect(result.type).toBe("response");
    expect(handleApi).toHaveBeenCalledWith(expect.any(Request), "/api/hello", null);
  });

  it("applies afterFiles rewrite when page match is dynamic", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/dynamic-route");
    const result = await runPagesRequest(
      req,
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue({ route: { isDynamic: true } }),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [{ source: "/dynamic-route", destination: "/rewritten" }],
          fallback: [],
        },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/rewritten",
      undefined,
      expect.any(Headers),
    );
  });

  it("skips afterFiles rewrite when static page match exists", async () => {
    const renderPage = makeRenderPage(200);
    const req = makeRequest("/static-page");
    const result = await runPagesRequest(
      req,
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue({ route: { isDynamic: false } }),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [{ source: "/static-page", destination: "/rewritten" }],
          fallback: [],
        },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    // Should be called with original URL, not rewritten
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/static-page",
      undefined,
      expect.any(Headers),
    );
  });

  it("continues afterFiles rewrites until a Pages destination resolves", async () => {
    const renderPage = makeRenderPage(200);
    const matchPageRoute = vi.fn((pathname: string) =>
      pathname === "/resolved" ? { route: { isDynamic: false } } : null,
    );
    await runPagesRequest(
      makeRequest("/from"),
      baseDeps({
        configRewrites: {
          beforeFiles: [],
          afterFiles: [
            { source: "/from", destination: "/missing" },
            { source: "/missing", destination: "/resolved#section" },
          ],
          fallback: [],
        },
        matchPageRoute,
        renderPage,
      }),
    );

    expect(matchPageRoute).toHaveBeenCalledWith("/resolved", expect.any(Request));
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/resolved#section",
      undefined,
      expect.any(Headers),
    );
  });
});

// 14. Render intent when renderPage absent → {type:"render"}
describe("render intent", () => {
  it("emits render intent when renderPage absent", async () => {
    const req = makeRequest("/page");
    const result = await runPagesRequest(req, baseDeps());
    expect(result.type).toBe("render");
    if (result.type !== "render") return;
    expect(result.resolvedUrl).toBe("/page");
    expect(result.isDataReq).toBe(false);
  });

  it("emits render intent with isDataReq when isDataReq is true", async () => {
    const req = makeRequest("/page");
    const result = await runPagesRequest(req, baseDeps({ isDataReq: true }));
    expect(result.type).toBe("render");
    if (result.type !== "render") return;
    expect(result.isDataReq).toBe(true);
    expect(result.renderOptions).toEqual({ isDataReq: true });
  });

  it("stages the final dev fallback rewrite target for data navigation", async () => {
    const result = await runPagesRequest(
      makeRequest("/to-blog/post"),
      baseDeps({
        isDataReq: true,
        matchPageRoute: () => null,
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [
            {
              source: "/to-blog/:slug",
              destination: "/fallback-true-blog/:slug",
            },
          ],
        },
      }),
    );

    expect(result.type).toBe("render");
    if (result.type !== "render") return;
    expect(result.resolvedUrl).toBe("/fallback-true-blog/post");
    expect(result.stagedHeaders["x-nextjs-rewrite"]).toBe("/fallback-true-blog/post");
  });
});

// 15. {type:"response"} from renderPage (happy path)
describe("render via renderPage callback", () => {
  it("returns response from renderPage", async () => {
    const renderPage = makeRenderPage(200, "Hello World");
    const req = makeRequest("/about");
    const result = await runPagesRequest(req, baseDeps({ renderPage }));
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
  });
});

// 16. shouldDeferErrorPageOnMiss: 404 → fallback rewrite → re-render
describe("fallback rewrites on 404", () => {
  it("re-enters Worker assets after fallback rewrites", async () => {
    const serveFilesystemRoute = vi.fn(async (pathname: string, _headers, phase) =>
      pathname === "/file.txt" && phase === "fallback"
        ? new Response("worker fallback asset")
        : false,
    );
    const result = await runPagesRequest(
      makeRequest("/sv/fallback-files/file.txt"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        matchPageRoute: vi.fn().mockReturnValue(null),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [
            {
              source: "/:locale/fallback-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
        },
        serveFilesystemRoute,
        renderPage: makeRenderPage(404),
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    await expect(result.response.text()).resolves.toBe("worker fallback asset");
    expect(serveFilesystemRoute).toHaveBeenLastCalledWith("/file.txt", {}, "fallback");
  });

  it("dispatches rewritten API routes after fallback filesystem misses", async () => {
    const serveFilesystemRoute = vi.fn(async () => false);
    const handleApi = vi.fn(async () => new Response("worker fallback api"));
    const result = await runPagesRequest(
      makeRequest("/sv/fallback-files/api/hello"),
      baseDeps({
        i18nConfig: { locales: ["en", "sv", "nl"], defaultLocale: "en" },
        matchPageRoute: vi.fn().mockReturnValue(null),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [
            {
              source: "/:locale/fallback-files/:path*",
              destination: "/:path*",
              locale: false,
            },
          ],
        },
        serveFilesystemRoute,
        handleApi,
        renderPage: makeRenderPage(404),
      }),
    );

    expect(result.type).toBe("response");
    expect(handleApi).toHaveBeenCalledWith(expect.any(Request), "/api/hello", null);
  });

  it("uses fallback rewrite when page misses and renders 404", async () => {
    let callCount = 0;
    const renderPage = vi.fn(async (_req: Request, url: string) => {
      callCount++;
      if (callCount === 1) {
        // First call: return 404
        return new Response("not found", { status: 404 });
      }
      // After fallback rewrite: return 200
      return new Response(`ok ${url}`, { status: 200 });
    });

    const req = makeRequest("/missing-page");
    const result = await runPagesRequest(
      req,
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue(null), // no page match
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [{ source: "/missing-page", destination: "/fallback" }],
        },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    expect(callCount).toBe(2);
  });

  it("continues fallback rewrites after an unresolved destination", async () => {
    const renderPage = vi.fn(
      async (_req: Request, resolvedUrl: string) =>
        new Response(resolvedUrl, { status: resolvedUrl.startsWith("/resolved") ? 200 : 404 }),
    );
    const result = await runPagesRequest(
      makeRequest("/from"),
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue(null),
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [
            { source: "/from", destination: "/missing" },
            { source: "/missing", destination: "/resolved#section" },
          ],
        },
        renderPage,
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(200);
    expect(renderPage).toHaveBeenLastCalledWith(
      expect.any(Request),
      "/resolved#section",
      undefined,
      expect.any(Headers),
    );
  });
});

// 17. shouldDeferErrorPageOnMiss: 404 → no fallback → deferred error page re-render
describe("deferred error page re-render on 404", () => {
  it("re-renders without renderErrorPageOnMiss when no fallback matches", async () => {
    let callCount = 0;
    const renderPage = vi.fn(async (_req: Request, _url: string, opts?: PagesRenderOptions) => {
      callCount++;
      if (opts?.renderErrorPageOnMiss === false) {
        // Initial deferred call: return 404
        return new Response("not found", { status: 404 });
      }
      // Second call: render actual error page
      return new Response("error page", { status: 404 });
    });

    const req = makeRequest("/missing-page");
    const result = await runPagesRequest(
      req,
      baseDeps({
        matchPageRoute: vi.fn().mockReturnValue(null), // no page match
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(result.response.status).toBe(404);
    expect(callCount).toBe(2);
    // Second call should NOT have renderErrorPageOnMiss: false
    expect(renderPage.mock.calls[1][2]).toBeUndefined();
  });

  it("does not defer or run fallback rewrites for a data request (x-nextjs-data, worker path)", async () => {
    // Worker scenario: it never normalizes /_next/data paths (isDataReq stays
    // false) but flags the request via the x-nextjs-data header (isDataRequest).
    // A data-request miss must render once directly, not defer + run fallback.
    const renderPage = makeRenderPage(404, "not found");
    const fallback = [{ source: "/missing-page", destination: "/fallback" }];
    const req = makeRequest("/missing-page");
    const result = await runPagesRequest(
      req,
      baseDeps({
        isDataReq: false,
        isDataRequest: true,
        matchPageRoute: vi.fn().mockReturnValue(null), // no page match
        configRewrites: { beforeFiles: [], afterFiles: [], fallback },
        renderPage,
      }),
    );
    expect(result.type).toBe("response");
    // Rendered exactly once with no defer option, and the fallback never fired.
    expect(renderPage).toHaveBeenCalledTimes(1);
    expect(renderPage.mock.calls[0][2]).toBeUndefined();
  });

  it("resolves fallback rewrites before rendering a production data request", async () => {
    const renderPage = vi.fn(
      async (_request: Request, url: string) => new Response(`data ${url}`, { status: 200 }),
    );
    const matchPageRoute = vi.fn((pathname: string) =>
      pathname === "/fallback-target" ? ({ route: { isDynamic: false } } as any) : null,
    );

    const result = await runPagesRequest(
      makeRequest("/missing-page"),
      baseDeps({
        isDataRequest: true,
        matchPageRoute,
        configRewrites: {
          beforeFiles: [],
          afterFiles: [],
          fallback: [{ source: "/missing-page", destination: "/fallback-target?from=fallback" }],
        },
        renderPage,
      }),
    );

    expect(result.type).toBe("response");
    if (result.type !== "response") return;
    expect(renderPage).toHaveBeenCalledOnce();
    expect(renderPage).toHaveBeenCalledWith(
      expect.any(Request),
      "/fallback-target?from=fallback",
      undefined,
      expect.any(Headers),
    );
    expect(result.response.headers.get("x-nextjs-rewrite")).toBe("/fallback-target?from=fallback");
  });
});

// 19. preserveCredentialHeaders: isExternalUrl(resolvedUrl) → passed to applyMiddlewareRequestHeaders
describe("preserveCredentialHeaders", () => {
  it("preserves credential headers when resolvedUrl is external", async () => {
    // When middleware rewrites to an external URL, the Authorization header
    // should be forwarded. We verify by ensuring the pipeline reaches external proxy.
    const mockFetch = vi
      .spyOn(globalThis, "fetch")
      .mockResolvedValueOnce(new Response("proxied", { status: 200 }));

    const req = makeRequest("/internal", { Authorization: "Bearer token" });
    const result = await runPagesRequest(
      req,
      baseDeps({
        runMiddleware: makeMiddleware({
          continue: true,
          rewriteUrl: "https://external.com/api",
          responseHeaders: [["x-middleware-request-authorization", "Bearer token"]],
        }),
      }),
    );
    expect(result.type).toBe("response");
    // Verify fetch was called (external proxy triggered)
    expect(mockFetch).toHaveBeenCalled();
    mockFetch.mockRestore();
  });
});

// 20. wrapMiddlewareWithBasePath: shared adapter helper that re-adds the basePath
// to the middleware request URL. Used by prod-server.ts and the generated worker
// entry (deploy.ts) so middleware sees the original (pre-stripping) URL.
describe("wrapMiddlewareWithBasePath", () => {
  it("passes through unchanged when hadBasePath is false (out-of-basePath request)", async () => {
    const runMiddleware = makeMiddleware({ continue: true });
    const wrapped = wrapMiddlewareWithBasePath(runMiddleware, "/root", false);
    // Out-of-basePath requests stay bare so middleware sees nextUrl.basePath === "" (#1830).
    expect(wrapped).toBe(runMiddleware);
    await wrapped(makeRequest("/dashboard"), null, { isDataRequest: false });
    expect(runMiddleware.mock.calls[0][0].url).toBe("http://localhost/dashboard");
  });

  it("passes through unchanged when basePath is empty", () => {
    const runMiddleware = makeMiddleware({ continue: true });
    expect(wrapMiddlewareWithBasePath(runMiddleware, "", true)).toBe(runMiddleware);
  });

  it("re-adds the basePath to the request URL when hadBasePath is true", async () => {
    const runMiddleware = makeMiddleware({ continue: true });
    const wrapped = wrapMiddlewareWithBasePath(runMiddleware, "/root", true);
    expect(wrapped).not.toBe(runMiddleware);
    const result = await wrapped(makeRequest("/dashboard?q=1", { "x-test": "kept" }), "ctx", {
      isDataRequest: true,
    });
    expect(result.continue).toBe(true);
    const [mwReq, ctx, opts] = runMiddleware.mock.calls[0];
    expect(mwReq.url).toBe("http://localhost/root/dashboard?q=1");
    // Headers, ctx, and opts are forwarded untouched.
    expect(mwReq.headers.get("x-test")).toBe("kept");
    expect(ctx).toBe("ctx");
    expect(opts).toEqual({ isDataRequest: true });
  });

  it("does not double-add an already-present basePath (addBasePathToPathname is idempotent)", async () => {
    const runMiddleware = makeMiddleware({ continue: true });
    const wrapped = wrapMiddlewareWithBasePath(runMiddleware, "/root", true);
    await wrapped(makeRequest("/root/dashboard"), null, { isDataRequest: false });
    expect(runMiddleware.mock.calls[0][0].url).toBe("http://localhost/root/dashboard");
  });
});
