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

const apiMock = vi.hoisted(() => ({
  getProjects: vi.fn(),
  createProject: vi.fn(),
  updateProject: vi.fn(),
  copyProject: vi.fn(),
  uploadMedia: vi.fn(),
  parseMedia: vi.fn(),
  uploadDicomBatch: vi.fn(),
  deleteProject: vi.fn(),
  getTask: vi.fn(),
}));

vi.mock('../lib/api', () => ({
  getProjects: apiMock.getProjects,
  createProject: apiMock.createProject,
  updateProject: apiMock.updateProject,
  copyProject: apiMock.copyProject,
  uploadMedia: apiMock.uploadMedia,
  parseMedia: apiMock.parseMedia,
  uploadDicomBatch: apiMock.uploadDicomBatch,
  deleteProject: apiMock.deleteProject,
  getTask: apiMock.getTask,
}));

describe('ProjectLibrary', () => {
  beforeEach(() => {
    resetStore();
    vi.clearAllMocks();
    apiMock.getProjects.mockResolvedValue([]);
  });

  it('loads projects and selects one into the workspace', async () => {
    const onProjectSelect = vi.fn();
    apiMock.getProjects.mockResolvedValueOnce([
      { id: 'p1', name: 'Demo Project', status: 'ready', frames: 3, fps: '30FPS' },
    ]);

    render(<ProjectLibrary onProjectSelect={onProjectSelect} />);

    fireEvent.click(await screen.findByText('Demo Project'));
    expect(useStore.getState().currentProject?.id).toBe('p1');
    expect(onProjectSelect).toHaveBeenCalled();
    expect(screen.getByText('支持导入视频文件、DICOM序列文件')).toBeInTheDocument();
  });

  it('shows the generated frame sequence FPS on project cards instead of source FPS', async () => {
    apiMock.getProjects.mockResolvedValueOnce([
      {
        id: 'p-fps',
        name: 'Frame Rate Demo',
        status: 'ready',
        frames: 120,
        fps: '12FPS',
        parse_fps: 12,
        original_fps: 29.97,
        video_path: 'uploads/demo.mp4',
      },
    ]);

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);

    expect(await screen.findByText('12FPS')).toBeInTheDocument();
    expect(screen.getByText('原 30.0fps')).toBeInTheDocument();
    expect(screen.queryByText('30FPS')).not.toBeInTheDocument();
  });

  it('does not expose manual project creation from the project library header', async () => {
    render(<ProjectLibrary onProjectSelect={vi.fn()} />);

    await waitFor(() => expect(apiMock.getProjects).toHaveBeenCalled());
    expect(screen.queryByRole('button', { name: '新建项目' })).not.toBeInTheDocument();
  });

  it('imports video by creating a project and uploading media without parsing frames', async () => {
    apiMock.createProject.mockResolvedValueOnce({ id: 'p3', name: 'clip.mp4', status: 'pending' });
    apiMock.uploadMedia.mockResolvedValueOnce({ url: 'http://file', id: 'object' });
    apiMock.getProjects.mockResolvedValue([]);

    const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    const input = container.querySelector('input[accept="video/*"]') as HTMLInputElement;
    const file = new File(['video'], 'clip.mp4', { type: 'video/mp4' });
    fireEvent.change(input, { target: { files: [file] } });
    fireEvent.click(await screen.findByRole('button', { name: '开始导入' }));

    await waitFor(() => expect(apiMock.createProject).toHaveBeenCalledWith(expect.objectContaining({
      name: 'clip.mp4',
    })));
    expect(apiMock.uploadMedia).toHaveBeenCalledWith(file, 'p3', expect.objectContaining({
      onProgress: expect.any(Function),
    }));
    expect(apiMock.parseMedia).not.toHaveBeenCalled();
    expect(await screen.findByRole('status')).toHaveTextContent('视频导入成功');
  });

  it('visualizes video upload progress while importing media', async () => {
    let resolveUpload: ((value: { url: string; id: string }) => void) | undefined;
    apiMock.createProject.mockResolvedValueOnce({ id: 'p-progress', name: 'large.mp4', status: 'pending' });
    apiMock.uploadMedia.mockImplementationOnce((_file, _projectId, options) => {
      options.onProgress({ loaded: 50, total: 100, percent: 50 });
      return new Promise((resolve) => {
        resolveUpload = resolve;
      });
    });

    const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    const input = container.querySelector('input[accept="video/*"]') as HTMLInputElement;
    const file = new File(['video'], 'large.mp4', { type: 'video/mp4' });
    fireEvent.change(input, { target: { files: [file] } });
    fireEvent.click(await screen.findByRole('button', { name: '开始导入' }));

    expect(await screen.findByText('正在上传视频文件')).toBeInTheDocument();
    expect(screen.getByRole('progressbar', { name: '导入进度' })).toHaveAttribute('aria-valuenow', '50');

    await act(async () => {
      resolveUpload?.({ url: 'http://file', id: 'object' });
    });
    expect(await screen.findByText('视频导入完成')).toBeInTheDocument();
  });

  it('generates frames from an imported video with the selected FPS', async () => {
    apiMock.getProjects
      .mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'pending', frames: 0, video_path: 'uploads/clip.mp4', parse_fps: 30 }])
      .mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'parsing', frames: 0, video_path: 'uploads/clip.mp4', parse_fps: 12 }])
      .mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'ready', frames: 24, video_path: 'uploads/clip.mp4', parse_fps: 12, thumbnail_url: 'http://thumb/frame.jpg' }]);
    apiMock.parseMedia.mockResolvedValueOnce({ id: 22, status: 'queued', progress: 0 });
    apiMock.getTask.mockResolvedValueOnce({ id: 22, status: 'success', progress: 100, message: '解析完成' });

    const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);

    fireEvent.click(await screen.findByRole('button', { name: '生成帧' }));
    fireEvent.change(container.querySelector('input[type="range"]') as HTMLInputElement, { target: { value: '12' } });
    fireEvent.click(screen.getByRole('button', { name: '开始生成帧' }));

    await waitFor(() => expect(apiMock.parseMedia).toHaveBeenCalledWith('p4', { parseFps: 12 }));
    expect(await screen.findByRole('status')).toHaveTextContent('生成帧任务已入队 #22');
    expect(await screen.findByText('12FPS')).toBeInTheDocument();
    expect(screen.getByText('正在生成视频帧')).toBeInTheDocument();

    await waitFor(() => expect(apiMock.getTask).toHaveBeenCalledWith(22), { timeout: 2500 });
    expect(await screen.findByAltText('clip.mp4')).toHaveAttribute('src', 'http://thumb/frame.jpg');
    expect(await screen.findByRole('status')).toHaveTextContent('视频帧生成完成，项目封面已自动更新');
  });

  it('allows regenerating an already parsed video and clears the current workspace cache', async () => {
    apiMock.getProjects
      .mockResolvedValueOnce([{ id: 'p-ready', name: 'ready.mp4', status: 'ready', frames: 1500, video_path: 'uploads/ready.mp4', parse_fps: 30, source_type: 'video' }])
      .mockResolvedValueOnce([{ id: 'p-ready', name: 'ready.mp4', status: 'parsing', frames: 0, video_path: 'uploads/ready.mp4', parse_fps: 24, source_type: 'video' }]);
    apiMock.parseMedia.mockResolvedValueOnce({ status: 'queued', progress: 0 });
    useStore.setState({
      currentProject: { id: 'p-ready', name: 'ready.mp4', status: 'ready' },
      frames: [{ id: 'old-frame', projectId: 'p-ready', index: 0, url: '/old.jpg', width: 640, height: 360 }],
      masks: [{ id: 'old-mask', frameId: 'old-frame', pathData: 'M 0 0 Z', label: 'old', color: '#fff' }],
      selectedMaskIds: ['old-mask'],
    });

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);

    fireEvent.click(await screen.findByRole('button', { name: '重新生成帧' }));
    expect(screen.getByText('重新生成会清空该项目现有帧序列、标注和 mask，再按新的 FPS 从源视频生成帧。')).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '开始生成帧' }));

    await waitFor(() => expect(apiMock.parseMedia).toHaveBeenCalledWith('p-ready', { parseFps: 30 }));
    expect(useStore.getState().frames).toEqual([]);
    expect(useStore.getState().masks).toEqual([]);
    expect(useStore.getState().selectedMaskIds).toEqual([]);
  });

  it('hides frame generation while editing a project name', async () => {
    apiMock.getProjects.mockResolvedValueOnce([
      { id: 'p-edit', name: 'Editable Clip', status: 'pending', frames: 0, video_path: 'uploads/editable.mp4', parse_fps: 30, source_type: 'video' },
    ]);

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    expect(await screen.findByRole('button', { name: '生成帧' })).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: '修改项目名称 Editable Clip' }));

    expect(screen.queryByRole('button', { name: '生成帧' })).not.toBeInTheDocument();
  });

  it('does not show frame generation for DICOM projects', async () => {
    apiMock.getProjects.mockResolvedValueOnce([
      { id: 'p-dicom', name: 'DICOM Series', status: 'ready', frames: 0, video_path: 'uploads/dicom', source_type: 'dicom' },
    ]);

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);

    expect(await screen.findByText('DICOM Series')).toBeInTheDocument();
    expect(screen.getByText('DICOM')).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '生成帧' })).not.toBeInTheDocument();
  });

  it('deletes a project from the project card without entering the workspace', async () => {
    const onProjectSelect = vi.fn();
    apiMock.getProjects.mockResolvedValueOnce([
      { id: 'p5', name: 'Delete Me', status: 'ready', frames: 3, fps: '30FPS' },
      { id: 'p6', name: 'Keep Me', status: 'ready', frames: 1, fps: '30FPS' },
    ]);
    apiMock.deleteProject.mockResolvedValueOnce(undefined);
    useStore.setState({
      currentProject: { id: 'p5', name: 'Delete Me', status: 'ready' },
      frames: [{ id: 'f1', projectId: 'p5', index: 0, url: '/1.jpg', width: 640, height: 360 }],
      masks: [{ id: 'm1', frameId: 'f1', pathData: 'M 0 0 Z', label: 'Mask', color: '#06b6d4' }],
      selectedMaskIds: ['m1'],
    });

    render(<ProjectLibrary onProjectSelect={onProjectSelect} />);
    fireEvent.click(await screen.findByRole('button', { name: '删除项目 Delete Me' }));
    fireEvent.click(screen.getByRole('button', { name: '确认删除' }));

    await waitFor(() => expect(apiMock.deleteProject).toHaveBeenCalledWith('p5'));
    expect(onProjectSelect).not.toHaveBeenCalled();
    expect(useStore.getState().projects.map((project) => project.id)).toEqual(['p6']);
    expect(useStore.getState().currentProject).toBeNull();
    expect(useStore.getState().frames).toEqual([]);
    expect(useStore.getState().masks).toEqual([]);
    expect(useStore.getState().selectedMaskIds).toEqual([]);
  });

  it('renames a project from the project card without entering the workspace', async () => {
    const onProjectSelect = vi.fn();
    apiMock.getProjects.mockResolvedValueOnce([
      { id: 'p7', name: 'Old Name', status: 'ready', frames: 3, fps: '30FPS' },
    ]);
    apiMock.updateProject.mockResolvedValueOnce({ id: 'p7', name: 'New Name', status: 'ready', frames: 3, fps: '30FPS' });
    useStore.setState({
      currentProject: { id: 'p7', name: 'Old Name', status: 'ready' },
    });

    render(<ProjectLibrary onProjectSelect={onProjectSelect} />);
    fireEvent.click(await screen.findByRole('button', { name: '修改项目名称 Old Name' }));
    fireEvent.change(screen.getByDisplayValue('Old Name'), { target: { value: 'New Name' } });
    fireEvent.click(screen.getByRole('button', { name: '保存项目名称 Old Name' }));

    await waitFor(() => expect(apiMock.updateProject).toHaveBeenCalledWith('p7', { name: 'New Name' }));
    expect(onProjectSelect).not.toHaveBeenCalled();
    expect(useStore.getState().projects[0]).toEqual(expect.objectContaining({ id: 'p7', name: 'New Name' }));
    expect(useStore.getState().currentProject).toEqual(expect.objectContaining({ id: 'p7', name: 'New Name' }));
    expect(await screen.findByRole('status')).toHaveTextContent('项目名称已更新');
  });

  it('copies a project as a reset project from the project card', async () => {
    const onProjectSelect = vi.fn();
    apiMock.getProjects
      .mockResolvedValueOnce([
        { id: 'p8', name: 'Source Project', status: 'ready', frames: 3, fps: '30FPS' },
      ])
      .mockResolvedValueOnce([
        { id: 'p9', name: 'Source Project 副本', status: 'ready', frames: 3, fps: '30FPS' },
        { id: 'p8', name: 'Source Project', status: 'ready', frames: 3, fps: '30FPS' },
      ]);
    apiMock.copyProject.mockResolvedValueOnce({ id: 'p9', name: 'Source Project 副本', status: 'ready', frames: 3, fps: '30FPS' });

    render(<ProjectLibrary onProjectSelect={onProjectSelect} />);
    fireEvent.click(await screen.findByRole('button', { name: '复制项目 Source Project' }));
    fireEvent.click(screen.getByRole('button', { name: /新项目重置/ }));

    await waitFor(() => expect(apiMock.copyProject).toHaveBeenCalledWith('p8', { mode: 'reset' }));
    expect(onProjectSelect).not.toHaveBeenCalled();
    expect(useStore.getState().projects.map((project) => project.id)).toEqual(['p9', 'p8']);
    expect(await screen.findByRole('status')).toHaveTextContent('已复制为重置项目：Source Project 副本');
  });

  it('copies a project with all content from the project card', async () => {
    apiMock.getProjects
      .mockResolvedValueOnce([
        { id: 'p10', name: 'Annotated Project', status: 'ready', frames: 2, fps: '30FPS' },
      ])
      .mockResolvedValueOnce([
        { id: 'p11', name: 'Annotated Project 副本', status: 'ready', frames: 2, fps: '30FPS' },
        { id: 'p10', name: 'Annotated Project', status: 'ready', frames: 2, fps: '30FPS' },
      ]);
    apiMock.copyProject.mockResolvedValueOnce({ id: 'p11', name: 'Annotated Project 副本', status: 'ready', frames: 2, fps: '30FPS' });

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    fireEvent.click(await screen.findByRole('button', { name: '复制项目 Annotated Project' }));
    fireEvent.click(screen.getByRole('button', { name: /全内容复制/ }));

    await waitFor(() => expect(apiMock.copyProject).toHaveBeenCalledWith('p10', { mode: 'full' }));
    expect(await screen.findByRole('status')).toHaveTextContent('已全内容复制项目：Annotated Project 副本');
  });

  it('imports valid DICOM files in natural filename order and parses the returned project', async () => {
    apiMock.uploadDicomBatch.mockResolvedValueOnce({ project_id: 77, uploaded_count: 3, message: 'ok' });
    apiMock.parseMedia.mockResolvedValueOnce({ frames_extracted: 3 });

    const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    const input = container.querySelector('input[accept=".dcm"]') as HTMLInputElement;
    const ten = new File(['dcm10'], '10.dcm', { type: 'application/dicom' });
    const two = new File(['dcm2'], '2.dcm', { type: 'application/dicom' });
    const one = new File(['dcm1'], '1.dcm', { type: 'application/dicom' });
    const ignored = new File(['txt'], 'notes.txt', { type: 'text/plain' });
    fireEvent.change(input, { target: { files: [ten, ignored, two, one] } });

    await waitFor(() => expect(apiMock.uploadDicomBatch).toHaveBeenCalledWith([one, two, ten], undefined, expect.objectContaining({
      onProgress: expect.any(Function),
    })));
    expect(apiMock.parseMedia).toHaveBeenCalledWith('77');
    expect(await screen.findByRole('status')).toHaveTextContent('DICOM 导入完成: 3 个文件');
  });

  it('visualizes DICOM upload progress and parsing queue handoff', async () => {
    let resolveDicomUpload: ((value: { project_id: number; uploaded_count: number; message: string }) => void) | undefined;
    apiMock.uploadDicomBatch.mockImplementationOnce(() => new Promise((resolve) => {
      resolveDicomUpload = resolve;
    }));
    apiMock.parseMedia.mockResolvedValueOnce({ id: 44, status: 'queued', progress: 0 });
    apiMock.getTask
      .mockResolvedValueOnce({ id: 44, status: 'running', progress: 55, message: '正在写入帧索引' })
      .mockResolvedValueOnce({ id: 44, status: 'success', progress: 100, message: '解析完成' });

    const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    const input = container.querySelector('input[accept=".dcm"]') as HTMLInputElement;
    const one = new File(['dcm1'], '1.dcm', { type: 'application/dicom' });
    const two = new File(['dcm2'], '2.dcm', { type: 'application/dicom' });
    fireEvent.change(input, { target: { files: [two, one] } });

    await waitFor(() => expect(apiMock.uploadDicomBatch).toHaveBeenCalled());
    const progressOptions = apiMock.uploadDicomBatch.mock.calls[0][2];
    await act(async () => {
      progressOptions.onProgress({ loaded: 80, total: 100, percent: 80 });
    });

    expect(await screen.findByText('正在上传 DICOM 序列')).toBeInTheDocument();
    expect(screen.getByText('2 文件')).toBeInTheDocument();
    expect(screen.getByRole('progressbar', { name: '导入进度' })).toHaveAttribute('aria-valuenow', '80');

    vi.useFakeTimers();
    await act(async () => {
      resolveDicomUpload?.({ project_id: 78, uploaded_count: 2, message: 'ok' });
    });
    expect(screen.getByText('正在解析 DICOM 序列')).toBeInTheDocument();
    expect(screen.getByRole('progressbar', { name: '导入进度' })).toHaveAttribute('aria-valuenow', '0');
    await act(async () => {
      vi.advanceTimersByTime(1200);
    });
    expect(apiMock.getTask).toHaveBeenCalledWith(44);
    expect(screen.getByText('正在写入帧索引')).toBeInTheDocument();
    await act(async () => {
      vi.advanceTimersByTime(1200);
    });
    expect(screen.getByText('DICOM 导入完成')).toBeInTheDocument();
    vi.useRealTimers();
  });
});
