import { expect, test } from "bun:test";
import { gzipSync } from "bun";
import { generateTargets, parse } from "../src/index.ts";

const ast = parse(`component ConfirmDialog {
  props { title: string open: boolean = false }
  state { loading: boolean = false }
  server state { internalId: string = "secret" }
  outputs { confirm(payload: string) close() }
  functions {
    client async function confirm(value: string): Promise<void> {
      const saved = await server.confirm(value)
      output.confirm(saved)
    }
    server async function confirm(value: string): Promise<string> { return value }
    shared function normalize(value: string): string { return value.trim() }
  }
  view { <button>{title}</button> }
}`);

test("emits separate browser and server function targets", () => {
  const targets = generateTargets(ast);
  expect(targets.browser).toContain('"confirm": async function');
  expect(targets.browser).toContain('"normalize": function');
  expect(targets.server).toContain("async function confirm");
  expect(targets.rpc).toEqual([
    expect.objectContaining({ component: "ConfirmDialog", function: "confirm" }),
  ]);
  expect(targets.declarations).toContain("interface ConfirmDialogOutputs");
  expect(targets.declarations).toContain("interface ConfirmDialogServerCalls");
});

test("component contracts retain declared union options", () => {
  const targets = generateTargets(
    parse(`component SizeBox {
    props { size: "small" | "medium" | "large" = "medium" }
    view { <div></div> }
  }`),
  );
  expect(targets.contract.props[0]?.options).toEqual(["small", "medium", "large"]);
});

test("generates a standalone browser module for imported stores", () => {
  const targets = generateTargets(
    parse(`page store SearchStore {
    state { query: string = "" }
    client state { focused: boolean = false }
    server state { secret: string = "hidden" }
    computed { empty: boolean = query.length === 0 }
    functions { client function setQuery(value: string): void { query = value } }
    persist { storage = "session" include = ["query"] version = 1 }
  }`),
  );
  expect(targets.browser).toContain("__wrnexusStoreRegistry");
  expect(targets.browser).toContain('name: "SearchStore"');
  expect(targets.browser).toContain('"query"');
  expect(targets.browser).not.toContain('"secret": ("hidden")');
  expect(targets.declarations).toContain("interface SearchStoreInstance");
});

test("browser codegen avoids reserved prop bindings and parameter collisions", () => {
  const targets = generateTargets(
    parse(`component ReservedBindings {
    props { class: string = "" output: string = "" }
    state { value: string = "" }
    outputs { change(payload: { value: string }) }
    functions {
      client function update(output: string): void {
        value = output
      }
      client function notify(): void {
        output.change({ value: value })
      }
    }
    view { <button>{class}</button> }
  }`),
  );
  expect(targets.browser).not.toContain("const { class }");
  expect(targets.browser).not.toContain("const output = context.output;\n    const output");
  expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
});

test("RPC manifests expose only server functions referenced through server.name", () => {
  const targets = generateTargets(
    parse(`component SecureActions {
    functions {
      client async function saveClient(): Promise<void> { await server.save("ok") }
      server async function save(value: string): Promise<string> { return value }
      server function internalSecret(): string { return "secret" }
    }
    view { <button @click='saveClient()'>Save</button> }
  }`),
  );
  expect(targets.rpc.map((entry) => entry.function)).toEqual(["save"]);
  expect(targets.server).toContain("internalSecret");
});

test("generated browser stores bind typed RPC, persistence validation, and HMR", () => {
  const targets = generateTargets(
    parse(`global store UserStore {
    state { user: string | null = null count: number = 0 }
    functions {
      client async function refresh(): Promise<void> { user = await server.loadCurrentUser() }
      server async function loadCurrentUser(): Promise<string | null> { return "Ajay" }
      server function internalOnly(): string { return "secret" }
    }
    persist {
      storage = "local"
      include = ["count"]
      version = 2
      migrations { function migrate(value, fromVersion, toVersion) { return value } }
      validate { function validate(value) { return value && typeof value === "object" ? value : null } }
    }
  }`),
  );
  expect(targets.browser).toContain("const server = context.server");
  expect(targets.browser).toContain("currentDefinition.persist.migrate");
  expect(targets.browser).toContain("currentDefinition.persist.validate");
  expect(targets.browser).toContain("__wrnexusApplyStoreHotUpdate");
  expect(targets.rpc.map((entry) => entry.function)).toEqual(["loadCurrentUser"]);
});

test("browser codegen binds peer client functions through the scoped function table", () => {
  const targets = generateTargets(
    parse(`component PeerCalls {
    state { value: number = 0 }
    functions {
      client function increment(): void { value += 1 }
      client function run(): void { increment() }
    }
    view { <button @click='run()'>{value}</button> }
  }`),
  );
  expect(targets.browser).toContain('implementations["increment"]');
  const executable = new Function(
    `${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
  )() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
  const state = { value: 0 };
  const functions = executable.bindClientScope({
    state,
    props: {},
    output: {},
    server: {},
    refs: {},
  });
  functions.run?.();
  expect(state.value).toBe(1);
});

test("deferred timer state writes commit after the client function returns", async () => {
  const targets = generateTargets(
    parse(`component DeferredWrite {
      state { value: number = 0 }
      functions {
        client function later(): void { setTimeout(() => { value = 2 }, 0) }
      }
      view { <button @click='later()'>{value}</button> }
    }`),
  );
  const executable = new Function(
    `${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
  )() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
  let rendered = "0";
  const state = new Proxy(
    { value: 0 },
    {
      set(target, property, value) {
        Reflect.set(target, property, value);
        rendered = String(value);
        return true;
      },
    },
  );
  const functions = executable.bindClientScope({
    state,
    props: {},
    output: {},
    server: {},
    refs: {},
  });

  functions.later?.();
  expect(state.value).toBe(0);
  await new Promise((resolve) => setTimeout(resolve, 10));
  expect(state.value).toBe(2);
  expect(rendered).toBe("2");
});

test("the deferred commit helper does not shadow an authored commit function", () => {
  const targets = generateTargets(
    parse(`component AuthoredCommit {
      state { value: number = 0 }
      functions {
        client function commit(): void { value = 1 }
        client function save(): void { commit() }
      }
      view { <button @click='save()'>{value}</button> }
    }`),
  );

  expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
  expect(targets.browser).not.toContain("const commit = __wrnexusCommit");
});

test("generated client modules stay below decoded-size and duplication ratchets", () => {
  const states = Array.from({ length: 12 }, (_, index) => `s${index}: number = ${index}`).join(" ");
  const functions = Array.from(
    { length: 20 },
    (_, index) =>
      `client function f${index}(): void { s${index % 12} += 1 ${Array.from(
        { length: 20 },
        (_unused, peer) => (peer === index ? "" : `if (false) f${peer}()`),
      ).join(" ")} }`,
  ).join("\n");
  const browser = generateTargets(
    parse(`component ClientSizeStress {
      state { ${states} }
      functions { ${functions} }
      view { <button></button> }
    }`),
  ).browser;
  const decoded = new TextEncoder().encode(browser).length;
  const compressed = gzipSync(new TextEncoder().encode(browser)).length;

  expect(decoded).toBeLessThanOrEqual(13_000);
  expect(decoded / compressed).toBeLessThanOrEqual(10);
});
