import { describe, expect, it } from "vitest";
import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js";
import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js";
import type { Pose, PoseTarget } from "../../src/kdl/types.js";

async function createRuntime() {
  const runtime = createKdlWorkerRuntime();
  await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] });
  return runtime;
}

function pose(x: number, y: number, z: number): Pose {
  return {
    position: [x, y, z],
    quaternion: [0, 0, 0, 1]
  };
}

describe("pose transform and offset API", () => {
  it("normalizes pose inputs from rpy and quaternion forms", async () => {
    const runtime = await createRuntime();
    const rpyResponse = await dispatchKdlRpcRequest(runtime, {
      id: 2,
      method: "normalizePose",
      payload: [{ xyz: [1, 2, 3], rpy: [0, 0, Math.PI / 2] }]
    });
    const quatResponse = await dispatchKdlRpcRequest(runtime, {
      id: 3,
      method: "normalizePose",
      payload: [{ xyz: [0, 0, 0], quat: [0, 0, 0, 2] }]
    });

    expect(rpyResponse.ok).toBe(true);
    expect((rpyResponse.result as Pose).position).toEqual([1, 2, 3]);
    expect((rpyResponse.result as Pose).quaternion[2]).toBeCloseTo(Math.SQRT1_2);
    expect((rpyResponse.result as Pose).quaternion[3]).toBeCloseTo(Math.SQRT1_2);
    expect(quatResponse.result).toMatchObject({
      position: [0, 0, 0],
      quaternion: [0, 0, 0, 1]
    });
  });

  it("composes poses and computes an inverse pose", async () => {
    const runtime = await createRuntime();
    const composeResponse = await dispatchKdlRpcRequest(runtime, {
      id: 4,
      method: "composePose",
      payload: [pose(1, 0, 0), pose(0, 2, 0)]
    });
    expect(composeResponse.result).toMatchObject({
      position: [1, 2, 0],
      quaternion: [0, 0, 0, 1]
    });

    const inverseResponse = await dispatchKdlRpcRequest(runtime, {
      id: 5,
      method: "inversePose",
      payload: [pose(1, 2, 3)]
    });
    expect(inverseResponse.result).toMatchObject({
      position: [-1, -2, -3],
      quaternion: [0, 0, 0, 1]
    });

    const identityResponse = await dispatchKdlRpcRequest(runtime, {
      id: 6,
      method: "composePose",
      payload: [pose(1, 2, 3), inverseResponse.result]
    });
    expect((identityResponse.result as Pose).position[0]).toBeCloseTo(0);
    expect((identityResponse.result as Pose).position[1]).toBeCloseTo(0);
    expect((identityResponse.result as Pose).position[2]).toBeCloseTo(0);
  });

  it("applies frame, target, and tool using the same order as FK", async () => {
    const runtime = await createRuntime();
    const target: PoseTarget = {
      id: "pick",
      pose: pose(0.5, 0.1, 0.2)
    };
    const response = await dispatchKdlRpcRequest(runtime, {
      id: 7,
      method: "applyToolAndFrame",
      payload: [target, pose(0, 0, 0.18), pose(0.8, 0, 0.2)]
    });

    const result = response.result as Pose;
    expect(result.position[0]).toBeCloseTo(1.3);
    expect(result.position[1]).toBeCloseTo(0.1);
    expect(result.position[2]).toBeCloseTo(0.58);
    expect(result.quaternion).toEqual([0, 0, 0, 1]);
  });

  it("applies offset in frame/world by left composition and tool by right composition", async () => {
    const runtime = await createRuntime();
    const target: PoseTarget = {
      id: "pick",
      pose: {
        position: [1, 2, 3],
        quaternion: [0, 0, Math.SQRT1_2, Math.SQRT1_2]
      },
      frame: pose(10, 0, 0)
    };

    const frameOffset = await dispatchKdlRpcRequest(runtime, {
      id: 8,
      method: "applyOffset",
      payload: [target, { mode: "frame", xyz: [0.1, 0, 0] }]
    });
    const worldOffset = await dispatchKdlRpcRequest(runtime, {
      id: 9,
      method: "applyOffset",
      payload: [target, { mode: "world", xyz: [0, 0.2, 0] }]
    });
    const toolOffset = await dispatchKdlRpcRequest(runtime, {
      id: 10,
      method: "applyOffset",
      payload: [target, { mode: "tool", xyz: [0.1, 0, 0] }]
    });

    expect((frameOffset.result as PoseTarget).id).toBe("pick");
    expect((frameOffset.result as PoseTarget).frame).toEqual(target.frame);
    expect((frameOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1.1);
    expect((frameOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2);
    expect((worldOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1);
    expect((worldOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2.2);
    expect((toolOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1);
    expect((toolOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2.1);
  });

  it("returns structured diagnostics for invalid pose inputs", async () => {
    const runtime = await createRuntime();
    const response = await dispatchKdlRpcRequest(runtime, {
      id: 11,
      method: "normalizePose",
      payload: [{ xyz: [1, 2], rpy: [0, 0, 0] }]
    });

    expect(response).toMatchObject({
      ok: false,
      error: {
        code: "KDL_INVALID_POSE",
        diagnostics: [
          {
            severity: "error",
            code: "KDL_INVALID_POSE"
          }
        ]
      }
    });
  });
});
