/**
 * next/head shim unit tests.
 *
 * Mirrors test cases from Next.js test/unit/next-head-rendering.test.ts,
 * plus comprehensive coverage for vinext's Head SSR collection, HTML
 * generation, allowed tags, and escaping.
 */
import { describe, it, expect, vi, beforeEach } from "vite-plus/test";
import React from "react";
import ReactDOMServer from "react-dom/server";
import Head, {
  resetSSRHead,
  getSSRHeadHTML,
  escapeAttr,
  reduceHeadChildren,
  setDocumentInitialHead,
  _applyHeadPropsToElement,
  _syncClientHead,
  _clientHeadChildren,
} from "../packages/vinext/src/shims/head.js";

// ─── SSR rendering (mirrors Next.js test/unit/next-head-rendering.test.ts) ──

describe("Rendering next/head", () => {
  beforeEach(() => {
    resetSSRHead();
  });

  it("should render outside of Next.js without error", () => {
    // Next.js test: renderToString(<><Head /><p>hello world</p></>)
    // Verifies Head doesn't throw when used standalone
    const html = ReactDOMServer.renderToString(
      React.createElement(
        React.Fragment,
        null,
        React.createElement(Head, null),
        React.createElement("p", null, "hello world"),
      ),
    );
    expect(html).toContain("hello world");
  });

  it("returns null (no rendered output in body)", () => {
    const html = ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("title", null, "My Page")),
    );
    // Head always returns null — elements are collected, not rendered inline
    expect(html).toBe("");
  });
});

// ─── SSR head collection ────────────────────────────────────────────────

describe("Head SSR collection", () => {
  beforeEach(() => {
    resetSSRHead();
  });

  it("collects title element", () => {
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("title", null, "My Page Title")),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("<title");
    expect(headHtml).toContain("My Page Title");
    expect(headHtml).toContain("</title>");
    expect(headHtml).toContain('data-next-head=""');
  });

  it("collects meta elements as self-closing", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("meta", { name: "description", content: "A test page" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('<meta name="description" content="A test page"');
    expect(headHtml).toContain("/>"); // self-closing
    expect(headHtml).not.toContain("</meta>");
  });

  it("collects link elements as self-closing", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("link", { rel: "stylesheet", href: "/styles.css" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('<link rel="stylesheet" href="/styles.css"');
    expect(headHtml).toContain("/>"); // self-closing
  });

  it("collects style elements", () => {
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("style", null, "body { color: red; }")),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("<style");
    // Text content is HTML-escaped
    expect(headHtml).toContain("body { color: red; }");
  });

  it("collects script elements", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("script", { src: "/analytics.js", async: true }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('<script src="/analytics.js" async');
    expect(headHtml).toContain("</script>");
  });

  it("collects base element as self-closing", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("base", { href: "https://example.com/" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('<base href="https://example.com/"');
    expect(headHtml).toContain("/>"); // self-closing
  });

  it("collects noscript elements", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("noscript", null, "JavaScript is required"),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("<noscript");
    expect(headHtml).toContain("JavaScript is required");
    expect(headHtml).toContain("</noscript>");
  });

  it("collects multiple head elements in order", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("title", null, "First"),
        React.createElement("meta", { name: "viewport", content: "width=device-width" }),
        React.createElement("link", { rel: "icon", href: "/favicon.ico" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("First");
    expect(headHtml).toContain("viewport");
    expect(headHtml).toContain("favicon.ico");
  });

  it("resets head between renders", () => {
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("title", null, "Page 1")),
    );
    expect(getSSRHeadHTML()).toContain("Page 1");

    resetSSRHead();

    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("title", null, "Page 2")),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("Page 2");
    expect(headHtml).not.toContain("Page 1");
  });

  it("returns only default charset/viewport when no user head elements", () => {
    // Even without any user `<Head>`, vinext emits next/head's defaultHead()
    // (charset + viewport) to match Next.js's canonical head ordering.
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('<meta charset="utf-8"');
    expect(headHtml).toContain('<meta name="viewport"');
    // No other tags.
    expect(headHtml).not.toContain("<title");
    expect(headHtml).not.toContain("<link");
  });

  it("dedupes keyed tags across multiple Head instances and keeps the last one", () => {
    // Next.js documents `key` as the dedupe mechanism for next/head tags:
    // https://github.com/vercel/next.js/blob/canary/docs/02-pages/04-api-reference/01-components/head.mdx
    ReactDOMServer.renderToString(
      React.createElement(
        React.Fragment,
        null,
        React.createElement(
          Head,
          null,
          React.createElement("meta", {
            property: "og:title",
            content: "Original Title",
            key: "og-title",
          }),
        ),
        React.createElement(
          Head,
          null,
          React.createElement("meta", {
            property: "og:title",
            content: "Updated Title",
            key: "og-title",
          }),
        ),
      ),
    );

    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('content="Updated Title"');
    expect(headHtml).not.toContain('content="Original Title"');
    expect(headHtml.match(/property="og:title"/g)).toHaveLength(1);
  });

  it("dedupes keyed tags across Head instances when one Head has multiple children", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        React.Fragment,
        null,
        React.createElement(
          Head,
          null,
          React.createElement("meta", {
            property: "og:title",
            content: "Title A",
            key: "og-title",
          }),
          React.createElement("meta", {
            name: "description",
            content: "Desc A",
            key: "desc",
          }),
        ),
        React.createElement(
          Head,
          null,
          React.createElement("meta", {
            property: "og:title",
            content: "Title B",
            key: "og-title",
          }),
        ),
      ),
    );

    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('content="Title B"');
    expect(headHtml).toContain('content="Desc A"');
    expect(headHtml).not.toContain('content="Title A"');
    expect(headHtml.match(/property="og:title"/g)).toHaveLength(1);
  });
});

describe("Head reduction", () => {
  it("dedupes keyed tags and keeps the last matching element", () => {
    const reduced = reduceHeadChildren([
      React.createElement("meta", {
        property: "og:title",
        content: "Original Title",
        key: "og-title",
      }),
      React.createElement("meta", {
        property: "og:title",
        content: "Updated Title",
        key: "og-title",
      }),
    ]);

    expect(reduced).toHaveLength(1);
    const dedupedMeta = reduced[0] as React.ReactElement<{ content?: string }> | undefined;
    expect(dedupedMeta?.props.content).toBe("Updated Title");
  });

  it("dedupes meta[name] tags without explicit keys using the last value", () => {
    const reduced = reduceHeadChildren([
      [
        React.createElement("meta", {
          name: "description",
          content: "Description A",
        }),
        React.createElement("meta", {
          name: "description",
          content: "Description B",
        }),
      ],
    ]);

    expect(reduced).toHaveLength(1);
    const dedupedMeta = reduced[0] as React.ReactElement<{ content?: string }> | undefined;
    expect(dedupedMeta?.props.content).toBe("Description B");
  });
});

// ─── Disallowed tags ────────────────────────────────────────────────────

describe("Head disallowed tags", () => {
  beforeEach(() => {
    resetSSRHead();
  });

  it("ignores <div> tag (not allowed in head)", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("div", null, "bad")),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).not.toContain("<div");
    expect(headHtml).not.toContain("bad");
    warn.mockRestore();
  });

  it("ignores <iframe> tag (security concern)", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("iframe", { src: "https://evil.com" })),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).not.toContain("<iframe");
    expect(headHtml).not.toContain("evil.com");
    warn.mockRestore();
  });

  it("ignores component elements (non-string type)", () => {
    function CustomComponent() {
      return React.createElement("meta", { name: "custom" });
    }
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement(CustomComponent)),
    );
    const headHtml = getSSRHeadHTML();
    // Component elements are ignored because child.type is not a string
    expect(headHtml).not.toContain('name="custom"');
  });

  it("keeps allowed tags while ignoring disallowed ones", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("title", null, "Good"),
        React.createElement("div", null, "Bad"),
        React.createElement("meta", { name: "good" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("Good");
    expect(headHtml).toContain('name="good"');
    expect(headHtml).not.toContain("<div");
    warn.mockRestore();
  });
});

// ─── HTML/Attribute escaping ────────────────────────────────────────────

describe("Head escaping", () => {
  beforeEach(() => {
    resetSSRHead();
  });

  it("escapes HTML in text content", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("title", null, 'Page <script>alert("xss")</script>'),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("&lt;script&gt;");
    expect(headHtml).not.toContain("<script>alert");
  });

  it("escapes HTML in attribute values", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("meta", { name: 'test"value', content: "a<b>c&d" }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain("&quot;");
    expect(headHtml).toContain("&lt;");
    expect(headHtml).toContain("&amp;");
  });

  it("renders dangerouslySetInnerHTML raw on SSR", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("script", {
          dangerouslySetInnerHTML: { __html: 'console.log("hello")' },
        }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('console.log("hello")');
  });

  it("empty dangerouslySetInnerHTML.__html takes precedence over children on SSR", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        // oxlint-disable-next-line react/no-danger-with-children
        React.createElement("style", {
          dangerouslySetInnerHTML: { __html: "" },
          // oxlint-disable-next-line react/no-children-prop
          children: "fallback",
        }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).not.toContain("fallback");
    expect(headHtml).toMatch(/<style[^>]*><\/style>/);
  });

  it("converts className to class attribute", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("style", { className: "critical" }, "body{}"),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain('class="critical"');
    expect(headHtml).not.toContain("className");
  });

  it("renders boolean true attributes as bare attribute name", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("script", { src: "/app.js", async: true, defer: true }),
      ),
    );
    const headHtml = getSSRHeadHTML();
    expect(headHtml).toContain(" async ");
    expect(headHtml).toContain(" defer ");
  });
});

describe("Head client sync", () => {
  function createElementDouble() {
    const attributes = new Map<string, string>();
    return {
      attributes,
      innerHTML: "",
      textContent: "",
      setAttribute(name: string, value: string) {
        attributes.set(name, value);
      },
    };
  }

  it("applies dangerouslySetInnerHTML to client-managed head elements", () => {
    // Next.js client reference:
    // packages/next/src/client/head-manager.ts reactElementToDOM()
    // sets el.innerHTML from dangerouslySetInnerHTML.__html.
    const element = createElementDouble();

    _applyHeadPropsToElement(element, {
      dangerouslySetInnerHTML: { __html: "body { color: red; }" },
    });

    expect(element.innerHTML).toBe("body { color: red; }");
  });

  it("ignores malformed dangerouslySetInnerHTML without __html key", () => {
    // dangerouslySetInnerHTML: {} has no __html key, so getDangerouslySetInnerHTML
    // returns undefined. The client falls through to children (matching SSR behavior).
    const element = createElementDouble();
    element.innerHTML = "previous";

    _applyHeadPropsToElement(element, {
      dangerouslySetInnerHTML: {},
    });

    // No valid __html and no children — content is unchanged.
    expect(element.innerHTML).toBe("previous");
  });

  it("falls through to children when dangerouslySetInnerHTML has no __html key", () => {
    const element = createElementDouble();

    _applyHeadPropsToElement(element, {
      dangerouslySetInnerHTML: {},
      children: "fallback",
    });

    // Malformed dangerouslySetInnerHTML is ignored, children win.
    expect(element.textContent).toBe("fallback");
  });

  it("empty dangerouslySetInnerHTML.__html takes precedence over children on client", () => {
    const element = createElementDouble();
    _applyHeadPropsToElement(element, {
      children: "fallback",
      dangerouslySetInnerHTML: { __html: "" },
    });
    expect(element.innerHTML).toBe("");
    expect(element.textContent).toBe("");
  });

  it("prefers dangerouslySetInnerHTML over children on client-managed head elements", () => {
    const element = createElementDouble();

    _applyHeadPropsToElement(element, {
      children: "children content",
      dangerouslySetInnerHTML: { __html: "raw content" },
    });

    expect(element.innerHTML).toBe("raw content");
    expect(element.textContent).toBe("");
  });

  it("sets textContent from children when dangerouslySetInnerHTML is absent", () => {
    const element = createElementDouble();
    _applyHeadPropsToElement(element, { children: "hello" });
    expect(element.textContent).toBe("hello");
    expect(element.innerHTML).toBe("");
  });

  it("sets textContent from array children by joining them", () => {
    const element = createElementDouble();
    _applyHeadPropsToElement(element, { children: ["a", "b", "c"] });
    expect(element.textContent).toBe("abc");
    expect(element.innerHTML).toBe("");
  });

  it("maps JSX attribute names to HTML form when applying to DOM", () => {
    // The SSR path emits `<meta http-equiv="..." />` (hyphenated). The client
    // path must use the same mapping or hydration will mismatch. setAttribute
    // is case-insensitive for HTML elements, so `charSet` would lowercase to
    // `charset` by coincidence — but `httpEquiv` would become `httpequiv`
    // (no hyphen), not `http-equiv`.
    const element = createElementDouble();
    _applyHeadPropsToElement(element, {
      httpEquiv: "X-UA-Compatible",
      content: "IE=edge",
    });
    expect(element.attributes.get("http-equiv")).toBe("X-UA-Compatible");
    expect(element.attributes.get("httpEquiv")).toBeUndefined();
    expect(element.attributes.get("content")).toBe("IE=edge");
  });

  it("maps JSX charSet attribute to HTML charset when applying to DOM", () => {
    const element = createElementDouble();
    _applyHeadPropsToElement(element, { charSet: "utf-8" });
    expect(element.attributes.get("charset")).toBe("utf-8");
    expect(element.attributes.get("charSet")).toBeUndefined();
  });
});

// ─── escapeAttr utility ─────────────────────────────────────────────────

describe("escapeAttr", () => {
  it("escapes ampersand", () => {
    expect(escapeAttr("a&b")).toBe("a&amp;b");
  });

  it("escapes double quotes", () => {
    expect(escapeAttr('a"b')).toBe("a&quot;b");
  });

  it("escapes angle brackets", () => {
    expect(escapeAttr("a<b>c")).toBe("a&lt;b&gt;c");
  });

  it("returns safe strings unchanged", () => {
    expect(escapeAttr("hello world")).toBe("hello world");
  });

  it("escapes all special chars together", () => {
    expect(escapeAttr('&"<>')).toBe("&amp;&quot;&lt;&gt;");
  });
});

// ─── charset / viewport ordering (issue #1569) ─────────────────────────────
//
// Mirrors Next.js's test/e2e/next-head/index.test.ts assertion that
// `<meta charset>` is emitted first, then `<meta viewport>`, then user tags,
// all carrying `data-next-head=""`. Next.js's `defaultHead()` in
// shared/lib/head.tsx is what seeds those defaults — vinext must include the
// same defaults via getSSRHeadHTML().

describe("Head default tags (charset/viewport ordering, issue #1569)", () => {
  beforeEach(() => {
    resetSSRHead();
    setDocumentInitialHead([]);
  });

  it("emits charset first, then viewport, before user tags", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("meta", { name: "test-head-1", content: "hello" }),
      ),
    );

    const html = getSSRHeadHTML();
    const charsetIdx = html.indexOf('charset="utf-8"');
    const viewportIdx = html.indexOf('name="viewport"');
    const userIdx = html.indexOf('name="test-head-1"');

    expect(charsetIdx).toBeGreaterThanOrEqual(0);
    expect(viewportIdx).toBeGreaterThan(charsetIdx);
    expect(userIdx).toBeGreaterThan(viewportIdx);
  });

  it("default tags carry data-next-head attribute", () => {
    const html = getSSRHeadHTML();
    // Match the segment from the charset tag through the next "/>"
    const charsetMatch = html.match(/<meta charset="utf-8"[^/]*\/>/);
    expect(charsetMatch).not.toBeNull();
    expect(charsetMatch?.[0]).toContain('data-next-head=""');

    const viewportMatch = html.match(/<meta name="viewport"[^/]*\/>/);
    expect(viewportMatch).not.toBeNull();
    expect(viewportMatch?.[0]).toContain('data-next-head=""');
  });

  it("user-supplied charset overrides the default (charSet meta-type dedupe)", () => {
    // charSet is special: META_TYPES dedupe always treats it as unique-only,
    // so any user-supplied charset replaces the default regardless of key.
    ReactDOMServer.renderToString(
      React.createElement(Head, null, React.createElement("meta", { charSet: "utf-16" })),
    );

    const html = getSSRHeadHTML();
    expect(html).toContain('charset="utf-16"');
    expect(html).not.toContain('charset="utf-8"');
  });

  it("user-supplied viewport with key='viewport' overrides the default", () => {
    // The default viewport carries `key="viewport"`. User tags must use the
    // same key to dedupe — matches Next.js's `key`-based merge semantics.
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("meta", {
          name: "viewport",
          content: "width=500",
          key: "viewport",
        }),
      ),
    );

    const html = getSSRHeadHTML();
    expect(html).toContain('content="width=500"');
    expect(html).not.toContain('content="width=device-width"');
  });

  it("emits defaults even when no user <Head> is mounted", () => {
    const html = getSSRHeadHTML();
    expect(html).toContain('<meta charset="utf-8"');
    expect(html).toContain('<meta name="viewport"');
  });
});

// ─── _document.getInitialProps() head merge (issue #1569) ──────────────────
//
// Mirrors Next.js's test/e2e/next-head/index.test.ts assertion that head tags
// returned from a user `_document.getInitialProps()` appear in the rendered
// `<head>`. vinext's SSR pipeline forwards them to the head shim via
// setDocumentInitialHead() so they flow through the same dedupe pipeline.

describe("Head _document.getInitialProps() merge (issue #1569)", () => {
  beforeEach(() => {
    resetSSRHead();
    setDocumentInitialHead([]);
  });

  it("includes meta tags returned by setDocumentInitialHead in the output", () => {
    ReactDOMServer.renderToString(
      React.createElement(
        Head,
        null,
        React.createElement("meta", { name: "test-head-1", content: "hello" }),
      ),
    );
    setDocumentInitialHead([
      React.createElement("meta", { name: "test-head-initial-props", content: "hello" }),
    ]);

    const html = getSSRHeadHTML();
    expect(html).toContain('name="test-head-initial-props"');
    expect(html).toContain('content="hello"');
    // The user's next/head tag still appears.
    expect(html).toContain('name="test-head-1"');
  });

  it("resets between renders so initial-props head does not leak", () => {
    setDocumentInitialHead([
      React.createElement("meta", { name: "leaked", content: "should-be-gone" }),
    ]);
    expect(getSSRHeadHTML()).toContain('name="leaked"');

    resetSSRHead();

    expect(getSSRHeadHTML()).not.toContain('name="leaked"');
  });
});

// ─── client hydration sync keeps defaults & order (issue #1569 / #1677) ─────
//
// Underlying issue #1569 was fixed for SSR by #1677; this exercises the client
// path, which regressed: on the client, the first <Head> mount runs
// _syncClientHead(). It must:
//   1. keep the server-rendered charset / viewport defaults (which carry
//      data-next-head="") — they must not vanish after hydration, and
//   2. reconcile in place (diff via isEqualNode) rather than wipe-and-rebuild,
//      so SSR head ordering is preserved and <meta charset> stays first.
// This mirrors Next.js's reduceComponents() (always seeds defaultHead()) and
// head-manager.ts updateElements() (in-place reconciliation).

describe("Head client sync (defaults + order, issue #1569 / #1677)", () => {
  // Minimal DOM double — the repo has no jsdom/happy-dom. Only the surface
  // _syncClientHead() touches is implemented.
  class FakeElement {
    attributes = new Map<string, string>();
    innerHTML = "";
    textContent = "";
    parentNode: FakeHead | null = null;
    constructor(public readonly tagName: string) {}
    setAttribute(name: string, value: string): void {
      this.attributes.set(name, value);
    }
    getAttribute(name: string): string | null {
      return this.attributes.has(name) ? this.attributes.get(name)! : null;
    }
    hasAttribute(name: string): boolean {
      return this.attributes.has(name);
    }
    isEqualNode(other: unknown): boolean {
      if (!(other instanceof FakeElement)) return false;
      if (this.tagName.toLowerCase() !== other.tagName.toLowerCase()) return false;
      if (this.attributes.size !== other.attributes.size) return false;
      for (const [k, v] of this.attributes) {
        if (other.attributes.get(k) !== v) return false;
      }
      return this.textContent === other.textContent && this.innerHTML === other.innerHTML;
    }
  }

  class FakeHead {
    children: FakeElement[] = [];
    appendChild(el: FakeElement): void {
      el.parentNode?.removeChild(el);
      el.parentNode = this;
      this.children.push(el);
    }
    prepend(el: FakeElement): void {
      el.parentNode?.removeChild(el);
      el.parentNode = this;
      this.children.unshift(el);
    }
    removeChild(el: FakeElement): void {
      const idx = this.children.indexOf(el);
      if (idx >= 0) this.children.splice(idx, 1);
      el.parentNode = null;
    }
    querySelectorAll(selector: string): FakeElement[] {
      if (selector !== "[data-next-head]") return [];
      return this.children.filter((el) => el.hasAttribute("data-next-head"));
    }
    querySelector(selector: string): FakeElement | null {
      if (selector !== "meta[charset]") return null;
      return (
        this.children.find(
          (el) => el.tagName.toLowerCase() === "meta" && el.hasAttribute("charset"),
        ) ?? null
      );
    }
  }

  function installFakeDocument(): {
    restore: () => void;
    head: FakeHead;
    createElement: (tag: string) => FakeElement;
  } {
    const head = new FakeHead();
    const createElement = (tag: string) => new FakeElement(tag);
    const fakeDocument = { createElement, head };
    const prevDocument = (globalThis as Record<string, unknown>).document;
    (globalThis as Record<string, unknown>).document = fakeDocument;
    return {
      head,
      createElement,
      restore: () => {
        (globalThis as Record<string, unknown>).document = prevDocument;
      },
    };
  }

  // Build a server-rendered <head> matching what SSR emits for the example:
  // charset, viewport, title (all data-next-head), followed by a modulepreload
  // link that is NOT vinext-managed.
  function seedServerHead(head: FakeHead, createElement: (tag: string) => FakeElement) {
    const charset = createElement("meta");
    charset.setAttribute("charset", "utf-8");
    charset.setAttribute("data-next-head", "");
    const viewport = createElement("meta");
    viewport.setAttribute("name", "viewport");
    viewport.setAttribute("content", "width=device-width");
    viewport.setAttribute("data-next-head", "");
    const title = createElement("title");
    title.textContent = "Cloudflare Pages Router";
    title.setAttribute("data-next-head", "");
    const modulepreload = createElement("link");
    modulepreload.setAttribute("rel", "modulepreload");
    modulepreload.setAttribute("href", "/_next/static/index.js");
    head.children.push(charset, viewport, title, modulepreload);
    for (const el of head.children) el.parentNode = head;
    return { charset, viewport, title, modulepreload };
  }

  beforeEach(() => {
    _clientHeadChildren.clear();
  });

  it("preserves SSR order and reuses matching nodes (no reorder, no churn)", () => {
    const { head, createElement, restore } = installFakeDocument();
    try {
      const server = seedServerHead(head, createElement);

      // The page's <Head> re-declares the same title on the client.
      _clientHeadChildren.set(
        Symbol("test"),
        React.createElement("title", null, "Cloudflare Pages Router"),
      );
      _syncClientHead();

      // Order is unchanged and the exact same node instances are reused.
      expect(head.children).toEqual([
        server.charset,
        server.viewport,
        server.title,
        server.modulepreload,
      ]);
      // No duplicate charset/viewport.
      const metas = head.children.filter((el) => el.tagName === "meta");
      expect(metas.filter((el) => el.getAttribute("charset") === "utf-8")).toHaveLength(1);
      expect(metas.filter((el) => el.getAttribute("name") === "viewport")).toHaveLength(1);
    } finally {
      restore();
    }
  });

  it("appends genuinely new tags without disturbing existing order", () => {
    const { head, createElement, restore } = installFakeDocument();
    try {
      const server = seedServerHead(head, createElement);

      _clientHeadChildren.set(
        Symbol("test"),
        React.createElement(
          React.Fragment,
          null,
          React.createElement("title", null, "Cloudflare Pages Router"),
          React.createElement("meta", { name: "description", content: "hi" }),
        ),
      );
      _syncClientHead();

      // Defaults + title kept in place; the new description meta is appended.
      const description = head.children.find((el) => el.getAttribute("name") === "description");
      expect(description).toBeDefined();
      expect(head.children.indexOf(server.charset)).toBe(0);
      expect(head.children.indexOf(server.viewport)).toBe(1);
      expect(head.children.indexOf(server.title)).toBe(2);
      // The new tag lands after the previously-existing children.
      expect(head.children.indexOf(description!)).toBeGreaterThan(
        head.children.indexOf(server.modulepreload),
      );
    } finally {
      restore();
    }
  });

  it("removes stale vinext-managed tags that are no longer desired", () => {
    const { head, createElement, restore } = installFakeDocument();
    try {
      seedServerHead(head, createElement);
      const stale = createElement("meta");
      stale.setAttribute("name", "stale");
      stale.setAttribute("data-next-head", "");
      head.appendChild(stale);

      // Client <Head> declares only the title (defaults come from defaultHead()).
      _clientHeadChildren.set(
        Symbol("test"),
        React.createElement("title", null, "Cloudflare Pages Router"),
      );
      _syncClientHead();

      expect(head.children.includes(stale)).toBe(false);
      // Defaults remain.
      const metas = head.children.filter((el) => el.tagName === "meta");
      expect(metas.some((el) => el.getAttribute("charset") === "utf-8")).toBe(true);
      expect(metas.some((el) => el.getAttribute("name") === "viewport")).toBe(true);
    } finally {
      restore();
    }
  });

  it("prepends a newly-created <meta charset> so it stays first", () => {
    const { head, createElement, restore } = installFakeDocument();
    try {
      // <head> has a non-managed script first and no charset yet.
      const script = createElement("script");
      script.setAttribute("src", "/a.js");
      head.appendChild(script);

      _clientHeadChildren.set(Symbol("test"), null);
      _syncClientHead();

      // defaultHead()'s charset must be prepended ahead of the script.
      expect(head.children[0]?.tagName).toBe("meta");
      expect(head.children[0]?.getAttribute("charset")).toBe("utf-8");
    } finally {
      restore();
    }
  });

  it("lets a user key='viewport' override the default viewport on the client", () => {
    // Mirrors the SSR override test (head.test.ts) on the client path: the
    // default viewport carries key="viewport", so a user tag with the same key
    // wins the dedupe in reduceHeadChildren — same precedence as Next.js.
    const { head, restore } = installFakeDocument();
    try {
      _clientHeadChildren.set(
        Symbol("test"),
        React.createElement("meta", {
          name: "viewport",
          content: "width=500",
          key: "viewport",
        }),
      );
      _syncClientHead();

      const viewports = head.children.filter(
        (el) => el.tagName === "meta" && el.getAttribute("name") === "viewport",
      );
      // Exactly one viewport, and it is the user's override.
      expect(viewports).toHaveLength(1);
      expect(viewports[0]?.getAttribute("content")).toBe("width=500");
      // The default charset is still emitted alongside the override.
      expect(
        head.children.some((el) => el.tagName === "meta" && el.getAttribute("charset") === "utf-8"),
      ).toBe(true);
    } finally {
      restore();
    }
  });

  it("emits defaults even when the client <Head> has no children", () => {
    const { head, restore } = installFakeDocument();
    try {
      _clientHeadChildren.set(Symbol("test"), null);
      _syncClientHead();

      const metas = head.children.filter((el) => el.tagName === "meta");
      expect(metas.some((el) => el.getAttribute("charset") === "utf-8")).toBe(true);
      expect(metas.some((el) => el.getAttribute("name") === "viewport")).toBe(true);
    } finally {
      restore();
    }
  });
});
