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 { CanvasArea } from './CanvasArea';

const apiMock = vi.hoisted(() => ({
  predictMask: vi.fn(),
}));

vi.mock('../lib/api', () => ({
  predictMask: apiMock.predictMask,
}));

describe('CanvasArea', () => {
  const frame = { id: 'frame-1', projectId: 'project-1', index: 0, url: '/frame.jpg', width: 640, height: 360 };

  beforeEach(() => {
    resetStore();
    vi.clearAllMocks();
  });

  it('calls AI prediction with the active frame when a point prompt is placed', async () => {
    useStore.setState({
      activeTemplateId: '2',
      activeClass: { id: 'c1', name: '胆囊', color: '#ff0000', zIndex: 20 },
      activeClassId: 'c1',
    });
    apiMock.predictMask.mockResolvedValueOnce({
      masks: [
        {
          id: 'mask-1',
          pathData: 'M 0 0 L 10 0 L 10 10 Z',
          label: 'AI Mask',
          color: '#06b6d4',
          segmentation: [[0, 0, 10, 0, 10, 10]],
          bbox: [0, 0, 10, 10],
          area: 100,
        },
      ],
    });

    render(<CanvasArea activeTool="point_pos" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-stage'));

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledWith({
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: [{ x: 120, y: 80, type: 'pos' }],
      box: undefined,
    }));
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      id: 'mask-1',
      frameId: 'frame-1',
      pathData: 'M 0 0 L 10 0 L 10 10 Z',
      templateId: '2',
      classId: 'c1',
      className: '胆囊',
      classZIndex: 20,
      label: '胆囊',
      color: '#ff0000',
      saveStatus: 'draft',
    }));
  });

  it('refines one SAM2 candidate mask from an initial box with positive and negative points', async () => {
    apiMock.predictMask
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-box',
            pathData: 'M 10 10 L 90 10 L 90 90 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[10, 10, 90, 10, 90, 90]],
            bbox: [10, 10, 80, 80],
            area: 6400,
          },
        ],
      })
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-refined-pos',
            pathData: 'M 20 20 L 80 20 L 80 80 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[20, 20, 80, 20, 80, 80]],
            bbox: [20, 20, 60, 60],
            area: 3600,
          },
        ],
      })
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-refined-neg',
            pathData: 'M 30 30 L 70 30 L 70 70 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[30, 30, 70, 30, 70, 70]],
            bbox: [30, 30, 40, 40],
            area: 1600,
          },
        ],
      });

    const { rerender } = render(<CanvasArea activeTool="box_select" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.mouseDown(stage, { clientX: 120, clientY: 80 });
    fireEvent.mouseMove(stage, { clientX: 260, clientY: 200 });
    fireEvent.mouseUp(stage, { clientX: 260, clientY: 200 });

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenNthCalledWith(1, {
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: undefined,
      box: { x1: 120, y1: 80, x2: 260, y2: 200 },
    }));
    await waitFor(() => expect(useStore.getState().masks).toHaveLength(1));

    rerender(<CanvasArea activeTool="point_pos" frame={frame} />);
    fireEvent.click(stage, { clientX: 150, clientY: 100 });

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenNthCalledWith(2, {
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: [{ x: 150, y: 100, type: 'pos' }],
      box: { x1: 120, y1: 80, x2: 260, y2: 200 },
    }));
    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      id: 'mask-box',
      segmentation: [[20, 20, 80, 20, 80, 80]],
      metadata: expect.objectContaining({
        source: 'sam2_interactive',
        promptPointCount: 1,
      }),
    }));

    rerender(<CanvasArea activeTool="point_neg" frame={frame} />);
    fireEvent.click(stage, { clientX: 300, clientY: 150 });

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenNthCalledWith(3, {
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: [
        { x: 150, y: 100, type: 'pos' },
        { x: 300, y: 150, type: 'neg' },
      ],
      box: { x1: 120, y1: 80, x2: 260, y2: 200 },
      options: { auto_filter_background: true, min_score: 0.05 },
    }));
    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      id: 'mask-box',
      segmentation: [[30, 30, 70, 30, 70, 70]],
      points: [[150, 100]],
      metadata: expect.objectContaining({ promptPointCount: 2, promptNegativePointCount: 1 }),
    }));
  });

  it('removes the SAM2 candidate when a negative point filters it out', async () => {
    apiMock.predictMask
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-box',
            pathData: 'M 10 10 L 90 10 L 90 90 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[10, 10, 90, 10, 90, 90]],
            bbox: [10, 10, 80, 80],
            area: 6400,
          },
        ],
      })
      .mockResolvedValueOnce({ masks: [] });

    const { rerender } = render(<CanvasArea activeTool="box_select" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.mouseDown(stage, { clientX: 120, clientY: 80 });
    fireEvent.mouseMove(stage, { clientX: 260, clientY: 200 });
    fireEvent.mouseUp(stage, { clientX: 260, clientY: 200 });

    await waitFor(() => expect(useStore.getState().masks).toHaveLength(1));

    rerender(<CanvasArea activeTool="point_neg" frame={frame} />);
    fireEvent.click(stage, { clientX: 180, clientY: 120 });

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenNthCalledWith(2, {
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: [{ x: 180, y: 120, type: 'neg' }],
      box: { x1: 120, y1: 80, x2: 260, y2: 200 },
      options: { auto_filter_background: true, min_score: 0.05 },
    }));
    await waitFor(() => expect(useStore.getState().masks).toHaveLength(0));
    expect(await screen.findByText(/反向点已排除当前候选区域/)).toBeInTheDocument();
  });

  it('deletes a workspace SAM2 prompt point before the stage can add another point', async () => {
    apiMock.predictMask
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-prompt',
            pathData: 'M 10 10 L 90 10 L 90 90 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[10, 10, 90, 10, 90, 90]],
            bbox: [10, 10, 80, 80],
            area: 6400,
          },
        ],
      })
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-refined',
            pathData: 'M 20 20 L 80 20 L 80 80 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[20, 20, 80, 20, 80, 80]],
            bbox: [20, 20, 60, 60],
            area: 3600,
          },
        ],
      })
      .mockResolvedValueOnce({
        masks: [
          {
            id: 'mask-after-delete',
            pathData: 'M 30 30 L 70 30 L 70 70 Z',
            label: 'AI Mask',
            color: '#06b6d4',
            segmentation: [[30, 30, 70, 30, 70, 70]],
            bbox: [30, 30, 40, 40],
            area: 1600,
          },
        ],
      });

    const { rerender } = render(<CanvasArea activeTool="point_pos" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.click(stage, { clientX: 120, clientY: 80 });
    await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(1));

    rerender(<CanvasArea activeTool="point_neg" frame={frame} />);
    fireEvent.click(stage, { clientX: 220, clientY: 140 });
    await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(2));
    const promptOuterCircles = () => screen.getAllByTestId('konva-circle')
      .filter((element) => ['#22c55e', '#ef4444'].includes(element.getAttribute('data-fill') || ''));
    expect(promptOuterCircles()).toHaveLength(2);

    fireEvent.click(promptOuterCircles()[0]);

    await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(3));
    expect(apiMock.predictMask).toHaveBeenLastCalledWith({
      imageId: 'frame-1',
      imageWidth: 640,
      imageHeight: 360,
      model: 'sam2.1_hiera_tiny',
      points: [{ x: 220, y: 140, type: 'neg' }],
      box: undefined,
      options: { auto_filter_background: true, min_score: 0.05 },
    });
    expect(promptOuterCircles()).toHaveLength(1);
  });

  it('renders only masks that belong to the current frame', () => {
    useStore.setState({
      masks: [
        { id: 'm1', frameId: 'frame-1', pathData: 'M 0 0 Z', label: 'A', color: '#fff' },
        { id: 'm2', frameId: 'frame-2', pathData: 'M 1 1 Z', label: 'B', color: '#000' },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);

    expect(screen.getAllByTestId('konva-path')).toHaveLength(1);
    expect(screen.getByText('遮罩数: 1')).toBeInTheDocument();
  });

  it('publishes the selected mask ids for the ontology panel', async () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          annotationId: '42',
          frameId: 'frame-1',
          pathData: 'M 0 0 L 10 0 L 10 10 Z',
          label: '胆囊',
          color: '#fff',
          segmentation: [[0, 0, 10, 0, 10, 10]],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'));

    await waitFor(() => expect(useStore.getState().selectedMaskIds).toEqual(['m1']));
    expect(screen.getByText('当前图层: 胆囊 #42')).toBeInTheDocument();
  });

  it('keeps a mask selected when opening the workspace polygon editor from AI results', () => {
    useStore.setState({
      selectedMaskIds: ['m1'],
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 0 0 L 10 0 L 10 10 Z',
          label: 'A',
          color: '#fff',
          segmentation: [[0, 0, 10, 0, 10, 10]],
        },
      ],
    });

    render(<CanvasArea activeTool="edit_polygon" frame={frame} />);

    expect(useStore.getState().selectedMaskIds).toEqual(['m1']);
    expect(screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#ffffff')).toHaveLength(3);
  });

  it('renders imported GT seed points for editable point regions', () => {
    useStore.setState({
      masks: [
        {
          id: 'gt-1',
          frameId: 'frame-1',
          pathData: 'M 0 0 L 10 0 L 10 10 Z',
          label: 'GT',
          color: '#22c55e',
          points: [[120, 80]],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);

    expect(screen.getAllByTestId('konva-circle')).toHaveLength(2);
  });

  it('selects a polygon mask and drags a vertex into dirty saved state', () => {
    useStore.setState({
      masks: [
        {
          id: 'annotation-99',
          annotationId: '99',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Saved',
          color: '#06b6d4',
          saved: true,
          saveStatus: 'saved',
          segmentation: [[10, 10, 90, 10, 90, 40]],
          bbox: [10, 10, 80, 30],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'));
    const handles = screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#ffffff');
    expect(handles).toHaveLength(3);

    fireEvent.mouseUp(handles[0], { clientX: 20, clientY: 30 });

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      pathData: 'M 20 30 L 90 10 L 90 40 Z',
      segmentation: [[20, 30, 90, 10, 90, 40]],
      bbox: [20, 10, 70, 30],
      area: 1050,
      saveStatus: 'dirty',
      saved: false,
    }));
  });

  it('moves a polygon vertex directly while dragging without a prior vertex click', () => {
    useStore.setState({
      selectedMaskIds: ['draft-1'],
      masks: [
        {
          id: 'draft-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Draft',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [[10, 10, 90, 10, 90, 40]],
          bbox: [10, 10, 80, 30],
        },
      ],
    });

    render(<CanvasArea activeTool="edit_polygon" frame={frame} />);
    const handles = screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#ffffff');

    fireEvent.mouseDown(handles[0]);
    fireEvent.mouseMove(handles[0], { clientX: 25, clientY: 35 });

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      pathData: 'M 25 35 L 90 10 L 90 40 Z',
      segmentation: [[25, 35, 90, 10, 90, 40]],
      saveStatus: 'draft',
    }));
  });

  it('deletes a selected polygon vertex without dropping below three points', () => {
    useStore.setState({
      masks: [
        {
          id: 'draft-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 L 10 40 Z',
          label: 'Draft',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [[10, 10, 90, 10, 90, 40, 10, 40]],
          bbox: [10, 10, 80, 30],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'));
    const handles = screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#ffffff');
    fireEvent.click(handles[0]);
    fireEvent.keyDown(window, { key: 'Delete' });

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      pathData: 'M 90 10 L 90 40 L 10 40 Z',
      segmentation: [[90, 10, 90, 40, 10, 40]],
      saveStatus: 'draft',
    }));
  });

  it('deletes the selected draft mask with Delete when no vertex is selected', () => {
    useStore.setState({
      masks: [
        {
          id: 'draft-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Draft',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [[10, 10, 90, 10, 90, 40]],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'));
    fireEvent.keyDown(window, { key: 'Delete' });

    expect(useStore.getState().masks).toEqual([]);
    expect(useStore.getState().maskHistory.at(-1)).toEqual([
      expect.objectContaining({ id: 'draft-1' }),
    ]);
  });

  it('deletes the selected saved mask locally and notifies the backend deletion callback', () => {
    const onDeleteMaskAnnotations = vi.fn();
    useStore.setState({
      masks: [
        {
          id: 'annotation-99',
          annotationId: '99',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Saved',
          color: '#06b6d4',
          saveStatus: 'saved',
          saved: true,
          segmentation: [[10, 10, 90, 10, 90, 40]],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} onDeleteMaskAnnotations={onDeleteMaskAnnotations} />);
    fireEvent.click(screen.getByTestId('konva-path'));
    fireEvent.keyDown(window, { key: 'Backspace' });

    expect(useStore.getState().masks).toEqual([]);
    expect(onDeleteMaskAnnotations).toHaveBeenCalledWith(['99']);
  });

  it('inserts a polygon vertex from an edge midpoint handle', () => {
    useStore.setState({
      masks: [
        {
          id: 'draft-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Draft',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [[10, 10, 90, 10, 90, 40]],
          bbox: [10, 10, 80, 30],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'));
    const edgeHandles = screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#22d3ee');
    fireEvent.click(edgeHandles[0]);

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      segmentation: [[10, 10, 50, 10, 90, 10, 90, 40]],
      pathData: 'M 10 10 L 50 10 L 90 10 L 90 40 Z',
      saveStatus: 'draft',
    }));
  });

  it('selects a polygon with the edit tool and inserts a vertex by double-clicking an edge', () => {
    useStore.setState({
      masks: [
        {
          id: 'draft-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 40 Z',
          label: 'Draft',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [[10, 10, 90, 10, 90, 40]],
          bbox: [10, 10, 80, 30],
        },
      ],
    });

    render(<CanvasArea activeTool="edit_polygon" frame={frame} />);
    const path = screen.getByTestId('konva-path');
    fireEvent.click(path);
    fireEvent.doubleClick(path, { clientX: 50, clientY: 10 });

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      segmentation: [[10, 10, 50, 10, 90, 10, 90, 40]],
      pathData: 'M 10 10 L 50 10 L 90 10 L 90 40 Z',
      saveStatus: 'draft',
    }));
  });

  it('edits the selected polygon in a multi-polygon mask', () => {
    useStore.setState({
      masks: [
        {
          id: 'multi-1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 50 10 L 50 40 Z M 100 100 L 150 100 L 150 140 Z',
          label: 'Multi',
          color: '#06b6d4',
          saveStatus: 'draft',
          segmentation: [
            [10, 10, 50, 10, 50, 40],
            [100, 100, 150, 100, 150, 140],
          ],
          bbox: [10, 10, 140, 130],
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    const paths = screen.getAllByTestId('konva-path');
    fireEvent.click(paths[1]);
    const vertexHandles = screen.getAllByTestId('konva-circle')
      .filter((element) => element.getAttribute('data-fill') === '#ffffff');
    fireEvent.mouseUp(vertexHandles[0], { clientX: 120, clientY: 120 });

    expect(useStore.getState().masks[0].segmentation).toEqual([
      [10, 10, 50, 10, 50, 40],
      [120, 120, 150, 100, 150, 140],
    ]);
  });

  it('merges selected draft masks with polygon union', () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 50 L 10 50 Z',
          label: 'A',
          color: '#06b6d4',
          segmentation: [[10, 10, 90, 10, 90, 50, 10, 50]],
        },
        {
          id: 'm2',
          frameId: 'frame-1',
          pathData: 'M 50 30 L 120 30 L 120 80 L 50 80 Z',
          label: 'B',
          color: '#ff0000',
          segmentation: [[50, 30, 120, 30, 120, 80, 50, 80]],
        },
      ],
    });

    render(<CanvasArea activeTool="area_merge" frame={frame} />);
    expect(screen.getByText('已选 0')).toBeInTheDocument();
    const paths = screen.getAllByTestId('konva-path');
    fireEvent.click(paths[0]);
    expect(screen.getByText('已选 1')).toBeInTheDocument();
    expect(screen.queryAllByTestId('konva-circle')).toHaveLength(0);
    fireEvent.click(paths[1]);
    expect(screen.getByText('已选 2')).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: '合并选中' }));

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      id: 'm1',
      segmentation: [[10, 10, 90, 10, 90, 30, 120, 30, 120, 80, 50, 80, 50, 50, 10, 50]],
      bbox: [10, 10, 110, 70],
      saveStatus: 'draft',
    }));
  });

  it('removes overlap from the primary selected mask with polygon difference', () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 50 L 10 50 Z',
          label: 'A',
          color: '#06b6d4',
          segmentation: [[10, 10, 90, 10, 90, 50, 10, 50]],
        },
        {
          id: 'm2',
          frameId: 'frame-1',
          pathData: 'M 50 30 L 120 30 L 120 80 L 50 80 Z',
          label: 'B',
          color: '#ff0000',
          segmentation: [[50, 30, 120, 30, 120, 80, 50, 80]],
        },
      ],
    });

    render(<CanvasArea activeTool="area_remove" frame={frame} />);
    const paths = screen.getAllByTestId('konva-path');
    fireEvent.click(paths[0]);
    fireEvent.click(paths[1]);
    const selectedPaths = screen.getAllByTestId('konva-path');
    expect(selectedPaths[0]).toHaveAttribute('data-stroke', '#facc15');
    expect(selectedPaths[0]).toHaveAttribute('data-dash', '');
    expect(selectedPaths[1]).toHaveAttribute('data-stroke', '#fb7185');
    expect(selectedPaths[1]).toHaveAttribute('data-dash', '6,4');
    fireEvent.click(screen.getByRole('button', { name: '从主区域去除' }));

    expect(useStore.getState().masks).toHaveLength(2);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      id: 'm1',
      segmentation: [[10, 10, 90, 10, 90, 30, 50, 30, 50, 50, 10, 50]],
      bbox: [10, 10, 80, 40],
      saveStatus: 'draft',
    }));
    expect(useStore.getState().masks[1].id).toBe('m2');
  });

  it('renders inner overlap removal as a hole in the primary mask', () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 110 10 L 110 110 L 10 110 Z',
          label: 'A',
          color: '#06b6d4',
          segmentation: [[10, 10, 110, 10, 110, 110, 10, 110]],
        },
        {
          id: 'm2',
          frameId: 'frame-1',
          pathData: 'M 40 40 L 80 40 L 80 80 L 40 80 Z',
          label: 'B',
          color: '#ff0000',
          segmentation: [[40, 40, 80, 40, 80, 80, 40, 80]],
        },
      ],
    });

    render(<CanvasArea activeTool="area_remove" frame={frame} />);
    const paths = screen.getAllByTestId('konva-path');
    fireEvent.click(paths[0]);
    fireEvent.click(paths[1]);
    fireEvent.click(screen.getByRole('button', { name: '从主区域去除' }));

    const [primary] = useStore.getState().masks;
    expect(primary).toEqual(expect.objectContaining({
      id: 'm1',
      area: 8400,
      bbox: [10, 10, 100, 100],
      metadata: expect.objectContaining({ hasHoles: true }),
    }));
    expect(primary.segmentation).toHaveLength(2);
    expect(screen.getAllByTestId('konva-path')[0]).toHaveAttribute('data-fill-rule', 'evenodd');
  });

  it('creates a manual rectangle mask that can be undone and redone', () => {
    useStore.setState({
      activeTemplateId: '2',
      activeClass: { id: 'c1', name: '胆囊', color: '#ff0000', zIndex: 20 },
      activeClassId: 'c1',
    });

    render(<CanvasArea activeTool="create_rectangle" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.mouseDown(stage);
    fireEvent.mouseMove(stage);
    fireEvent.mouseUp(stage);

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      frameId: 'frame-1',
      label: '胆囊',
      color: '#ff0000',
      saveStatus: 'draft',
      segmentation: [[120, 80, 260, 80, 260, 200, 120, 200]],
      bbox: [120, 80, 140, 120],
    }));

    useStore.getState().undoMasks();
    expect(useStore.getState().masks).toEqual([]);
    useStore.getState().redoMasks();
    expect(useStore.getState().masks).toHaveLength(1);
  });

  it('creates a manual circle mask from a drag gesture', () => {
    render(<CanvasArea activeTool="create_circle" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.mouseDown(stage, { clientX: 120, clientY: 80 });
    fireEvent.mouseMove(stage, { clientX: 260, clientY: 200 });
    fireEvent.mouseUp(stage, { clientX: 260, clientY: 200 });

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      frameId: 'frame-1',
      label: '手工圆形',
      color: '#06b6d4',
      saveStatus: 'draft',
      bbox: [120, 80, 140, 120],
      metadata: expect.objectContaining({
        source: 'manual',
        shape: '圆形',
      }),
    }));
    expect(useStore.getState().masks[0].segmentation?.[0]).toHaveLength(64);
  });

  it('creates a manual line region from a drag gesture', () => {
    render(<CanvasArea activeTool="create_line" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.mouseDown(stage, { clientX: 120, clientY: 80 });
    fireEvent.mouseMove(stage, { clientX: 260, clientY: 200 });
    fireEvent.mouseUp(stage, { clientX: 260, clientY: 200 });

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      frameId: 'frame-1',
      label: '手工线段',
      color: '#06b6d4',
      saveStatus: 'draft',
      metadata: expect.objectContaining({
        source: 'manual',
        shape: '线段',
      }),
    }));
    expect(useStore.getState().masks[0].segmentation?.[0]).toHaveLength(8);
    expect(useStore.getState().masks[0].area).toBeGreaterThan(1000);
  });

  it('creates an editable point region on click', () => {
    render(<CanvasArea activeTool="create_point" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-stage'), { clientX: 120, clientY: 80 });

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      frameId: 'frame-1',
      label: '手工点区域',
      color: '#06b6d4',
      saveStatus: 'draft',
      points: [[120, 80]],
      bbox: expect.arrayContaining([115, 75]),
      metadata: expect.objectContaining({
        source: 'manual',
        shape: '点区域',
      }),
    }));
  });

  it('creates a point region when clicking over an existing mask', () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 200 10 L 200 200 Z',
          label: 'Existing',
          color: '#06b6d4',
          segmentation: [[10, 10, 200, 10, 200, 200]],
        },
      ],
    });

    render(<CanvasArea activeTool="create_point" frame={frame} />);
    fireEvent.click(screen.getByTestId('konva-path'), { clientX: 120, clientY: 80 });

    expect(useStore.getState().masks).toHaveLength(2);
    expect(useStore.getState().masks[1]).toEqual(expect.objectContaining({
      metadata: expect.objectContaining({ shape: '点区域' }),
      points: [[120, 80]],
    }));
  });

  it('finalizes a clicked polygon with Enter', () => {
    render(<CanvasArea activeTool="create_polygon" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    expect(screen.getByText(/点击画布添加顶点/)).toBeInTheDocument();
    fireEvent.click(stage, { clientX: 120, clientY: 80 });
    fireEvent.click(stage, { clientX: 220, clientY: 80 });
    fireEvent.click(stage, { clientX: 180, clientY: 160 });
    expect(screen.getByText(/点击黄色首节点或按 Enter 闭合完成/)).toBeInTheDocument();
    fireEvent.keyDown(window, { key: 'Enter' });

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0].metadata).toEqual(expect.objectContaining({
      source: 'manual',
      shape: '多边形',
    }));
  });

  it('closes a clicked polygon by clicking the first node again', () => {
    render(<CanvasArea activeTool="create_polygon" frame={frame} />);
    const stage = screen.getByTestId('konva-stage');
    fireEvent.click(stage, { clientX: 120, clientY: 80 });
    fireEvent.click(stage, { clientX: 220, clientY: 80 });
    fireEvent.click(stage, { clientX: 180, clientY: 160 });

    const handles = screen.getAllByTestId('konva-circle');
    expect(handles[0]).toHaveAttribute('data-fill', '#facc15');
    fireEvent.click(handles[0]);

    expect(useStore.getState().masks).toHaveLength(1);
    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      pathData: 'M 120 80 L 220 80 L 180 160 Z',
      segmentation: [[120, 80, 220, 80, 180, 160]],
      metadata: expect.objectContaining({
        source: 'manual',
        shape: '多边形',
      }),
    }));
    expect(screen.queryAllByTestId('konva-circle')).toHaveLength(0);
  });

  it('shows contextual guidance for boolean selection ordering', () => {
    useStore.setState({
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          pathData: 'M 10 10 L 90 10 L 90 50 Z',
          label: 'A',
          color: '#06b6d4',
          segmentation: [[10, 10, 90, 10, 90, 50]],
        },
        {
          id: 'm2',
          frameId: 'frame-1',
          pathData: 'M 50 30 L 120 30 L 120 80 Z',
          label: 'B',
          color: '#ff0000',
          segmentation: [[50, 30, 120, 30, 120, 80]],
        },
      ],
    });

    render(<CanvasArea activeTool="area_remove" frame={frame} />);
    expect(screen.getByText(/先点击要保留的主区域/)).toBeInTheDocument();
    fireEvent.click(screen.getAllByTestId('konva-path')[0]);

    expect(screen.getByText(/第一个是保留主区域/)).toBeInTheDocument();
  });

  it('applies the selected class to current-frame masks and marks saved masks dirty', () => {
    useStore.setState({
      activeTemplateId: '2',
      activeClass: { id: 'c1', name: '胆囊', color: '#ff0000', zIndex: 20 },
      activeClassId: 'c1',
      masks: [
        {
          id: 'm1',
          frameId: 'frame-1',
          annotationId: '99',
          pathData: 'M 0 0 Z',
          label: '旧标签',
          color: '#06b6d4',
          saved: true,
          saveStatus: 'saved',
        },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} />);
    fireEvent.click(screen.getByRole('button', { name: '应用分类' }));

    expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
      templateId: '2',
      classId: 'c1',
      className: '胆囊',
      classZIndex: 20,
      label: '胆囊',
      color: '#ff0000',
      saveStatus: 'dirty',
      saved: false,
    }));
  });

  it('delegates clear to the workspace handler so saved annotations can be deleted', () => {
    const onClearMasks = vi.fn();
    useStore.setState({
      masks: [
        { id: 'm1', frameId: 'frame-1', pathData: 'M 0 0 Z', label: 'A', color: '#fff' },
      ],
    });

    render(<CanvasArea activeTool="move" frame={frame} onClearMasks={onClearMasks} />);
    fireEvent.click(screen.getByRole('button', { name: '清空遮罩' }));

    expect(onClearMasks).toHaveBeenCalled();
    expect(useStore.getState().masks).toHaveLength(1);
  });
});
