import { describe, expect, it, vi } from "vitest";
import { computed, ref } from "vue";
import { useChatScroll } from "../src/dimsdk/pages/ChatPage/logic/layout.ts";

if (!(globalThis as any).document) {
  const { Window } = await import("happy-dom");
  const window = new Window();
  (globalThis as any).window = window;
  (globalThis as any).document = window.document;
  Object.defineProperty(globalThis, "navigator", {
    configurable: true,
    value: window.navigator,
  });
  (globalThis as any).Node = window.Node;
  (globalThis as any).Element = window.Element;
  (globalThis as any).HTMLElement = window.HTMLElement;
  (globalThis as any).SVGElement = window.SVGElement;
  (globalThis as any).Event = window.Event;
  (globalThis as any).CustomEvent = window.CustomEvent;
  (globalThis as any).MouseEvent = window.MouseEvent;
  (globalThis as any).KeyboardEvent = window.KeyboardEvent;
  (globalThis as any).getComputedStyle = window.getComputedStyle.bind(window);
}

describe("dimsdk chat wheel", () => {
  it("prevents default when scroll applies", () => {
    const lines = computed(() =>
      Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
    );
    const mainH = computed(() => 5);
    const chatScroll = useChatScroll({ lines, mainH });
    chatScroll.stickToBottom.value = false;
    chatScroll.scrollTop.value = 0;

    let prevented = 0;
    chatScroll.onWheel({ deltaY: 1, preventDefault: () => prevented += 1 });

    expect(prevented).toBe(1);
    expect(chatScroll.scrollTop.value).toBe(2);
  });

  it("applies line-unit wheel input immediately without batching", () => {
    const previousBatchMs = process.env.DIMCODE_CHAT_SCROLL_WHEEL_BATCH_MS;
    process.env.DIMCODE_CHAT_SCROLL_WHEEL_BATCH_MS = "16";

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 10;

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(8);
    } finally {
      if (previousBatchMs == null)
        delete process.env.DIMCODE_CHAT_SCROLL_WHEEL_BATCH_MS;
      else
        process.env.DIMCODE_CHAT_SCROLL_WHEEL_BATCH_MS = previousBatchMs;
    }
  });

  it("applies transcript offsets for DOM smooth scrolling", () => {
    const originalRaf = globalThis.requestAnimationFrame;
    const originalCancelRaf = globalThis.cancelAnimationFrame;
    const frameCallbacks: FrameRequestCallback[] = [];
    let nextFrameId = 1;
    globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
      frameCallbacks.push(cb);
      return nextFrameId += 1;
    }) as typeof globalThis.requestAnimationFrame;
    globalThis.cancelAnimationFrame = (() => {}) as typeof globalThis.cancelAnimationFrame;

    try {
      const offsets: number[] = [];
      const viewports: Array<{ topPx: number; heightPx: number } | null> = [];
      const renderer = ref({
        container: document.createElement("div"),
        metrics: { cellWidth: 8, cellHeight: 10 },
        dispose: () => {},
        refresh: () => {},
        setPlaneOffset: (_plane: string, px: number) => offsets.push(px),
        setPlaneViewport: (
          _plane: string,
          viewport: { topPx: number; heightPx: number } | null,
        ) => viewports.push(viewport),
      } as any);
      const lines = computed(() =>
        Array.from({ length: 100 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 5);
      const chatScroll = useChatScroll({ lines, mainY: 3, mainH, renderer });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 10;

      expect(chatScroll.visibleStartRow.value).toBe(0);
      expect(chatScroll.visibleLines.value.length).toBe(5);
      expect(viewports.at(-1)).toEqual({ topPx: 30, heightPx: 50 });

      chatScroll.onWheel({ deltaY: 10, preventDefault: () => {} });
      expect(frameCallbacks.length).toBeGreaterThan(0);
      frameCallbacks.shift()?.(16);

      expect(chatScroll.visibleStartRow.value).toBe(-1);
      expect(chatScroll.visibleLines.value.length).toBe(7);
      expect(offsets.some((px) => px !== 0)).toBe(true);
    } finally {
      globalThis.requestAnimationFrame = originalRaf;
      globalThis.cancelAnimationFrame = originalCancelRaf;
    }
  });

  it("treats explicit pixel wheel events as pixel input even for small integer deltas", () => {
    const originalRaf = globalThis.requestAnimationFrame;
    const originalCancelRaf = globalThis.cancelAnimationFrame;
    const frameCallbacks: FrameRequestCallback[] = [];
    let nextFrameId = 1;
    globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
      frameCallbacks.push(cb);
      return nextFrameId += 1;
    }) as typeof globalThis.requestAnimationFrame;
    globalThis.cancelAnimationFrame = (() => {}) as typeof globalThis.cancelAnimationFrame;

    try {
      const offsets: number[] = [];
      const renderer = ref({
        container: document.createElement("div"),
        metrics: { cellWidth: 8, cellHeight: 10 },
        dispose: () => {},
        refresh: () => {},
        setPlaneOffset: (_plane: string, px: number) => offsets.push(px),
        setPlaneViewport: () => {},
      } as any);
      const lines = computed(() =>
        Array.from({ length: 100 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 5);
      const chatScroll = useChatScroll({ lines, mainH, renderer });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 10;

      chatScroll.onWheel({ deltaY: 1, deltaMode: 0, preventDefault: () => {} });

      expect(chatScroll.scrollTop.value).toBe(10);
      expect(frameCallbacks.length).toBeGreaterThan(0);

      frameCallbacks.shift()?.(16);

      expect(chatScroll.visibleStartRow.value).toBe(-1);
      expect(offsets.some((px) => px !== 0)).toBe(true);
    } finally {
      globalThis.requestAnimationFrame = originalRaf;
      globalThis.cancelAnimationFrame = originalCancelRaf;
    }
  });

  it("suppresses ghostty line-unit bounce from opposite wheel ticks", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 10;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      now += 10;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      now += 10;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      now += 15;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(6);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("accelerates sustained ghostty line-unit scrolling", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 40;

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(38);

      now += 12;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(32);

      now += 12;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(26);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("allows larger ghostty line-unit steps on tall viewports", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 24);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 100;

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(98);

      now += 12;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(88);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("holds ghostty reversal noise until the opposite direction is sustained", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 10;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      now += 1;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(12);

      now += 16;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(18);

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(18);

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(18);

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(18);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not reverse from a single sparse opposite ghostty tick during a slow gesture", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 100;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(102);

      now += 95;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(108);

      now += 95;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(108);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not reverse from a single very sparse opposite ghostty tick during a slow gesture", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 100;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(102);

      now += 190;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(104);

      now += 190;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(104);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not reverse from a single opposite ghostty tick after a 361ms slow-gesture gap", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 1_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 100;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(102);

      now += 361;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(102);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("starts moving immediately when leaving the top edge in ghostty", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 2_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 2;

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(0);

      now += 10;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(2);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not leave the top edge from a single opposite ghostty tick", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 2_250;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 0;

      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(0);

      now += 10;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(0);

      now += 10;
      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(2);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not snap back to the top from ghostty edge-release noise", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 2_500;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = 0;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(2);

      now += 10;
      for (let i = 0; i < 4; i += 1)
        chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(2);

      now += 20;
      for (let i = 0; i < 4; i += 1)
        chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(2);

      now += 90;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      now += 15;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      now += 15;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(0);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not leave the bottom edge from sustained ghostty edge noise", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 2_800;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = chatScroll.maxScroll.value;

      for (let i = 0; i < 6; i += 1)
        chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 10;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 100;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value - 2);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not leave the bottom edge from a single opposite tick after outward pressure", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 2_920;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = chatScroll.maxScroll.value;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 20;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 20;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value - 2);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });

  it("does not leave the bottom edge from a single opposite tick right after arriving there", () => {
    const previousGhostty = process.env.GHOSTTY_RESOURCES_DIR;
    process.env.GHOSTTY_RESOURCES_DIR = "/tmp/ghostty";
    const nowSpy = vi.spyOn(Date, "now");
    let now = 3_000;
    nowSpy.mockImplementation(() => now);

    try {
      const lines = computed(() =>
        Array.from({ length: 500 }, (_, i) => ({ text: `line ${i}`, segments: [] } as any)),
      );
      const mainH = computed(() => 8);
      const chatScroll = useChatScroll({ lines, mainH });
      chatScroll.stickToBottom.value = false;
      chatScroll.scrollTop.value = chatScroll.maxScroll.value - 2;

      chatScroll.onWheel({ deltaY: 1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 50;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value);

      now += 10;
      chatScroll.onWheel({ deltaY: -1, preventDefault: () => {} });
      expect(chatScroll.scrollTop.value).toBe(chatScroll.maxScroll.value - 2);
    } finally {
      nowSpy.mockRestore();
      if (previousGhostty == null)
        delete process.env.GHOSTTY_RESOURCES_DIR;
      else
        process.env.GHOSTTY_RESOURCES_DIR = previousGhostty;
    }
  });
});
