import { describe, expect, it } from "vitest";
import type {
  GrlDataDeclaration,
  GrlOperationDeclaration,
  GrlPathDeclaration,
  GrlProcedureDeclaration,
  GrlTargetDeclaration
} from "../../src/grl/ast/index.js";
import type { PathEventInstruction } from "../../src/grl/ir/index.js";
import { parseGrl } from "../../src/grl/parser/index.js";
import {
  buildMotionContext,
  compileOperation,
  compileOperationActionIo,
  compilePathEventIo,
  compilePathToPlanRequest,
  parseIoFlowStatements,
  type IoMap
} from "../../src/grl/semantic/index.js";

const PROGRAM = `language grl 0.1
module Main
  const speed v = joint(50 %)
  const zone zf = fine
  target home = joint_target { joints: [0 deg] }
  path io_path {
    defaults { speed: v, zone: zf }
    point p0 movej home
    event at p0 distance 0 mm pulse io.do[20] duration 100 ms
  }
  operation io_op {
    kind: handling
    path: io_path
    start_action:
      wait io.di[4] == true timeout 500 ms on_timeout alarm "part missing"
    end_action:
      pulse io.do[5] duration 250 ms
  }
  proc main()
    io.do[1] = true
    io.go[2] = 16
    io.alias.grip_close = false
    wait all(io.di[1] == true, io.di[2] == false) timeout 2 s on_timeout alarm "Clamp close timeout"
    wait any(rising(io.di[3]), falling(io.di[4]), changed(io.ai[1]))
    wait io.di[5] == true timeout 1 s on_timeout call recover
    pulse io.do[3] duration 200 ms
  end
end
`;

const IO_MAP: IoMap = {
  aliases: {
    grip_close: { domain: "do", index: 6, raw: "io.do[6]" }
  },
  allowedRanges: {
    ai: { min: 1, max: 8 },
    di: { min: 1, max: 16 },
    do: { min: 1, max: 32 },
    go: { min: 1, max: 4 }
  }
};

function declarations() {
  return parseGrl(PROGRAM).module.declarations;
}

function motionContext(decls = declarations()) {
  return buildMotionContext(
    decls.filter(
      (decl): decl is GrlDataDeclaration | GrlTargetDeclaration =>
        decl.kind === "DataDeclaration" || decl.kind === "TargetDeclaration"
    )
  );
}

function pathsByName(paths: GrlPathDeclaration[]) {
  return new Map(paths.map((path) => [path.name, path]));
}

describe("GRL IO, wait, and pulse compilation", () => {
  it("compiles procedure IO writes, wait conditions, timeout actions, and pulse traces", () => {
    const procedure = declarations().find(
      (decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration"
    )!;

    const instructions = parseIoFlowStatements(procedure.bodyTokens, IO_MAP);

    expect(instructions).toHaveLength(7);
    expect(instructions[0]).toMatchObject({
      kind: "IO_WRITE",
      target: { domain: "do", index: 1, raw: "io.do[1]" },
      value: true
    });
    expect(instructions[1]).toMatchObject({
      kind: "IO_WRITE",
      target: { domain: "go", index: 2, raw: "io.go[2]" },
      value: 16
    });
    expect(instructions[2]).toMatchObject({
      kind: "IO_WRITE",
      target: { domain: "do", index: 6, raw: "io.do[6]" },
      value: false
    });
    expect(instructions[3]).toMatchObject({
      kind: "WAIT",
      condition: "all ( io . di [ 1 ] == true , io . di [ 2 ] == false )",
      timeout: 2,
      onTimeout: { kind: "alarm", value: "Clamp close timeout" }
    });
    expect(instructions[4]).toMatchObject({
      kind: "WAIT",
      condition: "any ( rising ( io . di [ 3 ] ) , falling ( io . di [ 4 ] ) , changed ( io . ai [ 1 ] ) )"
    });
    expect(instructions[5]).toMatchObject({
      kind: "WAIT",
      condition: "io . di [ 5 ] == true",
      timeout: 1,
      onTimeout: { kind: "call", value: "recover" }
    });
    expect(instructions[6]).toMatchObject({
      kind: "PULSE",
      target: { domain: "do", index: 3, raw: "io.do[3]" },
      duration: 0.2,
      trace: [
        { time: 0, action: "set", target: { domain: "do", index: 3 }, value: true },
        { time: 0.2, action: "reset", target: { domain: "do", index: 3 }, value: false }
      ]
    });
  });

  it("validates IO addresses against configured ranges", () => {
    const procedure = parseGrl(`language grl 0.1
module Main
  proc main()
    io.do[99] = true
  end
end
`).module.declarations.find((decl): decl is GrlProcedureDeclaration => decl.kind === "ProcedureDeclaration")!;

    expect(() =>
      parseIoFlowStatements(procedure.bodyTokens, { allowedRanges: { do: { min: 1, max: 16 } } })
    ).toThrowError(expect.objectContaining({ code: "GRL_IO_ADDRESS_NOT_FOUND" }));
  });

  it("expands path event IO metadata into pulse IR without entering KDL motion segments", () => {
    const decls = declarations();
    const path = decls.find((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration")!;
    const compiled = compilePathToPlanRequest(path, motionContext(decls), {
      startJoints: [0],
      sampleTime: 0.004
    });

    expect(compiled.request.segments).toHaveLength(1);
    expect(compiled.request.events).toHaveLength(1);
    expect(compilePathEventIo(compiled.events[0]!, IO_MAP)).toEqual([
      expect.objectContaining({
        kind: "PULSE",
        target: { domain: "do", index: 20, raw: "io.do[20]" },
        duration: 0.1
      })
    ]);
  });

  it("expands operation action metadata into wait and pulse IR with preserved units", () => {
    const decls = declarations();
    const operation = decls.find(
      (decl): decl is GrlOperationDeclaration => decl.kind === "OperationDeclaration"
    )!;
    const paths = decls.filter((decl): decl is GrlPathDeclaration => decl.kind === "PathDeclaration");
    const compiled = compileOperation(operation, pathsByName(paths));

    expect(compileOperationActionIo(compiled.startActions[0]!, IO_MAP)).toEqual([
      expect.objectContaining({
        kind: "WAIT",
        condition: "io . di [ 4 ] == true",
        timeout: 0.5,
        onTimeout: { kind: "alarm", value: "part missing" }
      })
    ]);
    expect(compileOperationActionIo(compiled.endActions[0]!, IO_MAP)).toEqual([
      expect.objectContaining({
        kind: "PULSE",
        target: { domain: "do", index: 5, raw: "io.do[5]" },
        duration: 0.25
      })
    ]);
  });

  it("lexes statement fallback metadata so unit literals and booleans remain typed", () => {
    const event: PathEventInstruction = {
      timing: "at",
      pointId: "p0",
      kind: "pulse",
      data: { statement: "pulse io.do[7] duration 125 ms" }
    };

    expect(compilePathEventIo(event, IO_MAP)).toEqual([
      expect.objectContaining({
        kind: "PULSE",
        target: { domain: "do", index: 7, raw: "io.do[7]" },
        duration: 0.125
      })
    ]);
  });
});
