import { describe, expect, it } from "vitest";
import {
  buildClipRangeSelection,
  buildPromptCopyText,
  buildTimelineElementAgentPrompt,
  buildTimelineAgentPrompt,
  getTimelineEditCapabilities,
  hasPatchableTimelineTarget,
  resolveBlockedTimelineEditIntent,
  resolveTimelineAutoScroll,
  resolveTimelineMove,
  resolveTimelineResize,
  type TimelinePromptElement,
} from "./timelineEditing";

describe("resolveTimelineMove", () => {
  it("moves timing based on horizontal drag and snaps to centiseconds", () => {
    expect(
      resolveTimelineMove(
        {
          start: 1.25,
          track: 2,
          duration: 2,
          originClientX: 100,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 8,
          trackOrder: [0, 1, 2, 3, 4],
        },
        245,
        200,
      ),
    ).toEqual({ start: 2.7, track: 2 });
  });

  it("moves layers based on vertical drag and clamps to the allowed range", () => {
    expect(
      resolveTimelineMove(
        {
          start: 2,
          track: 1,
          duration: 3,
          originClientX: 200,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 10,
          trackOrder: [0, 1, 5, 9],
        },
        150,
        390,
      ),
    ).toEqual({ start: 1.5, track: 9 });
  });

  it("prevents moving before zero or past the last valid start", () => {
    expect(
      resolveTimelineMove(
        {
          start: 0.2,
          track: 0,
          duration: 4,
          originClientX: 300,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 6,
          trackOrder: [0, 10, 20],
        },
        -100,
        -200,
      ),
    ).toEqual({ start: 0, track: -1 });

    expect(
      resolveTimelineMove(
        {
          start: 5.8,
          track: 10,
          duration: 4,
          originClientX: 300,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 6,
          trackOrder: [0, 10, 20],
        },
        500,
        200,
      ),
    ).toEqual({ start: 6, track: 10 });
  });

  it("creates a new top track when dragged past the first row threshold", () => {
    expect(
      resolveTimelineMove(
        {
          start: 1,
          track: 0,
          duration: 2,
          originClientX: 100,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 8,
          trackOrder: [0, 10, 20],
        },
        100,
        150,
      ),
    ).toEqual({ start: 1, track: -1 });
  });

  it("creates a new bottom track when dragged past the last row threshold", () => {
    expect(
      resolveTimelineMove(
        {
          start: 1,
          track: 20,
          duration: 2,
          originClientX: 100,
          originClientY: 200,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 8,
          trackOrder: [0, 10, 20],
        },
        100,
        250,
      ),
    ).toEqual({ start: 1, track: 21 });
  });

  it("accounts for scroll displacement while dragging", () => {
    expect(
      resolveTimelineMove(
        {
          start: 1,
          track: 0,
          duration: 2,
          originClientX: 100,
          originClientY: 200,
          originScrollLeft: 0,
          originScrollTop: 0,
          currentScrollLeft: 100,
          currentScrollTop: 144,
          pixelsPerSecond: 100,
          trackHeight: 72,
          maxStart: 8,
          trackOrder: [0, 1, 2, 3],
        },
        100,
        200,
      ),
    ).toEqual({ start: 2, track: 2 });
  });
});

describe("hasPatchableTimelineTarget", () => {
  it("returns true when the clip has a DOM id", () => {
    expect(hasPatchableTimelineTarget({ domId: "hero-card" })).toBe(true);
  });

  it("returns true when the clip has a selector", () => {
    expect(hasPatchableTimelineTarget({ selector: ".hero-card" })).toBe(true);
  });

  it("returns false when the clip has no stable patch target", () => {
    expect(hasPatchableTimelineTarget({})).toBe(false);
  });
});

describe("getTimelineEditCapabilities", () => {
  it("does not disable editable audio just because it spans multiple scenes", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "audio",
        duration: 8,
        selector: "#voiceover",
        sourceDuration: 8,
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("allows full editing of generic motion clips with authored timing", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "section",
        duration: 2,
        selector: ".feature-card",
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("keeps implicit layout layers selectable but not timeline-editable", () => {
    expect(
      getTimelineEditCapabilities({
        duration: 8,
        selector: ".scene-shell",
        tag: "div",
        timingSource: "implicit",
      }),
    ).toEqual({
      canMove: false,
      canTrimStart: false,
      canTrimEnd: false,
    });
  });

  it("allows move and both trims for patchable media clips with offset support", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "video",
        duration: 2,
        selector: "#media-card",
        playbackStartAttr: "media-start",
        sourceDuration: 10,
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("treats wrapped media clips with media metadata as deterministic", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "div",
        duration: 2,
        selector: "#media-card",
        playbackStartAttr: "media-start",
        sourceDuration: 10,
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("allows full editing for patchable composition hosts", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "div",
        duration: 3,
        selector: '[data-composition-id="intro"]',
        compositionSrc: "compositions/intro.html",
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("locks all timeline edits for clips with data-timeline-locked", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "div",
        duration: 8,
        selector: '[data-composition-id="caption-highlight"]',
        compositionSrc: "compositions/components/caption-highlight.html",
        timelineLocked: true,
      }),
    ).toEqual({
      canMove: false,
      canTrimStart: false,
      canTrimEnd: false,
    });
  });

  it("allows full editing of explicitly authored generic elements", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "div",
        duration: 4,
        selector: "#hero-card",
        timingSource: "authored",
      }),
    ).toEqual({
      canMove: true,
      canTrimStart: true,
      canTrimEnd: true,
    });
  });

  it("disables all timeline edits for clips without a patchable target", () => {
    expect(
      getTimelineEditCapabilities({
        tag: "video",
        duration: 2,
        sourceDuration: 10,
      }),
    ).toEqual({
      canMove: false,
      canTrimStart: false,
      canTrimEnd: false,
    });
  });
});

describe("resolveBlockedTimelineEditIntent", () => {
  it("returns move when the clip body is blocked", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 160,
        offsetX: 80,
        handleWidth: 18,
        capabilities: {
          canMove: false,
          canTrimStart: false,
          canTrimEnd: false,
        },
      }),
    ).toBe("move");
  });

  it("returns resize-start when the left edge is blocked", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 160,
        offsetX: 8,
        handleWidth: 18,
        capabilities: {
          canMove: false,
          canTrimStart: false,
          canTrimEnd: true,
        },
      }),
    ).toBe("resize-start");
  });

  it("returns resize-end when the right edge is blocked", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 160,
        offsetX: 154,
        handleWidth: 18,
        capabilities: {
          canMove: false,
          canTrimStart: true,
          canTrimEnd: false,
        },
      }),
    ).toBe("resize-end");
  });

  it("does not block the left edge when the clip can still be moved", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 160,
        offsetX: 8,
        handleWidth: 18,
        capabilities: {
          canMove: true,
          canTrimStart: false,
          canTrimEnd: true,
        },
      }),
    ).toBe(null);
  });

  it("does not swallow the full surface of a narrow movable clip", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 12,
        offsetX: 6,
        handleWidth: 18,
        capabilities: {
          canMove: true,
          canTrimStart: false,
          canTrimEnd: false,
        },
      }),
    ).toBe(null);
  });

  it("returns null when the relevant edit is supported", () => {
    expect(
      resolveBlockedTimelineEditIntent({
        width: 160,
        offsetX: 8,
        handleWidth: 18,
        capabilities: {
          canMove: true,
          canTrimStart: true,
          canTrimEnd: true,
        },
      }),
    ).toBe(null);
  });
});

describe("buildClipRangeSelection", () => {
  it("anchors the full clip range at the click position", () => {
    expect(
      buildClipRangeSelection({ start: 1.25, duration: 3.5 }, { anchorX: 320, anchorY: 180 }),
    ).toEqual({
      start: 1.25,
      end: 4.75,
      anchorX: 320,
      anchorY: 180,
    });
  });
});
describe("resolveTimelineAutoScroll", () => {
  it("does not scroll when the pointer stays away from the edges", () => {
    expect(
      resolveTimelineAutoScroll(
        {
          left: 100,
          top: 100,
          right: 500,
          bottom: 400,
        },
        300,
        250,
      ),
    ).toEqual({ x: 0, y: 0 });
  });

  it("scrolls upward and leftward near the top-left edge", () => {
    expect(
      resolveTimelineAutoScroll(
        {
          left: 100,
          top: 100,
          right: 500,
          bottom: 400,
        },
        110,
        120,
      ),
    ).toEqual({ x: -9, y: -6 });
  });

  it("scrolls downward and rightward near the bottom-right edge", () => {
    expect(
      resolveTimelineAutoScroll(
        {
          left: 100,
          top: 100,
          right: 500,
          bottom: 400,
        },
        490,
        380,
      ),
    ).toEqual({ x: 9, y: 6 });
  });
});

describe("buildTimelineAgentPrompt", () => {
  it("includes the selected range, elements, and user request", () => {
    const elements: TimelinePromptElement[] = [
      { id: "title", tag: "div", start: 1, duration: 3, track: 0 },
      { id: "music", tag: "audio", start: 0, duration: 8, track: 2 },
    ];

    const text = buildTimelineAgentPrompt({
      rangeStart: 1,
      rangeEnd: 4,
      elements,
      prompt: "Move the title later and lower the music",
    });

    expect(text).toContain("Time range: 0:01 — 0:04");
    expect(text).toContain("#title (div)");
    expect(text).toContain("#music (audio)");
    expect(text).toContain("Move the title later and lower the music");
  });
});

describe("buildTimelineElementAgentPrompt", () => {
  it("includes the clip context and guidance for agent-based edits", () => {
    expect(
      buildTimelineElementAgentPrompt({
        id: "feature-card",
        tag: "section",
        start: 1.4,
        duration: 1.6,
        track: 1,
        sourceFile: "index.html",
        selector: "#feature-card",
      }),
    ).toContain("If this clip is animated with GSAP");
  });
});
describe("resolveTimelineResize", () => {
  it("shrinks clip duration from the right edge", () => {
    expect(
      resolveTimelineResize(
        {
          start: 1,
          duration: 3,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 10,
        },
        "end",
        40,
      ),
    ).toEqual({ start: 1, duration: 2.4, playbackStart: undefined });
  });

  it("trims media from the left edge by advancing playback start and clip start", () => {
    expect(
      resolveTimelineResize(
        {
          start: 1,
          duration: 3,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 10,
          playbackStart: 0.5,
          playbackRate: 1,
        },
        "start",
        150,
      ),
    ).toEqual({ start: 1.5, duration: 2.5, playbackStart: 1 });
  });

  it("can seed front trim from an implicit zero playback start", () => {
    expect(
      resolveTimelineResize(
        {
          start: 0,
          duration: 8,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 8,
          playbackStart: 0,
          playbackRate: 1,
        },
        "start",
        200,
      ),
    ).toEqual({ start: 1, duration: 7, playbackStart: 1 });
  });

  it("prevents extending media left past available source before media-start", () => {
    expect(
      resolveTimelineResize(
        {
          start: 1,
          duration: 3,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 10,
          playbackStart: 0.2,
          playbackRate: 1,
        },
        "start",
        0,
      ),
    ).toEqual({ start: 0.8, duration: 3.2, playbackStart: 0 });
  });

  it("trims generic element start without media offset", () => {
    expect(
      resolveTimelineResize(
        {
          start: 2,
          duration: 4,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 10,
        },
        "start",
        200,
      ),
    ).toEqual({ start: 3, duration: 3, playbackStart: undefined });
  });

  it("extends generic element start leftward to time zero", () => {
    expect(
      resolveTimelineResize(
        {
          start: 1,
          duration: 3,
          originClientX: 100,
          pixelsPerSecond: 100,
          minStart: 0,
          maxEnd: 10,
        },
        "start",
        -200,
      ),
    ).toEqual({ start: 0, duration: 4, playbackStart: undefined });
  });
});

describe("buildPromptCopyText", () => {
  it("returns a trimmed prompt for the copy-prompt action", () => {
    expect(buildPromptCopyText("  Tighten the headline timing  ")).toBe(
      "Tighten the headline timing",
    );
  });
});
