import { describe, expect, it } from "vitest";
import type { GrlDataDeclaration, GrlPathDeclaration, GrlProcedureDeclaration, GrlTargetDeclaration } from "../../src/grl/ast/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import { parseGrlExpression } from "../../src/grl/parser/expressionParser.js";
import {
  buildMotionContext,
  compileGrlDataDeclaration,
  compileGrlTargetDeclaration,
  compilePathToPlanRequest,
  parseIoFlowStatements
} from "../../src/grl/semantic/index.js";
import { evaluateNumberExpression } from "../../src/grl/semantic/constantExpression.js";
import { lexGrl } from "../../src/grl/lexer/index.js";

function expression(source: string) {
  return parseGrlExpression(lexGrl(source, { preserveComments: false }).filter((token) => token.kind !== "eof"));
}

describe("GRL expression arithmetic and constant folding", () => {
  it("parses arithmetic, comparison, and logical precedence into AST nodes", () => {
    expect(expression("1 + 2 * 3")).toMatchObject({
      kind: "BinaryExpression",
      operator: "+",
      right: {
        kind: "BinaryExpression",
        operator: "*"
      }
    });
    expect(expression("(1 + 2) * 3")).toMatchObject({
      kind: "BinaryExpression",
      operator: "*",
      left: {
        kind: "BinaryExpression",
        operator: "+"
      }
    });
    expect(expression("not (a == b) or c != d")).toMatchObject({
      kind: "BinaryExpression",
      operator: "or",
      left: {
        kind: "UnaryExpression",
        operator: "not"
      }
    });
  });

  it("evaluates math functions, trigonometry, units, and diagnostics", () => {
    expect(evaluateNumberExpression(expression("100 + 50 mm/s"), {
      expectedKind: "linear_velocity",
      defaultUnit: "mm/s"
    })).toBeCloseTo(0.15);
    expect(evaluateNumberExpression(expression("(10 + 5) deg"), {
      expectedKind: "angle",
      defaultUnit: "deg"
    })).toBeCloseTo(Math.PI / 12);
    expect(evaluateNumberExpression(expression("clamp(20 mm, 1 mm, 10 mm)"), {
      expectedKind: "length",
      defaultUnit: "mm"
    })).toBeCloseTo(0.01);
  });

  it("reports stable expression errors", () => {
    expect(() => evaluateNumberExpression(expression("sqrt(-1)"))).toThrowError(expect.objectContaining({
      code: "GRL_EXPR_DOMAIN"
    }));
    expect(() => evaluateNumberExpression(expression("10 mm + 2 s"))).toThrowError(expect.objectContaining({
      code: "GRL_EXPR_UNIT_MISMATCH"
    }));
    expect(() => evaluateNumberExpression(expression("1 / 0"))).toThrowError(expect.objectContaining({
      code: "GRL_EXPR_DIV_ZERO"
    }));
  });

  it("folds speed, zone, target, path event, wait, and pulse expressions", () => {
    const program = parseGrl(`language grl 0.1
module MathMotion
  const num blend_base = 5 + 5
  const speed v_pick = linear(100 + 50 mm/s)
  const speed v_safe = linear(max(50 mm/s, 200 mm/s / 2))
  const zone z_app = z(clamp(blend_base mm, 1 mm, 50 mm))
  target home = joint_target {
    joints: [0 deg, (10 + 5) deg, -90 deg]
  }
  target pick = pose_target {
    pose: pose(400 + 50 mm, 20 * 2 mm, sqrt(90000) mm, 0 deg, 0 deg, atan2(1, 1))
  }
  path main_path {
    defaults { speed: v_pick, zone: z_app }
    point p0 movej home speed linear(100 + 50 mm/s) zone z(5 + 5 mm)
    event at p0 distance 5 + 5 mm pulse io.do[1] duration 50 + 50 ms
  }
  proc main()
    wait io.di[1] == true timeout 1 + 1 s
    pulse io.do[2] duration 50 + 50 ms
  end
end
`);
    const declarations = program.module.declarations;
    const [blendBase, vPick, vSafe] = declarations.filter(
      (decl): decl is GrlDataDeclaration => decl.kind === "DataDeclaration"
    );
    expect(compileGrlDataDeclaration(blendBase!)).toMatchObject({ value: 10 });
    expect(compileGrlDataDeclaration(vPick!).value).toMatchObject({ kind: "linear" });
    expect((compileGrlDataDeclaration(vPick!).value as { velocity: number }).velocity).toBeCloseTo(0.15);
    expect(compileGrlDataDeclaration(vSafe!).value).toMatchObject({ kind: "linear" });
    expect((compileGrlDataDeclaration(vSafe!).value as { velocity: number }).velocity).toBeCloseTo(0.1);

    const [home, pick] = declarations.filter((decl): decl is GrlTargetDeclaration => decl.kind === "TargetDeclaration");
    expect(compileGrlTargetDeclaration(home!)).toMatchObject({
      target: {
        joints: [0, Math.PI / 12, -Math.PI / 2]
      }
    });
    const pickTarget = compileGrlTargetDeclaration(pick!);
    expect("pose" in pickTarget.target && pickTarget.target.pose.position).toEqual([0.45, 0.04, 0.3]);

    const context = buildMotionContext(
      declarations.filter(
        (decl): decl is GrlDataDeclaration | GrlTargetDeclaration =>
          decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration"
      )
    );
    const path = declarations.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!;
    const compiledPath = compilePathToPlanRequest(path, context, {
      startJoints: [0, 0, 0],
      sampleTime: 0.004
    });
    expect(compiledPath.request.segments[0]).toMatchObject({
      speed: { kind: "linear" },
      zone: { kind: "distance", value: 0.01 }
    });
    expect((compiledPath.request.segments[0]?.speed as { velocity: number }).velocity).toBeCloseTo(0.15);
    expect(compiledPath.events[0]).toMatchObject({ distance: 0.01 });

    const procedure = declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;
    expect(parseIoFlowStatements(procedure.bodyTokens)).toEqual([
      expect.objectContaining({ kind: "WAIT", timeout: 2 }),
      expect.objectContaining({ kind: "PULSE", duration: 0.1 })
    ]);
  });
});
