import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import type { HistoryLocationRow, TrackStop } from '../../api/types';
import type { AMapLike } from '../../integrations/amap';
import { TrackMap } from './TrackMap';

const overlayOff = vi.fn();
const overlaySetMap = vi.fn();
const mapOff = vi.fn();
const mapDestroy = vi.fn();
const markerListeners: Array<{ overlay: TestOverlay; event: string; handler: () => void }> = [];
const overlayInstances: TestOverlay[] = [];
const mapInstances: TestMap[] = [];

class TestOverlay {
  constructor() { overlayInstances.push(this); }
  on = vi.fn((event: string, handler: () => void) => markerListeners.push({ overlay: this, event, handler }));
  off = overlayOff;
  setMap = overlaySetMap;
  setPosition = vi.fn();
  setPath = vi.fn();
}

class TestMap {
  constructor() { mapInstances.push(this); }
  add = vi.fn();
  addControl = vi.fn();
  setFitView = vi.fn();
  destroy = mapDestroy;
  on = vi.fn();
  off = mapOff;
  panTo = vi.fn();
}

class TestScale {}
class TestToolBar {}
class TestSize {}
class TestPixel {}
class TestMassMarks {}

function amapMock(): AMapLike {
  return {
    Map: TestMap as unknown as AMapLike['Map'],
    Marker: TestOverlay as unknown as AMapLike['Marker'],
    Polyline: TestOverlay 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']
  };
}

const points = [{
  vin: 'LTEST000000000001',
  plate: '粤A12345',
  protocol: 'JT808',
  deviceTime: '2026-07-16T00:00:00Z',
  serverTime: '2026-07-16T00:00:01Z',
  longitude: 113.26,
  latitude: 23.13,
  speedKmh: 0,
  totalMileageKm: 100
}, {
  vin: 'LTEST000000000001',
  plate: '粤A12345',
  protocol: 'JT808',
  deviceTime: '2026-07-16T00:10:00Z',
  serverTime: '2026-07-16T00:10:01Z',
  longitude: 113.28,
  latitude: 23.15,
  speedKmh: 30,
  totalMileageKm: 105
}] as HistoryLocationRow[];

const stops = [{
  index: 0,
  startTime: '2026-07-16T00:00:00Z',
  endTime: '2026-07-16T00:05:00Z',
  durationSeconds: 300,
  pointCount: 1,
  longitude: 113.26,
  latitude: 23.13,
  sampledIndex: 0,
  evidence: 'GPS 推断'
}] as TrackStop[];

afterEach(() => {
  cleanup();
  delete window.__LINGNIU_APP_CONFIG__;
  delete window.AMapLoader;
  overlayOff.mockReset();
  overlaySetMap.mockReset();
  mapOff.mockReset();
  mapDestroy.mockReset();
  markerListeners.length = 0;
  overlayInstances.length = 0;
  mapInstances.length = 0;
});

test('defers the map runtime until valid data exists, retries a transient failure, and releases it when cleared', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  const load = vi.fn()
    .mockRejectedValueOnce(new Error('temporary map network failure'))
    .mockResolvedValueOnce(amapMock());
  window.AMapLoader = { load };
  const view = render(<TrackMap
    points={[]}
    stops={[]}
    activeIndex={0}
    showStops={false}
    follow
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);

  expect(load).not.toHaveBeenCalled();
  expect(mapInstances).toHaveLength(0);
  expect(view.container.querySelector('.v2-map-state')).not.toBeInTheDocument();

  view.rerender(<TrackMap
    points={points}
    stops={[]}
    activeIndex={0}
    showStops={false}
    follow
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);
  expect(await screen.findByText(/地图加载失败/)).toBeInTheDocument();
  fireEvent.click(screen.getByRole('button', { name: /重新加载地图/ }));
  await waitFor(() => expect(mapInstances).toHaveLength(1));
  expect(load).toHaveBeenCalledTimes(2);
  expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();

  view.rerender(<TrackMap
    points={[]}
    stops={[]}
    activeIndex={0}
    showStops={false}
    follow
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);
  await waitFor(() => expect(mapDestroy).toHaveBeenCalledTimes(1));
  expect(mapInstances).toHaveLength(1);
});

test('does not initialize the map for rows without a valid coordinate', () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  const load = vi.fn(async () => amapMock());
  window.AMapLoader = { load };
  render(<TrackMap
    points={[{ ...points[0], longitude: 0, latitude: 0 }]}
    stops={[]}
    activeIndex={0}
    showStops={false}
    follow
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);

  expect(load).not.toHaveBeenCalled();
  expect(mapInstances).toHaveLength(0);
  expect(document.body).toHaveTextContent('当前轨迹没有可展示的有效坐标');
});

test('uses the playback cadence for follow animation and advances the passed path', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const view = render(<TrackMap
    points={points}
    stops={[]}
    activeIndex={0}
    showStops={false}
    follow
    followDurationMs={65}
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);

  await waitFor(() => expect(mapInstances).toHaveLength(1));
  view.rerender(<TrackMap
    points={points}
    stops={[]}
    activeIndex={1}
    showStops={false}
    follow
    followDurationMs={65}
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);

  await waitFor(() => expect(mapInstances[0].panTo).toHaveBeenLastCalledWith(expect.any(Array), 65));
  expect(overlayInstances.some((overlay) => overlay.setPath.mock.calls.some(([path]) => Array.isArray(path) && path.length === 2))).toBe(true);
});

test('detaches every marker listener and map listener on unmount', async () => {
  window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
  window.AMapLoader = { load: vi.fn(async () => amapMock()) };
  const view = render(<TrackMap
    points={points}
    stops={stops}
    activeIndex={0}
    showStops
    follow
    onSelectIndex={() => undefined}
    onFollowChange={() => undefined}
  />);

  await waitFor(() => expect(markerListeners).toHaveLength(3));
  view.unmount();

  for (const listener of markerListeners) {
    expect(overlayOff).toHaveBeenCalledWith(listener.event, listener.handler);
  }
  expect(mapOff).toHaveBeenCalledWith('dragstart', expect.any(Function));
  expect(overlaySetMap).toHaveBeenCalled();
  expect(mapDestroy).toHaveBeenCalledTimes(1);
});
