import { render } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { AgentThread } from '../../src/components/agent-workspace/agent-thread';

vi.mock('sonner', () => ({
  toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
}));

type Message = {
  _id: string;
  _creationTime: number;
  role: 'user' | 'assistant';
  content: string;
  status?: string;
};

const message = (id: string, content: string): Message => ({
  _id: id,
  _creationTime: Date.now(),
  role: 'assistant',
  content,
  status: 'complete',
});

const baseProps = {
  jobId: 'job-1',
  events: [],
  interactions: [],
  workspaceChanges: [],
  disabled: false,
  agentTurnActive: true,
  onOpenFile: vi.fn(),
  onOpenDiff: vi.fn(),
};

const renderThread = (messages: Message[]) =>
  render(
    // Types are exercised at runtime; cast the fixture shapes to satisfy the
    // Doc<> generics without pulling the full backend types into the test.
    <AgentThread {...baseProps} messages={messages as never} />,
  );

/** Stubs the scroll geometry of the messages list so we control near-bottom. */
const stubScrollGeometry = (
  container: HTMLElement,
  geometry: { scrollHeight: number; scrollTop: number; clientHeight: number },
) => {
  const node = container.querySelector<HTMLElement>('.overflow-y-auto');
  if (!node) throw new Error('scroll container not found');
  const scrollTo = vi.fn();
  Object.defineProperty(node, 'scrollTo', {
    configurable: true,
    value: scrollTo,
  });
  Object.defineProperty(node, 'scrollHeight', {
    configurable: true,
    get: () => geometry.scrollHeight,
  });
  Object.defineProperty(node, 'clientHeight', {
    configurable: true,
    get: () => geometry.clientHeight,
  });
  let scrollTop = geometry.scrollTop;
  Object.defineProperty(node, 'scrollTop', {
    configurable: true,
    get: () => scrollTop,
    set: (value: number) => {
      scrollTop = value;
    },
  });
  return { node, scrollTo };
};

afterEach(() => {
  vi.clearAllMocks();
});

describe('AgentThread auto-scroll', () => {
  it('does not yank to the bottom while the user is scrolled up', () => {
    const { container, rerender } = renderThread([message('m1', 'first')]);
    const { scrollTo } = stubScrollGeometry(container, {
      scrollHeight: 1000,
      scrollTop: 100,
      clientHeight: 200,
    });

    rerender(
      <AgentThread
        {...baseProps}
        messages={[message('m1', 'first'), message('m2', 'second')] as never}
      />,
    );

    expect(scrollTo).not.toHaveBeenCalled();
  });

  it('auto-scrolls to newest when already near the bottom', () => {
    const { container, rerender } = renderThread([message('m1', 'first')]);
    const { scrollTo } = stubScrollGeometry(container, {
      scrollHeight: 1000,
      scrollTop: 780,
      clientHeight: 200,
    });

    rerender(
      <AgentThread
        {...baseProps}
        messages={[message('m1', 'first'), message('m2', 'second')] as never}
      />,
    );

    expect(scrollTo).toHaveBeenCalledWith({
      top: 1000,
      behavior: 'smooth',
    });
  });
});
