import { describe, it, expect } from "vitest";
import { buildMotionPathGeometry, nearestPointOnPath } from "./motionPathGeometry";
import type { ReadTween } from "../../hooks/gsapRuntimeKeyframes";

const kf = (percentage: number, x: number, y: number) => ({ percentage, properties: { x, y } });

describe("buildMotionPathGeometry", () => {
  it("builds a linear path with keyframe-ref nodes from an x/y tween", () => {
    const read: ReadTween = { keyframes: [kf(0, 10, 20), kf(100, 200, 80)] };
    const geo = buildMotionPathGeometry(read);
    expect(geo).not.toBeNull();
    expect(geo!.kind).toBe("linear");
    expect(geo!.points).toBe("10,20 200,80");
    expect(geo!.nodes).toEqual([
      { x: 10, y: 20, ref: { type: "keyframe", pct: 0 } },
      { x: 200, y: 80, ref: { type: "keyframe", pct: 100 } },
    ]);
  });

  it("preserves order and percentages for intermediate keyframes", () => {
    const read: ReadTween = { keyframes: [kf(0, 0, 0), kf(50, 50, 90), kf(100, 100, 0)] };
    const geo = buildMotionPathGeometry(read);
    expect(geo!.nodes.map((n) => n.ref)).toEqual([
      { type: "keyframe", pct: 0 },
      { type: "keyframe", pct: 50 },
      { type: "keyframe", pct: 100 },
    ]);
  });

  it("builds an arc path with waypoint-index refs when arcPath is present", () => {
    const read: ReadTween = {
      keyframes: [kf(0, 0, 0), kf(50, 60, 40), kf(100, 120, 10)],
      arcPath: { enabled: true, autoRotate: false, segments: [{ curviness: 1 }, { curviness: 1 }] },
    };
    const geo = buildMotionPathGeometry(read);
    expect(geo!.kind).toBe("arc");
    expect(geo!.nodes.map((n) => n.ref)).toEqual([
      { type: "waypoint", index: 0 },
      { type: "waypoint", index: 1 },
      { type: "waypoint", index: 2 },
    ]);
  });

  it("returns null for a tween with no positional keyframes", () => {
    const read: ReadTween = {
      keyframes: [
        { percentage: 0, properties: { opacity: 0 } },
        { percentage: 100, properties: { opacity: 1 } },
      ],
    };
    expect(buildMotionPathGeometry(read)).toBeNull();
  });

  it("draws a single-axis (x-only) tween, defaulting the missing axis to 0", () => {
    // Regression: an `x`-only tween (e.g. `to({ x: -260 })`) carries no `y`, so the
    // builder used to skip every node → no path until the user added the 2nd axis.
    const read: ReadTween = {
      keyframes: [
        { percentage: 0, properties: { x: 0 } },
        { percentage: 100, properties: { x: -260 } },
      ],
    };
    const geo = buildMotionPathGeometry(read);
    expect(geo).not.toBeNull();
    expect(geo!.points).toBe("0,0 -260,0"); // y defaults to 0 → horizontal path
  });

  it("draws a y-only tween too (x defaults to 0)", () => {
    const read: ReadTween = {
      keyframes: [
        { percentage: 0, properties: { y: 0 } },
        { percentage: 100, properties: { y: 500 } },
      ],
    };
    expect(buildMotionPathGeometry(read)!.points).toBe("0,0 0,500");
  });

  it("excludes keyframes missing a coordinate without throwing", () => {
    const read: ReadTween = {
      keyframes: [kf(0, 10, 20), { percentage: 50, properties: { x: 100 } }, kf(100, 200, 80)],
    };
    const geo = buildMotionPathGeometry(read);
    expect(geo!.nodes).toHaveLength(2);
    expect(geo!.points).toBe("10,20 200,80");
  });

  it("returns null when fewer than two valid nodes remain", () => {
    const read: ReadTween = { keyframes: [kf(0, 10, 20)] };
    expect(buildMotionPathGeometry(read)).toBeNull();
  });

  it("returns null for null input", () => {
    expect(buildMotionPathGeometry(null)).toBeNull();
  });
});

describe("nearestPointOnPath", () => {
  const nodes = [
    { x: 0, y: 0 },
    { x: 100, y: 0 },
    { x: 100, y: 100 },
  ];

  it("projects onto the nearest segment and reports its index + fraction", () => {
    const p = nearestPointOnPath(50, 20, nodes);
    expect(p).toEqual({ x: 50, y: 0, segIndex: 0, t: 0.5, dist: 20 });
  });

  it("reports t at the segment endpoints (0 at start, clamps to 1 past the end)", () => {
    expect(nearestPointOnPath(0, 5, nodes)).toMatchObject({ segIndex: 0, t: 0 });
    expect(nearestPointOnPath(110, 0, nodes)).toMatchObject({ segIndex: 0, t: 1 });
  });

  it("picks the second segment when closer to it", () => {
    const p = nearestPointOnPath(120, 50, nodes);
    expect(p).toMatchObject({ x: 100, y: 50, segIndex: 1 });
  });

  it("clamps to an endpoint when the projection falls past the segment", () => {
    const p = nearestPointOnPath(-40, -10, nodes);
    expect(p).toMatchObject({ x: 0, y: 0, segIndex: 0 });
  });

  it("returns null for fewer than two nodes", () => {
    expect(nearestPointOnPath(0, 0, [{ x: 0, y: 0 }])).toBeNull();
  });
});
