import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import type { MonitorMapResponse } from '../../api/types';
import { wgs84ToGcj02, type AMapLike, type AMapMap, type AMapMassPoint } from '../../integrations/amap';
import { advancePointMotions, FleetMap, interpolateMassPoints, massPointFingerprint, movingPointIDs, pointMoveDurationMs, pointMoveFrameMs } from './FleetMap';

const setData = vi.fn<(data: AMapMassPoint[]) => void>();
const setStyle = vi.fn();
const addLabels = vi.fn();
const removeLabels = vi.fn();
const clearLabels = vi.fn();
const setLabelsMap = vi.fn();
const markerSetMap = vi.fn();
const markerSetPosition = vi.fn();
const labelSetPosition = vi.fn();
const setZoomAndCenter = vi.fn();
const setCenter = vi.fn(() => mapHandlers.get('moveend')?.({}));
const panTo = vi.fn();
const mapOff = vi.fn();
const mapDestroy = vi.fn();
const massOff = vi.fn();
const getZoom = vi.fn(() => 5);
const getBounds = vi.fn((): ReturnType<NonNullable<AMapMap['getBounds']>> => ({}));
const mapHandlers = new Map<string, (event: unknown) => void>();
const markerOptions: Record<string, unknown>[] = [];
const mapOptions: Record<string, unknown>[] = [];
const labelLayerOptions: Record<string, unknown>[] = [];

function useFastAnimationFrames(stepMs = 2_500) {
  let timestamp = 0;
  return vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
    timestamp += stepMs;
    return window.setTimeout(() => callback(timestamp), 0);
  });
}

class TestMap {
  constructor(_container: HTMLDivElement, options: Record<string, unknown>) {
    mapOptions.push(options);
  }
  add = vi.fn();
  addControl = vi.fn();
  destroy = mapDestroy;
  on = vi.fn((event: string, handler: (value: unknown) => void) => mapHandlers.set(event, handler));
  off = mapOff;
  getZoom = getZoom;
  getBounds = getBounds;
  setZoomAndCenter = setZoomAndCenter;
  setCenter = setCenter;
  panTo = panTo;
}

class TestMassMarks {
  on = vi.fn();
  off = massOff;
  setMap = vi.fn();
  setData = setData;
  setStyle = setStyle;
}

class TestScale {}
class TestToolBar {}
class TestSize {}
class TestPixel {}
class TestLabelsLayer {
  constructor(options: Record<string, unknown> = {}) {
    labelLayerOptions.push(options);
  }
  add = addLabels;
  remove = removeLabels;
  clear = clearLabels;
  setMap = setLabelsMap;
}
class TestLabelMarker {
  setPosition = labelSetPosition;
  constructor(public options: Record<string, unknown>) {}
}
class TestMarker {
  setMap = markerSetMap;
  setPosition = markerSetPosition;
  constructor(options: Record<string, unknown>) {
    markerOptions.push(options);
  }
}

const monitorMap: MonitorMapResponse = {
  mode: 'clusters',
  zoom: 5,
  total: 12,
  truncated: false,
  points: [],
  clusters: [{
    id: 'cluster-1',
    longitude: 121.1,
    latitude: 30.6,
    count: 12,
    online: 8,
    offline: 4,
    driving: 3,
    idle: 5,
    unknown: 0
  }],
  asOf: '2026-07-14T01:00:00Z'
};

const pointMap: MonitorMapResponse = {
  ...monitorMap,
  mode: 'points',
  zoom: 13,
  total: 2,
  clusters: [],
  points: [{
    vin: 'LTEST000000000001',
    plate: '粤A12345',
    protocol: 'JT808',
    protocols: ['JT808'],
    longitude: 113.26,
    latitude: 23.13,
    speedKmh: 42,
    socPercent: 80,
    totalMileageKm: 1234,
    lastSeen: '2026-07-14T01:00:00Z',
    reportIntervalMs: 10_000,
    status: 'driving'
  }, {
    vin: 'LTEST000000000002',
    plate: '粤B67890',
    protocol: 'JT808',
    protocols: ['JT808'],
    longitude: 113.28,
    latitude: 23.15,
    speedKmh: 0,
    socPercent: 72,
    totalMileageKm: 2234,
    lastSeen: '2026-07-14T01:00:00Z',
    reportIntervalMs: 30_000,
    status: 'idle'
  }]
};

test('keeps large fleet redraw fingerprints constant-size and sensitive to point changes', () => {
  const manyPoints = Array.from({ length: 10_000 }, (_, index) => ({
    ...pointMap.points[index % pointMap.points.length],
    vin: `LARGE-FLEET-${index}`,
    longitude: 113.26 + index / 1_000_000
  }));
  const fingerprint = massPointFingerprint('points', [], manyPoints, []);
  const changed = massPointFingerprint('points', [], [
    ...manyPoints.slice(0, -1),
    { ...manyPoints[manyPoints.length - 1], longitude: 114.5 }
  ], []);

  expect(fingerprint.length).toBeLessThan(40);
  expect(changed).not.toBe(fingerprint);
  expect(massPointFingerprint('points', [], manyPoints, [])).toBe(fingerprint);
});

test('interpolates existing mass points at constant speed while keeping new points stable', () => {
  const previous: AMapMassPoint[] = [{ id: 'moving', label: 'A', style: 0, lnglat: [10, 20] }];
  const target: AMapMassPoint[] = [
    { id: 'moving', label: 'A', style: 2, lnglat: [14, 24] },
    { id: 'new', label: 'B', style: 1, lnglat: [30, 40] }
  ];

  expect(interpolateMassPoints(previous, target, 0)).toEqual([
    { ...target[0], lnglat: [10, 20] },
    target[1]
  ]);
  expect(interpolateMassPoints(previous, target, .5)[0]).toEqual({ ...target[0], lnglat: [12, 22] });
  expect(interpolateMassPoints(previous, target, 1)).toEqual(target);
});

test('does not interpolate across an authoritative location source switch', () => {
  const previous: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.2, 23.1], sourceToken: 'JT808:terminal-a' }];
  const sameSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.3, 23.2], sourceToken: 'JT808:terminal-a' }];
  const switchedSource: AMapMassPoint[] = [{ id: 'vehicle', label: 'A', style: 0, lnglat: [113.4, 23.3], sourceToken: 'GB32960:canonical' }];

  expect(movingPointIDs(previous, sameSource).has('vehicle')).toBe(true);
  expect(movingPointIDs(previous, switchedSource).has('vehicle')).toBe(false);
});

test('uses each protocol report interval with bounded fallbacks for point motion', () => {
  expect(pointMoveDurationMs(10_000)).toBe(10_000);
  expect(pointMoveDurationMs(30_000)).toBe(30_000);
  expect(pointMoveDurationMs(60_000)).toBe(60_000);
  expect(pointMoveDurationMs(undefined)).toBe(15_000);
  expect(pointMoveDurationMs(100)).toBe(1_000);
  expect(pointMoveDurationMs(300_000)).toBe(120_000);
});

test('advances each vehicle independently according to its report interval', () => {
  const points: AMapMassPoint[] = [
    { id: 'JT808', label: 'A', style: 0, lnglat: [0, 0] },
    { id: 'GB32960', label: 'B', style: 0, lnglat: [0, 0] }
  ];
  const pointArray = points;
  const pointObjects = [...points];
  advancePointMotions([
    { point: points[0], start: [0, 0], target: [30, 30], durationMs: 10_000 },
    { point: points[1], start: [0, 0], target: [30, 30], durationMs: 30_000 }
  ], 10_000);

  expect(points[0].lnglat).toEqual([30, 30]);
  expect(points[1].lnglat).toEqual([10, 10]);
  expect(points).toBe(pointArray);
  expect(points[0]).toBe(pointObjects[0]);
  expect(points[1]).toBe(pointObjects[1]);
});

test('reduces animation frame frequency as fleet redraw pressure grows', () => {
  expect(pointMoveFrameMs(1, 1)).toBe(100);
  expect(pointMoveFrameMs(200, 10)).toBeLessThan(pointMoveFrameMs(800, 120));
  expect(pointMoveFrameMs(2_000, 1_000)).toBe(220);
});

function amapMock(): AMapLike {
  return {
    Map: TestMap as unknown as AMapLike['Map'],
    Marker: TestMarker as unknown as AMapLike['Marker'],
    Polyline: class {} as unknown as AMapLike['Polyline'],
    Scale: TestScale,
    ToolBar: TestToolBar,
    Size: TestSize as unknown as AMapLike['Size'],
    Pixel: TestPixel as unknown as AMapLike['Pixel'],
    MassMarks: TestMassMarks as unknown as AMapLike['MassMarks'],
    LabelsLayer: TestLabelsLayer as unknown as NonNullable<AMapLike['LabelsLayer']>,
    LabelMarker: TestLabelMarker as unknown as NonNullable<AMapLike['LabelMarker']>
  };
}

afterEach(() => {
  cleanup();
  delete window.__LINGNIU_APP_CONFIG__;
  delete window.AMapLoader;
  setData.mockReset();
  setStyle.mockReset();
  addLabels.mockReset();
  removeLabels.mockReset();
  clearLabels.mockReset();
  setLabelsMap.mockReset();
  markerSetMap.mockReset();
  markerSetPosition.mockReset();
  labelSetPosition.mockReset();
  setZoomAndCenter.mockReset();
  setCenter.mockReset();
  panTo.mockReset();
  mapOff.mockReset();
  mapDestroy.mockReset();
  massOff.mockReset();
  getZoom.mockReset();
  getZoom.mockReturnValue(5);
  getBounds.mockReset();
  getBounds.mockReturnValue({});
  mapHandlers.clear();
  markerOptions.length = 0;
  mapOptions.length = 0;
  labelLayerOptions.length = 0;
  vi.restoreAllMocks();
});

test('recovers in place after a transient AMap failure and renders data that arrived while retrying', async () => {
  let resolveAMap!: (value: AMapLike) => void;
  const delayedAMap = new Promise<AMapLike>((resolve) => {
    resolveAMap = resolve;
  });
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  const load = vi.fn()
    .mockRejectedValueOnce(new Error('temporary map network failure'))
    .mockImplementationOnce(() => delayedAMap);
  window.AMapLoader = { load };

  render(
    <FleetMap
      vehicles={[]}
      monitorMap={monitorMap}
      onSelect={() => undefined}
    />
  );

  expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
  fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
  expect(setData).not.toHaveBeenCalled();

  await act(async () => {
    resolveAMap(amapMock());
    await delayedAMap;
  });

  await waitFor(() => expect(setData).toHaveBeenCalledWith([
    expect.objectContaining({ id: 'cluster-1', lnglat: wgs84ToGcj02(121.1, 30.6), label: '12 辆' })
  ]));
  const clusterStyles = setStyle.mock.calls[setStyle.mock.calls.length - 1]?.[0] as Array<{ url: string }>;
  expect(decodeURIComponent(clusterStyles[5].url)).toContain('>12</text>');
  expect(decodeURIComponent(clusterStyles[5].url)).not.toContain('10+');
  expect(load).toHaveBeenCalledTimes(2);
  expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();
});

test('restores the saved monitor viewport without forcing the selected vehicle zoom', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const bounds = '113.100000,23.000000,113.400000,23.300000';

  render(<FleetMap
    vehicles={[]}
    monitorMap={pointMap}
    selectedVin="LTEST000000000001"
    initialViewport={{ zoom: 13, bounds }}
    onSelect={() => undefined}
  />);

  await waitFor(() => expect(mapOptions).toHaveLength(1));
  expect(mapOptions[0]).toEqual(expect.objectContaining({
    zoom: 13,
    center: wgs84ToGcj02(113.25, 23.15)
  }));
  await waitFor(() => expect(markerSetMap).toHaveBeenCalled());
  expect(setZoomAndCenter).not.toHaveBeenCalled();
  expect(panTo).not.toHaveBeenCalled();
});

test('detaches AMap listeners and destroys the map on unmount', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };

  const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
  await waitFor(() => expect(setData).toHaveBeenCalled());
  view.unmount();

  expect(massOff).toHaveBeenCalledWith('click', expect.any(Function));
  expect(mapOff).toHaveBeenCalledWith('moveend', expect.any(Function));
  expect(mapOff).toHaveBeenCalledWith('zoomend', expect.any(Function));
  expect(mapOff).toHaveBeenCalledWith('dragstart', expect.any(Function));
  expect(mapDestroy).toHaveBeenCalledTimes(1);
});

test('cancels an in-flight point animation when the map unmounts', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
  const cancelFrame = vi.spyOn(window, 'cancelAnimationFrame');
  const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
  await waitFor(() => expect(setData).toHaveBeenCalled());

  view.rerender(<FleetMap vehicles={[]} monitorMap={{
    ...pointMap,
    points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
  }} onSelect={() => undefined} />);
  await waitFor(() => expect(requestFrame).toHaveBeenCalled());
  view.unmount();

  expect(cancelFrame).toHaveBeenCalledWith(expect.any(Number));
});

test('skips unchanged refresh redraws and smoothly moves points without replacing their labels', async () => {
  useFastAnimationFrames();
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
  await waitFor(() => expect(addLabels).toHaveBeenCalled());
  const dataCalls = setData.mock.calls.length;
  const styleCalls = setStyle.mock.calls.length;
  const addCalls = addLabels.mock.calls.length;
  const removeCalls = removeLabels.mock.calls.length;
  const clearCalls = clearLabels.mock.calls.length;

  await act(async () => {
    view.rerender(<FleetMap vehicles={[]} monitorMap={{ ...pointMap, asOf: '2026-07-14T01:00:15Z' }} onSelect={() => undefined} />);
    await Promise.resolve();
  });
  expect(setData).toHaveBeenCalledTimes(dataCalls);
  expect(setStyle).toHaveBeenCalledTimes(styleCalls);
  expect(addLabels).toHaveBeenCalledTimes(addCalls);
  expect(removeLabels).toHaveBeenCalledTimes(removeCalls);
  expect(clearLabels).toHaveBeenCalledTimes(clearCalls);

  view.rerender(<FleetMap vehicles={[]} monitorMap={{
    ...pointMap,
    asOf: '2026-07-14T01:00:30Z',
    points: [{ ...pointMap.points[0], longitude: 113.27 }, pointMap.points[1]]
  }} onSelect={() => undefined} />);
  await waitFor(() => {
    const latest = setData.mock.calls[setData.mock.calls.length - 1]?.[0];
    expect(latest?.[0].lnglat).toEqual(wgs84ToGcj02(113.27, 23.13));
  }, { timeout: 1_800 });
  expect(setData.mock.calls.length).toBeGreaterThan(dataCalls + 1);
  const animationFrameBuffers = setData.mock.calls.slice(dataCalls).map(([frameData]) => frameData);
  expect(new Set(animationFrameBuffers).size).toBe(1);
  expect(setStyle).toHaveBeenCalledTimes(styleCalls);
  expect(removeLabels).toHaveBeenCalledTimes(removeCalls);
  expect(addLabels).toHaveBeenCalledTimes(addCalls);
  expect(labelSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.13));
});

test('does not traverse unchanged point arrays for an asOf-only refresh', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  let pointReads = 0;
  const observedPoints = new Proxy(pointMap.points, {
    get(target, property, receiver) {
      if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1;
      return Reflect.get(target, property, receiver);
    }
  });
  const observedMap = { ...pointMap, points: observedPoints };
  const view = render(<FleetMap vehicles={[]} monitorMap={observedMap} onSelect={() => undefined} />);
  await waitFor(() => expect(addLabels).toHaveBeenCalled());
  pointReads = 0;

  await act(async () => {
    view.rerender(<FleetMap
      vehicles={[]}
      monitorMap={{ ...observedMap, asOf: '2026-07-14T01:00:15Z' }}
      onSelect={() => undefined}
    />);
    await Promise.resolve();
  });

  expect(pointReads).toBe(0);
});

test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
  const [west, south] = wgs84ToGcj02(113, 22);
  const [east, north] = wgs84ToGcj02(114, 24);
  getZoom.mockReturnValue(13);
  getBounds.mockReturnValue({
    getSouthWest: () => ({ getLng: () => west, getLat: () => south }),
    getNorthEast: () => ({ getLng: () => east, getLat: () => north })
  });
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const onViewportChange = vi.fn();

  render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} onViewportChange={onViewportChange} />);

  await waitFor(() => expect(onViewportChange).toHaveBeenCalled());
  const viewport = onViewportChange.mock.calls[onViewportChange.mock.calls.length - 1]?.[0] as { zoom: number; bounds: string };
  expect(viewport.zoom).toBe(13);
  const bounds = viewport.bounds.split(',').map(Number);
  expect(Math.abs(bounds[0] - 113)).toBeLessThan(0.0005);
  expect(Math.abs(bounds[1] - 22)).toBeLessThan(0.0005);
  expect(Math.abs(bounds[2] - 114)).toBeLessThan(0.0005);
  expect(Math.abs(bounds[3] - 24)).toBeLessThan(0.0005);
});

test('renders one selected plate and smoothly follows it until the map is dragged', async () => {
  useFastAnimationFrames();
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };

  const view = render(
    <FleetMap
      vehicles={[]}
      monitorMap={pointMap}
      selectedVin="LTEST000000000001"
      onSelect={() => undefined}
    />
  );

  await waitFor(() => expect(addLabels).toHaveBeenCalled());
  const renderedLabels = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
  const selectedPlate = renderedLabels.find((marker) => (marker.options.text as { content: string }).content === '粤A12345');
  const floatingPlate = renderedLabels.find((marker) => (marker.options.text as { content: string }).content === '粤B67890');
  expect(selectedPlate).toBeDefined();
  expect(floatingPlate).toBeDefined();
  expect((selectedPlate!.options.text as { style: unknown }).style).toEqual((floatingPlate!.options.text as { style: unknown }).style);
  expect(selectedPlate!.options).toEqual(expect.objectContaining({ rank: 100, zIndex: 10 }));
  expect(floatingPlate!.options).toEqual(expect.objectContaining({ rank: 1, zIndex: 1 }));
  expect((floatingPlate!.options.text as { style: Record<string, unknown> }).style).toEqual(expect.objectContaining({
    fillColor: '#174d9f',
    backgroundColor: '#eef5ff',
    borderColor: '#7fb0fa',
    borderWidth: 1,
    borderRadius: 6,
    padding: [5, 9],
    fontSize: 11,
    shadowColor: 'rgba(18, 104, 243, 0.18)',
    shadowBlur: 14,
    shadowOffsetY: 5
  }));
  expect(mapOptions).toContainEqual(expect.objectContaining({ mapStyle: 'amap://styles/whitesmoke' }));
  await waitFor(() => expect(markerOptions).toContainEqual(expect.objectContaining({
    content: expect.stringContaining('粤A12345')
  })));
  expect(markerOptions[markerOptions.length - 1]?.content).not.toContain('<span>');
  expect(markerOptions[markerOptions.length - 1]?.content).toContain('is-driving');
  expect(markerSetMap).toHaveBeenCalled();
  expect(renderedLabels).toHaveLength(2);
  expect(setZoomAndCenter).toHaveBeenCalledTimes(1);
  expect(setZoomAndCenter).toHaveBeenLastCalledWith(15, wgs84ToGcj02(113.26, 23.13));

  view.rerender(
    <FleetMap
      vehicles={[]}
      monitorMap={{
        ...pointMap,
        asOf: '2026-07-14T01:00:15Z',
        points: [{ ...pointMap.points[0], longitude: 113.27, latitude: 23.14 }]
      }}
      selectedVin="LTEST000000000001"
      onSelect={() => undefined}
    />
  );
  await waitFor(() => expect(markerSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 });
  expect(setCenter).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14));
  expect(panTo).not.toHaveBeenCalled();
  expect(setZoomAndCenter).toHaveBeenCalledTimes(1);

  const follow = screen.getByRole('button', { name: '跟随车辆' });
  expect(follow).toHaveAttribute('aria-pressed', 'true');
  act(() => mapHandlers.get('dragstart')?.({}));
  expect(follow).toHaveAttribute('aria-pressed', 'false');

  const panCountAfterDrag = panTo.mock.calls.length;
  view.rerender(
    <FleetMap
      vehicles={[]}
      monitorMap={{
        ...pointMap,
        asOf: '2026-07-14T01:00:30Z',
        points: [{ ...pointMap.points[0], longitude: 113.29, latitude: 23.16 }, pointMap.points[1]]
      }}
      selectedVin="LTEST000000000001"
      onSelect={() => undefined}
    />
  );
  await waitFor(() => expect(markerSetPosition).toHaveBeenLastCalledWith(wgs84ToGcj02(113.29, 23.16)), { timeout: 1_800 });
  expect(panTo).toHaveBeenCalledTimes(panCountAfterDrag);
  fireEvent.click(follow);
  await waitFor(() => expect(follow).toHaveAttribute('aria-pressed', 'true'));
  expect(panTo).toHaveBeenLastCalledWith(wgs84ToGcj02(113.29, 23.16), 650);

  getZoom.mockReturnValue(20);
  view.rerender(
    <FleetMap
      vehicles={[]}
      monitorMap={{ ...pointMap, asOf: '2026-07-14T01:00:45Z' }}
      selectedVin="LTEST000000000002"
      onSelect={() => undefined}
    />
  );
  await waitFor(() => expect(panTo).toHaveBeenLastCalledWith(wgs84ToGcj02(113.28, 23.15), 650));
  expect(setZoomAndCenter).toHaveBeenCalledTimes(1);
  expect(markerOptions[markerOptions.length - 1]?.content).toContain('is-idle');

  const toggle = screen.getByRole('button', { name: '悬浮车牌' });
  expect(toggle).toHaveAttribute('aria-pressed', 'true');
  fireEvent.click(toggle);
  await waitFor(() => expect(toggle).toHaveAttribute('aria-pressed', 'false'));
  expect(removeLabels).toHaveBeenCalled();
  const hiddenFloatingLabels = removeLabels.mock.calls[removeLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
  expect(hiddenFloatingLabels).toHaveLength(1);
  expect((hiddenFloatingLabels[0].options.text as { content: string }).content).toBe('粤A12345');
});

test('does not treat intermediate programmatic follow frames as user viewport changes', async () => {
  useFastAnimationFrames();
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const onViewportChange = vi.fn();
  const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} selectedVin="LTEST000000000001" onSelect={() => undefined} onViewportChange={onViewportChange} />);
  await waitFor(() => expect(onViewportChange).toHaveBeenCalled());
  onViewportChange.mockReset();

  view.rerender(<FleetMap vehicles={[]} monitorMap={{
    ...pointMap,
    points: [{ ...pointMap.points[0], longitude: 113.27, latitude: 23.14 }, pointMap.points[1]]
  }} selectedVin="LTEST000000000001" onSelect={() => undefined} onViewportChange={onViewportChange} />);

  await waitFor(() => expect(setCenter).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 });
  await waitFor(() => expect(onViewportChange).toHaveBeenCalledTimes(1));
});

test('renders every nearby plate with staggered positions at maximum zoom', async () => {
  getZoom.mockReturnValue(20);
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const crowdedMap: MonitorMapResponse = {
    ...pointMap,
    zoom: 20,
    total: 3,
    points: [
      pointMap.points[0],
      { ...pointMap.points[1], longitude: 113.26001, latitude: 23.13001 },
      { ...pointMap.points[1], vin: 'LTEST000000000003', plate: '粤C24680', longitude: 113.26002, latitude: 23.13002 }
    ]
  };

  const view = render(<FleetMap vehicles={[]} monitorMap={crowdedMap} onSelect={() => undefined} />);

  await waitFor(() => expect(addLabels).toHaveBeenCalled());
  expect(labelLayerOptions).toContainEqual(expect.objectContaining({ zooms: [19, 20], zIndex: 110, collision: false, allowCollision: true }));
  const denseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
  expect(denseMarkers).toHaveLength(3);
  const textOffsets = denseMarkers.map((marker) => JSON.stringify((marker.options.text as { offset: [number, number] }).offset));
  expect(new Set(textOffsets).size).toBe(3);
  expect(denseMarkers.every((marker) => marker.options.icon == null)).toBe(true);

  const labelRenderCount = addLabels.mock.calls.length;
  view.rerender(<FleetMap vehicles={[]} monitorMap={crowdedMap} selectedVin="LTEST000000000002" onSelect={() => undefined} />);
  await waitFor(() => expect(addLabels.mock.calls.length).toBeGreaterThan(labelRenderCount));
  const selectedDenseMarkers = addLabels.mock.calls[addLabels.mock.calls.length - 1]?.[0] as TestLabelMarker[];
  expect(selectedDenseMarkers).toHaveLength(1);
  expect(selectedDenseMarkers.every((marker) => marker.options.icon == null)).toBe(true);
  expect(selectedDenseMarkers.find((marker) => marker.options.rank === 100)).toBeDefined();
});
