import { act, fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { resetStore } from '../test/storeTestUtils';
import { useStore } from '../store/useStore';
import { FrameTimeline } from './FrameTimeline';

describe('FrameTimeline', () => {
  beforeEach(() => {
    resetStore();
    vi.useRealTimers();
  });

  it('renders empty state when no frames are loaded', () => {
    render(<FrameTimeline />);

    expect(screen.getByText('暂无帧数据')).toBeInTheDocument();
    expect(screen.getByText('0')).toBeInTheDocument();
  });

  it('changes the current frame through thumbnails and range input', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
    });

    render(<FrameTimeline />);
    fireEvent.click(screen.getByAltText('frame-1'));
    expect(useStore.getState().currentFrameIndex).toBe(1);

    fireEvent.change(screen.getByRole('slider'), { target: { value: '3' } });
    expect(useStore.getState().currentFrameIndex).toBe(2);
  });

  it('shows current and total timeline time based on project fps', () => {
    useStore.setState({
      currentProject: { id: 'p1', name: 'P', status: 'ready', parse_fps: 10 },
      currentFrameIndex: 1,
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
    });

    render(<FrameTimeline />);

    expect(screen.getAllByText('00:00.10').length).toBeGreaterThan(0);
    expect(screen.getAllByText('00:00.20').length).toBeGreaterThan(0);
  });

  it('renders a processing progress bar with red annotation markers and blue propagation segments', () => {
    useStore.setState({
      currentFrameIndex: 1,
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
      masks: [
        { id: 'm1', frameId: 'f2', pathData: 'M 0 0 Z', label: 'Draft', color: '#06b6d4' },
        {
          id: 'm2',
          frameId: 'f3',
          annotationId: '9',
          pathData: 'M 0 0 Z',
          label: 'Saved',
          color: '#22c55e',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
        { id: 'outside', frameId: 'other-frame', pathData: 'M 0 0 Z', label: 'Other', color: '#fff' },
      ],
    });

    render(<FrameTimeline />);

    expect(screen.getByLabelText('视频处理进度条')).toBeInTheDocument();
    expect(screen.getByText('人工/AI 1 帧 · 自动传播 1 帧')).toBeInTheDocument();
    expect(screen.getByTestId('current-frame-line')).toHaveStyle({ left: '50%' });
    expect(screen.getByTestId('current-frame-line').className).toContain('bg-white');
    expect(screen.getAllByTestId('propagated-frame-segment')).toHaveLength(1);
    expect(screen.getByTestId('propagated-frame-segment').className).toContain('bg-blue-500');
    expect(screen.getAllByTestId('annotated-frame-marker')).toHaveLength(1);
    expect(screen.getByTestId('annotated-frame-marker').className).toContain('bg-red-500');
    expect(screen.queryByLabelText('跳转到已编辑帧 3')).not.toBeInTheDocument();
  });

  it('renders propagation history with newest bright and old records capped to one blue threshold', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
        { id: 'f4', projectId: 'p1', index: 3, url: '/4.jpg', width: 640, height: 360 },
        { id: 'f5', projectId: 'p1', index: 4, url: '/5.jpg', width: 640, height: 360 },
        { id: 'f6', projectId: 'p1', index: 5, url: '/6.jpg', width: 640, height: 360 },
        { id: 'f7', projectId: 'p1', index: 6, url: '/7.jpg', width: 640, height: 360 },
      ],
      masks: Array.from({ length: 7 }, (_, index) => ({
        id: `tracked-${index + 1}`,
        frameId: `f${index + 1}`,
        pathData: 'M 0 0 Z',
        label: 'Tracked',
        color: '#3b82f6',
        metadata: { source: 'sam2.1_hiera_tiny_propagation' },
      })),
    });

    render(
      <FrameTimeline
        propagationHistory={[
          { id: 'history-1', startFrame: 1, endFrame: 1, colorIndex: 0, label: '第一次传播' },
          { id: 'history-2', startFrame: 2, endFrame: 2, colorIndex: 1, label: '第二次传播' },
          { id: 'history-3', startFrame: 3, endFrame: 3, colorIndex: 2, label: '第三次传播' },
          { id: 'history-4', startFrame: 4, endFrame: 4, colorIndex: 3, label: '第四次传播' },
          { id: 'history-5', startFrame: 5, endFrame: 5, colorIndex: 4, label: '第五次传播' },
          { id: 'history-6', startFrame: 6, endFrame: 6, colorIndex: 5, label: '第六次传播' },
          { id: 'history-7', startFrame: 7, endFrame: 7, colorIndex: 6, label: '第七次传播' },
        ]}
      />,
    );

    const segments = screen.getAllByTestId('propagation-history-segment');
    expect(segments).toHaveLength(7);
    expect(segments[0]).toHaveAttribute('title', '第一次传播');
    expect(segments[0]).toHaveStyle({ left: '0%' });
    expect(segments[0]).toHaveAttribute('data-recency-level', '4');
    expect(segments[1]).toHaveAttribute('data-recency-level', '4');
    expect(segments[2]).toHaveAttribute('data-recency-level', '4');
    expect(segments[3]).toHaveAttribute('data-recency-level', '3');
    expect(segments[4]).toHaveAttribute('data-recency-level', '2');
    expect(segments[5]).toHaveAttribute('data-recency-level', '1');
    expect(segments[6]).toHaveAttribute('data-recency-level', '0');
    const oldestStyle = segments[0].getAttribute('style') || '';
    const newestStyle = segments[6].getAttribute('style') || '';
    expect(oldestStyle).not.toContain('linear-gradient');
    expect(newestStyle).not.toContain('linear-gradient');
    expect(segments[0].style.backgroundColor).toBe(segments[1].style.backgroundColor);
    expect(segments[6].style.backgroundColor).not.toBe(segments[0].style.backgroundColor);
  });

  it('does not color propagation history frames after all masks on those frames are gone', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
      masks: [],
    });

    render(
      <FrameTimeline
        propagationHistory={[
          { id: 'history-empty', startFrame: 1, endFrame: 3, colorIndex: 0, label: '已删除传播' },
        ]}
      />,
    );

    expect(screen.queryByTestId('propagation-history-segment')).not.toBeInTheDocument();
    expect(screen.queryByTestId('propagated-frame-segment')).not.toBeInTheDocument();
    expect(screen.queryByTestId('annotated-frame-marker')).not.toBeInTheDocument();
    expect(screen.getByText('人工/AI 0 帧 · 自动传播 0 帧')).toBeInTheDocument();
  });

  it('splits propagation history around frames that no longer have propagated masks', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
        { id: 'f4', projectId: 'p1', index: 3, url: '/4.jpg', width: 640, height: 360 },
        { id: 'f5', projectId: 'p1', index: 4, url: '/5.jpg', width: 640, height: 360 },
      ],
      masks: [
        {
          id: 'tracked-2',
          frameId: 'f2',
          pathData: 'M 0 0 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
        {
          id: 'tracked-4',
          frameId: 'f4',
          pathData: 'M 0 0 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
      ],
    });

    render(
      <FrameTimeline
        propagationHistory={[
          { id: 'history-sparse', startFrame: 1, endFrame: 5, colorIndex: 0, label: '稀疏传播' },
        ]}
      />,
    );

    const segments = screen.getAllByTestId('propagation-history-segment');
    expect(segments).toHaveLength(2);
    expect(segments[0]).toHaveStyle({ left: '20%', width: '20%' });
    expect(segments[1]).toHaveStyle({ left: '60%', width: '20%' });
  });

  it('jumps from the processing progress bar and frame status markers', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
        { id: 'f4', projectId: 'p1', index: 3, url: '/4.jpg', width: 640, height: 360 },
        { id: 'f5', projectId: 'p1', index: 4, url: '/5.jpg', width: 640, height: 360 },
      ],
      masks: [
        { id: 'm1', frameId: 'f2', pathData: 'M 0 0 Z', label: 'Draft', color: '#ef4444' },
        {
          id: 'm2',
          frameId: 'f4',
          pathData: 'M 0 0 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
      ],
    });

    render(<FrameTimeline />);

    const processingBar = screen.getByLabelText('视频处理进度条');
    vi.spyOn(processingBar, 'getBoundingClientRect').mockReturnValue({
      left: 0,
      right: 100,
      top: 0,
      bottom: 10,
      width: 100,
      height: 10,
      x: 0,
      y: 0,
      toJSON: () => ({}),
    });
    fireEvent.pointerDown(processingBar, { clientX: 50, pointerId: 1 });
    expect(useStore.getState().currentFrameIndex).toBe(2);

    fireEvent.click(screen.getByRole('button', { name: '跳转到人工/AI 标注帧 2' }));
    expect(useStore.getState().currentFrameIndex).toBe(1);

    fireEvent.click(screen.getByRole('button', { name: '跳转到自动传播帧 4' }));
    expect(useStore.getState().currentFrameIndex).toBe(3);
  });

  it('hides the propagation range overlay until range selection is active', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
    });

    render(<FrameTimeline propagationRange={{ startFrame: 1, endFrame: 3 }} />);

    expect(screen.queryByTestId('propagation-range-overlay')).not.toBeInTheDocument();
  });

  it('uses red thumbnail borders for manual or AI frames and blue for propagated frames', () => {
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
        { id: 'f4', projectId: 'p1', index: 3, url: '/4.jpg', width: 640, height: 360 },
        { id: 'f5', projectId: 'p1', index: 4, url: '/5.jpg', width: 640, height: 360 },
      ],
      masks: [
        { id: 'm1', frameId: 'f2', pathData: 'M 0 0 Z', label: 'Draft', color: '#ef4444' },
        {
          id: 'm2',
          frameId: 'f3',
          pathData: 'M 0 0 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { propagated_from_frame_id: 'f1' },
        },
        { id: 'm3', frameId: 'f4', pathData: 'M 1 1 Z', label: 'Manual', color: '#ef4444' },
        {
          id: 'm4',
          frameId: 'f4',
          pathData: 'M 2 2 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
        {
          id: 'm5',
          frameId: 'f5',
          pathData: 'M 3 3 Z',
          label: 'Tracked after smoothing',
          color: '#3b82f6',
          metadata: { source_annotation_id: 7, source_mask_id: 'annotation-7' },
        },
      ],
    });

    render(<FrameTimeline />);

    expect(screen.getByAltText('frame-0').closest('div')?.className).toContain('border-cyan-500');
    expect(screen.getByAltText('frame-1').closest('div')?.className).toContain('border-red-500');
    expect(screen.getByAltText('frame-2').closest('div')?.className).toContain('border-blue-500');
    const manuallyAdjustedPropagatedTile = screen.getByAltText('frame-3').closest('div');
    expect(manuallyAdjustedPropagatedTile?.className).toContain('border-red-500');
    expect(manuallyAdjustedPropagatedTile?.className).toContain('inset_0_0_0_2px_rgba(59,130,246,0.85)');
    expect(screen.getByAltText('frame-4').closest('div')?.className).toContain('border-blue-500');
    expect(screen.getByAltText('frame-4').closest('div')?.className).not.toContain('border-red-500');
  });

  it('keeps the current frame blue border while showing an inner red ring for annotated frames', () => {
    useStore.setState({
      currentFrameIndex: 1,
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
      ],
      masks: [
        { id: 'm1', frameId: 'f2', pathData: 'M 0 0 Z', label: 'Draft', color: '#ef4444' },
        {
          id: 'm2',
          frameId: 'f2',
          pathData: 'M 1 1 Z',
          label: 'Tracked',
          color: '#3b82f6',
          metadata: { source: 'sam2.1_hiera_tiny_propagation' },
        },
      ],
    });

    render(<FrameTimeline />);

    const currentAnnotatedTile = screen.getByAltText('frame-1').closest('div');
    expect(currentAnnotatedTile?.className).toContain('border-cyan-500');
    expect(currentAnnotatedTile?.className).toContain('inset_0_0_0_2px_rgba(239,68,68,0.95)');
  });

  it('selects a propagation range from the playback and processing progress bars', () => {
    const onPropagationRangeChange = vi.fn();
    useStore.setState({
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
        { id: 'f4', projectId: 'p1', index: 3, url: '/4.jpg', width: 640, height: 360 },
        { id: 'f5', projectId: 'p1', index: 4, url: '/5.jpg', width: 640, height: 360 },
      ],
    });

    render(
      <FrameTimeline
        propagationRange={{ startFrame: 2, endFrame: 4 }}
        propagationRangeSelectionActive
        onPropagationRangeChange={onPropagationRangeChange}
      />,
    );

    const playbackBar = screen.getByTestId('playback-progress-bar');
    vi.spyOn(playbackBar, 'getBoundingClientRect').mockReturnValue({
      left: 0,
      right: 100,
      top: 0,
      bottom: 10,
      width: 100,
      height: 10,
      x: 0,
      y: 0,
      toJSON: () => ({}),
    });
    fireEvent.pointerDown(playbackBar, { clientX: 25, pointerId: 1 });
    fireEvent.pointerMove(playbackBar, { clientX: 75, pointerId: 1 });
    fireEvent.pointerUp(playbackBar, { clientX: 75, pointerId: 1 });

    expect(onPropagationRangeChange).toHaveBeenLastCalledWith(2, 4);
    expect(screen.getAllByTestId('propagation-range-overlay')).toHaveLength(2);
    const boundaryLines = screen.getAllByTestId('range-boundary-line');
    expect(boundaryLines).toHaveLength(2);
    expect(boundaryLines[0]).toHaveStyle({ left: '25%' });
    expect(boundaryLines[0].className).toContain('bg-fuchsia-400');
    expect(boundaryLines[1]).toHaveStyle({ left: '75%' });
    expect(boundaryLines[1].className).toContain('bg-lime-300');
  });

  it('changes frames with left and right arrow keys without leaving bounds', () => {
    useStore.setState({
      currentFrameIndex: 1,
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
    });

    render(<FrameTimeline />);
    fireEvent.keyDown(window, { key: 'ArrowRight' });
    expect(useStore.getState().currentFrameIndex).toBe(2);

    fireEvent.keyDown(window, { key: 'ArrowRight' });
    expect(useStore.getState().currentFrameIndex).toBe(2);

    fireEvent.keyDown(window, { key: 'ArrowLeft' });
    expect(useStore.getState().currentFrameIndex).toBe(1);
  });

  it('does not change frames while typing in editable fields', () => {
    useStore.setState({
      currentFrameIndex: 1,
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
        { id: 'f3', projectId: 'p1', index: 2, url: '/3.jpg', width: 640, height: 360 },
      ],
    });

    render(
      <>
        <input aria-label="annotation-name" />
        <FrameTimeline />
      </>,
    );
    fireEvent.keyDown(screen.getByLabelText('annotation-name'), { key: 'ArrowRight' });

    expect(useStore.getState().currentFrameIndex).toBe(1);
  });

  it('plays forward using the project parse fps and stops at the end', () => {
    vi.useFakeTimers();
    useStore.setState({
      currentProject: { id: 'p1', name: 'P', status: 'ready', parse_fps: 10 },
      frames: [
        { id: 'f1', projectId: 'p1', index: 0, url: '/1.jpg', width: 640, height: 360 },
        { id: 'f2', projectId: 'p1', index: 1, url: '/2.jpg', width: 640, height: 360 },
      ],
    });

    const { container } = render(<FrameTimeline />);
    fireEvent.click(container.querySelector('button')!);

    act(() => {
      vi.advanceTimersByTime(100);
    });

    expect(useStore.getState().currentFrameIndex).toBe(1);

    act(() => {
      vi.advanceTimersByTime(100);
    });

    expect(screen.getByText('播放序列 (F5)')).toBeInTheDocument();
  });
});
