/**
 * CveIntelService: daily KEV + EPSS refresh, air-gap tolerant, read-time join.
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';

let tmpDir: string;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
let CveIntelService: typeof import('../services/CveIntelService').CveIntelService;

beforeAll(async () => {
  tmpDir = await setupTestDb();
  ({ DatabaseService } = await import('../services/DatabaseService'));
  ({ CveIntelService } = await import('../services/CveIntelService'));
});

afterAll(() => cleanupTestDb(tmpDir));

function db() {
  return DatabaseService.getInstance();
}

function reset(): void {
  const raw = (db() as unknown as { db: { prepare: (s: string) => { run: () => void } } }).db;
  raw.prepare('DELETE FROM cve_intel').run();
  raw.prepare('DELETE FROM vulnerability_details').run();
  raw.prepare('DELETE FROM vulnerability_scans').run();
  db().updateGlobalSetting('cve_intel_enabled', '1');
}

beforeEach(reset);
afterEach(() => vi.restoreAllMocks());

function jsonOk(body: unknown) {
  return { ok: true, status: 200, json: async () => body } as unknown as Response;
}

/** Routes by exact host: www.cisa.gov -> KEV, api.first.org -> EPSS. */
function stubFetch(kev: unknown, epss: unknown): ReturnType<typeof vi.fn> {
  const mock = vi.fn(async (url: string | URL) => {
    const host = new URL(String(url)).hostname;
    if (host === 'www.cisa.gov') return jsonOk(kev);
    if (host === 'api.first.org') return jsonOk(epss);
    throw new Error(`unexpected url ${String(url)}`);
  });
  vi.stubGlobal('fetch', mock);
  return mock;
}

function seedFinding(cve: string): void {
  const scanId = db().createVulnerabilityScan({
    node_id: 1, image_ref: `img-${cve}:1`, image_digest: `sha256:${cve}`, scanned_at: Date.now(),
    total_vulnerabilities: 1, critical_count: 1, high_count: 0, medium_count: 0, low_count: 0,
    unknown_count: 0, fixable_count: 0, secret_count: 0, misconfig_count: 0, scanners_used: 'vuln',
    highest_severity: 'CRITICAL', os_info: null, trivy_version: null, scan_duration_ms: null,
    triggered_by: 'manual', status: 'completed', error: null, stack_context: null,
  });
  db().insertVulnerabilityDetails(scanId, [{
    vulnerability_id: cve, pkg_name: 'pkg', installed_version: '1', fixed_version: null,
    severity: 'CRITICAL', title: null, description: null, primary_url: null,
  }]);
}

describe('CveIntelService.refresh', () => {
  it('upserts KEV membership and joins it at read time', async () => {
    stubFetch({ vulnerabilities: [{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' }] }, { data: [] });
    await CveIntelService.getInstance().refresh();
    const intel = db().getCveIntel(['CVE-2024-0001']);
    expect(intel.get('CVE-2024-0001')).toMatchObject({ kev: true, kevDate: '2024-01-01' });
  });

  it('fetches EPSS only for CVEs present in stored findings', async () => {
    seedFinding('CVE-2024-1111');
    const mock = stubFetch({ vulnerabilities: [] }, { data: [{ cve: 'CVE-2024-1111', epss: '0.5', percentile: '0.9' }] });
    await CveIntelService.getInstance().refresh();
    expect(db().getCveIntel(['CVE-2024-1111']).get('CVE-2024-1111')).toMatchObject({ epssScore: 0.5, epssPercentile: 0.9 });
    expect(mock.mock.calls.some((c) => String(c[0]).includes('CVE-2024-1111'))).toBe(true);
  });

  it('skips the EPSS fetch entirely when no CVEs are present', async () => {
    const mock = stubFetch({ vulnerabilities: [] }, { data: [] });
    await CveIntelService.getInstance().refresh();
    expect(mock.mock.calls.some((c) => new URL(String(c[0])).hostname === 'api.first.org')).toBe(false);
    // KEV is still attempted.
    expect(mock.mock.calls.some((c) => new URL(String(c[0])).hostname === 'www.cisa.gov')).toBe(true);
  });

  it('keeps the cached intel when a fetch fails (air-gap tolerant)', async () => {
    db().replaceKev([{ cve_id: 'CVE-2024-0002', date_added: '2023-12-31' }], Date.now());
    vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('network down'); }));
    await expect(CveIntelService.getInstance().refresh()).resolves.toBeUndefined();
    expect(db().getCveIntel(['CVE-2024-0002']).get('CVE-2024-0002')?.kev).toBe(true);
  });

  it('does no network fetch when disabled by setting', async () => {
    db().updateGlobalSetting('cve_intel_enabled', '0');
    const mock = stubFetch({ vulnerabilities: [] }, { data: [] });
    await CveIntelService.getInstance().refresh();
    expect(mock).not.toHaveBeenCalled();
  });

  it('getCveIntel returns an empty map for no ids and does not crash', () => {
    expect(db().getCveIntel([]).size).toBe(0);
  });
});
