import { afterEach, expect, test, vi } from 'vitest';
import { API_QUERY_TIMEOUT_MS, api } from './client';
import { getAccessToken, PLATFORM_UNAUTHORIZED_EVENT, setAccessToken } from '../v2/auth/session';

function boundedRequest(init: Record<string, unknown> = {}) {
  return { ...init, signal: expect.any(AbortSignal) };
}

afterEach(() => {
  vi.restoreAllMocks();
  vi.useRealTimers();
  window.sessionStorage.clear();
});

test('authenticated requests use the session-only bearer token', async () => {
  window.sessionStorage.setItem('vehicle-platform.access-token', 'operator-secret-token');
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { name: 'operator-a', role: 'operator', authMode: 'enforce' } }) } as Response);
  await api.session();
  const [, init] = fetchMock.mock.calls[0];
  expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer operator-secret-token');
  expect(window.localStorage.getItem('vehicle-platform.access-token')).toBeNull();
});

test('history export downloads use bearer authentication and retain the server filename', async () => {
  setAccessToken('customer-export-token');
  const payload = new Blob(['export'], { type: 'text/csv' });
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    headers: new Headers({ 'X-Export-Name': encodeURIComponent('历史数据_customer-a.csv'), 'Content-Type': 'text/csv' }),
    blob: async () => payload
  } as Response);

  const result = await api.downloadHistoryExport('exp customer');

  expect(result.filename).toBe('历史数据_customer-a.csv');
  expect(result.blob.size).toBe(6);
  expect(result.blob.type).toBe('text/csv');
  const [path, init] = fetchMock.mock.calls[0];
  expect(path).toBe('/api/v2/exports/exp%20customer/download');
  expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer customer-export-token');
});

test('a protected 401 terminates the client session but login validation errors stay local', async () => {
  window.sessionStorage.setItem('vehicle-platform.access-token', 'expired-token');
  const unauthorized = vi.fn();
  window.addEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
  vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: false,
    status: 401,
    json: async () => ({ error: { message: '访问令牌无效' } })
  } as Response);

  await expect(api.monitorSummary()).rejects.toThrow('访问令牌无效');
  expect(unauthorized).toHaveBeenCalledTimes(1);
  expect(getAccessToken()).toBe('');
  await expect(api.session()).rejects.toThrow('访问令牌无效');
  expect(unauthorized).toHaveBeenCalledTimes(1);
  window.removeEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
});

test('parallel protected 401 responses invalidate one client session exactly once', async () => {
  setAccessToken('expired-parallel-token');
  const unauthorized = vi.fn();
  window.addEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
  vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: false,
    status: 401,
    json: async () => ({ error: { message: '访问令牌已过期' } })
  } as Response);

  const results = await Promise.allSettled([api.monitorSummary(), api.metricCatalog(), api.opsHealth()]);

  expect(results.every((result) => result.status === 'rejected')).toBe(true);
  expect(unauthorized).toHaveBeenCalledTimes(1);
  expect(getAccessToken()).toBe('');
  window.removeEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
});

test('a delayed 401 from an old token cannot terminate a newer login', async () => {
  setAccessToken('old-token');
  const unauthorized = vi.fn();
  window.addEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
  let resolveResponse!: (response: Response) => void;
  vi.spyOn(globalThis, 'fetch').mockReturnValue(new Promise<Response>((resolve) => { resolveResponse = resolve; }));

  const oldRequest = api.monitorSummary();
  setAccessToken('new-token');
  resolveResponse({
    ok: false,
    status: 401,
    json: async () => ({ error: { message: '旧令牌已过期' } })
  } as Response);

  await expect(oldRequest).rejects.toThrow('旧令牌已过期');
  expect(unauthorized).not.toHaveBeenCalled();
  expect(getAccessToken()).toBe('new-token');
  window.removeEventListener(PLATFORM_UNAUTHORIZED_EVENT, unauthorized);
});

test('durable alert APIs keep versioned actions, rules and notification reads explicit', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-alert', timestamp: 1 }) } as Response);
  await api.alertEventsV2({ status: 'unprocessed', limit: 20, offset: 0 });
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events', boundedRequest({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'unprocessed', limit: 20, offset: 0 }) }));
  await api.actOnAlertV2('alert 1', { version: 2, action: 'acknowledge', note: '已确认' });
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events/alert%201/actions', boundedRequest({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 2, action: 'acknowledge', note: '已确认' }) }));
  await api.readAlertNotificationsV2([7, 8]);
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/notifications/read', boundedRequest({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [7, 8] }) }));
});

test('access APIs post one shared filter contract and version threshold updates', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-access', timestamp: 1783094400000 })
  } as Response);
  const query = { protocol: 'JT808', onlineState: 'offline', limit: 50, offset: 0 };
  await api.accessVehicles(query);
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/vehicles', boundedRequest({
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
  }));

  const unresolved = { protocol: 'JT808', limit: 20, offset: 0 };
  await api.accessUnresolvedIdentities(unresolved);
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/unresolved-identities', boundedRequest({
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(unresolved)
  }));

  await api.updateAccessThresholds({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] });
  expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/thresholds', boundedRequest({
    method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] })
  }));
});

test('vehicle profile API uses encoded VIN and optimistic version updates', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile', timestamp: 1 }) } as Response);
  const input = { modelName: '氢燃料重卡', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '2026-07-01T08:30', runtimeSeconds: 3600, version: 2 };
  await api.updateVehicleProfile('VIN 001', input);
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/VIN%20001/profile', boundedRequest({
    method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
  }));
});

test('latest telemetry uses the encoded vehicle identity path', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { categories: [], values: [] }, traceId: 'trace-telemetry', timestamp: 1 }) } as Response);
  await api.latestTelemetry('粤A 001');
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/%E7%B2%A4A%20001/telemetry/latest', boundedRequest());
});

test('vehicle profile sync posts an explicit dry-run and conflict policy contract', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile-sync', timestamp: 1 }) } as Response);
  const input = {
    sourceSystem: 'oem-tsp', sourceVersion: 'snapshot-1', conflictPolicy: 'preserve' as const, dryRun: true,
    items: [{ vin: 'VIN001', modelName: '车型一', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '', runtimeSeconds: null }]
  };
  await api.syncVehicleProfiles(input);
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicle-profiles/sync', boundedRequest({
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
  }));
});

test('trackPlayback preserves the bounded V2 query contract', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({ data: { vin: 'VIN001', points: [], events: [], sources: [], summary: { pointCount: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 })
  } as Response);
  await api.trackPlayback(new URLSearchParams({ keyword: '粤AG18312', maxPoints: '1200' }));
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/tracks?keyword=%E7%B2%A4AG18312&maxPoints=1200', boundedRequest());
});

test('monitor viewport requests forward AbortSignal so obsolete pans can be cancelled', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { points: [], clusters: [] } }) } as Response);
  const controller = new AbortController();
  await api.monitorMap(new URLSearchParams({ zoom: '13', bounds: '113,22,114,23' }), controller.signal);
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/monitor/map?zoom=13&bounds=113%2C22%2C114%2C23', { signal: expect.any(AbortSignal) });
});

test('monitor workspace aggregates the map screen behind one abortable request', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { summary: {}, vehicles: { items: [] }, map: { points: [], clusters: [] } } }) } as Response);
  const controller = new AbortController();
  await api.monitorWorkspace(new URLSearchParams({ zoom: '13', railLimit: '200' }), controller.signal);
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/monitor/workspace?zoom=13&railLimit=200', { signal: expect.any(AbortSignal) });
});

test('high-volume history, track, and mileage requests forward AbortSignal', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
  const controller = new AbortController();
  const params = new URLSearchParams({ keyword: 'VIN001' });

  await api.trackPlayback(params, controller.signal);
  await api.historyData(params, controller.signal);
  await api.historySeries(params, controller.signal);
  await api.dailyMileage(params, controller.signal);
  await api.mileageStatistics(params, controller.signal);

  for (const [, init] of fetchMock.mock.calls) expect(init?.signal).toBeInstanceOf(AbortSignal);
});

test('route-scoped platform reads forward AbortSignal on navigation cleanup', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
  const controller = new AbortController();
  const signal = controller.signal;

  await api.session(signal);
  await api.metricCatalog(signal);
  await api.historyMetricCatalog(signal);
  await api.historyExports(signal);
  await api.accessSummary({}, signal);
  await api.accessVehicles({ limit: 50, offset: 0 }, signal);
  await api.accessUnresolvedIdentities({ limit: 20, offset: 0 }, signal);
  await api.accessThresholds(signal);
  await api.alertSummaryV2({}, signal);
  await api.alertEventsV2({ limit: 20, offset: 0 }, signal);
  await api.alertEventV2('event-1', signal);
  await api.alertRulesV2(signal);
  await api.alertNotificationsV2(new URLSearchParams({ limit: '100' }), signal);
  await api.vehicleDetail(new URLSearchParams({ keyword: 'VIN001' }), signal);
  await api.latestTelemetry('VIN001', signal);
  await api.reverseGeocode(new URLSearchParams({ longitude: '113', latitude: '23' }), signal);
  await api.opsHealth(signal);
  await api.sourceReadiness(signal);

  expect(fetchMock).toHaveBeenCalledTimes(18);
  for (const [, init] of fetchMock.mock.calls) expect(init?.signal).toBeInstanceOf(AbortSignal);
});

test('turns a stalled route query into a bounded retryable timeout', async () => {
  vi.useFakeTimers();
  const controller = new AbortController();
  let transportSignal: AbortSignal | undefined;
  vi.spyOn(globalThis, 'fetch').mockImplementation((_path, init) => {
    transportSignal = init?.signal ?? undefined;
    return new Promise<Response>((_resolve, reject) => {
      transportSignal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
    });
  });

  const timedOut = expect(api.monitorSummary(new URLSearchParams(), controller.signal)).rejects.toMatchObject({
    name: 'ApiRequestTimeoutError',
    message: expect.stringContaining('15 秒')
  });
  await vi.advanceTimersByTimeAsync(API_QUERY_TIMEOUT_MS);

  await timedOut;
  expect(transportSignal?.aborted).toBe(true);
  expect(controller.signal.aborted).toBe(false);
});

test('bounds a stalled mutation and aborts its transport without an upstream signal', async () => {
  vi.useFakeTimers();
  let transportSignal: AbortSignal | undefined;
  vi.spyOn(globalThis, 'fetch').mockImplementation((_path, init) => {
    transportSignal = init?.signal ?? undefined;
    return new Promise<Response>((_resolve, reject) => {
      transportSignal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
    });
  });

  const timedOut = expect(api.actOnAlertV2('alert-1', { version: 1, action: 'acknowledge', note: '' })).rejects.toMatchObject({
    name: 'ApiRequestTimeoutError',
    message: expect.stringContaining('15 秒')
  });
  await vi.advanceTimersByTimeAsync(API_QUERY_TIMEOUT_MS);

  await timedOut;
  expect(transportSignal).toBeInstanceOf(AbortSignal);
  expect(transportSignal?.aborted).toBe(true);
});

test('preserves navigation cancellation instead of reporting it as a timeout', async () => {
  const controller = new AbortController();
  let transportSignal: AbortSignal | undefined;
  vi.spyOn(globalThis, 'fetch').mockImplementation((_path, init) => {
    transportSignal = init?.signal ?? undefined;
    return new Promise<Response>((_resolve, reject) => {
      transportSignal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
    });
  });

  const cancelled = expect(api.monitorSummary(new URLSearchParams(), controller.signal)).rejects.toMatchObject({ name: 'AbortError' });
  controller.abort();

  await cancelled;
  expect(transportSignal?.aborted).toBe(true);
});

test('rawFramesQuery posts structured JSON instead of URL query strings', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: { items: [], total: 0, limit: 20, offset: 0 },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  await api.rawFramesQuery({
    keyword: '粤AG18312',
    protocol: 'GB32960',
    fields: ['gb32960.vehicle.speed_kmh'],
    includeFields: true,
    limit: 20,
    offset: 0
  });

  expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', boundedRequest({
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      keyword: '粤AG18312',
      protocol: 'GB32960',
      fields: ['gb32960.vehicle.speed_kmh'],
      includeFields: true,
      limit: 20,
      offset: 0
    })
  }));
});

test('vehicleResolve sends keyword to the identity resolution endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        lookupKey: '粤AG18312',
        resolved: true,
        vin: 'LB9A32A24R0LS1426',
        plate: '粤AG18312',
        phone: '13307795425',
        oem: 'G7s',
        protocols: ['GB32960', 'JT808'],
        online: true,
        lastSeen: '2026-07-03 20:12:10'
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.vehicleResolve(new URLSearchParams({ keyword: '粤AG18312' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/vehicles/resolve?keyword=%E7%B2%A4AG18312', boundedRequest());
  expect(result.vin).toBe('LB9A32A24R0LS1426');
});

test('vehicleServiceOverview sends keyword to the lightweight overview endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        vin: 'LB9A32A24R0LS1426',
        plate: '粤AG18312',
        phone: '13307795425',
        oem: 'G7s',
        protocols: ['GB32960', 'JT808'],
        primaryProtocol: 'JT808',
        coverageStatus: 'partial',
        sourceCount: 2,
        onlineSourceCount: 1,
        lastSeen: '2026-07-03 20:12:10',
        realtimeCount: 2,
        historyCount: 0,
        rawCount: 0,
        mileageCount: 0,
        qualityIssueCount: 0,
        serviceStatus: {
          status: 'degraded',
          severity: 'warning',
          title: '来源不完整',
          detail: '1/2 个来源在线',
          sourceCount: 2,
          onlineSourceCount: 1
        }
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.vehicleServiceOverview(new URLSearchParams({ keyword: '粤AG18312' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312', boundedRequest());
  expect(result.coverageStatus).toBe('partial');
  expect(result.serviceStatus?.status).toBe('degraded');
});

test('vehicleServiceOverviews posts keywords to the batch overview endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        items: [
          { vin: 'VIN001', plate: '粤AG18312', protocols: ['JT808'], coverageStatus: 'online', sourceCount: 1, onlineSourceCount: 1 },
          { vin: 'VIN002', plate: '豫A88888', protocols: ['YUTONG_MQTT'], coverageStatus: 'online', sourceCount: 1, onlineSourceCount: 1 }
        ],
        total: 2,
        limit: 200,
        offset: 0
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.vehicleServiceOverviews({
    keywords: ['粤AG18312', 'LMRKH9AC2R1004087'],
    limit: 200,
    offset: 0
  });

  expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/overviews', boundedRequest({
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      keywords: ['粤AG18312', 'LMRKH9AC2R1004087'],
      limit: 200,
      offset: 0
    })
  }));
  expect(result.total).toBe(2);
});

test('vehicleServiceSummary reads the vehicle service summary endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        totalVehicles: 1033,
        boundVehicles: 1024,
        onlineVehicles: 208,
        singleSourceVehicles: 391,
        multiSourceVehicles: 181,
        noDataVehicles: 461,
        identityRequiredVehicles: 9,
        serviceStatuses: [
          { status: 'healthy', title: '服务正常', count: 161 },
          { status: 'no_data', title: '暂无数据来源', count: 461 }
        ],
        protocols: [{ protocol: 'JT808', online: 157, total: 388 }]
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.vehicleServiceSummary();

  expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', boundedRequest());
  expect(result.totalVehicles).toBe(1033);
  expect(result.multiSourceVehicles).toBe(181);
  expect(result.serviceStatuses.find((item) => item.status === 'no_data')?.count).toBe(461);
});

test('reverseGeocode reads the server-side AMap reverse geocode endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        provider: 'AMap',
        longitude: 113.2,
        latitude: 23.1,
        formattedAddress: '广东省广州市天河区测试路',
        province: '广东省',
        city: '广州市',
        district: '天河区',
        adcode: '440106'
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.reverseGeocode(new URLSearchParams({ longitude: '113.2', latitude: '23.1' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/map/reverse-geocode?longitude=113.2&latitude=23.1', boundedRequest());
  expect(result.formattedAddress).toBe('广东省广州市天河区测试路');
});

test('qualityNotificationPlan reads alert rules and priority issues from backend', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        summary: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] },
        rules: [{ issueType: 'NO_SOURCE', title: '无数据来源', level: 'P0', owner: '平台接入', trigger: '无来源', notify: '立即通知', sla: '30 分钟确认', count: 1 }],
        policies: [{ name: 'P0 实时中断', target: '接入运维', channel: '邮件', condition: '无来源', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }],
        priorityIssues: [{ vin: 'VIN001', plate: '粤A001', phone: '', sourceEndpoint: '', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '', detail: '无来源', priority: 'P0', actionLabel: '确认平台转发', actionDetail: '确认平台转发', sla: '30 分钟确认', vehicleLabel: '粤A001 / VIN001', realtimeHash: '#/realtime?keyword=VIN001', historyHash: '#/history?keyword=VIN001', rawHash: '#/history-query?keyword=VIN001&tab=raw', vehicleHash: '#/detail?keyword=VIN001', notificationText: '【P0 告警通知】无数据来源' }],
        activeRuleCount: 1,
        p0RuleCount: 1
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.qualityNotificationPlan(new URLSearchParams({ issueType: 'NO_SOURCE' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/quality/notification-plan?issueType=NO_SOURCE', boundedRequest());
  expect(result.rules[0].level).toBe('P0');
  expect(result.policies[0].escalationMinutes).toBe(30);
  expect(result.policies[0].acceptanceCriteria).toContain('来源恢复');
  expect(result.priorityIssues[0].notificationText).toContain('告警通知');
});

test('alertEvents reads the vehicle alert event endpoint', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        items: [{
          vin: 'VIN001',
          plate: '粤A001',
          phone: '13307795425',
          sourceEndpoint: '115.29.187.205:808',
          protocol: 'JT808',
          issueType: 'LINK_GAP',
          severity: 'warning',
          lastSeen: '2026-07-03 20:12:10',
          detail: '来源间断'
        }],
        total: 1,
        limit: 20,
        offset: 0
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.alertEvents(new URLSearchParams({ protocol: 'JT808', limit: '20' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/alert-events?protocol=JT808&limit=20', boundedRequest());
  expect(result.items[0].issueType).toBe('LINK_GAP');
});

test('onlineVehicleStatuses reads paged online vehicle status rows', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        items: [{
          vin: 'LB9A32A24R0LS1426',
          plate: '粤AG18312',
          phone: '13307795425',
          oem: 'G7s',
          protocols: ['JT808', 'GB32960'],
          online: false,
          lastSeen: '2026-07-03 20:12:10',
          offlineDurationMinutes: 18,
          sourceCount: 2,
          onlineSourceCount: 0,
          bindingStatus: 'bound'
        }],
        total: 1,
        limit: 10,
        offset: 0
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.onlineVehicleStatuses(new URLSearchParams({ keyword: '粤AG18312', limit: '10' }));

  expect(fetchMock).toHaveBeenCalledWith('/api/statistics/online-vehicles?keyword=%E7%B2%A4AG18312&limit=10', boundedRequest());
  expect(result.items[0].offlineDurationMinutes).toBe(18);
});

test('sourceReadiness reads ops source readiness plan', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: true,
    json: async () => ({
      data: {
        totalVehicles: 1033,
        onlineVehicles: 208,
        kafkaLag: 0,
        activeConnections: 178,
        redisOnlineKeys: 258,
        platformRelease: 'platform-source-test',
        sources: [{
          protocol: 'GB32960',
          role: '整车与氢能实时数据主来源',
          online: 73,
          total: 340,
          onlineRate: 21.47,
          missingVehicles: 693,
          severity: 'warning',
          status: '覆盖不足',
          evidence: '在线 73/340',
          action: '核对平台转发清单',
          acceptance: '缺失来源车辆下降',
          vehiclesHash: '#/vehicles?missingProtocol=GB32960',
          realtimeHash: '#/realtime?protocol=GB32960',
          historyHash: '#/history-query?protocol=GB32960&tab=raw',
          alertHash: '#/alert-events?protocol=GB32960'
        }]
      },
      traceId: 'trace-test',
      timestamp: 1783094400000
    })
  } as Response);

  const result = await api.sourceReadiness();

  expect(fetchMock).toHaveBeenCalledWith('/api/ops/source-readiness', boundedRequest());
  expect(result.platformRelease).toBe('platform-source-test');
  expect(result.sources[0].protocol).toBe('GB32960');
});

test('api errors include backend message, detail, and trace id', async () => {
  vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: false,
    status: 500,
    json: async () => ({
      error: {
        code: 'INTERNAL',
        message: '服务处理失败',
        detail: 'TDengine timeout'
      },
      traceId: 'trace-error',
      timestamp: 1783094400000
    })
  } as Response);

  await expect(api.opsHealth()).rejects.toThrow('服务处理失败: TDengine timeout (traceId: trace-error)');
});
