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

test("parses explicit rendering modes and the never hydration alias", () => {
  const ast = parse(`page Marketing {
    render = "static"
    hydrate = "never"
    view { <h1>Fast</h1> }
  }`);
  expect(ast.renderMode).toBe("static");
  expect(ast.hydrate).toBe("none");
  expect(() => parse('page Invalid { render = "sometimes" view { <p>No</p> } }')).toThrow(
    "Unknown render mode",
  );
});

test("parses a declarative cache policy", () => {
  const ast = parse(`page Dashboard {
    cache {
      strategy = "stale-while-revalidate"
      ttl = "5m"
      tags = ["users", "dashboard"]
      vary = ["tenant", "language"]
    }
    view { <h1>Dashboard</h1> }
  }`);
  expect(ast.cache).toEqual({
    strategy: "stale-while-revalidate",
    ttl: "5m",
    tags: '["users", "dashboard"]',
    vary: '["tenant", "language"]',
  });
});

test("parses native array and object literals in state and component props", () => {
  const ast = parse(`
component Navigation {
  state items = [{"label":"Home","href":"/"}]
  state options = {"dense":true,"nested":{"label":"A {brace}"}}
  view {
    <Navbar items={items} />
    <Sidebar items={[{"label":"Cases","href":"/cases"}]} options={{"dense":true}} />
  }
}
`);

  expect(ast.states[0]?.expr).toBe('[{"label":"Home","href":"/"}]');
  expect(ast.states[1]?.expr).toBe('{"dense":true,"nested":{"label":"A {brace}"}}');

  const elements = ast.view.filter((node) => node.type === "element");
  const navbar = elements[0];
  const sidebar = elements[1];
  expect(navbar?.type).toBe("element");
  expect(sidebar?.type).toBe("element");
  if (navbar?.type !== "element" || sidebar?.type !== "element") return;

  expect(navbar.attrs.find((attr) => attr.name === "items")?.value).toBe("{items}");
  expect(sidebar.attrs.find((attr) => attr.name === "items")?.value).toBe(
    '{[{"label":"Cases","href":"/cases"}]}',
  );
  expect(sidebar.attrs.find((attr) => attr.name === "options")?.value).toBe('{{"dense":true}}');
});

test("parses formatted multiline arrays and objects in state", () => {
  const ast = parse(`component Navigation {
    state items = [
      {
        "label": "Home",
        "href": "/"
      },
      {
        "label": "Services",
        "children": [
          { "label": "Reports", "href": "/reports" }
        ]
      }
    ]
    state options = {
      "dense": true,
      "columns": 4
    }
    view { <nav></nav> }
  }`);

  expect(ast.states[0]?.expr).toContain('"Services"');
  expect(ast.states[0]?.expr).toContain('"Reports"');
  expect(ast.states[1]?.expr).toContain('"columns": 4');
});

test("parses WRN 0.3 execution, data, security, and reactivity blocks", () => {
  const ast = parse(`page Dashboard {
    runtime = "universal"
    hydrate = "visible"
    state count = 1
    computed {
      doubled = count * 2
    }
    effect {
      console.log(doubled)
    }
    security {
      auth = "required"
      csrf = "true"
    }
    load server {
      return { count: 1 }
    }
    load client {
      return { refreshed: true }
    }
    action save(input) {
      return input
    }
    view { <button @click="count++">{doubled}</button> }
  }`);

  expect(ast.runtime).toBe("universal");
  expect(ast.hydrate).toBe("visible");
  expect(ast.computed).toEqual([{ name: "doubled", expr: "count * 2" }]);
  expect(ast.effects).toHaveLength(1);
  expect(ast.security).toEqual({ auth: "required", csrf: "true" });
  expect(ast.loads.map((load) => load.mode)).toEqual(["server", "client"]);
  expect(ast.actions).toEqual([expect.objectContaining({ name: "save", args: ["input"] })]);
});

test("diagnoses server-only interactive roots and accessibility issues", () => {
  const diagnostics = diagnose(
    `component AvatarButton {
      runtime = "server"
      state open = false
      view {
        <button @click="open = true"><img src="/avatar.png"></button>
      }
    }`,
    { file: "AvatarButton.wrn", accessibility: true },
  );

  expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain(
    "WRN-RUNTIME-SERVER-INTERACTIVE",
  );
  expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain("WRN-A11Y-001");
});

test("formats parser diagnostics with stable codes and source locations", () => {
  const source = `page Broken { runtime = "quantum"\n view { <main></main> } }`;
  const [diagnostic] = diagnose(source, { file: "Broken.wrn" });

  expect(diagnostic?.code).toBe("WRN-RUNTIME-TARGET");
  expect(formatDiagnostic(source, diagnostic!)).toContain("Broken.wrn");
});

test("parses keyed each blocks without changing legacy loop syntax", () => {
  const keyed = parse(`component Rows {
    props { rows = [] }
    view {
      {#each rows as row, index key row.id}
        <p>{index}: {row.name}</p>
      {/each}
    }
  }`);

  const loop = keyed.view.find((node) => node.type === "each");
  expect(loop).toEqual(
    expect.objectContaining({
      type: "each",
      list: "rows",
      item: "row",
      index: "index",
      key: "row.id",
    }),
  );

  const legacy = parse(`component Rows {
    props { rows = [] }
    view { {#each rows as row}<p>{row.name}</p>{/each} }
  }`);
  expect(legacy.view.find((node) => node.type === "each")).toEqual(
    expect.objectContaining({ type: "each", key: undefined }),
  );
});

test("parses public component event declarations inside props", () => {
  const ast = parse(`component SearchBox {
    props {
      value = ""
      @event search = function
      @event clear = function
    }
    view { <input value="{value}" /> }
  }`);

  expect(ast.props.map((prop) => prop.name)).toEqual(["value"]);
  expect(ast.events).toEqual([{ name: "search" }, { name: "clear" }]);
});

test("parses edge and worker execution targets", () => {
  for (const runtime of ["edge", "worker", "service-worker"] as const) {
    expect(parse(`page Runtime { runtime = "${runtime}" view { <p>Hi</p> } }`).runtime).toBe(
      runtime,
    );
  }
});

test("maps literal unions to their runtime primitive types", async () => {
  const { runtimeTypeOf } = await import("../src/types.ts");
  expect(runtimeTypeOf('"_blank" | "_self"')).toBe("string");
  expect(runtimeTypeOf("1 | 2 | 3")).toBe("number");
  expect(runtimeTypeOf("true | false")).toBe("boolean");
});

// An apostrophe in prose is not a string. The brace scanner used to treat one
// as an opening quote and swallow every brace until the next apostrophe, so a
// comment like "the Input's slot" broke the whole component with a baffling
// "Unbalanced braces" error pointing at the block's first line.
test("comments may contain apostrophes without unbalancing a block", () => {
  const source = `component Demo {
  style {
    /* The panel's own color -- do not inherit it. */
    .demo {
      color: red;
    }
    // A trailing note about the card's border.
    .demo-b {
      color: blue;
    }
  }
  view {
    <p>Docs at https://example.com/a//b are not comments.</p>
  }
}
`;
  const ast = parse(source);
  expect(ast.name).toBe("Demo");
  expect(ast.styles.join(" ")).toContain(".demo-b");
  // The URL in view text must survive: `//` is only a comment at line start.
  expect(JSON.stringify(ast.view)).toContain("https://example.com/a//b");
});
