import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
  runAbb120SpecSuite,
  type Abb120SpecManifest,
  type Abb120SpecProgramSource
} from "../../src/suites/abb120SpecSuite.js";

const WEB_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../..");
const SPEC_ROOT = join(WEB_ROOT, "tests", "fixtures", "abb120", "spec-programs");

function loadManifest(): Abb120SpecManifest {
  return JSON.parse(readFileSync(join(SPEC_ROOT, "manifest.json"), "utf8")) as Abb120SpecManifest;
}

function loadPrograms(manifest = loadManifest()): Abb120SpecProgramSource[] {
  return manifest.programs.map((entry) => ({
    program_id: entry.program_id,
    file: entry.file,
    text: readFileSync(join(SPEC_ROOT, entry.file), "utf8")
  }));
}

describe("working2 ABB120 GRL spec suite", () => {
  it("maps grammar sections 4 through 25 to concrete Runtime, Static, or Post programs", () => {
    const manifest = loadManifest();
    const sections = manifest.coverage_matrix.map((item) => item.spec_section);
    expect(sections).toEqual(Array.from({ length: 22 }, (_value, index) => index + 4));
    expect(manifest.programs).toHaveLength(19);
    expect(manifest.programs.filter((program) => program.coverage_level === "Runtime")).toHaveLength(12);
    expect(manifest.programs.filter((program) => program.coverage_level === "Static")).toHaveLength(4);
    expect(manifest.programs.filter((program) => program.coverage_level === "Post")).toHaveLength(3);

    const programById = new Map(manifest.programs.map((program) => [program.program_id, program]));
    for (const item of manifest.coverage_matrix) {
      expect(item.program_ids.length).toBeGreaterThan(0);
      for (const programId of item.program_ids) {
        const program = programById.get(programId);
        expect(program, `${item.spec_section} references missing ${programId}`).toBeDefined();
        expect(
          item.coverage_level.split("/"),
          `${item.spec_section} coverage level must include ${programId}`
        ).toContain(program!.coverage_level);
      }
    }
  });

  it("builds a W2 job with compile, controller, queue, trace, IO, post, and roundtrip evidence", () => {
    const manifest = loadManifest();
    const job = runAbb120SpecSuite(manifest, loadPrograms(manifest), {
      now: "2026-06-28T01:30:00.000Z",
      gitRevision: "test-revision"
    });

    expect(job.job_id).toMatch(/^W2-JOB-20260628013000-[a-f0-9]{8}$/);
    expect(job.suite_id).toBe("working2-abb120-spec");
    expect(job.robot_id).toBe("abb_irb120_3_58");
    expect(job.coverage.missingSections).toEqual([]);
    expect(job.programs.map((program) => program.program_id)).toEqual(manifest.programs.map((program) => program.program_id));

    const runtime = job.programs.filter((program) => program.coverage_level === "Runtime");
    expect(runtime.every((program) => program.compile.parsed)).toBe(true);
    expect(runtime.every((program) => program.compile.sourceMapEntries > 0)).toBe(true);
    expect(runtime.every((program) => program.controller?.commands.map((command) => command.command).includes("loadProgram"))).toBe(true);
    expect(runtime.every((program) => program.controller?.commands.map((command) => command.command).includes("resetFault"))).toBe(true);
    expect(runtime.some((program) => program.program_id === "W2_60_IOWaitPulse" && Object.keys(program.io?.image ?? {}).length > 0)).toBe(true);
    expect(runtime.some((program) => (program.trace ?? []).some((event) => event.kind === "wait"))).toBe(true);
    expect(runtime.some((program) => (program.motionQueue?.items.length ?? 0) > 0)).toBe(true);
    expect(runtime.some((program) =>
      program.program_id === "W2_99_FullSpecExample" &&
      program.compile.pathCount === 2 &&
      program.compile.operationCount === 2 &&
      (program.motionQueue?.items.some((item) => item.source.kind === "path" && item.source.operationId === "pick_op") ?? false)
    )).toBe(true);
    expect(runtime.every((program) => (program.trajectory?.sampleCount ?? 0) >= 0)).toBe(true);
    expect(runtime.some((program) =>
      (program.trajectory?.frames ?? []).some((frame) =>
        frame.joints.length === 6 &&
        frame.tcp.position.length === 3 &&
        frame.tcp.quaternion.length === 4
      )
    )).toBe(true);

    for (const program of job.programs) {
      const expected = manifest.programs.find((entry) => entry.program_id === program.program_id)!;
      expect(program.status, program.program_id).toBe(expected.expected_status);
      const codes = program.diagnostics.map((diagnostic) => diagnostic.code);
      for (const code of expected.expected_diagnostics) {
        expect(codes, program.program_id).toContain(code);
      }
      expect(codes, program.program_id).not.toContain("W2_EXPECTED_DIAGNOSTIC_MISSING");
      expect(codes, program.program_id).not.toContain("W2_EXPECTED_STATUS_MISMATCH");
    }

    const diagnostics = Object.fromEntries(job.programs.map((program) => [
      program.program_id,
      program.diagnostics.map((diagnostic) => diagnostic.code)
    ]));
    expect(diagnostics.W2_E10_ExpressionDiagnostics).toEqual(expect.arrayContaining([
      "GRL_EXPR_UNKNOWN_SYMBOL",
      "GRL_EXPR_UNKNOWN_FUNCTION",
      "GRL_EXPR_ARITY",
      "GRL_EXPR_DOMAIN",
      "GRL_EXPR_DIV_ZERO",
      "GRL_EXPR_UNIT_MISMATCH"
    ]));
    expect(diagnostics.W2_E20_SemanticDiagnostics).toEqual(expect.arrayContaining([
      "GRL_SYMBOL_DUPLICATE",
      "GRL_ARGUMENT_NOT_LVALUE"
    ]));
    expect(diagnostics.W2_E30_MotionDiagnostics).toEqual(expect.arrayContaining([
      "GRL_PATH_EMPTY",
      "KDL_JOINT_LIMIT",
      "KDL_TARGET_UNREACHABLE"
    ]));
    expect(diagnostics.W2_E40_ControlFlowDiagnostics).toEqual(expect.arrayContaining([
      "GRL_BREAK_OUTSIDE_FLOW"
    ]));
    expect(diagnostics.W2_E50_RuntimeTimeout).toEqual(expect.arrayContaining([
      "VC_WAIT_TIMEOUT"
    ]));

    const post = job.programs.filter((program) => program.coverage_level === "Post");
    expect(post.every((program) => program.post?.filenames.some((filename) => filename.endsWith(".mod")))).toBe(true);
    expect(post.every((program) => program.post?.filenames.some((filename) => filename.endsWith(".ls")))).toBe(true);
    expect(post.every((program) => program.post?.filenames.some((filename) => filename.endsWith(".src")))).toBe(true);
    expect(post.every((program) => program.roundtrip?.status.match(/pass|warn/))).toBe(true);
    expect(job.artifacts.jobJson).toContain(job.job_id);
  });
});
