/**
 * fetchAllScanVulnerabilities backs the scan sheet's CSV export. The detail
 * table renders a capped page, but the CSV promises the complete list, so this
 * helper must page past the backend's per-request cap (1000 rows) and collect
 * every finding. A scan with thousands of CVEs is the case this guards.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';

vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() }));

import { apiFetch } from '@/lib/api';
import { fetchAllScanVulnerabilities } from '../VulnerabilityScanSheet.export';
import type { VulnerabilityDetail } from '@/types/security';

const mockedFetch = vi.mocked(apiFetch);

function response(ok: boolean, body: unknown): Response {
  return { ok, json: async () => body } as unknown as Response;
}

function rows(n: number, startId = 0): VulnerabilityDetail[] {
  return Array.from({ length: n }, (_, i) => ({ id: startId + i }) as VulnerabilityDetail);
}

beforeEach(() => mockedFetch.mockReset());

describe('fetchAllScanVulnerabilities', () => {
  it('fetches a single page when the total fits under the per-request cap', async () => {
    mockedFetch.mockResolvedValueOnce(response(true, { items: rows(42), total: 42 }));
    const result = await fetchAllScanVulnerabilities(7);
    expect(result).toHaveLength(42);
    expect(mockedFetch).toHaveBeenCalledTimes(1);
    expect(mockedFetch).toHaveBeenCalledWith('/security/scans/7/vulnerabilities?limit=1000&offset=0');
  });

  it('pages past the cap until every row is collected', async () => {
    mockedFetch
      .mockResolvedValueOnce(response(true, { items: rows(1000, 0), total: 2500 }))
      .mockResolvedValueOnce(response(true, { items: rows(1000, 1000), total: 2500 }))
      .mockResolvedValueOnce(response(true, { items: rows(500, 2000), total: 2500 }));
    const result = await fetchAllScanVulnerabilities(3);
    expect(result).toHaveLength(2500);
    expect(mockedFetch).toHaveBeenCalledTimes(3);
    expect(mockedFetch).toHaveBeenNthCalledWith(1, '/security/scans/3/vulnerabilities?limit=1000&offset=0');
    expect(mockedFetch).toHaveBeenNthCalledWith(2, '/security/scans/3/vulnerabilities?limit=1000&offset=1000');
    expect(mockedFetch).toHaveBeenNthCalledWith(3, '/security/scans/3/vulnerabilities?limit=1000&offset=2000');
  });

  it('stops on a short page even when total over-reports (no infinite loop)', async () => {
    mockedFetch.mockResolvedValueOnce(response(true, { items: rows(10), total: 99999 }));
    const result = await fetchAllScanVulnerabilities(1);
    expect(result).toHaveLength(10);
    expect(mockedFetch).toHaveBeenCalledTimes(1);
  });

  it('throws when a page request fails rather than returning a partial list', async () => {
    mockedFetch.mockResolvedValueOnce(response(false, {}));
    await expect(fetchAllScanVulnerabilities(1)).rejects.toThrow('full vulnerability list');
  });
});
