import { test, expect } from "bun:test";
import { writeFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { generate, parse } from "../src/index.ts";
import { compileWireFile } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test";

test("explicit static rendering disables hydration metadata", () => {
  const output = compileWireFile(`page StaticPage {
    render = "static"
    state count = 0
    view { <button @click="count++">{count}</button> }
  }`);
  expect(output).toContain('export const __wrnexusRender = "static"');
  expect(output).toContain('data-wrn-hydrate="none"');
  expect(output).toContain('export const __wrnexusHydrate = "none"');
});

test("named data loads compile as parallel typed data entries", () => {
  const output = compileWireFile(`page Users {
    load users { return ["Ada"] }
    load server teams { return ["Core"] }
    view { <p>Users</p> }
  }`);
  expect(output).toContain("await Promise.all");
  expect(output).toContain('return { "users": __values[0], "teams": __values[1] }');
});

let seq = 0;
/** Compile a `.wrn` source and import the resulting module. */
async function compileAndImport(src: string): Promise<Record<string, unknown>> {
  const dir = join(tmpdir(), "wire-compiler-test");
  mkdirSync(dir, { recursive: true });
  const file = join(dir, `m${seq++}.ts`);
  writeFileSync(file, compileWireFile(src));
  return import(pathToFileURL(file).href);
}

type CompiledBehavior = {
  functions: string;
  lifecycle: {
    mount?: string;
    update?: string;
    unmount?: string;
  };
  watches: Array<{
    state: string;
    body: string;
  }>;
};

function extractCompiledBehavior(output: string): CompiledBehavior {
  const marker = 'data-wrn-behavior="';

  const start = output.indexOf(marker);

  expect(start).toBeGreaterThanOrEqual(0);

  const valueStart = start + marker.length;

  const valueEnd = output.indexOf('"', valueStart);

  expect(valueEnd).toBeGreaterThan(valueStart);

  const encoded = output.slice(valueStart, valueEnd);

  expect(encoded).toMatch(/^[A-Za-z0-9+/]+=*$/);

  return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")) as CompiledBehavior;
}

test("parses a page with a layout member", () => {
  const ast = parse(`page Home {\n  layout = "public"\n  view { <h1>Hi</h1> }\n}`);
  expect(ast.kind).toBe("page");
  expect(ast.name).toBe("Home");
  expect(ast.layout).toBe("public");
});

test("top-level imports are preserved and available to SSR view expressions", async () => {
  const source = `import { posix } from "node:path";

page Home {
  view { <PublicHeader signInHref="{posix.join('/sso', '/sign-in')}" /> }
}`;
  const ast = parse(source);
  expect(ast.imports).toEqual(['import { posix } from "node:path";']);

  const mod = await compileAndImport(source);
  const render = mod.default as (ctx: unknown) => string;
  expect(String(await render({}))).toContain('signInHref="/sso/sign-in"');
});

test("parses a component with props and state", () => {
  const ast = parse(
    `component C {\n  props {\n    n = 0\n  }\n  state count = n\n  view { <b>{count}</b> }\n}`,
  );
  expect(ast.kind).toBe("component");
  expect(ast.props.map((p) => p.name)).toEqual(["n"]);
  expect(ast.states.map((s) => s.name)).toEqual(["count"]);
});

test("component event declarations compile to public root metadata", async () => {
  const source = `component EventButton {
    props {
      label = "Save"
      @event complete = function
    }
    view { <button>{label}</button> }
  }`;
  const ast = parse(source);
  const output = generate(ast);
  const mod = await compileAndImport(source);
  const render = mod.render as (props: Record<string, unknown>) => string;

  expect(ast.events).toEqual([{ name: "complete" }]);
  expect(output).toContain('data-wrn-events="complete"');
  expect(render({})).toContain('data-wrn-events="complete"');
});

test("explicit nested component mounts serialize structured expression props", () => {
  const output = compileWireFile(`component Shell {
    state brand = { "label": "WRNexus" }
    state items = [{ "label": "Home", "href": "/" }]
    view {
      <div data-component="Navbar" brand={brand} items={items}></div>
    }
  }`);

  expect(output).toContain('data-component="Navbar"');
  expect(output).toContain('brand="${__wireProp(brand)}"');
  expect(output).toContain('items="${__wireProp(items)}"');
});

test("typed props, required props, state, custom types, and function parameters compile", () => {
  const source = `component TypedPicker {
  types {
    interface DateOption { value: string; label: string }
    type ChangeHandler = (value: string) => void
  }
  props {
    value: string = ""
    options: DateOption[] = []
    required: boolean = false
    onChange: ChangeHandler
  }
  state open: boolean = false
  functions {
    function choose(option: DateOption, index: number): void {
      open = false
      onChange(option.value)
    }
  }
  view { <button @click="choose(options[0], 0)">{value}</button> }
}`;
  const ast = parse(source);
  expect(ast.props.map(({ name, valueType, required }) => ({ name, valueType, required }))).toEqual(
    [
      { name: "value", valueType: "string", required: false },
      { name: "options", valueType: "DateOption[]", required: false },
      { name: "required", valueType: "boolean", required: false },
      { name: "onChange", valueType: "ChangeHandler", required: true },
    ],
  );
  expect(ast.states[0]).toMatchObject({ name: "open", valueType: "boolean", expr: "false" });
  const output = generate(ast);
  expect(output).toContain("interface DateOption");
  expect(output).toContain("export interface TypedPickerProps");
  expect(output).toContain('"onChange": ChangeHandler;');
  expect(output).toContain("let open: boolean = (false)");
  expect(output).toContain("function choose(option: DateOption, index: number): void");
  const behavior = extractCompiledBehavior(output);
  expect(behavior.functions).toContain("function choose(option, index)");
  expect(behavior.functions).not.toContain("option: DateOption");
});

test("typed declarations reject obvious initializer mismatches", () => {
  expect(() =>
    parse(`component Invalid {
      props {
        required: boolean = "yes"
      }
      view { <div></div> }
    }`),
  ).toThrow("Prop 'required' is declared as boolean, but its initializer is string");
  expect(() => parse(`page Invalid { state count: number = false\n view { <p></p> } }`)).toThrow(
    "State 'count' is declared as number, but its initializer is boolean",
  );
});

test("typed props enforce required values and runtime-compatible input", async () => {
  const component = await compileAndImport(`component TypedInput {
    props {
      label: string
      count: number = 0
      enabled: boolean = false
    }
    view { <span>{label}:{count}:{enabled}</span> }
  }`);
  const render = component.render as (props?: Record<string, unknown>) => string;
  expect(() => render()).toThrow("TypedInput requires prop 'label' (string)");
  const rendered = render({ label: "Total", count: "4", enabled: "true" });
  expect(rendered).toContain('<span data-text="label">Total</span>');
  expect(rendered).toContain('<span data-text="count">4</span>');
  expect(rendered).toContain('<span data-text="enabled">true</span>');
  expect(() => render({ label: "Total", count: "many" })).toThrow("Expected a finite number prop");
  expect(() => render({ label: "Total", enabled: "sometimes" })).toThrow("Expected a boolean prop");
});

test("HTML view: void elements, boolean attrs, comments, lone <", () => {
  const ast = parse(
    `component T {\n  view {\n    <input type="text" disabled>\n    <br/>\n    <!-- comment -->\n    <p>a < b</p>\n  }\n}`,
  );
  const html = compileWireFile(
    `component T {\n  view {\n    <input type="text" disabled>\n    <br/>\n    <!-- comment -->\n    <p>a < b</p>\n  }\n}`,
  );
  expect(html).toContain("<input");
  expect(html).toContain(" disabled");
  expect(html).toContain("<br>");
  expect(html).not.toContain("comment");
  expect(html).toContain("a < b");
  void ast;
});

test("dynamic HTML boolean attributes are omitted when false", async () => {
  const mod = await compileAndImport(`component BooleanAttributes {
    props {
      disabled = false
      checked = false
      required = false
      loading = false
    }
    view {
      <button disabled="{disabled || loading}">Save</button>
      <input checked="{checked}" required="{required}" />
    }
  }`);
  const render = mod.render as (props?: Record<string, unknown>) => string;

  const enabled = render();
  expect(enabled).not.toContain("<button disabled");
  expect(enabled).not.toContain("<input checked");
  expect(enabled).not.toContain("<input required");

  const disabled = render({ disabled: "true", checked: "true", required: "true" });
  expect(disabled).toContain("<button disabled");
  expect(disabled).toContain("<input checked");
  expect(disabled).toContain(" required");
});

test("components safely forward undeclared HTML attributes to their root", async () => {
  const mod = await compileAndImport(`component ForwardingButton {
    props {
      label = "Button"
      variant = "default"
      disabled = false
    }
    view {
      <button disabled="{disabled}">{label}</button>
    }
  }`);
  const render = mod.render as (props?: Record<string, unknown>) => string;
  const html = render({
    label: "Save",
    variant: "primary",
    formaction: "/submit-form",
    formenctype: "application/x-www-form-urlencoded",
    formmethod: "post",
    popovertarget: "myPopover",
    "aria-describedby": "save-help",
    "data-testid": "save",
    onclick: "alert(1)",
    style: "display:none",
    "data-wrn-bind-0": "unsafe",
  });

  expect(html).toContain('formaction="/submit-form"');
  expect(html).toContain('formenctype="application/x-www-form-urlencoded"');
  expect(html).toContain('formmethod="post"');
  expect(html).toContain('popovertarget="myPopover"');
  expect(html).toContain('aria-describedby="save-help"');
  expect(html).toContain('data-testid="save"');
  expect(html).not.toContain("variant=");
  expect(html).not.toContain("onclick=");
  expect(html).not.toContain("style=");
  expect(html).not.toContain('data-wrn-bind-0="unsafe"');
});

test("an explicit attrs spread overrides automatic root forwarding", async () => {
  const mod = await compileAndImport(`component WrappedControl {
    props {
      label = "Control"
      class = ""
    }
    view {
      <span class="{class}">
        <button {...attrs}>{label}</button>
      </span>
    }
  }`);
  const render = mod.render as (props?: Record<string, unknown>) => string;
  const html = render({ label: "Save", class: "wrapper", formaction: "/save" });

  expect(html).toContain('<span class="wrapper"');
  expect(html).not.toContain('<span formaction="/save"');
  expect(html).toContain('<button formaction="/save">');
  expect(html).toContain('<span data-text="label">Save</span>');
});

test("prop-driven component renders SSR content and exposes reactive prop signals", async () => {
  const mod = await compileAndImport(
    `component Button {\n  props {\n    label = "Button"\n    variant = "default"\n    class = ""\n  }\n  view { <button class="wire-btn wire-btn--{variant} {class}">{label}</button> }\n}`,
  );
  const render = mod.render as (p: Record<string, string>) => string;
  const out = render({ label: "Save <b>", variant: "primary", class: "mt-2" });
  expect(out).toContain('class="wire-btn wire-btn--primary mt-2"');
  expect(out).toContain("Save &lt;b&gt;"); // html-escaped
  expect(out).toContain("data-scope");
  expect(out).toContain('data-text="label"');
});

test("stateful component keeps both prop and state text reactive", async () => {
  const mod = await compileAndImport(
    `component Counter {\n  props {\n    start = 0\n    label = "Count"\n  }\n  state count = start\n  view { <button @click="count++">{label}: {count}</button> }\n}`,
  );
  const render = mod.render as (p: Record<string, string>) => string;
  const out = render({ start: "10", label: "Score" });
  expect(out).toContain('data-scope="start: 10, label: &quot;Score&quot;, count: 10"');
  expect(out).toContain('data-on-click="count++"');
  expect(out).toContain('<span data-text="label">Score</span>: <span data-text="count">10</span>');

  expect(out).toContain('data-scope="');

  expect(out).toContain("start: 10");

  expect(out).toContain("label: &quot;Score&quot;");

  expect(out).toContain("count: 10");
});

test("prop type coercion follows the default value's type", async () => {
  const mod = await compileAndImport(
    `component X {\n  props {\n    n = 0\n    s = "x"\n    b = false\n  }\n  view { <i>{n}{s}{b}</i> }\n}`,
  );
  // needsScope=false → seeds nothing; verify via a stateful variant instead:
  const mod2 = await compileAndImport(
    `component Y {\n  props {\n    n = 0\n  }\n  state v = n\n  view { <i @click="v++">{v}</i> }\n}`,
  );
  const out = (mod2.render as (p: Record<string, string>) => string)({ n: "42" });
  expect(out).toContain("v: 42"); // "42" coerced to number 42 (not '42')
  void mod;
});

test("platform events compile through the native runtime bridge", () => {
  const out = compileWireFile(`page Platform {
  state count = 0
  view {
    <button @browser-click="count++" @mobile-click="count = count + 2">Run</button>
  }
}`);
  expect(out).toContain('data-on-wrnexus-browser-click="count++"');
  expect(out).toContain('data-on-wrnexus-mobile-click="count = count + 2"');
});

test("{t:key} compiles to a data-t marker (both pages and components)", () => {
  const page = compileWireFile(`page P {\n  view { <h1>{t:home.title}</h1> }\n}`);
  expect(page).toContain('<span data-t="home.title"></span>');
  const comp = compileWireFile(`component C {\n  view { <h1>{t:x}</h1> }\n}`);
  expect(comp).toContain('<span data-t="x"></span>');
});

test("{t:} does NOT wrap a page in a data-scope (regression)", () => {
  // Only state / events force a reactive scope, not i18n markers.
  const page = compileWireFile(`page P {\n  view { <h1>{t:a}</h1> <p>{t:b}</p> }\n}`);
  expect(page).not.toContain("data-scope");
});

test("page state text bakes its initial value into a reactive data-text span", () => {
  const page = compileWireFile(
    `page Reactive {\n  state count = 3\n  view { <p>Count is {count}, doubled {count * 2}</p> }\n}`,
  );
  expect(page).toContain('<span data-text="count">3</span>'); // no-JS sees "3"
  expect(page).toContain('<span data-text="count * 2">6</span>'); // expression evaluated
  expect(page).toContain("data-scope"); // state still forces a reactive scope
});

test("page state attributes bake their initial value and retain a reactive binding", () => {
  const page = compileWireFile(`page Password {
  state show = false
  view {
    <input type="{show ? 'text' : 'password'}" aria-label="{show ? 'Hide' : 'Show'}">
    <button @click="show = !show">Toggle</button>
  }
}`);
  expect(page).toContain('type="password"');
  expect(page).toContain('aria-label="Show"');
  expect(page.match(/data-wrn-bind-/g)).toHaveLength(2);
  expect(page).toContain("show ? 'text' : 'password'");
});

test("request-dependent page state attributes resolve during server rendering", async () => {
  const mod = await compileAndImport(`page Playground {
  state label = ctx.url.searchParams.get("label") ?? "Default"
  state loading = ctx.url.searchParams.get("loading") ?? "false"
  view {
    <div data-component="Button" label="{label}" loading="{loading}"></div>
  }
}`);
  const render = mod.default as (ctx: { url: URL }) => Promise<string>;
  const html = await render({
    url: new URL("https://example.test/playground?label=Visible&loading=false"),
  });

  expect(html).toContain('label="Visible"');
  expect(html).toContain('loading="false"');
  expect(html).not.toContain('label=""');
});

test("data-for: loop-variable mustaches stay literal (not baked server-side)", () => {
  const comp = compileWireFile(
    `component TodoList {\n  state todos = []\n  view { <ul><li data-for="t in todos">{t.text}</li></ul> }\n}`,
  );
  // The <li> template keeps `{t.text}` for the client list renderer, and the
  // loop variable is never baked (which would be a server-side ReferenceError).
  expect(comp).toContain('data-for="t in todos"');
  expect(comp).toContain("{t.text}");
  expect(comp).not.toContain("__wireHtml(t.text)");
});

test("named slots pass through to the component output", () => {
  const out = compileWireFile(
    `component Card {\n  view { <div><slot name="header"></slot><slot></slot></div> }\n}`,
  );
  expect(out).toContain('<slot name="header">');
  expect(out).toContain("<slot></slot>");
});

test("capitalized self-closing tags compile to component mounts", () => {
  const source = `
page ComponentPage {
  view {
    <UserCard
      name="Ajay"
      role="Admin"
    />
  }
}
`;

  const output = generate(parse(source));

  expect(output).toContain('data-component="UserCard"');

  expect(output).toContain('name="Ajay"');

  expect(output).toContain('role="Admin"');

  expect(output).not.toContain("<UserCard");
});

test("capitalized component tags preserve slot children", () => {
  const source = `
page SlotPage {
  view {
    <Card>
      <h2>Hello</h2>
      <p>Component content</p>
    </Card>
  }
}
`;

  const output = generate(parse(source));

  expect(output).toContain('data-component="Card"');

  expect(output).toContain("<h2>Hello</h2>");

  expect(output).toContain("<p>Component content</p>");
});

test("component tags work inside reusable components", () => {
  const source = `
component ParentCard {
  props {
    title = "Hello"
  }

  view {
    <ChildCard title="{title}" />
  }
}
`;

  const output = generate(parse(source));

  expect(output).toContain('data-component="ChildCard"');

  expect(output).toContain('title="${__wireProp(title)}"');

  expect(output).toContain("function __wireProp");
});

test("native array and object props serialize through component mounts", async () => {
  const mod = await compileAndImport(`
component NativeProps {
  state items = [{"label":"Home","href":"/"}]
  view {
    <Navbar items={items} options={{"dense":true}} />
    <Sidebar items={[{"label":"Cases","href":"/cases"}]} />
  }
}
`);
  const render = mod.render as (props?: Record<string, unknown>) => string;
  const html = render();

  expect(html).toContain(
    'items="[{&quot;label&quot;:&quot;Home&quot;,&quot;href&quot;:&quot;/&quot;}]"',
  );
  expect(html).toContain('options="{&quot;dense&quot;:true}"');
  expect(html).toContain(
    'items="[{&quot;label&quot;:&quot;Cases&quot;,&quot;href&quot;:&quot;/cases&quot;}]"',
  );
});

test("malformed structured prop strings fail with the prop name", async () => {
  const mod = await compileAndImport(`component StructuredProps {
    props { items = [] options = {} }
    view { <div></div> }
  }`);
  const render = mod.render as (props?: Record<string, unknown>) => string;

  expect(() => render({ items: "not-json" })).toThrow("Expected an array prop 'items'");
  expect(() => render({ options: "[]" })).toThrow("Expected an object prop 'options'");
});

test("parses a layout block", () => {
  const ast = parse(`
layout AppLayout {
  view {
    <main>{content}</main>
  }
}
`);

  expect(ast.kind).toBe("layout");
  expect(ast.name).toBe("AppLayout");
});

test("layouts compile as reusable render modules", () => {
  const output = generate(
    parse(`
layout AppLayout {
  view {
    <main>{content}</main>
  }
}
`),
  );

  expect(output).toContain('export const __wrnexusLayout = "AppLayout"');

  expect(output).toContain("${__wireRaw(content)}");

  expect(output).toContain("export function render");
});
test("parses component lifecycle hooks", () => {
  const ast = parse(`
component BackToTop {
  state visible = false

  lifecycle {
    mount {
      visible = true
    }

    update {
      console.log(visible)
    }

    unmount {
      visible = false
    }
  }

  view {
    <button>Top</button>
  }
}
`);

  expect(ast.lifecycle.mount).toContain("visible = true");

  expect(ast.lifecycle.update).toContain("console.log(visible)");

  expect(ast.lifecycle.unmount).toContain("visible = false");
});
test("parses state watchers", () => {
  const ast = parse(`
component Menu {
  state open = false

  watch open {
    console.log("Open changed", open)
  }

  view {
    <button>{open}</button>
  }
}
`);

  expect(ast.watches).toEqual([
    {
      state: "open",
      body: expect.stringContaining('console.log("Open changed", open)'),
    },
  ]);
});
test("rejects watching an undeclared state", () => {
  expect(() =>
    parse(`
component Broken {
  watch missing {
    console.log(missing)
  }

  view {
    <div>Broken</div>
  }
}
`),
  ).toThrow("Cannot watch undeclared state 'missing'");
});
test("rejects unknown lifecycle hooks", () => {
  expect(() =>
    parse(`
component Broken {
  lifecycle {
    created {
      console.log("invalid")
    }
  }

  view {
    <div>Broken</div>
  }
}
`),
  ).toThrow("Unknown lifecycle hook 'created'");
});
test("component functions compile into browser behavior metadata", () => {
  const output = generate(
    parse(`
component Counter {
  state count = 0

  functions {
    function increment() {
      count += 1
    }
  }

  view {
    <button @click="increment()">
      {count}
    </button>
  }
}
`),
  );

  expect(output).toContain("data-wrn-behavior");

  expect(output).toContain("function increment()");

  expect(output).toContain("export const __wrnexusBehavior");
});
test("component lifecycle hooks compile into behavior metadata", () => {
  const output = generate(
    parse(`
component LifecycleTest {
  state ready = false

  lifecycle {
    mount {
      ready = true
    }

    update {
      console.log(ready)
    }

    unmount {
      ready = false
    }
  }

  view {
    <div>{ready}</div>
  }
}
`),
  );

  const behavior = extractCompiledBehavior(output);

  expect(behavior.lifecycle.mount).toContain("ready = true");

  expect(behavior.lifecycle.update).toContain("console.log(ready)");

  expect(behavior.lifecycle.unmount).toContain("ready = false");
});
test("component watchers compile into behavior metadata", () => {
  const output = generate(
    parse(`
component WatchTest {
  state visible = false

  watch visible {
    console.log("changed", visible)
  }

  view {
    <div>{visible}</div>
  }
}
`),
  );

  const behavior = extractCompiledBehavior(output);

  expect(behavior.watches).toHaveLength(1);

  expect(behavior.watches[0]?.state).toBe("visible");

  expect(behavior.watches[0]?.body).toContain('console.log("changed", visible)');
});
test("behavior-only components still receive a reactive scope", () => {
  const output = generate(
    parse(`
component BehaviorOnly {
  functions {
    function sayHello() {
      console.log("hello")
    }
  }

  lifecycle {
    mount {
      sayHello()
    }
  }

  view {
    <div>Hello</div>
  }
}
`),
  );

  expect(output).toContain("data-scope");

  expect(output).toContain("data-wrn-behavior");
});
test("component behavior metadata safely encodes multiline code", () => {
  const output = generate(
    parse(`
component BackToTop {
  state visible = false

  functions {
    function updateVisibility() {
      visible = window.scrollY > 500
    }

    function scrollToTop() {
      window.scrollTo({
        top: 0,
        behavior: "smooth"
      })
    }
  }

  lifecycle {
    mount {
      updateVisibility()
    }
  }

  watch visible {
    console.log(
      value,
      previous
    )
  }

  view {
    <button @click="scrollToTop()">
      Top
    </button>
  }
}
`),
  );

  expect(output).toContain("data-wrn-behavior=");

  expect(output).not.toContain('data-wrn-behavior="{');

  expect(output).not.toContain('data-wrn-behavior="%7B');
});

test("component watchers compile into behavior metadata", () => {
  const output = generate(
    parse(`
component WatchTest {
  state visible = false

  watch visible {
    console.log("changed", visible)
  }

  view {
    <div>{visible}</div>
  }
}
`),
  );

  const match = /data-wrn-behavior="([^"]+)"/.exec(output);

  expect(match).not.toBeNull();

  const behavior = extractBehavior(output);

  expect(behavior.watches).toEqual([
    {
      state: "visible",
      body: expect.stringContaining('console.log("changed", visible)'),
    },
  ]);
});
test("component lifecycle hooks compile into behavior metadata", () => {
  const output = generate(
    parse(`
component LifecycleTest {
  state ready = false

  lifecycle {
    mount {
      ready = true
    }

    update {
      console.log(ready)
    }

    unmount {
      ready = false
    }
  }

  view {
    <div>{ready}</div>
  }
}
`),
  );

  const match = /data-wrn-behavior="([^"]+)"/.exec(output);

  expect(match).not.toBeNull();

  const behavior = extractBehavior(output);

  expect(behavior.lifecycle.mount).toContain("ready = true");

  expect(behavior.lifecycle.update).toContain("console.log(ready)");

  expect(behavior.lifecycle.unmount).toContain("ready = false");
});
function extractBehavior(output: string): {
  functions: string;
  lifecycle: {
    mount?: string;
    update?: string;
    unmount?: string;
  };
  watches: Array<{
    state: string;
    body: string;
  }>;
} {
  const match = /data-wrn-behavior="([^"]+)"/.exec(output);

  expect(match).not.toBeNull();

  return JSON.parse(Buffer.from(match![1], "base64").toString("utf8"));
}
test("component interpolated class attributes remain reactive", () => {
  const output = generate(
    parse(`
component CookieToggle {
  state personalizationCookies = false

  view {
    <span
      class="base {personalizationCookies ? 'enabled' : 'disabled'}"
    >
      Cookie
    </span>
  }
}
`),
  );

  const match = output.match(/data-wrn-bind-class="([^"]+)"/);

  expect(match).not.toBeNull();

  const encodedBinding = match?.[1] ?? "";

  const decodedBinding = encodedBinding
    .replaceAll("&quot;", '"')
    .replaceAll("&#39;", "'")
    .replaceAll("&amp;", "&")
    .replaceAll("&lt;", "<")
    .replaceAll("&gt;", ">");

  const binding = JSON.parse(decodedBinding) as [string, string];

  expect(binding[0]).toBe("class");

  expect(binding[1]).toBe("base {personalizationCookies ? 'enabled' : 'disabled'}");
});
test("parses class directives containing Tailwind arbitrary values", () => {
  const ast = parse(`
component PageHeader {
  state centered = false

  view {
    <div
      class="py-12"
      class:lg:grid-cols-[minmax(0,1fr)_auto]="!centered"
      class:dark:bg-white/10="centered"
      class:w-[calc(100%-2rem)]="centered"
    >
      Header
    </div>
  }
}
`);

  expect(ast).toBeDefined();
});
test("compiles Tailwind arbitrary values in class directives", () => {
  const output = generate(
    parse(`
component PageHeader {
  state centered = false

  view {
    <div
      class="py-12"
      class:lg:grid-cols-[minmax(0,1fr)_auto]="!centered"
      class:dark:bg-white/10="centered"
    >
      Header
    </div>
  }
}
`),
  );

  expect(output).toContain("lg:grid-cols-[minmax(0,1fr)_auto]");

  expect(output).toContain("dark:bg-white/10");
});
test("class directives accept single-quoted brace expressions", () => {
  const output = generate(
    parse(`
component MarketingSectionHeader {
  props {
    align = "start"
  }

  view {
    <header
      class="max-w-3xl"
      class:mx-auto='{align === "center"}'
      class:text-center='{align === "center"}'
    >
      Header
    </header>
  }
}
`),
  );

  expect(output).toContain('${(align === "center") ? " mx-auto" : ""}');
  expect(output).toContain('${(align === "center") ? " text-center" : ""}');
  expect(output).not.toContain('({align === "center"})');
  expect(output).toContain("&quot;align === \\\\&quot;center\\\\&quot;&quot;");
});

test("components support each blocks", () => {
  const output = generate(
    parse(`
component PageHeader {
  props {
    highlights = []
  }

  view {
    <div>
      {#each highlights as highlight}
        <span>{highlight.title}</span>
      {/each}
    </div>
  }
}
`),
  );

  expect(output).toContain("highlights");

  expect(output).toContain("highlight.title");

  expect(output).not.toContain("supported in pages, not components");
});
test("components support if blocks", () => {
  const output = generate(
    parse(`
component Banner {
  props {
    visible = true
  }

  view {
    {#if visible}
      <div>Visible</div>
    {:else}
      <div>Hidden</div>
    {/if}
  }
}
`),
  );

  expect(output).toContain("visible");
  expect(output).toContain("Visible");
  expect(output).toContain("Hidden");
});
test("component array props support each blocks", () => {
  const output = generate(
    parse(`
component HighlightList {
  props {
    highlights = []
  }

  view {
    {#each highlights as highlight}
      <p>{highlight.title}</p>
    {/each}
  }
}
`),
  );

  expect(output).toContain("Array.isArray(def)");

  expect(output).toContain("JSON.parse(v)");

  expect(output).toContain("highlights");
});
test("page component props evaluate whole expressions", () => {
  const output = generate(
    parse(`
page Home {
  view {
    <HighlightList
      enabled="{true}"
      highlights="{[
        { title: 'First' },
        { title: 'Second' }
      ]}"
    />
  }
}
`),
  );

  expect(output).toContain("__wrnexusPropAttr(true)");

  expect(output).toContain("__wrnexusPropAttr([");
});
test("nested component props retain parent-owned reactive bindings", () => {
  const output = generate(
    parse(`component Parent {
      props { value = 0 }
      state count = value
      view { <Child value={count} /> }
    }`),
  );

  expect(output).toContain("data-wrn-prop-bind-0");
  expect(output).toContain("[&quot;value&quot;,&quot;{count}&quot;]");
  expect(output).toContain('lowerName.startsWith("data-wrn-prop-bind-")');
});
test("component functions are available during server rendering", () => {
  const output = generate(
    parse(`
component FAQAccordion {
  props {
    allowMultiple = false
    items = []
  }

  state openIndexes = []

  functions {
    function isOpen(index) {
      return openIndexes.includes(index)
    }

    function toggleItem(index) {
      if (isOpen(index)) {
        openIndexes = openIndexes.filter(
          (itemIndex) =>
            itemIndex !== index
        )
      } else if (allowMultiple) {
        openIndexes = [
          ...openIndexes,
          index
        ]
      } else {
        openIndexes = [index]
      }
    }
  }

  view {
    {#each items as item, index}
      <button
        class:hidden="!isOpen(index)"
        @click="toggleItem(index)"
      >
        {item.title}
      </button>
    {/each}
  }
}
`),
  );

  expect(output).toContain("function isOpen(index)");

  expect(output).toContain("function toggleItem(index)");

  expect(output).toContain("let openIndexes =");

  expect(output).toContain("isOpen(index)");
});
test("data-for conditional classes do not evaluate loop variables during SSR", () => {
  const output = generate(
    parse(`
component Accordion {
  props {
    items = []
  }

  state openIndexes = []

  functions {
    function isOpen(index) {
      return openIndexes.includes(index)
    }
  }

  view {
    <article
      data-for="item, index in items"
      class:open="isOpen(index)"
    >
      {item.title}
    </article>
  }
}
`),
  );

  expect(output).toContain('data-for="item, index in items"');

  expect(output).toContain("data-wrn-class-0");

  expect(output).not.toContain('${(isOpen(index)) ? " open" : ""}');
});

test("data-for parser supports optional stable key expressions", async () => {
  const { parseForExpr } = await import("../src/codegen.ts");

  expect(parseForExpr("item, index in items key item.id")).toEqual({
    item: "item",
    index: "index",
    list: "items",
    key: "item.id",
  });

  expect(parseForExpr("item in items")).toEqual({
    item: "item",
    index: undefined,
    list: "items",
    key: undefined,
  });
});
test("server-rendered each locals work in reactive handlers", async () => {
  const firstLocals = Buffer.from(
    JSON.stringify({
      item: {
        question: "First",
      },
      index: 0,
    }),
    "utf8",
  ).toString("base64");

  const secondLocals = Buffer.from(
    JSON.stringify({
      item: {
        question: "Second",
      },
      index: 1,
    }),
    "utf8",
  ).toString("base64");

  const dom = mountHtml(`
      <div data-scope="openIndex: -1">
        <article
          data-wrn-loop-locals="${firstLocals}"
        >
          <button
            type="button"
            data-on-click="openIndex = index"
          >
            First
          </button>

          <span
            data-wrn-class-0='["open","openIndex === index"]'
          ></span>
        </article>

        <article
          data-wrn-loop-locals="${secondLocals}"
        >
          <button
            type="button"
            data-on-click="openIndex = index"
          >
            Second
          </button>

          <span
            data-wrn-class-0='["open","openIndex === index"]'
          ></span>
        </article>
      </div>
    `);

  const buttons = dom.querySelectorAll("button") as HTMLButtonElement[];

  buttons[1]!.click();

  await Promise.resolve();

  const articles = dom.querySelectorAll("article");

  expect(articles[0]!.querySelector("span")?.classList.contains("open")).toBe(false);

  expect(articles[1]!.querySelector("span")?.classList.contains("open")).toBe(true);
}, 20_000);

test("WRN 0.3 metadata, computed values, loaders, and actions compile additively", () => {
  const source = `page Dashboard {
    runtime = "universal"
    hydrate = "idle"
    state count = 2
    computed { doubled = count * 2 }
    effect { console.log(doubled) }
    security { auth = "required" }
    load server { return { count: 2 } }
    load client { return { refreshed: true } }
    action save(input) { return input }
    view { <button @click="count++">{doubled}</button> }
  }`;
  const output = compileWireFile(source);

  expect(output).toContain('export const __wrnexusRuntime = "universal"');
  expect(output).toContain('export const __wrnexusHydrate = "idle"');
  expect(output).toContain("export async function __wrnexusLoad");
  expect(output).toContain("export async function __wrnexusClientLoad");
  expect(output).toContain("export async function save(input)");
  expect(output).toContain("export const __wrnexusActions");
  expect(output).toContain('data-wrn-hydrate="idle"');
});
test("page state is available inside server-rendered if blocks", () => {
  const source = `
page Pricing {
  state billingCycle = "monthly"

  view {
    {#if billingCycle === "annual"}
      <p>Annual</p>
    {:else}
      <p>Monthly</p>
    {/if}
  }
}
`;

  const ast = parse(source);
  const code = generate(ast);

  expect(code).toContain("const { billingCycle } = __state;");
  expect(code).toContain('billingCycle === "annual"');
});
test("page state SSR condition renders without ReferenceError", async () => {
  const root = mkdtempSync(join(tmpdir(), "wrnexus-page-state-"));
  const generatedFile = join(root, "pricing.generated.ts");

  try {
    const source = `
page Pricing {
  state billingCycle = "monthly"

  view {
    {#if billingCycle === "annual"}
      <p>Annual</p>
    {:else}
      <p>Monthly</p>
    {/if}
  }
}
`;

    const code = generate(parse(source));

    expect(code).toContain("const { billingCycle } = __state;");
    expect(code).toContain('billingCycle === "annual"');

    writeFileSync(generatedFile, code, "utf8");

    const moduleUrl = pathToFileURL(generatedFile).href + `?test=${Date.now()}-${Math.random()}`;

    const generatedModule = await import(moduleUrl);

    const html = await generatedModule.default({});

    expect(html).toContain("<p>Monthly</p>");
    expect(html).not.toContain("<p>Annual</p>");
  } finally {
    rmSync(root, {
      recursive: true,
      force: true,
    });
  }
});
test("page state is declared for server-rendered control blocks", () => {
  const source = `
page Pricing {
  state billingCycle = "monthly"

  view {
    {#if billingCycle === "annual"}
      <p>Annual</p>
    {:else}
      <p>Monthly</p>
    {/if}
  }
}
`;

  const code = generate(parse(source));

  expect(code).toContain("const { billingCycle } = __state;");

  expect(code).toContain('billingCycle === "annual"');
});

test("style blocks compile with stable page/component/layout metadata", () => {
  const page = compileWireFile(`page StyledPage {
    style { .shared { color: red; } }
    view { <main class="shared">Page</main> }
  }`);
  const component = compileWireFile(`component StyledCard {
    style { .shared { color: blue; } }
    view { <article class="shared">Card</article> }
  }`);
  const layout = compileWireFile(`layout StyledLayout {
    style { .shared { color: green; } }
    view { <div class="shared"><slot></slot></div> }
  }`);

  expect(page).toContain('data-wrnexus-style-kind="page"');
  expect(component).toContain('data-wrnexus-style-kind="component"');
  expect(layout).toContain('data-wrnexus-style-kind="layout"');
  expect(page).toContain("export const __wrnexusStyles");
  expect(component).toContain("export const __wrnexusStyles");
  expect(layout).toContain("export const __wrnexusStyles");
  expect(page).toMatch(/data-wrnexus-style-id="wrn-page-[a-z0-9]+"/);
});

test("strict-mode reserved prop names compile through safe local references", () => {
  const output = compileWireFile(`component Visibility {
    props { private: boolean = false }
    view { {#if private}<span>Private</span>{/if} }
  }`);

  expect(output).toContain("const __p_private: boolean");
  expect(output).toContain("(__p_private) ?");
  expect(output).not.toContain("const private:");
});

// data-show holds an expression the client re-evaluates, so it must survive
// compilation verbatim. Interpolating `{open || visible}` to the literal
// "false" froze every overlay that used the braced form -- Drawer, Dropdown,
// Popover, ContextMenu and Tooltip could never open.
test("data-show keeps its expression instead of being interpolated away", () => {
  const source = `component Panel {
  props {
    open: boolean = false
  }
  state visible = false
  view {
    <div class="panel" data-show='{open || visible}'>body</div>
  }
}
`;
  const code = compileWireFile(source, "Panel.wrn");
  expect(code).toContain(`data-show="open || visible"`);
  expect(code).not.toContain(`data-show="false"`);
});

// Props and state are destructured into the same scope as the function body,
// so a local that shares one of their names used to emit a redeclaration and
// break the whole generated module with a parse error naming no source line.
test("a local variable may shadow a prop without breaking the module", () => {
  const source = `component Sized {
  props {
    size: number = 10
  }
  state total = 0
  functions {
    client function recompute() {
      var size = 4
      var total = size * 2
      return total
    }
  }
  view {
    <div class="sized">{total}</div>
  }
}
`;
  const code = compileWireFile(source, "Sized.wrn");
  // The alias must be dropped, not emitted alongside the local declaration.
  expect(code).not.toContain("const { size } = context.props;");
  expect(code).toContain("var size = 4");
});

// A boolean attribute whose expression names a loop variable cannot be
// resolved on the server -- __wireBooleanAttr runs at render time, where the
// loop variable does not exist, and the generated module failed outright.
test("a boolean attribute bound to a loop variable binds on the client", () => {
  const source = `component Picker {
  state rows = []
  functions {
    shared function isOn(row) {
      return row.on
    }
  }
  view {
    <ul>
      <li data-for="row in rows" data-key="row.id">
        <input type="checkbox" checked='{isOn(row)}' />
      </li>
    </ul>
  }
}
`;
  const code = compileWireFile(source, "Picker.wrn");
  expect(code).not.toContain('__wireBooleanAttr("checked"');
  expect(code).toContain("data-wrn-bind-");
});

test("block comments inside props report the authoring restriction", () => {
  expect(() =>
    compileWireFile(`component Example {
      props {
        /* use a line comment */
        label: string = "Example"
      }
      view { <span>{label}</span> }
    }`),
  ).toThrow("Block comments are not allowed inside props {}; use // line comments instead");
});

test("state page reports its collision with the page keyword", () => {
  expect(() =>
    compileWireFile(`page Example {
      state page = 1
      view { <span>{page}</span> }
    }`),
  ).toThrow("State name 'page' collides with the WRN 'page' keyword");
});
