import { describe, expect, it, vi } from "vitest";
import { parseHTML } from "linkedom";
import {
  scopeCssToComposition,
  wrapInlineScriptWithErrorBoundary,
  wrapScopedCompositionScript,
} from "./compositionScoping";

describe("composition scoping", () => {
  it("scopes regular selectors while preserving global at-rules", () => {
    const scoped = scopeCssToComposition(
      `
@import url("https://example.com/font.css");
.title, .card:hover { opacity: 0; }
@media (min-width: 800px) {
  .title { transform: translateY(30px); }
}
@keyframes rise {
  from { opacity: 0; }
  to { opacity: 1; }
}
[data-composition-id="scene"] .already { color: red; }
body { margin: 0; }
`,
      "scene",
    );

    expect(scoped).toContain('@import url("https://example.com/font.css");');
    expect(scoped).toContain(
      '[data-composition-id="scene"] .title, [data-composition-id="scene"] .card:hover',
    );
    expect(scoped).toContain('[data-composition-id="scene"] .title { transform');
    expect(scoped).toContain("@keyframes rise");
    expect(scoped).toContain("from { opacity: 0; }");
    expect(scoped).toContain('[data-composition-id="scene"] .already { color: red; }');
    expect(scoped).toContain("body { margin: 0; }");
  });

  it("wraps classic scripts without render-loop requestAnimationFrame waits", () => {
    const wrapped = wrapScopedCompositionScript("window.__ran = true;", "scene");

    expect(wrapped).toContain('var __hfCompId = "scene";');
    expect(wrapped).toContain("new Proxy(window.document");
    expect(wrapped).toContain("new Proxy(__hfBaseGsap");
    expect(wrapped).not.toContain("requestAnimationFrame");
  });

  it("normalizes root timing attributes when scoping selectors", () => {
    const scoped = scopeCssToComposition(
      '[data-composition-id="scene"][data-start="0"] .title { opacity: 0; }',
      "scene",
    );

    expect(scoped).toContain('[data-composition-id="scene"] .title { opacity: 0; }');
    expect(scoped).not.toContain('[data-start="0"]');
  });

  it("exposes a scoped __hyperframes.getVariables that reads __hfVariablesByComp[compId]", () => {
    const { document } = parseHTML(`<div data-composition-id="card-1"></div>`);
    const fakeWindow: Record<string, unknown> = {
      document,
      __timelines: {},
      __hfVariablesByComp: {
        "card-1": { title: "Pro", price: "$29" },
        "card-2": { title: "Enterprise", price: "Custom" },
      },
      __hyperframes: {
        getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
        fitTextFontSize: () => undefined,
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `window.__captured = __hyperframes.getVariables();`,
      "card-1",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
  });

  it("scoped getVariables reads from the runtime composition id when it differs", () => {
    const { document } = parseHTML(`<div data-composition-id="scene"></div>`);
    const fakeWindow: Record<string, unknown> = {
      document,
      __timelines: {},
      __hfVariablesByComp: {
        scene: { title: "Wrong" },
        scene__hf1: { title: "Right" },
      },
      __hyperframes: {
        getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
        fitTextFontSize: () => undefined,
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `window.__captured = __hyperframes.getVariables();`,
      "scene",
      "[HyperFrames] composition script error:",
      undefined,
      "scene__hf1",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__captured).toEqual({ title: "Right" });
  });

  it("scoped getVariables returns {} when __hfVariablesByComp has no entry for the comp", () => {
    const { document } = parseHTML(`<div data-composition-id="missing"></div>`);
    const fakeWindow: Record<string, unknown> = {
      document,
      __timelines: {},
      __hyperframes: {
        getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
        fitTextFontSize: () => undefined,
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `window.__captured = __hyperframes.getVariables();`,
      "missing",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__captured).toEqual({});
  });

  it("scoped getVariables returns a fresh object — mutations don't leak into the shared table", () => {
    const { document } = parseHTML(`<div data-composition-id="card-1"></div>`);
    const variablesByComp: Record<string, Record<string, unknown>> = {
      "card-1": { title: "Pro" },
    };
    const fakeWindow: Record<string, unknown> = {
      document,
      __timelines: {},
      __hfVariablesByComp: variablesByComp,
      __hyperframes: {
        getVariables: () => ({}),
        fitTextFontSize: () => undefined,
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `var v = __hyperframes.getVariables(); v.title = "MUTATED"; v.added = "extra";`,
      "card-1",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(variablesByComp["card-1"]).toEqual({ title: "Pro" });
  });

  it("preserves static methods on classes exposed through window", () => {
    const { document } = parseHTML(`<div data-composition-id="scene"></div>`);
    class FakeTexts {
      static mountChars() {
        return "ok";
      }
    }
    const fakeWindow: Record<string, unknown> = {
      document,
      __timelines: {},
      Texts: FakeTexts,
    };
    const wrapped = wrapScopedCompositionScript(
      `window.__capturedMountCharsType = typeof window.Texts?.mountChars;`,
      "scene",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__capturedMountCharsType).toBe("function");
  });

  it("executes document and GSAP selectors inside the composition root", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene" data-start="intro"><h1 class="title">Scene</h1></div>
      <div data-composition-id="other"><h1 class="title">Other</h1></div>
    `);
    const gsapTargets: string[][] = [];
    const fakeWindow = {
      document,
      __selectedTitle: "",
      __selectedRootTitle: "",
      __timelines: {},
      gsap: {
        timeline: () => ({
          to(targets: Element[]) {
            gsapTargets.push(Array.from(targets).map((target) => target.textContent || ""));
            return this;
          },
        }),
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `
const tl = gsap.timeline({ paused: true });
tl.to('.title', { opacity: 1 });
tl.to('[data-composition-id="scene"][data-start="0"] .title', { opacity: 1 });
window.__selectedTitle = document.querySelector('.title')?.textContent || '';
window.__selectedRootTitle = document.querySelector('[data-composition-id="scene"][data-start="0"] .title')?.textContent || '';
window.__timelines.scene = tl;
`,
      "scene",
    );

    new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);

    expect(fakeWindow.__selectedTitle).toBe("Scene");
    expect(fakeWindow.__selectedRootTitle).toBe("Scene");
    expect(gsapTargets).toEqual([["Scene"], ["Scene"]]);
  });

  it("scopes getElementById when duplicate IDs exist across composition roots", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene-a"><canvas id="gl-canvas"></canvas></div>
      <div data-composition-id="scene-b"><canvas id="gl-canvas"></canvas></div>
    `);
    const fakeWindow = {
      document,
      __selectedComp: "",
      __timelines: {},
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__selectedComp =
  document.getElementById("gl-canvas")
    ?.closest("[data-composition-id]")
    ?.getAttribute("data-composition-id") || "null";
`,
      "scene-b",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__selectedComp).toBe("scene-b");
  });

  it("scopes getElementById for IDs that need CSS selector escaping", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene-a"><div id="clip:1"></div></div>
      <div data-composition-id="scene-b"><div id="clip:1"></div></div>
    `);
    const fakeWindow = {
      document,
      __selectedComp: "",
      __timelines: {},
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__selectedComp =
  document.getElementById("clip:1")
    ?.closest("[data-composition-id]")
    ?.getAttribute("data-composition-id") || "null";
`,
      "scene-b",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__selectedComp).toBe("scene-b");
  });

  it("scopes authored root id lookups after the flattened root drops its literal id", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene">
        <div data-hf-authored-id="scene-root">
          <h1 class="title">Scene</h1>
        </div>
      </div>
    `);
    const fakeWindow = {
      document,
      __selectedTitle: "",
      __timelines: {},
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__selectedTitle =
  document.getElementById("scene-root")
    ?.querySelector(".title")
    ?.textContent || "missing";
`,
      "scene",
      "[HyperFrames] composition script error:",
      undefined,
      "scene",
      "scene-root",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__selectedTitle).toBe("Scene");
  });

  it("does not rewrite authored root hash text inside CSS attribute values", () => {
    const scoped = scopeCssToComposition(
      'a[href="#scene-root"] { color: red; }',
      "scene",
      undefined,
      "scene-root",
    );

    expect(scoped).toContain('[data-composition-id="scene"] a[href="#scene-root"]');
    expect(scoped).not.toContain('[href="[data-hf-authored-id=');
  });

  it("does not rewrite authored root hash text inside querySelector attribute values", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene">
        <a class="jump" href="#scene-root">Jump</a>
        <div data-hf-authored-id="scene-root"></div>
      </div>
    `);
    const fakeWindow = {
      document,
      __selectedHref: "",
      __timelines: {},
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__selectedHref =
  document.querySelector('a[href="#scene-root"]')
    ?.getAttribute("href") || "missing";
`,
      "scene",
      "[HyperFrames] composition script error:",
      undefined,
      "scene",
      "scene-root",
    );

    new Function("window", wrapped)(fakeWindow);

    expect(fakeWindow.__selectedHref).toBe("#scene-root");
  });

  it("normalizes gsap.utils.selector() selectors for authored root ids and root timing attrs", () => {
    const { document } = parseHTML(`
      <div data-composition-id="scene" data-start="0">
        <div data-hf-authored-id="scene-root">
          <h1 class="title">Scene</h1>
        </div>
      </div>
      <div data-composition-id="other" data-start="0">
        <div data-hf-authored-id="scene-root">
          <h1 class="title">Other</h1>
        </div>
      </div>
    `);
    const fakeWindow = {
      document,
      __selectedRootCount: 0,
      __selectedTimedCount: 0,
      __selectedTitle: "",
      __timelines: {},
      gsap: {
        utils: {},
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `
const select = gsap.utils.selector(document.querySelector('[data-composition-id="scene"]'));
window.__selectedRootCount = select('#scene-root').length;
window.__selectedTimedCount = select('[data-composition-id="scene"][data-start="0"] .title').length;
window.__selectedTitle = select('#scene-root .title')[0]?.textContent || "missing";
`,
      "scene",
      "[HyperFrames] composition script error:",
      undefined,
      "scene",
      "scene-root",
    );

    new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);

    expect(fakeWindow.__selectedRootCount).toBe(1);
    expect(fakeWindow.__selectedTimedCount).toBe(1);
    expect(fakeWindow.__selectedTitle).toBe("Scene");
  });

  it("reads scoped proxy accessors with the original target receiver", () => {
    const root = {
      contains(node: unknown) {
        return node === root;
      },
    };
    const body = { tagName: "BODY" };
    const fakeDocument = {
      querySelector(selector: string) {
        return selector === '[data-composition-id="scene"]' ? root : null;
      },
      querySelectorAll() {
        return [];
      },
      getElementById() {
        return null;
      },
      get body() {
        if (this !== fakeDocument) {
          throw new TypeError("Illegal invocation");
        }
        return body;
      },
    };
    const location = { href: "https://example.test/scene" };
    const fakeUtils = {
      get marker() {
        if (this !== fakeUtils) {
          throw new TypeError("Illegal invocation");
        }
        return "utils-ok";
      },
    };
    const fakeGsap = {
      utils: fakeUtils,
      get version() {
        if (this !== fakeGsap) {
          throw new TypeError("Illegal invocation");
        }
        return "gsap-ok";
      },
    };
    const fakeWindow = {
      document: fakeDocument,
      __bodyTag: "",
      __href: "",
      __windowSet: "",
      __gsapVersion: "",
      __utilsMarker: "",
      __timelines: {},
      gsap: fakeGsap,
      get location() {
        if (this !== fakeWindow) {
          throw new TypeError("Illegal invocation");
        }
        return location;
      },
      set customValue(value: string) {
        if (this !== fakeWindow) {
          throw new TypeError("Illegal invocation");
        }
        this.__windowSet = value;
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__bodyTag = document.body.tagName;
window.__href = window.location.href;
window.customValue = "window-set-ok";
window.__gsapVersion = gsap.version;
window.__utilsMarker = gsap.utils.marker;
`,
      "scene",
    );
    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

    try {
      new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
    } finally {
      errorSpy.mockRestore();
    }

    expect(fakeWindow.__bodyTag).toBe("BODY");
    expect(fakeWindow.__href).toBe("https://example.test/scene");
    expect(fakeWindow.__windowSet).toBe("window-set-ok");
    expect(fakeWindow.__gsapVersion).toBe("gsap-ok");
    expect(fakeWindow.__utilsMarker).toBe("utils-ok");
    expect(errorSpy).not.toHaveBeenCalled();
  });

  it("reads remapped timeline registry accessors with the original target receiver", () => {
    let timeline = "initial";
    const timelineRegistry = {
      get host() {
        if (this !== timelineRegistry) {
          throw new TypeError("Illegal invocation");
        }
        return timeline;
      },
      set host(value: string) {
        if (this !== timelineRegistry) {
          throw new TypeError("Illegal invocation");
        }
        timeline = value;
      },
    };
    const fakeWindow = {
      document: {
        querySelector() {
          return null;
        },
        querySelectorAll() {
          return [];
        },
      },
      __timelines: timelineRegistry,
      __beforeTimeline: "",
      __afterTimeline: "",
      gsap: {},
    };
    const wrapped = wrapScopedCompositionScript(
      `
window.__beforeTimeline = window.__timelines.scene;
window.__timelines.scene = "updated";
window.__afterTimeline = window.__timelines.scene;
`,
      "scene",
      "[HyperFrames] composition script error:",
      undefined,
      "host",
    );
    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

    try {
      new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
    } finally {
      errorSpy.mockRestore();
    }

    expect(fakeWindow.__beforeTimeline).toBe("initial");
    expect(fakeWindow.__afterTimeline).toBe("updated");
    expect(errorSpy).not.toHaveBeenCalled();
  });

  it("uses compound selector when authored root is the scoped element itself", () => {
    const scoped = scopeCssToComposition(
      "#chrome-overlay-root { --primary: #FFDC8B; }",
      "chrome-overlay",
      undefined,
      "chrome-overlay-root",
      { compoundAuthoredRoot: true },
    );

    // Both attributes are on the same element after inlining, so the selector
    // must be compound (no space) to match.
    expect(scoped).toContain(
      '[data-composition-id="chrome-overlay"][data-hf-authored-id="chrome-overlay-root"]',
    );
    expect(scoped).not.toContain(
      '[data-composition-id="chrome-overlay"] [data-hf-authored-id="chrome-overlay-root"]',
    );
  });

  it("uses compound selector for authored root with descendant combinators", () => {
    const scoped = scopeCssToComposition(
      "#chrome-overlay-root .chrome { display: flex; }",
      "chrome-overlay",
      undefined,
      "chrome-overlay-root",
      { compoundAuthoredRoot: true },
    );

    // The authored root part is compound with scope, .chrome is a descendant
    expect(scoped).toContain(
      '[data-composition-id="chrome-overlay"][data-hf-authored-id="chrome-overlay-root"] .chrome',
    );
    expect(scoped).not.toMatch(
      /\[data-composition-id="chrome-overlay"\]\s+\[data-hf-authored-id="chrome-overlay-root"\]\s+\.chrome/,
    );
  });

  it("still uses descendant selector for non-root selectors with authoredRootId", () => {
    const scoped = scopeCssToComposition(
      ".child-element { color: red; }",
      "chrome-overlay",
      undefined,
      "chrome-overlay-root",
    );

    // Regular child selectors still get a descendant combinator (space)
    expect(scoped).toContain('[data-composition-id="chrome-overlay"] .child-element');
  });

  it("escapes </script> in scoped composition script source to prevent injection", () => {
    const wrapped = wrapScopedCompositionScript(
      'window.payload = "</script><script>window.pwned = true;</script>";',
      "scene",
    );

    expect(wrapped).toContain("(function(document, gsap, window, __hyperframes)");
    expect(wrapped).not.toContain("</script><script>");
    expect(wrapped).toContain("<\\/script>");
  });

  it("wraps unscoped composition script source as a string literal", () => {
    const wrapped = wrapInlineScriptWithErrorBoundary(
      'window.payload = "</script><script>window.pwned = true;</script>";',
      "[HyperFrames] composition script error:",
    );

    expect(wrapped).toContain("Function(");
    expect(wrapped).toContain('\\"</script><script>window.pwned = true;</script>\\"');
  });

  it("rewrites #id CSS selectors to [data-hf-authored-id] when authoredRootId is provided", () => {
    const scoped = scopeCssToComposition(
      `#intro { background: #111; }
#intro .title { font-size: 120px; color: #fff; }`,
      "intro",
      undefined,
      "intro",
    );

    // #intro should become [data-hf-authored-id="intro"]
    expect(scoped).toContain('[data-hf-authored-id="intro"]');
    expect(scoped).toContain('[data-hf-authored-id="intro"] .title');
    // Raw #intro selectors should be gone
    expect(scoped).not.toMatch(/#intro\b/);
  });

  it('does not rewrite [id="intro"] attribute selectors', () => {
    // The function only targets #intro hash selectors, not [id="intro"] attribute selectors
    const result = scopeCssToComposition(
      '[id="intro"] .title { color: red; }',
      "intro",
      undefined,
      "intro",
    );
    expect(result).toContain('[id="intro"]');
  });

  it("wraps scripts with authored root id normalization for #id GSAP selectors", () => {
    const { document } = parseHTML(`
      <div data-composition-id="intro">
        <div data-hf-authored-id="intro">
          <div class="title">HELLO</div>
        </div>
      </div>
    `);
    const gsapTargets: string[][] = [];
    const fakeWindow = {
      document,
      __timelines: {},
      gsap: {
        timeline: () => ({
          fromTo(targets: Element[], _from: unknown, _to: unknown) {
            gsapTargets.push(Array.from(targets).map((t) => t.textContent || ""));
            return this;
          },
        }),
      },
    };
    const wrapped = wrapScopedCompositionScript(
      `
var tl = gsap.timeline({ paused: true });
tl.fromTo('#intro .title', { opacity: 0 }, { opacity: 1, duration: 0.5 }, 0.2);
window.__timelines['intro'] = tl;
`,
      "intro",
      "[HyperFrames] composition script error:",
      undefined,
      "intro",
      "intro",
    );

    new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);

    // The scoped script should resolve '#intro .title' against the
    // data-hf-authored-id="intro" element, finding the .title child.
    expect(gsapTargets).toEqual([["HELLO"]]);
  });
});
