import { describe, expect, it, vi } from "vitest";

import {
  ThreadMotionCoordinator,
  type ThreadMotionGeometry,
  type ThreadMotionScheduler,
} from "@/components/thread/thread-motion";

function motionHarness(initial?: Partial<ThreadMotionGeometry>) {
  let nextFrameId = 1;
  let cameraFollowing = false;
  const frames = new Map<number, FrameRequestCallback>();
  let geometry: ThreadMotionGeometry = {
    scrollTop: 400,
    scrollHeight: 1_900,
    clientHeight: 500,
    maxScrollTop: 1_400,
    composerHeight: 120,
    promptTop: 404,
    ...initial,
  };
  const scheduler: ThreadMotionScheduler = {
    request: vi.fn((callback) => {
      const id = nextFrameId;
      nextFrameId += 1;
      frames.set(id, callback);
      return id;
    }),
    cancel: vi.fn((id) => {
      frames.delete(id);
    }),
  };
  const camera = {
    cancel: vi.fn(() => {
      cameraFollowing = false;
    }),
    dispose: vi.fn(),
    jumpTo: vi.fn(),
    followTo: vi.fn(() => "settled" as const),
    isFollowing: vi.fn(() => cameraFollowing),
    navigateTo: vi.fn(() => {
      cameraFollowing = true;
      return "started" as const;
    }),
  };
  const onGeometry = vi.fn();
  const onAutoFollow = vi.fn();
  const measure = vi.fn(() => geometry);
  const coordinator = new ThreadMotionCoordinator({
    camera,
    measure,
    onGeometry,
    onAutoFollow,
    scheduler,
  });
  const advanceFrame = () => {
    const pending = [...frames.entries()];
    frames.clear();
    for (const [, callback] of pending) callback(16);
  };
  const setGeometry = (next: Partial<ThreadMotionGeometry>) => {
    const updated = { ...geometry, ...next };
    geometry = {
      ...updated,
      maxScrollTop: next.maxScrollTop
        ?? Math.max(0, updated.scrollHeight - updated.clientHeight),
    };
  };
  const setCameraFollowing = (following: boolean) => {
    cameraFollowing = following;
  };

  return {
    camera,
    coordinator,
    frames,
    measure,
    onAutoFollow,
    onGeometry,
    scheduler,
    advanceFrame,
    setCameraFollowing,
    setGeometry,
  };
}

describe("ThreadMotionCoordinator", () => {
  it("coalesces discrete invalidations into one authoritative geometry frame", () => {
    const {
      camera,
      coordinator,
      frames,
      measure,
      scheduler,
      advanceFrame,
    } = motionHarness();

    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    coordinator.invalidateGeometry();
    coordinator.invalidateGeometry();

    expect(scheduler.request).toHaveBeenCalledTimes(1);
    expect(frames).toHaveLength(1);
    expect(measure).not.toHaveBeenCalled();

    advanceFrame();

    expect(measure).toHaveBeenCalledTimes(1);
    expect(measure).toHaveBeenCalledWith("prompt-1");
    expect(camera.jumpTo).toHaveBeenCalledWith(404);
    expect(camera.followTo).toHaveBeenCalledWith(1_400);
    expect(coordinator.snapshot()).toMatchObject({
      mode: "follow-output",
      promptPositioned: true,
      measurementPending: false,
    });
  });

  it("reconciles observed layout growth before the next paint", () => {
    const {
      camera,
      coordinator,
      frames,
      scheduler,
      advanceFrame,
      setGeometry,
    } = motionHarness();

    coordinator.resumeAutoFollow();
    advanceFrame();
    camera.jumpTo.mockClear();
    scheduler.cancel.mockClear();

    setGeometry({ scrollTop: 1_400, scrollHeight: 2_054 });
    coordinator.invalidateGeometry();
    coordinator.reconcileObservedGeometry();

    expect(camera.jumpTo).toHaveBeenCalledWith(1_554);
    expect(scheduler.cancel).toHaveBeenCalledTimes(1);
    expect(frames).toHaveLength(0);
    expect(coordinator.snapshot()).toMatchObject({ measurementPending: false });
  });

  it("pins repeated output growth on each authoritative geometry frame", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    camera.cancel.mockClear();
    camera.followTo.mockClear();

    setGeometry({ scrollTop: 1_400, scrollHeight: 1_928 });
    coordinator.invalidateGeometry();
    coordinator.invalidateGeometry();
    advanceFrame();
    setGeometry({ scrollTop: 1_410, scrollHeight: 1_944 });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.followTo.mock.calls).toEqual([[1_428], [1_444]]);
    expect(camera.cancel).not.toHaveBeenCalled();
  });

  it.each([
    "geometry-before-completion",
    "completion-before-geometry",
  ] as const)("keeps following final layout for %s ordering", (ordering) => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    camera.cancel.mockClear();
    camera.followTo.mockClear();

    const applyFinalGeometry = () => {
      setGeometry({
        scrollTop: 1_400,
        scrollHeight: 2_400,
      });
      coordinator.invalidateGeometry();
    };
    if (ordering === "geometry-before-completion") applyFinalGeometry();
    coordinator.completeTurn();
    if (ordering === "completion-before-geometry") applyFinalGeometry();

    expect(coordinator.snapshot().mode).toBe("follow-completion");
    expect(camera.cancel).not.toHaveBeenCalled();
    advanceFrame();
    expect(camera.followTo).toHaveBeenLastCalledWith(1_900);

    setGeometry({
      scrollTop: 1_900,
      scrollHeight: 2_450,
    });
    coordinator.invalidateGeometry();
    advanceFrame();
    expect(camera.followTo).toHaveBeenLastCalledWith(1_950);
  });

  it("releases completion follow when composer editing begins", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    coordinator.completeTurn();
    camera.jumpTo.mockClear();
    camera.followTo.mockClear();
    camera.cancel.mockClear();

    coordinator.handleComposerInput();
    setGeometry({
      scrollTop: 1_400,
      scrollHeight: 1_940,
      composerHeight: 160,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.cancel).toHaveBeenCalledTimes(1);
    expect(camera.jumpTo).not.toHaveBeenCalled();
    expect(camera.followTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("idle");
  });

  it("remembers composer editing that begins before turn completion", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    camera.followTo.mockClear();
    camera.cancel.mockClear();

    coordinator.handleComposerInput();
    expect(coordinator.snapshot().mode).toBe("follow-output");

    coordinator.completeTurn();
    setGeometry({
      scrollTop: 1_400,
      scrollHeight: 1_940,
      composerHeight: 160,
    });
    advanceFrame();

    expect(camera.cancel).toHaveBeenCalledTimes(1);
    expect(camera.followTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("idle");
  });

  it("keeps turn identity stable when canonical replay replaces DOM ids", () => {
    const {
      camera,
      coordinator,
      measure,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-stable",
      promptId: "optimistic-prompt",
      hasOutput: true,
    });
    advanceFrame();
    camera.jumpTo.mockClear();
    measure.mockClear();

    setGeometry({ scrollHeight: 2_020, promptTop: 120 });
    coordinator.updateTurn({
      id: "turn-stable",
      promptId: "canonical-prompt",
      hasOutput: true,
    });
    advanceFrame();

    expect(measure).toHaveBeenCalledWith(null);
    expect(camera.jumpTo).not.toHaveBeenCalled();
    expect(camera.followTo).toHaveBeenLastCalledWith(1_520);
  });

  it("continues from the bottom when a running turn is restored before prompt identity", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
    } = motionHarness({
      scrollTop: 1_400,
    });

    coordinator.updateTurn({
      id: "turn-restored",
      promptId: null,
      hasOutput: true,
      entry: "restored",
    });
    advanceFrame();

    expect(camera.jumpTo).not.toHaveBeenCalled();
    expect(camera.followTo).toHaveBeenCalledWith(1_400);
    expect(coordinator.snapshot()).toMatchObject({
      mode: "follow-output",
      promptPositioned: true,
    });
  });

  it("continues measuring while browsing history but does not steal the viewport", () => {
    const {
      camera,
      coordinator,
      onGeometry,
      advanceFrame,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    camera.followTo.mockClear();
    onGeometry.mockClear();

    coordinator.takeUserControl();
    setGeometry({ scrollTop: 200, scrollHeight: 2_100 });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(onGeometry).toHaveBeenCalledTimes(1);
    expect(camera.followTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("browsing-history");

    coordinator.resumeAutoFollow();
    advanceFrame();
    expect(camera.followTo).toHaveBeenCalledWith(1_600);
  });

  it("keeps an explicitly resumed idle thread pinned to its latest bottom without animating", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness();

    coordinator.resumeAutoFollow();
    advanceFrame();
    expect(camera.jumpTo).toHaveBeenLastCalledWith(1_400);
    expect(coordinator.snapshot().mode).toBe("follow-latest");

    camera.jumpTo.mockClear();
    setGeometry({
      scrollTop: 1_400,
      scrollHeight: 2_000,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.jumpTo).toHaveBeenCalledWith(1_500);
    expect(camera.followTo).not.toHaveBeenCalled();

    coordinator.takeUserControl();
    camera.jumpTo.mockClear();
    setGeometry({
      scrollTop: 300,
      scrollHeight: 2_100,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.jumpTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("browsing-history");
  });

  it("treats scroll events as observations until explicit user intent takes control", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness({
      scrollTop: 700,
      scrollHeight: 1_200,
      clientHeight: 500,
      maxScrollTop: 700,
    });
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: false,
    });
    advanceFrame();
    camera.jumpTo.mockClear();

    setGeometry({
      scrollTop: 700,
      scrollHeight: 1_280,
    });
    expect(coordinator.observeScroll(false)).toBe("automatic");
    expect(coordinator.snapshot().mode).toBe("anchor-prompt");
    advanceFrame();
    expect(camera.jumpTo).toHaveBeenCalledWith(780);

    coordinator.takeUserControl();
    expect(coordinator.observeScroll(true)).toBe("user");
    expect(coordinator.snapshot().mode).toBe("browsing-history");

    expect(coordinator.observeScroll(false)).toBe("user");
    expect(coordinator.snapshot().mode).toBe("browsing-history");

    expect(coordinator.observeScroll(true)).toBe("automatic");
    expect(coordinator.snapshot().mode).toBe("anchor-prompt");
  });

  it("resumes shallow history browsing when user intent turns toward latest", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
    } = motionHarness({
      scrollTop: 1_400,
    });
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    camera.followTo.mockClear();

    coordinator.handleUserScrollIntent(true);
    expect(coordinator.observeScroll(true)).toBe("user");
    advanceFrame();
    expect(camera.followTo).not.toHaveBeenCalled();

    coordinator.handleUserScrollIntent(true, true);
    expect(coordinator.observeScroll(true)).toBe("automatic");
    expect(coordinator.snapshot().mode).toBe("follow-output");
    advanceFrame();
    expect(camera.followTo).toHaveBeenCalledWith(1_400);
  });

  it("resumes shallow history browsing from forward intent at the boundary", () => {
    const {
      advanceFrame,
      coordinator,
      onAutoFollow,
    } = motionHarness({
      scrollTop: 1_400,
    });
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();

    coordinator.handleUserScrollIntent(true);
    expect(coordinator.observeScroll(true)).toBe("user");

    coordinator.handleUserScrollIntent(false, true);
    expect(coordinator.snapshot().mode).toBe("follow-output");
    expect(onAutoFollow).toHaveBeenCalledTimes(1);
  });

  it("preserves history browsing when an active turn is cleared", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    coordinator.takeUserControl();
    camera.followTo.mockClear();

    coordinator.updateTurn({
      id: null,
      promptId: null,
      hasOutput: false,
    });
    advanceFrame();

    expect(coordinator.snapshot().mode).toBe("browsing-history");
    expect(camera.followTo).not.toHaveBeenCalled();
  });

  it("keeps history navigation independent from active turn completion", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    coordinator.navigateHistoryTo(900);
    camera.cancel.mockClear();

    coordinator.completeTurn();

    expect(camera.cancel).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("navigating-history");
  });

  it("moves from rail navigation to history browsing on user intent", () => {
    const {
      camera,
      coordinator,
      setCameraFollowing,
    } = motionHarness();

    coordinator.handleUserScrollIntent(false);
    expect(coordinator.snapshot().mode).toBe("idle");
    expect(camera.cancel).not.toHaveBeenCalled();

    coordinator.navigateHistoryTo(900);
    expect(camera.navigateTo).toHaveBeenCalledWith(900);
    expect(coordinator.snapshot().mode).toBe("navigating-history");
    expect(coordinator.observeScroll(false)).toBe("navigation");

    camera.cancel.mockClear();
    coordinator.handleUserScrollIntent(false);
    expect(camera.cancel).toHaveBeenCalledTimes(1);
    expect(coordinator.snapshot().mode).toBe("browsing-history");
    expect(coordinator.observeScroll(false)).toBe("user");

    coordinator.navigateHistoryTo(700);
    camera.cancel.mockClear();
    coordinator.takeUserControl();
    expect(camera.cancel).toHaveBeenCalledTimes(1);
    expect(coordinator.snapshot().mode).toBe("browsing-history");

    camera.cancel.mockClear();
    coordinator.takeUserControl();
    expect(camera.cancel).not.toHaveBeenCalled();

    coordinator.navigateHistoryTo(600);
    setCameraFollowing(false);
    expect(coordinator.observeScroll(false)).toBe("navigation");
    expect(coordinator.snapshot().mode).toBe("browsing-history");
  });

  it("retargets smooth latest navigation before resuming automatic follow", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setCameraFollowing,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    coordinator.takeUserControl();
    camera.followTo.mockClear();
    camera.navigateTo.mockClear();

    expect(coordinator.navigateLatestTo(1_400)).toBe("started");
    expect(coordinator.snapshot().mode).toBe("navigating-latest");

    setGeometry({
      scrollTop: 900,
      scrollHeight: 2_100,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.navigateTo.mock.calls).toEqual([[1_400], [1_600]]);
    expect(camera.followTo).not.toHaveBeenCalled();

    setGeometry({ scrollTop: 1_600 });
    setCameraFollowing(false);
    expect(coordinator.observeScroll(true)).toBe("navigation");
    expect(coordinator.snapshot().mode).toBe("follow-output");

    advanceFrame();
    expect(camera.followTo).toHaveBeenCalledWith(1_600);
  });

  it("keeps latest navigation through completion and final layout growth", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setCameraFollowing,
      setGeometry,
    } = motionHarness();
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: true,
    });
    advanceFrame();
    coordinator.takeUserControl();
    coordinator.navigateLatestTo(1_400);
    camera.followTo.mockClear();
    camera.navigateTo.mockClear();

    coordinator.completeTurn();
    setGeometry({
      scrollTop: 1_000,
      scrollHeight: 2_300,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.navigateTo).toHaveBeenCalledWith(1_800);
    expect(camera.followTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("navigating-latest");

    setGeometry({ scrollTop: 1_800 });
    setCameraFollowing(false);
    expect(coordinator.observeScroll(true)).toBe("navigation");
    expect(coordinator.snapshot().mode).toBe("follow-latest");

    camera.jumpTo.mockClear();
    setGeometry({
      scrollTop: 1_800,
      scrollHeight: 2_400,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.jumpTo).toHaveBeenCalledWith(1_900);
  });

  it("pins a waiting prompt to the exact lower boundary across all layout changes", () => {
    const {
      camera,
      coordinator,
      advanceFrame,
      setGeometry,
    } = motionHarness({
      scrollHeight: 930,
      clientHeight: 500,
      maxScrollTop: 430,
    });
    coordinator.updateTurn({
      id: "turn-1",
      promptId: "prompt-1",
      hasOutput: false,
    });
    advanceFrame();
    expect(camera.jumpTo).toHaveBeenLastCalledWith(430);

    setGeometry({
      composerHeight: 148,
      scrollHeight: 958,
      promptTop: 404,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.jumpTo.mock.calls).toEqual([[430], [458]]);
    expect(camera.followTo).not.toHaveBeenCalled();
    expect(coordinator.snapshot().mode).toBe("anchor-prompt");

    setGeometry({
      scrollTop: 458,
      scrollHeight: 990,
    });
    coordinator.invalidateGeometry();
    advanceFrame();

    expect(camera.jumpTo.mock.calls).toEqual([[430], [458], [490]]);
  });
});
