/**
 * Unit tests for fetch cache shim.
 *
 * Tests the patched fetch() with Next.js caching semantics:
 * - next.revalidate for TTL-based caching
 * - next.tags for tag-based invalidation
 * - cache: 'no-store' and cache: 'force-cache'
 * - Stale-while-revalidate behavior
 * - next property stripping
 * - Independent cache entries per URL
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

// We need to mock fetch at the module level BEFORE fetch-cache.ts captures
// `originalFetch`. Use vi.stubGlobal to intercept at import time.
let requestCount = 0;
const defaultFetchMockImplementation = async (
  input: string | URL | Request,
  _init?: RequestInit,
) => {
  requestCount++;
  const url =
    typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
  return new Response(JSON.stringify({ url, count: requestCount }), {
    status: 200,
    headers: { "content-type": "application/json" },
  });
};
const fetchMock = vi.fn(defaultFetchMockImplementation);

// Stub globalThis.fetch BEFORE importing modules that capture it
vi.stubGlobal("fetch", fetchMock);

// Now import — these will capture fetchMock as "originalFetch"
const {
  withFetchCache,
  runWithFetchCache,
  getCollectedFetchTags,
  getOriginalFetch,
  _resetPendingRefetches,
} = await import("../packages/vinext/src/shims/fetch-cache.js");
const { getCacheHandler, revalidateTag, MemoryCacheHandler, setCacheHandler } =
  await import("../packages/vinext/src/shims/cache.js");
const { runWithExecutionContext } = await import("../packages/vinext/src/shims/request-context.js");
const { createRequestContext, runWithRequestContext } =
  await import("../packages/vinext/src/shims/unified-request-context.js");

describe("fetch cache shim", () => {
  let cleanup: (() => void) | null = null;

  beforeEach(() => {
    // Reset state
    requestCount = 0;
    fetchMock.mockReset();
    fetchMock.mockImplementation(defaultFetchMockImplementation);
    // Reset the cache handler to a fresh instance for each test
    setCacheHandler(new MemoryCacheHandler());
    // Clear in-flight refetch dedup state
    _resetPendingRefetches();
    // Install the patched fetch
    cleanup = withFetchCache();
  });

  afterEach(() => {
    cleanup?.();
    cleanup = null;
  });

  // ── Basic caching with next.revalidate ──────────────────────────────

  it("caches fetch with next.revalidate and returns cached on second call", async () => {
    const res1 = await fetch("https://api.example.com/data", {
      next: { revalidate: 60 },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    // Second call should return cached data (no new network request)
    const res2 = await fetch("https://api.example.com/data", {
      next: { revalidate: 60 },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1); // Same count = cached
    expect(fetchMock).toHaveBeenCalledTimes(1); // Only one real fetch
  });

  it("cache: 'force-cache' caches indefinitely", async () => {
    const res1 = await fetch("https://api.example.com/force", {
      cache: "force-cache",
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/force", {
      cache: "force-cache",
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1); // Cached
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  // ── No caching (no-store, revalidate: 0, revalidate: false) ─────────

  it("cache: 'no-store' bypasses cache entirely", async () => {
    const res1 = await fetch("https://api.example.com/nostore", {
      cache: "no-store",
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/nostore", {
      cache: "no-store",
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Fresh fetch each time
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("next.revalidate: 0 skips caching", async () => {
    const res1 = await fetch("https://api.example.com/rev0", {
      next: { revalidate: 0 },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/rev0", {
      next: { revalidate: 0 },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Not cached
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("next.revalidate: false skips caching", async () => {
    const res1 = await fetch("https://api.example.com/revfalse", {
      next: { revalidate: false },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/revfalse", {
      next: { revalidate: false },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Not cached
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("no cache or next options passes through without caching", async () => {
    const res1 = await fetch("https://api.example.com/passthrough");
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/passthrough");
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Pass-through, no caching
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── Tag-based invalidation ──────────────────────────────────────────

  it("next.tags caches and revalidateTag invalidates", async () => {
    const res1 = await fetch("https://api.example.com/posts", {
      next: { tags: ["posts"] },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    // Cached
    const res2 = await fetch("https://api.example.com/posts", {
      next: { tags: ["posts"] },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Invalidate via tag
    await revalidateTag("posts");

    // Should re-fetch after tag invalidation
    const res3 = await fetch("https://api.example.com/posts", {
      next: { tags: ["posts"] },
    });
    const data3 = await res3.json();
    expect(data3.count).toBe(2); // Fresh fetch
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("revalidateTag only invalidates matching tags", async () => {
    // Cache two different tagged fetches
    await fetch("https://api.example.com/posts-tag", {
      next: { tags: ["posts"] },
    });
    await fetch("https://api.example.com/users-tag", {
      next: { tags: ["users"] },
    });
    expect(fetchMock).toHaveBeenCalledTimes(2);

    // Invalidate only "posts"
    await revalidateTag("posts");

    // Posts should re-fetch
    const postRes = await fetch("https://api.example.com/posts-tag", {
      next: { tags: ["posts"] },
    });
    const postData = await postRes.json();
    expect(postData.count).toBe(3); // Fresh fetch (count continues from 2)

    // Users should still be cached
    const userRes = await fetch("https://api.example.com/users-tag", {
      next: { tags: ["users"] },
    });
    const userData = await userRes.json();
    expect(userData.count).toBe(2); // Still the cached version
    expect(fetchMock).toHaveBeenCalledTimes(3); // Only posts re-fetched
  });

  // ── TTL expiry (stale-while-revalidate) ─────────────────────────────

  it("returns stale data after TTL expires and triggers background refetch", async () => {
    const res1 = await fetch("https://api.example.com/stale-test", {
      next: { revalidate: 1 },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    // Manually expire the cache entry (key is a SHA-256 hash, find it dynamically)
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000; // Expired 1 second ago
    }

    // Should return stale data immediately
    const res2 = await fetch("https://api.example.com/stale-test", {
      next: { revalidate: 1 },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1); // Stale data (same as first fetch)

    // Wait for background refetch
    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(2); // Original + background refetch
  });

  it("preserves Request bodies for stale background revalidation", async () => {
    const seenBodies: string[] = [];
    fetchMock.mockImplementation(async (input: string | URL | Request, _init?: RequestInit) => {
      requestCount++;
      const url =
        typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
      const body = input instanceof Request ? await input.clone().text() : "";
      seenBodies.push(body);
      return new Response(JSON.stringify({ url, count: requestCount, body }), {
        status: 200,
        headers: { "content-type": "application/json" },
      });
    });

    const makeRequest = () =>
      new Request("https://api.example.com/stale-request-body", {
        method: "POST",
        body: "request-body-content",
        headers: { "content-type": "text/plain" },
      });

    const res1 = await fetch(makeRequest(), { next: { revalidate: 1 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);
    expect(data1.body).toBe("request-body-content");

    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    const res2 = await fetch(makeRequest(), { next: { revalidate: 1 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(data2.body).toBe("request-body-content");

    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(seenBodies).toEqual(["request-body-content", "request-body-content"]);
  });

  it("registers stale background refetch with waitUntil when ExecutionContext is available", async () => {
    const waitUntilSpy = vi.fn<(p: Promise<unknown>) => void>();
    const mockCtx = { waitUntil: waitUntilSpy };

    await runWithExecutionContext(mockCtx, async () => {
      // Populate cache
      const res1 = await fetch("https://api.example.com/waituntil-test", {
        next: { revalidate: 1 },
      });
      expect((await res1.json()).count).toBe(1);

      // Manually expire the entry
      const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
      const store = (handler as any).store as Map<string, any>;
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }

      // Trigger stale hit — should fire background refetch via waitUntil
      const res2 = await fetch("https://api.example.com/waituntil-test", {
        next: { revalidate: 1 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Stale data returned

      expect(waitUntilSpy).toHaveBeenCalledTimes(1);
      expect(waitUntilSpy.mock.calls[0]![0]).toBeInstanceOf(Promise);

      // Wait for the refetch to complete
      await waitUntilSpy.mock.calls[0]![0];
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });

  it("registers stale background refetch with waitUntil inside a unified request scope", async () => {
    const waitUntilSpy = vi.fn<(p: Promise<unknown>) => void>();
    const mockCtx = { waitUntil: waitUntilSpy };

    await runWithExecutionContext(mockCtx, async () => {
      await runWithRequestContext(createRequestContext(), async () => {
        const res1 = await fetch("https://api.example.com/unified-waituntil-test", {
          next: { revalidate: 1 },
        });
        expect((await res1.json()).count).toBe(1);

        const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
        const store = (handler as any).store as Map<string, any>;
        for (const [, entry] of store) {
          entry.revalidateAt = Date.now() - 1000;
        }

        const res2 = await fetch("https://api.example.com/unified-waituntil-test", {
          next: { revalidate: 1 },
        });
        expect((await res2.json()).count).toBe(1);

        expect(waitUntilSpy).toHaveBeenCalledTimes(1);
        expect(waitUntilSpy.mock.calls[0]![0]).toBeInstanceOf(Promise);
        await waitUntilSpy.mock.calls[0]![0];
        expect(fetchMock).toHaveBeenCalledTimes(2);
      });
    });
  });

  it("deduplicates concurrent stale background refetches for the same cache key", async () => {
    // Use a deferred promise to control when the background refetch resolves,
    // ensuring all concurrent stale hits see stale data before the refetch completes.
    let resolveRefetch!: () => void;
    const refetchGate = new Promise<void>((r) => {
      resolveRefetch = r;
    });

    // Populate cache (first call resolves normally)
    const res1 = await fetch("https://api.example.com/dedup-stale", {
      next: { revalidate: 1 },
    });
    expect((await res1.json()).count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Subsequent calls wait on the gate before resolving
    fetchMock.mockImplementation(async (input: string | URL | Request) => {
      await refetchGate;
      requestCount++;
      const url =
        typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
      return new Response(JSON.stringify({ url, count: requestCount }), {
        status: 200,
        headers: { "content-type": "application/json" },
      });
    });

    // Expire the entry
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Fire 5 concurrent stale hits — should all return stale data
    // but only trigger ONE background refetch
    const results = await Promise.all(
      Array.from({ length: 5 }, () =>
        fetch("https://api.example.com/dedup-stale", {
          next: { revalidate: 1 },
        }),
      ),
    );

    // All 5 should return the stale data
    for (const res of results) {
      const data = await res.json();
      expect(data.count).toBe(1);
    }

    // Let the background refetch complete
    resolveRefetch();
    await new Promise((resolve) => setTimeout(resolve, 50));

    // Original fetch (1) + exactly one background refetch (1) = 2 total
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("allows a new background refetch after the previous one completes", async () => {
    // Populate cache
    await fetch("https://api.example.com/dedup-cycle", {
      next: { revalidate: 1 },
    });
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Expire the entry
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // First stale hit — triggers background refetch
    await fetch("https://api.example.com/dedup-cycle", {
      next: { revalidate: 1 },
    });
    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(2);

    // Expire again
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Second stale hit — should trigger a NEW background refetch
    // (the previous one completed and cleaned up)
    await fetch("https://api.example.com/dedup-cycle", {
      next: { revalidate: 1 },
    });
    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(3);
  });

  it("cleans up dedup entry when background refetch fails, allowing retry", async () => {
    // Populate cache
    await fetch("https://api.example.com/dedup-error", {
      next: { revalidate: 1 },
    });
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Make subsequent fetches reject
    fetchMock.mockImplementation(async () => {
      throw new Error("network down");
    });

    // Expire the entry
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Stale hit — background refetch will fail
    const res = await fetch("https://api.example.com/dedup-error", {
      next: { revalidate: 1 },
    });
    expect((await res.json()).count).toBe(1); // Still returns stale data
    await new Promise((resolve) => setTimeout(resolve, 50));

    // The failed refetch should have been called and cleaned up
    expect(fetchMock).toHaveBeenCalledTimes(2);

    // Restore working fetch and expire again
    fetchMock.mockImplementation(defaultFetchMockImplementation);
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // A new stale hit should trigger a fresh refetch (dedup entry was cleaned up)
    await fetch("https://api.example.com/dedup-error", {
      next: { revalidate: 1 },
    });
    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(3);
  });

  it("background revalidation does not cache error responses", async () => {
    // Populate cache with a good response
    const res1 = await fetch("https://api.example.com/revalidate-error-test", {
      next: { revalidate: 1 },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);
    expect(res1.status).toBe(200);

    // Manually expire the cache entry
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Make the upstream return a 500 error for the background refetch
    fetchMock.mockImplementationOnce(async () => {
      return new Response("Internal Server Error", {
        status: 500,
        headers: { "content-type": "text/plain" },
      });
    });

    // Should return stale data immediately (stale-while-revalidate)
    const res2 = await fetch("https://api.example.com/revalidate-error-test", {
      next: { revalidate: 1 },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1); // Stale data returned
    expect(res2.status).toBe(200);

    // Wait for background refetch to complete
    await new Promise((resolve) => setTimeout(resolve, 50));

    // The background refetch got a 500, so the cache should still hold the
    // original good response — not the error.
    // Expire the entry again to force another stale read from cache.
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Restore good fetch for next background refetch
    fetchMock.mockImplementation(defaultFetchMockImplementation);

    const res3 = await fetch("https://api.example.com/revalidate-error-test", {
      next: { revalidate: 1 },
    });
    // If the bug exists, this will be 500 (the error was cached).
    // If fixed, this will be 200 (the original good data was preserved).
    expect(res3.status).toBe(200);
  });

  it("force-cleans dedup entry after timeout when upstream fetch hangs", async () => {
    vi.useFakeTimers();
    try {
      // Populate cache
      await fetch("https://api.example.com/dedup-hang", {
        next: { revalidate: 1 },
      });
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Make subsequent fetches hang forever
      fetchMock.mockImplementation(() => new Promise(() => {}));

      // Expire the entry
      const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
      const store = (handler as any).store as Map<string, any>;
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }

      // Stale hit — background refetch hangs
      await fetch("https://api.example.com/dedup-hang", {
        next: { revalidate: 1 },
      });
      expect(fetchMock).toHaveBeenCalledTimes(2); // Hung fetch was called

      // Another stale hit before timeout — dedup suppresses it
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }
      await fetch("https://api.example.com/dedup-hang", {
        next: { revalidate: 1 },
      });
      expect(fetchMock).toHaveBeenCalledTimes(2); // Still suppressed

      // Advance past the 60s timeout — dedup entry should be force-cleaned
      vi.advanceTimersByTime(60_000);

      // Restore working fetch and expire again
      fetchMock.mockImplementation(defaultFetchMockImplementation);
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }

      // New stale hit should trigger a fresh refetch
      await fetch("https://api.example.com/dedup-hang", {
        next: { revalidate: 1 },
      });

      // Flush the microtask for the background refetch
      await vi.advanceTimersByTimeAsync(50);
      expect(fetchMock).toHaveBeenCalledTimes(3); // New refetch succeeded
    } finally {
      vi.useRealTimers();
    }
  });

  it("hung fetch settling after timeout does not evict replacement refetch", async () => {
    vi.useFakeTimers();
    try {
      let resolveHungFetch!: (resp: Response) => void;

      // Populate cache
      await fetch("https://api.example.com/dedup-race", {
        next: { revalidate: 1 },
      });
      expect(fetchMock).toHaveBeenCalledTimes(1);

      // Make next fetch hang until we resolve it manually
      fetchMock.mockImplementation(
        () =>
          new Promise<Response>((resolve) => {
            resolveHungFetch = resolve;
          }),
      );

      // Expire and trigger a stale hit — background refetch #1 hangs
      const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
      const store = (handler as any).store as Map<string, any>;
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }
      await fetch("https://api.example.com/dedup-race", {
        next: { revalidate: 1 },
      });
      expect(fetchMock).toHaveBeenCalledTimes(2); // Hung fetch was called

      // Advance past the 60s timeout — dedup entry is force-cleaned
      vi.advanceTimersByTime(60_000);

      // Restore working fetch for the replacement refetch
      fetchMock.mockImplementation(defaultFetchMockImplementation);

      // Expire and trigger a new stale hit — background refetch #2 starts
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }
      await fetch("https://api.example.com/dedup-race", {
        next: { revalidate: 1 },
      });

      // Let refetch #2 complete
      await vi.advanceTimersByTimeAsync(50);
      expect(fetchMock).toHaveBeenCalledTimes(3); // Replacement refetch ran

      // Now the hung refetch #1 finally settles — it must NOT evict #2's slot
      resolveHungFetch(
        new Response(JSON.stringify({ url: "stale", count: 999 }), {
          status: 200,
          headers: { "content-type": "application/json" },
        }),
      );
      await vi.advanceTimersByTimeAsync(50);

      // Expire again — a new stale hit should NOT start another refetch
      // because #2's slot should still be gone (it completed normally).
      // The key behavior: #1 settling did not delete #2's entry while #2 was live.
      // Since #2 already completed and cleaned up its own slot, a new refetch
      // should start normally (proving #1 didn't corrupt state).
      for (const [, entry] of store) {
        entry.revalidateAt = Date.now() - 1000;
      }
      await fetch("https://api.example.com/dedup-race", {
        next: { revalidate: 1 },
      });
      await vi.advanceTimersByTimeAsync(50);
      expect(fetchMock).toHaveBeenCalledTimes(4); // Clean new refetch
    } finally {
      vi.useRealTimers();
    }
  });

  // ── Independent cache entries per URL ───────────────────────────────

  it("different URLs get independent cache entries", async () => {
    const res1 = await fetch("https://api.example.com/url-a", {
      next: { revalidate: 60 },
    });
    const data1 = await res1.json();
    expect(data1.url).toBe("https://api.example.com/url-a");
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/url-b", {
      next: { revalidate: 60 },
    });
    const data2 = await res2.json();
    expect(data2.url).toBe("https://api.example.com/url-b");
    expect(data2.count).toBe(2); // Different URL = different cache

    // Re-fetch url-a should be cached
    const res3 = await fetch("https://api.example.com/url-a", {
      next: { revalidate: 60 },
    });
    const data3 = await res3.json();
    expect(data3.count).toBe(1); // Cached
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("same URL with different methods get separate cache entries", async () => {
    const getRes = await fetch("https://api.example.com/method-test", {
      method: "GET",
      next: { revalidate: 60 },
    });
    const getData = await getRes.json();
    expect(getData.count).toBe(1);

    const postRes = await fetch("https://api.example.com/method-test", {
      method: "POST",
      body: "test",
      next: { revalidate: 60 },
    });
    const postData = await postRes.json();
    expect(postData.count).toBe(2); // Different method = different cache

    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── next property stripping ─────────────────────────────────────────

  it("strips next property before passing to real fetch", async () => {
    await fetch("https://api.example.com/strip-test", {
      next: { revalidate: 60, tags: ["test"] },
      headers: { "X-Custom": "value" },
    });

    // Verify the mock was called with init that does NOT have `next`
    const call = fetchMock.mock.calls[0];
    const init = call[1] as RequestInit;
    expect(init).toBeDefined();
    expect((init as any).next).toBeUndefined();
    expect((init as any).headers).toEqual({ "X-Custom": "value" });
  });

  it("strips next property for no-store fetches too", async () => {
    await fetch("https://api.example.com/strip-nostore", {
      cache: "no-store",
      next: { tags: ["test"] },
    });

    const call = fetchMock.mock.calls[0];
    const init = call[1] as RequestInit;
    expect((init as any).next).toBeUndefined();
  });

  // ── Tag collection during rendering ─────────────────────────────────

  it("collects tags during render pass via getCollectedFetchTags", async () => {
    await fetch("https://api.example.com/tag-collect-a", {
      next: { tags: ["posts", "list"] },
    });
    await fetch("https://api.example.com/tag-collect-b", {
      next: { tags: ["users"] },
    });

    const tags = getCollectedFetchTags();
    expect(tags).toContain("posts");
    expect(tags).toContain("list");
    expect(tags).toContain("users");
    expect(tags).toHaveLength(3);
  });

  it("does not collect duplicate tags", async () => {
    await fetch("https://api.example.com/dup-tag-a", {
      next: { tags: ["data"] },
    });
    await fetch("https://api.example.com/dup-tag-b", {
      next: { tags: ["data"] },
    });

    const tags = getCollectedFetchTags();
    expect(tags.filter((t) => t === "data")).toHaveLength(1);
  });

  // ── Only caches successful responses ────────────────────────────────

  it("does not cache non-2xx responses", async () => {
    // Override mock to return 404 once
    fetchMock.mockImplementationOnce(async () => {
      requestCount++;
      return new Response("Not found", { status: 404 });
    });

    const res1 = await fetch("https://api.example.com/missing-page", {
      next: { revalidate: 60 },
    });
    expect(res1.status).toBe(404);

    // Should re-fetch since 404 wasn't cached
    const res2 = await fetch("https://api.example.com/missing-page", {
      next: { revalidate: 60 },
    });
    expect(res2.status).toBe(200);
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── URL and Request object inputs ───────────────────────────────────

  it("handles URL objects as input", async () => {
    const url = new URL("https://api.example.com/url-obj");
    const res = await fetch(url, { next: { revalidate: 60 } });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.count).toBe(1);

    // Cached on second call
    const res2 = await fetch(url, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("handles Request objects as input", async () => {
    const req = new Request("https://api.example.com/req-obj");
    const res = await fetch(req, { next: { revalidate: 60 } });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.count).toBe(1);

    // Cached on second call with same URL
    const req2 = new Request("https://api.example.com/req-obj");
    const res2 = await fetch(req2, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("includes Request object bodies in the cache key", async () => {
    const req1 = new Request("https://api.example.com/req-body", {
      method: "POST",
      body: "alpha",
      headers: { "content-type": "text/plain" },
    });
    const res1 = await fetch(req1, { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const req2 = new Request("https://api.example.com/req-body", {
      method: "POST",
      body: "bravo",
      headers: { "content-type": "text/plain" },
    });
    const res2 = await fetch(req2, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Different Request body = different cache
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("same Request object bodies hit the same cache entry", async () => {
    const req1 = new Request("https://api.example.com/req-body-same", {
      method: "POST",
      body: "same-body",
      headers: { "content-type": "text/plain" },
    });
    const res1 = await fetch(req1, { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const req2 = new Request("https://api.example.com/req-body-same", {
      method: "POST",
      body: "same-body",
      headers: { "content-type": "text/plain" },
    });
    const res2 = await fetch(req2, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1); // Same Request body = cached
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("Request FormData values with commas do not collide in the cache key", async () => {
    const formA = new FormData();
    formA.append("name", "a,b");
    formA.append("name", "c");

    const req1 = new Request("https://api.example.com/req-form-body", {
      method: "POST",
      body: formA,
    });
    const res1 = await fetch(req1, { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const formB = new FormData();
    formB.append("name", "a");
    formB.append("name", "b,c");

    const req2 = new Request("https://api.example.com/req-form-body", {
      method: "POST",
      body: formB,
    });
    const res2 = await fetch(req2, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Different Request FormData body = different cache
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("same Request FormData bodies hit the same cache entry despite generated multipart boundaries", async () => {
    const makeForm = () => {
      const form = new FormData();
      form.append("name", "same-value");
      return form;
    };

    const req1 = new Request("https://api.example.com/req-form-same", {
      method: "POST",
      body: makeForm(),
    });
    const res1 = await fetch(req1, { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const req2 = new Request("https://api.example.com/req-form-same", {
      method: "POST",
      body: makeForm(),
    });
    const res2 = await fetch(req2, { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("same multipart Request bodies hit the same cache entry even with different boundaries", async () => {
    const makeMultipartRequest = (boundary: string) =>
      new Request("https://api.example.com/req-form-boundary", {
        method: "POST",
        headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
        body: [
          `--${boundary}`,
          'Content-Disposition: form-data; name="name"',
          "",
          "same-value",
          `--${boundary}--`,
          "",
        ].join("\r\n"),
      });

    const res1 = await fetch(makeMultipartRequest("boundary-a"), { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch(makeMultipartRequest("boundary-b"), { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("malformed multipart Request bodies bypass cache instead of hashing raw bytes", async () => {
    const makeMalformedMultipartRequest = () =>
      new Request("https://api.example.com/req-form-malformed", {
        method: "POST",
        headers: { "content-type": "multipart/form-data; boundary=expected" },
        body: [
          "--actual",
          'Content-Disposition: form-data; name="name"',
          "",
          "value",
          "--actual--",
          "",
        ].join("\r\n"),
      });

    const res1 = await fetch(makeMalformedMultipartRequest(), { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch(makeMalformedMultipartRequest(), { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(2);
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("urlencoded Request bodies with different charset headers get separate cache entries", async () => {
    const makeRequest = (charset: string) =>
      new Request("https://api.example.com/req-form-charset", {
        method: "POST",
        headers: { "content-type": `application/x-www-form-urlencoded; charset=${charset}` },
        body: "name=value",
      });

    const res1 = await fetch(makeRequest("utf-8"), { next: { revalidate: 60 } });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch(makeRequest("shift_jis"), { next: { revalidate: 60 } });
    const data2 = await res2.json();
    expect(data2.count).toBe(2);
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── force-cache with next.revalidate ────────────────────────────────

  it("cache: 'force-cache' with next.revalidate uses the specified TTL", async () => {
    const res1 = await fetch("https://api.example.com/force-ttl", {
      cache: "force-cache",
      next: { revalidate: 1 },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    // Verify it's cached
    const res2 = await fetch("https://api.example.com/force-ttl", {
      cache: "force-cache",
      next: { revalidate: 1 },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(1);

    // Expire the cache manually (key is a SHA-256 hash, find it dynamically)
    const handler = getCacheHandler() as InstanceType<typeof MemoryCacheHandler>;
    const store = (handler as any).store as Map<string, any>;
    for (const [, entry] of store) {
      entry.revalidateAt = Date.now() - 1000;
    }

    // Should return stale
    const res3 = await fetch("https://api.example.com/force-ttl", {
      cache: "force-cache",
      next: { revalidate: 1 },
    });
    const data3 = await res3.json();
    expect(data3.count).toBe(1); // Stale data returned
    // Background refetch
    await new Promise((resolve) => setTimeout(resolve, 50));
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── Cleanup clears per-request state ─────────────────────────────────

  it("cleanup function clears collected tags", async () => {
    // Collect some tags
    await fetch("https://api.example.com/cleanup-test", {
      next: { tags: ["cleanup-tag"] },
    });
    expect(getCollectedFetchTags()).toContain("cleanup-tag");

    // Cleanup should reset tag state
    cleanup!();
    cleanup = null;
    expect(getCollectedFetchTags()).toHaveLength(0);

    // Re-install for afterEach cleanup
    cleanup = withFetchCache();
  });

  // ── getOriginalFetch ────────────────────────────────────────────────

  it("getOriginalFetch returns the module-level original fetch", () => {
    const orig = getOriginalFetch();
    expect(typeof orig).toBe("function");
    // It should be fetchMock since that was the global fetch when the module loaded
    expect(orig).toBe(fetchMock);
  });

  // ── next: {} empty passes through ───────────────────────────────────

  it("next: {} with no revalidate or tags passes through", async () => {
    const res1 = await fetch("https://api.example.com/empty-next", { next: {} });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/empty-next", { next: {} });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Not cached
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── Concurrent request isolation via ALS ─────────────────────────────

  it("concurrent runWithFetchCache calls have isolated tags", async () => {
    // Clean up the withFetchCache() from beforeEach — runWithFetchCache
    // manages its own ALS scope.
    cleanup?.();
    cleanup = null;

    const [tags1, tags2] = await Promise.all([
      runWithFetchCache(async () => {
        await fetch("https://api.example.com/concurrent-a", {
          next: { tags: ["request-1"] },
        });
        return getCollectedFetchTags();
      }),
      runWithFetchCache(async () => {
        await fetch("https://api.example.com/concurrent-b", {
          next: { tags: ["request-2"] },
        });
        return getCollectedFetchTags();
      }),
    ]);

    expect(tags1).toEqual(["request-1"]);
    expect(tags2).toEqual(["request-2"]);

    // Re-install for afterEach
    cleanup = withFetchCache();
  });

  // ── Auth header isolation in cache keys ─────────────────────────────

  describe("auth header cache isolation", () => {
    it("different Authorization headers produce separate cache entries", async () => {
      // Alice fetches with her token — explicitly opt into caching
      const res1 = await fetch("https://api.example.com/me", {
        headers: { Authorization: "Bearer alice-token" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // Bob fetches with his token — should NOT get Alice's cached response
      const res2 = await fetch("https://api.example.com/me", {
        headers: { Authorization: "Bearer bob-token" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different cache entry
      expect(fetchMock).toHaveBeenCalledTimes(2);

      // Alice fetches again — should get her cached response
      const res3 = await fetch("https://api.example.com/me", {
        headers: { Authorization: "Bearer alice-token" },
        next: { revalidate: 60 },
      });
      const data3 = await res3.json();
      expect(data3.count).toBe(1); // Cached from first request
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("different Cookie headers produce separate cache entries", async () => {
      const res1 = await fetch("https://api.example.com/profile", {
        headers: { Cookie: "session=alice" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // Bob's cookie should get a separate cache entry
      const res2 = await fetch("https://api.example.com/profile", {
        headers: { Cookie: "session=bob" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Fresh fetch, not Alice's data
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("requests without auth headers share cache (public data)", async () => {
      const res1 = await fetch("https://api.example.com/public", {
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // No auth headers → same cache entry
      const res2 = await fetch("https://api.example.com/public", {
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Cached
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("auth headers with force-cache still produce per-user cache entries", async () => {
      const res1 = await fetch("https://api.example.com/forced", {
        headers: { Authorization: "Bearer alice" },
        cache: "force-cache",
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/forced", {
        headers: { Authorization: "Bearer bob" },
        cache: "force-cache",
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Separate cache entry
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("auth headers with tags-only (no explicit revalidate) bypass cache", async () => {
      // When only tags are specified but no explicit revalidate or force-cache,
      // auth headers should cause a cache bypass
      const res1 = await fetch("https://api.example.com/tagged-auth", {
        headers: { Authorization: "Bearer alice" },
        next: { tags: ["user-data"] },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // Same user, same tags — should still bypass (no explicit cache opt-in)
      const res2 = await fetch("https://api.example.com/tagged-auth", {
        headers: { Authorization: "Bearer alice" },
        next: { tags: ["user-data"] },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Not cached — safety bypass
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("X-API-Key header is included in cache key", async () => {
      const res1 = await fetch("https://api.example.com/api-key", {
        headers: { "X-API-Key": "key-alice" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/api-key", {
        headers: { "X-API-Key": "key-bob" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different key = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("auth headers from Request object are included in cache key", async () => {
      const req1 = new Request("https://api.example.com/req-auth", {
        headers: { Authorization: "Bearer alice" },
      });
      const res1 = await fetch(req1, { next: { revalidate: 60 } });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const req2 = new Request("https://api.example.com/req-auth", {
        headers: { Authorization: "Bearer bob" },
      });
      const res2 = await fetch(req2, { next: { revalidate: 60 } });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different auth = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });

  // ── cache: 'no-cache' bypass ────────────────────────────────────────

  it("cache: 'no-cache' bypasses cache entirely", async () => {
    const res1 = await fetch("https://api.example.com/nocache", {
      cache: "no-cache" as RequestCache,
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/nocache", {
      cache: "no-cache" as RequestCache,
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Fresh fetch each time
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("cache: 'no-store' with auth headers bypasses cache", async () => {
    const res1 = await fetch("https://api.example.com/nostore-auth", {
      cache: "no-store",
      headers: { Authorization: "Bearer token" },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/nostore-auth", {
      cache: "no-store",
      headers: { Authorization: "Bearer token" },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Always fresh
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it("cache: 'no-cache' with auth headers bypasses cache", async () => {
    const res1 = await fetch("https://api.example.com/nocache-auth", {
      cache: "no-cache" as RequestCache,
      headers: { Cookie: "session=alice" },
    });
    const data1 = await res1.json();
    expect(data1.count).toBe(1);

    const res2 = await fetch("https://api.example.com/nocache-auth", {
      cache: "no-cache" as RequestCache,
      headers: { Cookie: "session=bob" },
    });
    const data2 = await res2.json();
    expect(data2.count).toBe(2); // Always fresh
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  // ── Cache key: body type handling ─────────────────────────────────

  describe("cache key body type handling", () => {
    it("different string bodies produce separate cache entries", async () => {
      const res1 = await fetch("https://api.example.com/body-str", {
        method: "POST",
        body: '{"type":"a"}',
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-str", {
        method: "POST",
        body: '{"type":"b"}',
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different body = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("same string bodies hit the same cache entry", async () => {
      const res1 = await fetch("https://api.example.com/body-same", {
        method: "POST",
        body: '{"query":"test"}',
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-same", {
        method: "POST",
        body: '{"query":"test"}',
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Same body = same cache
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("Uint8Array bodies are included in cache key", async () => {
      const bodyA = new TextEncoder().encode("payload-a");
      const bodyB = new TextEncoder().encode("payload-b");

      const res1 = await fetch("https://api.example.com/body-uint8", {
        method: "POST",
        body: bodyA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-uint8", {
        method: "POST",
        body: bodyB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different binary body = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("same Uint8Array bodies hit the same cache entry", async () => {
      const body1 = new TextEncoder().encode("same-payload");
      const body2 = new TextEncoder().encode("same-payload");

      const res1 = await fetch("https://api.example.com/body-uint8-same", {
        method: "POST",
        body: body1,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-uint8-same", {
        method: "POST",
        body: body2,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Same payload = same cache
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("Blob bodies are included in cache key", async () => {
      const blobA = new Blob(["blob-content-a"], { type: "text/plain" });
      const blobB = new Blob(["blob-content-b"], { type: "text/plain" });

      const res1 = await fetch("https://api.example.com/body-blob", {
        method: "POST",
        body: blobA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-blob", {
        method: "POST",
        body: blobB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different blob = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("FormData bodies are included in cache key", async () => {
      const formA = new FormData();
      formA.append("name", "alice");

      const formB = new FormData();
      formB.append("name", "bob");

      const res1 = await fetch("https://api.example.com/body-form", {
        method: "POST",
        body: formA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-form", {
        method: "POST",
        body: formB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different form data = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("FormData values with commas do not collide in the cache key", async () => {
      const formA = new FormData();
      formA.append("name", "a,b");
      formA.append("name", "c");

      const formB = new FormData();
      formB.append("name", "a");
      formB.append("name", "b,c");

      const res1 = await fetch("https://api.example.com/body-form-comma", {
        method: "POST",
        body: formA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-form-comma", {
        method: "POST",
        body: formB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different multi-value form data = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("FormData entry order is preserved in the cache key", async () => {
      const formA = new FormData();
      formA.append("a", "1");
      formA.append("b", "2");
      formA.append("a", "3");

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

      const res1 = await fetch("https://api.example.com/body-form-order", {
        method: "POST",
        body: formA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-form-order", {
        method: "POST",
        body: formB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2);
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("FormData file metadata is included in the cache key", async () => {
      const formA = new FormData();
      formA.append("file", new File(["same-bytes"], "a.txt", { type: "text/plain" }));

      const formB = new FormData();
      formB.append("file", new File(["same-bytes"], "b.bin", { type: "application/octet-stream" }));

      const res1 = await fetch("https://api.example.com/body-form-file-metadata", {
        method: "POST",
        body: formA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-form-file-metadata", {
        method: "POST",
        body: formB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2);
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("ReadableStream bodies are included in cache key", async () => {
      const streamA = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("stream-a"));
          controller.close();
        },
      });
      const streamB = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("stream-b"));
          controller.close();
        },
      });

      const res1 = await fetch("https://api.example.com/body-stream", {
        method: "POST",
        body: streamA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-stream", {
        method: "POST",
        body: streamB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different stream = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });

  // ── Cache key: header inclusion (all headers minus blocklist) ──────

  describe("cache key header inclusion", () => {
    it("different Accept headers produce separate cache entries", async () => {
      const res1 = await fetch("https://api.example.com/accept-test", {
        headers: { Accept: "application/json" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/accept-test", {
        headers: { Accept: "text/html" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different Accept = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("different Accept-Language headers produce separate cache entries", async () => {
      const res1 = await fetch("https://api.example.com/lang-test", {
        headers: { "Accept-Language": "en-US" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/lang-test", {
        headers: { "Accept-Language": "fr-FR" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different language = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("custom headers are included in cache key", async () => {
      const res1 = await fetch("https://api.example.com/custom-hdr", {
        headers: { "X-Feature-Flag": "variant-a" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/custom-hdr", {
        headers: { "X-Feature-Flag": "variant-b" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different custom header = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("traceparent and tracestate headers are excluded from cache key", async () => {
      const res1 = await fetch("https://api.example.com/trace-test", {
        headers: {
          traceparent: "00-trace-id-1-01",
          tracestate: "vendor=value1",
          "X-Custom": "same",
        },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // Same request but different trace headers — should hit cache
      const res2 = await fetch("https://api.example.com/trace-test", {
        headers: {
          traceparent: "00-trace-id-2-01",
          tracestate: "vendor=value2",
          "X-Custom": "same",
        },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Cached — trace headers excluded from key
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("same headers produce same cache entry regardless of order", async () => {
      const res1 = await fetch("https://api.example.com/hdr-order", {
        headers: new Headers([
          ["X-First", "1"],
          ["X-Second", "2"],
        ]),
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      // Headers in different construction order — Headers object normalizes
      const res2 = await fetch("https://api.example.com/hdr-order", {
        headers: new Headers([
          ["X-Second", "2"],
          ["X-First", "1"],
        ]),
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Same cache entry
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("requests with no headers vs with headers get separate cache entries", async () => {
      const res1 = await fetch("https://api.example.com/hdr-vs-none", {
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/hdr-vs-none", {
        headers: { "X-Extra": "present" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different cache entry
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });

  // ── Body restoration after cache key generation ───────────────────

  describe("body restoration (_ogBody)", () => {
    it("ReadableStream body is correctly passed to real fetch after cache key generation", async () => {
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("stream-body-content"));
          controller.close();
        },
      });

      await fetch("https://api.example.com/stream-restore", {
        method: "POST",
        body: stream,
        next: { revalidate: 60 },
      });

      // Verify the mock was called and the body was preserved as a stream
      expect(fetchMock).toHaveBeenCalledTimes(1);
      const call = fetchMock.mock.calls[0];
      const init = call[1] as RequestInit;
      expect(init.body).toBeInstanceOf(ReadableStream);
      const reader = (init.body as ReadableStream<Uint8Array>).getReader();
      const chunks: Uint8Array[] = [];
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
      }
      const length = chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
      const full = new Uint8Array(length);
      let offset = 0;
      for (const chunk of chunks) {
        full.set(chunk, offset);
        offset += chunk.byteLength;
      }
      const decoded = new TextDecoder().decode(full);
      expect(decoded).toBe("stream-body-content");
    });

    it("Blob body is correctly passed to real fetch after cache key generation", async () => {
      const blob = new Blob(["blob-body-content"], { type: "text/plain" });

      await fetch("https://api.example.com/blob-restore", {
        method: "POST",
        body: blob,
        next: { revalidate: 60 },
      });

      expect(fetchMock).toHaveBeenCalledTimes(1);
      const call = fetchMock.mock.calls[0];
      const init = call[1] as RequestInit;
      // The body should be a Blob (reconstructed)
      expect(init.body).toBeInstanceOf(Blob);
      const text = await (init.body as Blob).text();
      expect(text).toBe("blob-body-content");
    });

    it("Uint8Array body is correctly passed to real fetch after cache key generation", async () => {
      const body = new TextEncoder().encode("uint8-body-content");

      await fetch("https://api.example.com/uint8-restore", {
        method: "POST",
        body: body,
        next: { revalidate: 60 },
      });

      expect(fetchMock).toHaveBeenCalledTimes(1);
      const call = fetchMock.mock.calls[0];
      const init = call[1] as RequestInit;
      expect(init.body).toBeInstanceOf(Uint8Array);
      const decoded = new TextDecoder().decode(init.body as Uint8Array);
      expect(decoded).toBe("uint8-body-content");
    });

    it("string body is correctly passed to real fetch after cache key generation", async () => {
      await fetch("https://api.example.com/string-restore", {
        method: "POST",
        body: '{"key":"value"}',
        next: { revalidate: 60 },
      });

      expect(fetchMock).toHaveBeenCalledTimes(1);
      const call = fetchMock.mock.calls[0];
      const init = call[1] as RequestInit;
      expect(init.body).toBe('{"key":"value"}');
    });

    it("Request object body is still passed through after cache key generation", async () => {
      const request = new Request("https://api.example.com/request-restore", {
        method: "POST",
        body: "request-body-content",
        headers: { "content-type": "text/plain" },
      });

      await fetch(request, { next: { revalidate: 60 } });

      expect(fetchMock).toHaveBeenCalledTimes(1);
      const call = fetchMock.mock.calls[0];
      const forwardedRequest = call[0] as Request;
      expect(forwardedRequest).toBeInstanceOf(Request);
      expect(await forwardedRequest.text()).toBe("request-body-content");
    });

    it("already-consumed Request bodies bypass cache key generation and defer to the underlying fetch", async () => {
      fetchMock.mockImplementation(async (input: string | URL | Request, _init?: RequestInit) => {
        if (input instanceof Request && input.bodyUsed) {
          throw new TypeError("body already used");
        }
        return defaultFetchMockImplementation(input, _init);
      });

      const request = new Request("https://api.example.com/request-used", {
        method: "POST",
        body: "request-body-content",
        headers: { "content-type": "text/plain" },
      });
      await request.text();

      await expect(fetch(request, { next: { revalidate: 60 } })).rejects.toThrow(
        "body already used",
      );
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });
  });

  describe("cache key oversized body safeguards", () => {
    it("oversized Blob body bypasses cache and still fetches", async () => {
      const largeBlob = new Blob(["x".repeat(1024 * 1024 + 1)]);

      const res1 = await fetch("https://api.example.com/large-blob", {
        method: "POST",
        body: largeBlob,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/large-blob", {
        method: "POST",
        body: largeBlob,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // bypassed cache because body is oversized
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("oversized ReadableStream body bypasses cache and preserves stream body", async () => {
      const makeLargeStream = () =>
        new ReadableStream({
          start(controller) {
            controller.enqueue(new Uint8Array(1024 * 1024 + 1));
            controller.close();
          },
        });

      await fetch("https://api.example.com/large-stream", {
        method: "POST",
        body: makeLargeStream(),
        next: { revalidate: 60 },
      });

      await fetch("https://api.example.com/large-stream", {
        method: "POST",
        body: makeLargeStream(),
        next: { revalidate: 60 },
      });

      expect(fetchMock).toHaveBeenCalledTimes(2);
      const init = fetchMock.mock.calls[0][1] as RequestInit;
      expect(init.body).toBeInstanceOf(ReadableStream);
    });

    it("oversized Uint8Array body bypasses cache and still fetches", async () => {
      const largeBuffer = new Uint8Array(1024 * 1024 + 1);

      const res1 = await fetch("https://api.example.com/large-uint8", {
        method: "POST",
        body: largeBuffer,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/large-uint8", {
        method: "POST",
        body: largeBuffer,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // bypassed cache because body is oversized
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("oversized string body bypasses cache and still fetches", async () => {
      const largeString = "x".repeat(1024 * 1024 + 1);

      const res1 = await fetch("https://api.example.com/large-string", {
        method: "POST",
        body: largeString,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/large-string", {
        method: "POST",
        body: largeString,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // bypassed cache because body is oversized
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("oversized Request body bypasses cache without cloning the body when content-length exceeds the limit", async () => {
      const cloneSpy = vi.spyOn(Request.prototype, "clone");
      const request = new Request("https://api.example.com/large-request-stream", {
        method: "POST",
        headers: {
          "content-type": "application/octet-stream",
          "content-length": String(1024 * 1024 + 1),
        },
        body: new ReadableStream<Uint8Array>({
          pull(controller) {
            controller.enqueue(new Uint8Array([1]));
            controller.close();
          },
        }),
        duplex: "half",
      } as RequestInit & { duplex: "half" });

      try {
        const res = await fetch(request, { next: { revalidate: 60 } });
        const data = await res.json();

        expect(data.count).toBe(1);
        expect(fetchMock).toHaveBeenCalledTimes(1);
        expect(cloneSpy).not.toHaveBeenCalled();
        expect(request.bodyUsed).toBe(false);
      } finally {
        cloneSpy.mockRestore();
      }
    });

    it("ReadableStream with many small chunks accumulating past limit bypasses cache", async () => {
      const chunkSize = 64 * 1024; // 64 KiB per chunk
      const numChunks = 17; // 17 * 64 KiB = 1088 KiB > 1 MiB

      const makeLargeMultiChunkStream = () =>
        new ReadableStream({
          start(controller) {
            for (let i = 0; i < numChunks; i++) {
              controller.enqueue(new Uint8Array(chunkSize));
            }
            controller.close();
          },
        });

      const res1 = await fetch("https://api.example.com/large-multi-chunk", {
        method: "POST",
        body: makeLargeMultiChunkStream(),
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/large-multi-chunk", {
        method: "POST",
        body: makeLargeMultiChunkStream(),
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // bypassed cache because cumulative size exceeds limit
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("FormData with large File entry bypasses cache and still fetches", async () => {
      const largeContent = "x".repeat(1024 * 1024 + 1);
      const largeFile = new File([largeContent], "big.txt", { type: "text/plain" });
      const form = new FormData();
      form.append("file", largeFile);

      const res1 = await fetch("https://api.example.com/large-formdata", {
        method: "POST",
        body: form,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/large-formdata", {
        method: "POST",
        body: form,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // bypassed cache because file is oversized
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });

  // ── URLSearchParams body ──────────────────────────────────────────

  describe("URLSearchParams body", () => {
    it("different URLSearchParams bodies produce separate cache entries", async () => {
      const paramsA = new URLSearchParams({ q: "alpha" });
      const paramsB = new URLSearchParams({ q: "beta" });

      const res1 = await fetch("https://api.example.com/body-usp", {
        method: "POST",
        body: paramsA,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-usp", {
        method: "POST",
        body: paramsB,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2); // Different params = different cache
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });

    it("same URLSearchParams bodies hit the same cache entry", async () => {
      const params1 = new URLSearchParams({ q: "same" });
      const params2 = new URLSearchParams({ q: "same" });

      const res1 = await fetch("https://api.example.com/body-usp-same", {
        method: "POST",
        body: params1,
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-usp-same", {
        method: "POST",
        body: params2,
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(1); // Same params = cached
      expect(fetchMock).toHaveBeenCalledTimes(1);
    });

    it("explicit URLSearchParams charset headers remain part of the cache key", async () => {
      const res1 = await fetch("https://api.example.com/body-usp-charset", {
        method: "POST",
        body: new URLSearchParams({ q: "same" }),
        headers: { "content-type": "application/x-www-form-urlencoded; charset=utf-8" },
        next: { revalidate: 60 },
      });
      const data1 = await res1.json();
      expect(data1.count).toBe(1);

      const res2 = await fetch("https://api.example.com/body-usp-charset", {
        method: "POST",
        body: new URLSearchParams({ q: "same" }),
        headers: { "content-type": "application/x-www-form-urlencoded; charset=shift_jis" },
        next: { revalidate: 60 },
      });
      const data2 = await res2.json();
      expect(data2.count).toBe(2);
      expect(fetchMock).toHaveBeenCalledTimes(2);
    });
  });
});
