import { expect, test } from "bun:test";

import {
  detectMacroCalls,
  parseCreateMacro,
  parseMacroAnnotations,
  parseMacroLibrary,
  splitStatements,
} from "../src/commands/pipeline/duckdbMacros.ts";

// These mirror the accepted-shape cases of the backend parser
// (backend/parsers/windmill-parser/src/duckdb_macros.rs) so the local graph
// derives the same macro registry the server records at deploy.

test("parseCreateMacro: scalar, OR REPLACE / TEMP / TABLE, FUNCTION alias", () => {
  expect(parseCreateMacro("CREATE MACRO surrogate_key(a, b) AS md5(concat_ws('||', a, b))")).toEqual(
    { name: "surrogate_key", params: "a, b", isTable: false },
  );
  expect(parseCreateMacro("CREATE OR REPLACE TEMP MACRO top_n(t_max) AS TABLE SELECT * FROM t")).toEqual(
    { name: "top_n", params: "t_max", isTable: true },
  );
  // FUNCTION is DuckDB's alias for MACRO; name lowercased
  expect(parseCreateMacro("create function Dbl(a) as a * 2")).toEqual(
    { name: "dbl", params: "a", isTable: false },
  );
});

test("parseCreateMacro: default params + nested/string parens in the param list", () => {
  expect(
    parseCreateMacro("CREATE MACRO safe_div(a, b, fallback := (0)) AS CASE WHEN b = 0 THEN fallback ELSE a / b END")?.params,
  ).toBe("a, b, fallback := (0)");
  expect(parseCreateMacro("CREATE MACRO f(sep := '(') AS concat(sep, 'x')")?.params).toBe("sep := '('");
});

test("parseCreateMacro: rejects non-macro, qualified/quoted names, missing params/body", () => {
  expect(parseCreateMacro("SELECT 1")).toBeNull();
  expect(parseCreateMacro("CREATE TABLE t(x int)")).toBeNull();
  expect(parseCreateMacro("CREATE MACRO lake.m(a) AS a")).toBeNull();
  expect(parseCreateMacro('CREATE MACRO "weird name"(a) AS a')).toBeNull();
  expect(parseCreateMacro("CREATE MACRO m AS 1")).toBeNull(); // no params
  expect(parseCreateMacro("CREATE MACRO m(a) AS")).toBeNull(); // empty body
  expect(parseCreateMacro("CREATE OR MACRO m(a) AS a")).toBeNull(); // OR without REPLACE
});

test("splitStatements strips comments/strings and parseMacroLibrary skips setup", () => {
  const lib = `-- macros
ATTACH 'x.duckdb' AS ext;
CREATE MACRO m1(a) AS a; -- inline comment ; not a split
CREATE MACRO m2(b) AS b + 1;`;
  expect(splitStatements("SELECT 1; -- c ; still\nSELECT ';' AS x;").length).toBe(2);
  expect(parseMacroLibrary(lib).map((m) => m.name)).toEqual(["m1", "m2"]);
});

test("detectMacroCalls: word-boundary, case-insensitive, skips qualified/strings/comments", () => {
  const ns = new Set(["dbl", "avg_x"]);
  const found = detectMacroCalls("SELECT DBL(1), my_dbl(2), avg_x (3) FROM t", ns);
  expect([...found].sort()).toEqual(["avg_x", "dbl"]);
  expect(detectMacroCalls("SELECT lake.dbl(1)", new Set(["dbl"])).size).toBe(0);
  expect(detectMacroCalls("SELECT 'dbl(1)'", new Set(["dbl"])).size).toBe(0);
  expect(detectMacroCalls("-- dbl(1)\nSELECT 1", new Set(["dbl"])).size).toBe(0);
  expect(detectMacroCalls("SELECT dbl FROM t", new Set(["dbl"])).size).toBe(0); // no call parens
});

test("parseMacroAnnotations: leading-header only, marker stands alone, `// use` needs a path token", () => {
  expect(parseMacroAnnotations("-- macros\nCREATE MACRO m(a) AS a;")).toEqual({
    macros: true,
    useLibs: [],
  });
  // marker with trailing prose is not the macros marker
  expect(parseMacroAnnotations("-- macros are below\nSELECT 1;").macros).toBe(false);
  // `// use` accumulates path-shaped tokens, dedups, and stops at the body
  expect(
    parseMacroAnnotations("-- pipeline\n-- use f/lib/a\n-- use f/lib/a\n-- use notapath\nSELECT 1;").useLibs,
  ).toEqual(["f/lib/a"]);
  // an annotation after code is ignored (leading header only)
  expect(parseMacroAnnotations("SELECT 1;\n-- use f/lib/a").useLibs).toEqual([]);
});

test("parseMacroAnnotations accepts `//`, `--`, and `#` prefixes (backend parity, any language)", () => {
  // The canonical backend parser strips `//`/`--`/`#` regardless of language, so
  // a DuckDB library headed with `// macros` (not `-- macros`) must be detected.
  expect(parseMacroAnnotations("// macros\nCREATE MACRO m(a) AS a;").macros).toBe(true);
  expect(parseMacroAnnotations("# macros\nSELECT 1;").macros).toBe(true);
  expect(parseMacroAnnotations("// use f/lib/a\nSELECT 1;").useLibs).toEqual(["f/lib/a"]);
  // mixed prefixes in one header both register
  expect(parseMacroAnnotations("// pipeline\n-- use f/lib/a\nSELECT 1;").useLibs).toEqual(["f/lib/a"]);
});
