import { 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(),
  uploadMedia: vi.fn(),
  parseMedia: vi.fn(),
  uploadDicomBatch: vi.fn(),
  deleteProject: vi.fn(),
}));

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

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();
  });

  it('creates a new project from the modal', async () => {
    apiMock.createProject.mockResolvedValueOnce({ id: 'p2', name: 'New Project', status: 'pending' });

    render(<ProjectLibrary onProjectSelect={vi.fn()} />);
    fireEvent.click(screen.getByText('新建项目'));
    fireEvent.change(screen.getByPlaceholderText('输入项目名称'), { target: { value: 'New Project' } });
    fireEvent.change(screen.getByPlaceholderText('输入项目描述'), { target: { value: 'desc' } });
    fireEvent.click(screen.getByRole('button', { name: '创建' }));

    await waitFor(() => expect(apiMock.createProject).toHaveBeenCalledWith({
      name: 'New Project',
      description: 'desc',
    }));
    expect(useStore.getState().projects[0]).toEqual(expect.objectContaining({ id: 'p2' }));
  });

  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(apiMock.parseMedia).not.toHaveBeenCalled();
  });

  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 }]);
    apiMock.parseMedia.mockResolvedValueOnce({ id: 22, status: 'queued', progress: 0 });

    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 }));
  });

  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' }));

    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('imports only valid DICOM files and parses the returned project', async () => {
    apiMock.uploadDicomBatch.mockResolvedValueOnce({ project_id: 77, uploaded_count: 1, message: 'ok' });
    apiMock.parseMedia.mockResolvedValueOnce({ frames_extracted: 1 });

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

    await waitFor(() => expect(apiMock.uploadDicomBatch).toHaveBeenCalledWith([dcm]));
    expect(apiMock.parseMedia).toHaveBeenCalledWith('77');
  });
});
