import { test, expect } from "bun:test";
import { writeFileSync, mkdirSync } 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";

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("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("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("stateless component bakes props into server HTML (zero JS)", 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).not.toContain("data-scope"); // no reactivity → no scope
});

test("stateful component: state text baked into a reactive data-text span, prop text baked", 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: 'Score', count: 10\"");
  expect(out).toContain('data-on-click="count++"');
  // label baked as static text; count baked as its initial value AND kept live.
  expect(out).toContain('Score: <span data-text="count">10</span>');
});

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("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="${__wireAttr(title)}"');
});

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'}");
});
