import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
  setApiKey,
  getApiKey,
  checkAuth,
  getCertificates,
  getCertificate,
  getCertificateVersions,
  createCertificate,
  triggerRenewal,
  triggerDeployment,
  updateCertificate,
  archiveCertificate,
  revokeCertificate,
  bulkRevokeCertificates,
  exportCertificatePEM,
  downloadCertificatePEM,
  exportCertificatePKCS12,
  getAgents,
  getAgent,
  registerAgent,
  retireAgent,
  listRetiredAgents,
  getJobs,
  cancelJob,
  approveRenewal,
  rejectRenewal,
  getNotifications,
  markNotificationRead,
  getAuditEvents,
  getPolicies,
  createPolicy,
  updatePolicy,
  deletePolicy,
  getPolicyViolations,
  getRenewalPolicies,
  createRenewalPolicy,
  updateRenewalPolicy,
  deleteRenewalPolicy,
  getIssuers,
  createIssuer,
  testIssuerConnection,
  deleteIssuer,
  getTargets,
  createTarget,
  deleteTarget,
  testTargetConnection,
  getProfiles,
  getProfile,
  createProfile,
  updateProfile,
  deleteProfile,
  getOwners,
  getOwner,
  createOwner,
  updateOwner,
  deleteOwner,
  getTeams,
  getTeam,
  createTeam,
  updateTeam,
  deleteTeam,
  getAgentGroups,
  getAgentGroup,
  createAgentGroup,
  updateAgentGroup,
  deleteAgentGroup,
  getAgentGroupMembers,
  getHealth,
  getDashboardSummary,
  getCertificatesByStatus,
  getExpirationTimeline,
  getJobTrends,
  getIssuanceRate,
  getMetrics,
  getDiscoveredCertificates,
  getDiscoveredCertificate,
  claimDiscoveredCertificate,
  dismissDiscoveredCertificate,
  getDiscoveryScans,
  getDiscoverySummary,
  getNetworkScanTargets,
  getNetworkScanTarget,
  createNetworkScanTarget,
  updateNetworkScanTarget,
  deleteNetworkScanTarget,
  triggerNetworkScan,
  previewDigest,
  sendDigest,
  getJob,
  getJobVerification,
  getIssuer,
  getTarget,
  getPrometheusMetrics,
  getCertificateDeployments,
  getOCSPStatus,
  updateIssuer,
  updateTarget,
  getPolicy,
  listHealthChecks,
  getHealthCheck,
  createHealthCheck,
  updateHealthCheck,
  deleteHealthCheck,
  getHealthCheckHistory,
  acknowledgeHealthCheck,
  getHealthCheckSummary,
} from './client';

// Mock global fetch
const mockFetch = vi.fn();
globalThis.fetch = mockFetch;

function mockJsonResponse(data: unknown, status = 200) {
  return Promise.resolve({
    ok: status >= 200 && status < 300,
    status,
    json: () => Promise.resolve(data),
    statusText: 'OK',
  } as Response);
}

function mockErrorResponse(status: number, body: { message?: string; error?: string } = {}) {
  return Promise.resolve({
    ok: false,
    status,
    json: () => Promise.resolve(body),
    statusText: 'Error',
  } as Response);
}

describe('API Client', () => {
  beforeEach(() => {
    mockFetch.mockReset();
    setApiKey(null);
  });

  // ─── Auth ───────────────────────────────────────────

  describe('API Key management', () => {
    it('stores and retrieves API key', () => {
      expect(getApiKey()).toBeNull();
      setApiKey('test-key-123');
      expect(getApiKey()).toBe('test-key-123');
    });

    it('clears API key', () => {
      setApiKey('test-key');
      setApiKey(null);
      expect(getApiKey()).toBeNull();
    });
  });

  describe('Auth headers', () => {
    it('sends Authorization header when API key is set', async () => {
      setApiKey('my-secret-key');
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));

      await getCertificates();

      const [, init] = mockFetch.mock.calls[0];
      expect(init.headers['Authorization']).toBe('Bearer my-secret-key');
      expect(init.headers['Content-Type']).toBe('application/json');
    });

    it('omits Authorization header when no API key', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));

      await getCertificates();

      const [, init] = mockFetch.mock.calls[0];
      expect(init.headers['Authorization']).toBeUndefined();
      expect(init.headers['Content-Type']).toBe('application/json');
    });

    it('dispatches auth-required event on 401', async () => {
      const listener = vi.fn();
      window.addEventListener('certctl:auth-required', listener);
      mockFetch.mockReturnValueOnce(mockErrorResponse(401));

      await expect(getCertificates()).rejects.toThrow('Authentication required');
      expect(listener).toHaveBeenCalled();

      window.removeEventListener('certctl:auth-required', listener);
    });
  });

  // ─── checkAuth (M-003: surfaces user + admin) ──────

  describe('checkAuth', () => {
    // Post-M-003 /auth/check returns {status, user, admin}. The admin flag drives
    // GUI gating of admin-only affordances (bulk revoke, etc.). Authoritative
    // enforcement lives server-side — this test only pins the contract the
    // AuthProvider depends on.
    it('returns {status, user, admin} shape and sends Bearer token', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({ status: 'authenticated', user: 'ops-admin', admin: true }),
      );

      const resp = await checkAuth('test-api-key');

      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/auth/check');
      expect(init.headers['Authorization']).toBe('Bearer test-api-key');
      expect(init.headers['Content-Type']).toBe('application/json');
      expect(resp.status).toBe('authenticated');
      expect(resp.user).toBe('ops-admin');
      expect(resp.admin).toBe(true);
    });

    it('returns admin=false for non-admin callers', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({ status: 'authenticated', user: 'alice', admin: false }),
      );

      const resp = await checkAuth('alice-key');

      expect(resp.user).toBe('alice');
      expect(resp.admin).toBe(false);
    });

    it('throws on invalid API key', async () => {
      mockFetch.mockReturnValueOnce(mockErrorResponse(401));
      await expect(checkAuth('bad-key')).rejects.toThrow('Invalid API key');
    });
  });

  // ─── Error handling ─────────────────────────────────

  describe('Error handling', () => {
    it('throws with server error message', async () => {
      mockFetch.mockReturnValueOnce(mockErrorResponse(400, { message: 'Invalid request' }));
      await expect(getCertificates()).rejects.toThrow('Invalid request');
    });

    it('throws with error field from response', async () => {
      mockFetch.mockReturnValueOnce(mockErrorResponse(500, { error: 'Internal error' }));
      await expect(getCertificates()).rejects.toThrow('Internal error');
    });

    it('falls back to HTTP status text', async () => {
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: false,
          status: 503,
          json: () => Promise.reject(new Error('not json')),
          statusText: 'Service Unavailable',
        } as Response),
      );
      await expect(getCertificates()).rejects.toThrow('Service Unavailable');
    });
  });

  // ─── Certificates ───────────────────────────────────

  describe('Certificates', () => {
    it('getCertificates sends GET with default pagination', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getCertificates();
      expect(mockFetch).toHaveBeenCalledWith(
        '/api/v1/certificates?page=1&per_page=50',
        expect.objectContaining({ headers: expect.any(Object) }),
      );
    });

    it('getCertificates passes filter params', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getCertificates({ status: 'Active', environment: 'production' });
      const url = mockFetch.mock.calls[0][0] as string;
      expect(url).toContain('status=Active');
      expect(url).toContain('environment=production');
    });

    it('getCertificate fetches single cert by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'mc-test', common_name: 'test.com' }));
      const cert = await getCertificate('mc-test');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/certificates/mc-test');
      expect(cert.id).toBe('mc-test');
    });

    it('getCertificateVersions fetches versions', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getCertificateVersions('mc-test');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/certificates/mc-test/versions');
    });

    it('createCertificate sends POST with body', async () => {
      const certData = { common_name: 'new.example.com', issuer_id: 'iss-local' };
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'mc-new', ...certData }));
      await createCertificate(certData);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual(certData);
    });

    // C-001 scope-expansion regression: the OnboardingWizard CertificateStep
    // and the CertificatesPage CreateCertificateModal must both ship the full
    // six-field required payload (name, common_name, renewal_policy_id,
    // issuer_id, owner_id, team_id) — the handler's ValidateRequired contract
    // rejects anything less with HTTP 400. This test pins the wire shape so
    // that accidentally dropping a field from either UI surface fails CI
    // rather than only surfacing as a 400 at runtime.
    it('createCertificate accepts and transmits all six required fields', async () => {
      const wizardPayload = {
        name: 'API Production Cert',
        common_name: 'api.example.com',
        sans: ['www.example.com'],
        issuer_id: 'iss-local',
        owner_id: 'o-alice',
        team_id: 't-platform',
        renewal_policy_id: 'rp-standard',
        environment: 'production',
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'mc-new', ...wizardPayload }));
      await createCertificate(wizardPayload);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      // Assert every required field is present and intact
      expect(body.name).toBe('API Production Cert');
      expect(body.common_name).toBe('api.example.com');
      expect(body.issuer_id).toBe('iss-local');
      expect(body.owner_id).toBe('o-alice');
      expect(body.team_id).toBe('t-platform');
      expect(body.renewal_policy_id).toBe('rp-standard');
    });

    it('updateCertificate sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'mc-test', status: 'Active' }));
      await updateCertificate('mc-test', { status: 'Active' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-test');
      expect(init.method).toBe('PUT');
    });

    it('archiveCertificate sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'archived' }));
      await archiveCertificate('mc-test');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-test');
      expect(init.method).toBe('DELETE');
    });

    it('triggerRenewal sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'renewal triggered' }));
      await triggerRenewal('mc-test');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-test/renew');
      expect(init.method).toBe('POST');
    });

    it('triggerDeployment sends POST with target_id', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deployment triggered' }));
      await triggerDeployment('mc-test', 't-nginx');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-test/deploy');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual({ target_id: 't-nginx' });
    });

    it('revokeCertificate sends POST with reason', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ status: 'revoked' }));
      await revokeCertificate('mc-test', 'keyCompromise');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-test/revoke');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual({ reason: 'keyCompromise' });
    });

    it('bulkRevokeCertificates sends POST with criteria', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ total_matched: 3, total_revoked: 2, total_skipped: 1, total_failed: 0 }));
      await bulkRevokeCertificates({ reason: 'keyCompromise', profile_id: 'prof-tls', certificate_ids: ['mc-1', 'mc-2'] });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/bulk-revoke');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual({ reason: 'keyCompromise', profile_id: 'prof-tls', certificate_ids: ['mc-1', 'mc-2'] });
    });
  });

  // ─── Agents ─────────────────────────────────────────

  describe('Agents', () => {
    it('getAgents sends GET with pagination', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getAgents();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/agents?page=1&per_page=50');
    });

    it('getAgent fetches by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'a-web01', name: 'web01' }));
      const agent = await getAgent('a-web01');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/agents/a-web01');
      expect(agent.id).toBe('a-web01');
    });

    it('registerAgent sends POST', async () => {
      const agentData = { name: 'new-agent', hostname: 'host01' };
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'a-new', ...agentData }));
      await registerAgent(agentData);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/agents');
      expect(init.method).toBe('POST');
    });
  });

  // ─── Agent Retirement (I-004) ───────────────────────
  //
  // These tests pin the GUI's retirement contract against what the backend
  // will add in Phase 2b: soft-retire via DELETE, force-cascade via
  // ?force=true&reason=..., idempotent 204 on already-retired, 409 blocked
  // payload with counts, and a GET /agents/retired listing surface.
  //
  // All compile-fail until client.ts exports retireAgent + listRetiredAgents
  // — the shape of those exports is pinned here rather than assumed.
  describe('Agent Retirement (I-004)', () => {
    it('retireAgent sends DELETE without query when no force/reason', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({
          retired_at: '2026-04-18T12:00:00Z',
          already_retired: false,
          cascade: false,
        }),
      );
      await retireAgent('ag-1');
      const [url, init] = mockFetch.mock.calls[0];
      // Default soft-retire: bare path, no stray ? suffix.
      expect(url).toBe('/api/v1/agents/ag-1');
      expect(init.method).toBe('DELETE');
    });

    it('retireAgent propagates force+reason as URL query parameters', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({
          retired_at: '2026-04-18T12:00:00Z',
          already_retired: false,
          cascade: true,
          counts: { active_targets: 3, active_certificates: 7, pending_jobs: 2 },
        }),
      );
      await retireAgent('ag-1', { force: true, reason: 'decommissioning rack 7' });
      const [url, init] = mockFetch.mock.calls[0];
      // URLSearchParams encodes space as "+"; "decommissioning rack 7" → "decommissioning+rack+7"
      expect(url).toBe(
        '/api/v1/agents/ag-1?force=true&reason=decommissioning+rack+7',
      );
      expect(init.method).toBe('DELETE');
    });

    it('retireAgent omits force=false even when reason is supplied', async () => {
      // Client-side guard: the server's 400 ErrForceReasonRequired is the
      // fallback; the GUI should never silently promote reason-without-force
      // into a force call. Pins that reason-only still hits the soft path.
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({
          retired_at: '2026-04-18T12:00:00Z',
          already_retired: false,
          cascade: false,
        }),
      );
      await retireAgent('ag-1', { reason: 'routine decommission' });
      const [url] = mockFetch.mock.calls[0];
      // force defaults to false → query carries reason only.
      expect(url).toBe('/api/v1/agents/ag-1?reason=routine+decommission');
    });

    it('retireAgent surfaces the 409 dependency error message to the caller', async () => {
      mockFetch.mockReturnValueOnce(
        mockErrorResponse(409, {
          message: 'agent has 3 active targets, 7 active certificates, 2 pending jobs',
        }),
      );
      await expect(retireAgent('ag-1')).rejects.toThrow(
        /active targets|active certificates|pending jobs/,
      );
    });

    it('retireAgent treats 204 (already-retired) as success with empty body', async () => {
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 204,
          json: () => Promise.reject(new Error('204 has no body')),
          statusText: 'No Content',
        } as Response),
      );
      // fetchJSON normalises 204 to {} — caller must not crash.
      const result = await retireAgent('ag-1');
      expect(result).toBeDefined();
    });

    it('listRetiredAgents sends GET /agents/retired with default pagination', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }),
      );
      await listRetiredAgents();
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/agents/retired?page=1&per_page=50');
      // Default is GET — no explicit method means fetchJSON falls through.
      expect(init.method ?? 'GET').toBe('GET');
    });

    it('listRetiredAgents forwards page/per_page overrides', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({ data: [], total: 0, page: 2, per_page: 100 }),
      );
      await listRetiredAgents({ page: '2', per_page: '100' });
      const [url] = mockFetch.mock.calls[0];
      expect(url).toContain('page=2');
      expect(url).toContain('per_page=100');
    });
  });

  // ─── Jobs ───────────────────────────────────────────

  describe('Jobs', () => {
    it('getJobs sends GET with filters', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getJobs({ status: 'Pending', type: 'Renewal' });
      const url = mockFetch.mock.calls[0][0] as string;
      expect(url).toContain('status=Pending');
      expect(url).toContain('type=Renewal');
    });

    it('cancelJob sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'cancelled' }));
      await cancelJob('job-123');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/jobs/job-123/cancel');
      expect(init.method).toBe('POST');
    });
  });

  // ─── Notifications ──────────────────────────────────

  describe('Notifications', () => {
    it('getNotifications sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getNotifications();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/notifications');
    });

    it('markNotificationRead sends POST with auth headers', async () => {
      setApiKey('test-key');
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'marked as read' }));
      await markNotificationRead('notif-123');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/notifications/notif-123/read');
      expect(init.method).toBe('POST');
      expect(init.headers['Authorization']).toBe('Bearer test-key');
    });
  });

  // ─── Policies ───────────────────────────────────────

  describe('Policies', () => {
    it('getPolicies sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getPolicies();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/policies');
    });

    it('updatePolicy sends PUT with partial data', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'pol-1', enabled: false }));
      await updatePolicy('pol-1', { enabled: false });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/policies/pol-1');
      expect(init.method).toBe('PUT');
      expect(JSON.parse(init.body)).toEqual({ enabled: false });
    });

    it('deletePolicy sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deletePolicy('pol-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/policies/pol-1');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Renewal Policies (G-1) ─────────────────────────
  // Distinct from compliance Policies above. Populates the
  // `renewal_policy_id` dropdown on OnboardingWizard + CertificatesPage +
  // CertificateDetailPage.InlinePolicyEditor.  Hits `/api/v1/renewal-policies`.

  describe('RenewalPolicies', () => {
    it('getRenewalPolicies sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getRenewalPolicies();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/renewal-policies');
    });

    it('createRenewalPolicy sends POST with body', async () => {
      mockFetch.mockReturnValueOnce(
        mockJsonResponse({
          id: 'rp-new',
          name: 'New Policy',
          renewal_window_days: 30,
          max_retries: 3,
          retry_interval_seconds: 3600,
          auto_renew: true,
        }),
      );
      await createRenewalPolicy({
        name: 'New Policy',
        renewal_window_days: 30,
        max_retries: 3,
        retry_interval_seconds: 3600,
        auto_renew: true,
      });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/renewal-policies');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body).name).toBe('New Policy');
    });

    it('updateRenewalPolicy sends PUT with partial data', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'rp-default', name: 'Renamed' }));
      await updateRenewalPolicy('rp-default', { name: 'Renamed' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/renewal-policies/rp-default');
      expect(init.method).toBe('PUT');
      expect(JSON.parse(init.body)).toEqual({ name: 'Renamed' });
    });

    it('deleteRenewalPolicy sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteRenewalPolicy('rp-default');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/renewal-policies/rp-default');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Issuers ────────────────────────────────────────

  describe('Issuers', () => {
    it('getIssuers sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getIssuers();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/issuers');
    });

    it('testIssuerConnection sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'ok' }));
      await testIssuerConnection('iss-local');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers/iss-local/test');
      expect(init.method).toBe('POST');
    });

    it('deleteIssuer sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteIssuer('iss-local');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers/iss-local');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Targets ────────────────────────────────────────

  describe('Targets', () => {
    it('getTargets sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getTargets();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/targets');
    });

    it('createTarget sends POST', async () => {
      const targetData = { name: 'nginx-01', type: 'nginx', hostname: 'web01.example.com' };
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-new', ...targetData }));
      await createTarget(targetData);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/targets');
      expect(init.method).toBe('POST');
    });

    it('deleteTarget sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteTarget('t-nginx');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/targets/t-nginx');
      expect(init.method).toBe('DELETE');
    });

    it('testTargetConnection sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ status: 'success', message: 'Agent is online' }));
      await testTargetConnection('t-nginx');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/targets/t-nginx/test');
      expect(init.method).toBe('POST');
    });
  });

  // ─── Approval ──────────────────────────────────────

  describe('Renewal Approvals', () => {
    it('approveRenewal sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'approved' }));
      await approveRenewal('job-123');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/jobs/job-123/approve');
      expect(init.method).toBe('POST');
    });

    it('rejectRenewal sends POST with reason', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'rejected' }));
      await rejectRenewal('job-123', 'not authorized');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/jobs/job-123/reject');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual({ reason: 'not authorized' });
    });
  });

  // ─── Profiles ────────────────────────────────────────

  describe('Profiles', () => {
    it('getProfiles sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getProfiles();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/profiles');
    });

    it('getProfile fetches by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'prof-1', name: 'Standard' }));
      const profile = await getProfile('prof-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/profiles/prof-1');
      expect(profile.id).toBe('prof-1');
    });

    it('createProfile sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'prof-new', name: 'New Profile' }));
      await createProfile({ name: 'New Profile' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/profiles');
      expect(init.method).toBe('POST');
    });

    it('updateProfile sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'prof-1', name: 'Updated' }));
      await updateProfile('prof-1', { name: 'Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/profiles/prof-1');
      expect(init.method).toBe('PUT');
    });

    it('deleteProfile sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteProfile('prof-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/profiles/prof-1');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Owners ──────────────────────────────────────────

  describe('Owners', () => {
    it('getOwners sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getOwners();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/owners');
    });

    it('getOwner fetches by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'o-alice', name: 'Alice' }));
      const owner = await getOwner('o-alice');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/owners/o-alice');
      expect(owner.name).toBe('Alice');
    });

    it('createOwner sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'o-new', name: 'Bob' }));
      await createOwner({ name: 'Bob', email: 'bob@example.com' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/owners');
      expect(init.method).toBe('POST');
    });

    it('updateOwner sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'o-alice', name: 'Alice Updated' }));
      await updateOwner('o-alice', { name: 'Alice Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/owners/o-alice');
      expect(init.method).toBe('PUT');
    });

    it('deleteOwner sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteOwner('o-alice');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/owners/o-alice');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Teams ───────────────────────────────────────────

  describe('Teams', () => {
    it('getTeams sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getTeams();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/teams');
    });

    it('getTeam fetches by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-platform', name: 'Platform' }));
      const team = await getTeam('t-platform');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/teams/t-platform');
      expect(team.name).toBe('Platform');
    });

    it('createTeam sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-new', name: 'New Team' }));
      await createTeam({ name: 'New Team' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/teams');
      expect(init.method).toBe('POST');
    });

    it('updateTeam sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-platform', name: 'Updated' }));
      await updateTeam('t-platform', { name: 'Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/teams/t-platform');
      expect(init.method).toBe('PUT');
    });

    it('deleteTeam sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteTeam('t-platform');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/teams/t-platform');
      expect(init.method).toBe('DELETE');
    });
  });

  // ─── Agent Groups ────────────────────────────────────

  describe('Agent Groups', () => {
    it('getAgentGroups sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getAgentGroups();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/agent-groups');
    });

    it('getAgentGroup fetches by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'ag-linux', name: 'Linux Servers' }));
      const group = await getAgentGroup('ag-linux');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/agent-groups/ag-linux');
      expect(group.name).toBe('Linux Servers');
    });

    it('createAgentGroup sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'ag-new', name: 'New Group' }));
      await createAgentGroup({ name: 'New Group' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/agent-groups');
      expect(init.method).toBe('POST');
    });

    it('updateAgentGroup sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'ag-linux', name: 'Updated' }));
      await updateAgentGroup('ag-linux', { name: 'Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/agent-groups/ag-linux');
      expect(init.method).toBe('PUT');
    });

    it('deleteAgentGroup sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'deleted' }));
      await deleteAgentGroup('ag-linux');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/agent-groups/ag-linux');
      expect(init.method).toBe('DELETE');
    });

    it('getAgentGroupMembers fetches members', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getAgentGroupMembers('ag-linux');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/agent-groups/ag-linux/members');
    });
  });

  // ─── Policy Violations ───────────────────────────────

  describe('Policy Violations', () => {
    it('getPolicyViolations sends GET', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getPolicyViolations('pol-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/policies/pol-1/violations');
    });
  });

  // ─── Issuer Create ───────────────────────────────────

  describe('Issuer Create', () => {
    it('createIssuer sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-new', name: 'New Issuer' }));
      await createIssuer({ name: 'New Issuer', type: 'local_ca' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers');
      expect(init.method).toBe('POST');
    });

    it('createIssuer sends correct payload for VaultPKI type', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-vault', name: 'Vault PKI' }));
      const vaultPayload = {
        name: 'Vault PKI',
        type: 'VaultPKI',
        config: {
          addr: 'https://vault.internal:8200',
          token: 'hvs.test-token',
          mount: 'pki',
          role: 'web-certs',
          ttl: '8760h',
        },
      };
      await createIssuer(vaultPayload);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      expect(body.type).toBe('VaultPKI');
      expect(body.config.addr).toBe('https://vault.internal:8200');
      expect(body.config.role).toBe('web-certs');
    });

    it('createIssuer sends correct payload for DigiCert type', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-digicert', name: 'DigiCert' }));
      const digicertPayload = {
        name: 'DigiCert CertCentral',
        type: 'DigiCert',
        config: {
          api_key: 'test-api-key',
          org_id: '12345',
          product_type: 'ssl_basic',
        },
      };
      await createIssuer(digicertPayload);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      expect(body.type).toBe('DigiCert');
      expect(body.config.org_id).toBe('12345');
      expect(body.config.product_type).toBe('ssl_basic');
    });

    it('createIssuer sends correct payload for ACME with profile', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-acme-shortlived', name: 'ACME Shortlived' }));
      const acmePayload = {
        name: 'ACME Shortlived',
        type: 'acme',
        config: {
          directory_url: 'https://acme-v02.api.letsencrypt.org/directory',
          email: 'admin@example.com',
          challenge_type: 'http-01',
          profile: 'shortlived',
        },
      };
      await createIssuer(acmePayload);
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      expect(body.type).toBe('acme');
      expect(body.config.profile).toBe('shortlived');
      expect(body.config.directory_url).toBe('https://acme-v02.api.letsencrypt.org/directory');
    });
  });

  // ─── Audit ──────────────────────────────────────────

  describe('Audit', () => {
    it('getAuditEvents sends GET with filters', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getAuditEvents({ resource_type: 'certificate' });
      const url = mockFetch.mock.calls[0][0] as string;
      expect(url).toContain('resource_type=certificate');
    });
  });

  // ─── Stats ─────────────────────────────────────────

  describe('Stats', () => {
    it('getDashboardSummary calls /api/v1/stats/summary', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ total_certificates: 10 }));
      const result = await getDashboardSummary();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/summary');
      expect(result.total_certificates).toBe(10);
    });

    it('getCertificatesByStatus calls /api/v1/stats/certificates-by-status', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse([{ status: 'Active', count: 5 }]));
      const result = await getCertificatesByStatus();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/certificates-by-status');
      expect(result).toHaveLength(1);
    });

    it('getExpirationTimeline calls with days parameter', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse([]));
      await getExpirationTimeline(60);
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/expiration-timeline?days=60');
    });

    it('getExpirationTimeline uses default 30 days', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse([]));
      await getExpirationTimeline();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/expiration-timeline?days=30');
    });

    it('getJobTrends calls with days parameter', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse([]));
      await getJobTrends(14);
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/job-trends?days=14');
    });

    it('getIssuanceRate calls with days parameter', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse([]));
      await getIssuanceRate(7);
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/issuance-rate?days=7');
    });

    it('getMetrics calls /api/v1/metrics', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({
        gauge: { certificate_total: 10 },
        counter: { job_completed_total: 5 },
        uptime: { uptime_seconds: 3600 },
      }));
      const result = await getMetrics();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/metrics');
      expect(result.gauge.certificate_total).toBe(10);
    });
  });

  // ─── Health ─────────────────────────────────────────

  describe('Health', () => {
    it('getHealth calls /health endpoint', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ status: 'ok' }));
      const result = await getHealth();
      expect(mockFetch.mock.calls[0][0]).toBe('/health');
      expect(result.status).toBe('ok');
    });
  });

  // ─── Discovery ────────────────────────────────────

  describe('Discovery', () => {
    it('getDiscoveredCertificates calls with params', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getDiscoveredCertificates({ status: 'Unmanaged' });
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/discovered-certificates');
      expect(mockFetch.mock.calls[0][0]).toContain('status=Unmanaged');
    });

    it('getDiscoveredCertificate calls with id', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'dc-1', common_name: 'test.example.com' }));
      const result = await getDiscoveredCertificate('dc-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/discovered-certificates/dc-1');
      expect(result.common_name).toBe('test.example.com');
    });

    it('claimDiscoveredCertificate sends POST with managed cert id', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'claimed' }));
      await claimDiscoveredCertificate('dc-1', 'mc-api-prod');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/discovered-certificates/dc-1/claim');
      expect(init.method).toBe('POST');
      expect(JSON.parse(init.body)).toEqual({ managed_certificate_id: 'mc-api-prod' });
    });

    it('dismissDiscoveredCertificate sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'dismissed' }));
      await dismissDiscoveredCertificate('dc-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/discovered-certificates/dc-1/dismiss');
      expect(init.method).toBe('POST');
    });

    it('getDiscoveryScans calls endpoint', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getDiscoveryScans();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/discovery-scans');
    });

    it('getDiscoverySummary calls endpoint', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ Unmanaged: 5, Managed: 3, Dismissed: 1 }));
      const result = await getDiscoverySummary();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/discovery-summary');
      expect(result.Unmanaged).toBe(5);
    });
  });

  // ─── Network Scan Targets ────────────────────────

  describe('Network Scan Targets', () => {
    it('getNetworkScanTargets calls endpoint', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      await getNetworkScanTargets();
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/network-scan-targets');
    });

    it('getNetworkScanTarget calls with id', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'nst-1', name: 'DMZ' }));
      const result = await getNetworkScanTarget('nst-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/network-scan-targets/nst-1');
      expect(result.name).toBe('DMZ');
    });

    it('createNetworkScanTarget sends POST', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'nst-new', name: 'Production' }));
      await createNetworkScanTarget({ name: 'Production', cidrs: ['10.0.0.0/24'], ports: [443] });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/network-scan-targets');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      expect(body.name).toBe('Production');
      expect(body.cidrs).toEqual(['10.0.0.0/24']);
    });

    it('updateNetworkScanTarget sends PUT', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'nst-1', enabled: false }));
      await updateNetworkScanTarget('nst-1', { enabled: false });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/network-scan-targets/nst-1');
      expect(init.method).toBe('PUT');
    });

    it('deleteNetworkScanTarget sends DELETE', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({}, 204));
      await deleteNetworkScanTarget('nst-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/network-scan-targets/nst-1');
      expect(init.method).toBe('DELETE');
    });

    it('triggerNetworkScan sends POST to scan endpoint', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'scan triggered' }));
      await triggerNetworkScan('nst-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/network-scan-targets/nst-1/scan');
      expect(init.method).toBe('POST');
    });
  });

  // ─── Certificate Export ────────────────────────────────

  describe('Certificate Export', () => {
    it('exportCertificatePEM fetches PEM data as JSON', async () => {
      const pemResult = { cert_pem: 'CERT', chain_pem: 'CHAIN', full_pem: 'FULL' };
      mockFetch.mockReturnValueOnce(mockJsonResponse(pemResult));
      const result = await exportCertificatePEM('mc-1');
      const [url] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-1/export/pem');
      expect(result.cert_pem).toBe('CERT');
      expect(result.full_pem).toBe('FULL');
    });

    it('downloadCertificatePEM fetches blob with download=true', async () => {
      const mockBlob = new Blob(['pem-data'], { type: 'application/x-pem-file' });
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          blob: () => Promise.resolve(mockBlob),
        } as Response)
      );
      const blob = await downloadCertificatePEM('mc-1');
      const [url] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-1/export/pem?download=true');
      expect(blob).toBeInstanceOf(Blob);
    });

    it('downloadCertificatePEM includes auth header', async () => {
      setApiKey('export-key');
      const mockBlob = new Blob(['data']);
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          blob: () => Promise.resolve(mockBlob),
        } as Response)
      );
      await downloadCertificatePEM('mc-1');
      const [, init] = mockFetch.mock.calls[0];
      expect(init.headers['Authorization']).toBe('Bearer export-key');
    });

    it('exportCertificatePKCS12 sends POST with password', async () => {
      const mockBlob = new Blob([new Uint8Array([0x30])], { type: 'application/x-pkcs12' });
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          blob: () => Promise.resolve(mockBlob),
        } as Response)
      );
      const blob = await exportCertificatePKCS12('mc-1', 'mypass');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/certificates/mc-1/export/pkcs12');
      expect(init.method).toBe('POST');
      const body = JSON.parse(init.body);
      expect(body.password).toBe('mypass');
      expect(blob).toBeInstanceOf(Blob);
    });

    it('exportCertificatePKCS12 uses empty password by default', async () => {
      const mockBlob = new Blob([new Uint8Array([0x30])]);
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          blob: () => Promise.resolve(mockBlob),
        } as Response)
      );
      await exportCertificatePKCS12('mc-1');
      const [, init] = mockFetch.mock.calls[0];
      const body = JSON.parse(init.body);
      expect(body.password).toBe('');
    });
  });

  // ─── Profile (EKU / S/MIME) ─────────────────────────────

  describe('Profile for EKU Display', () => {
    it('getProfile fetches profile by ID with EKU data', async () => {
      const profileData = {
        id: 'prof-smime',
        name: 'S/MIME Email',
        allowed_ekus: ['emailProtection'],
        max_ttl_seconds: 31536000,
        enabled: true,
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse(profileData));
      const result = await getProfile('prof-smime');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/profiles/prof-smime');
      expect(result.allowed_ekus).toEqual(['emailProtection']);
    });

    it('getProfile returns profile with multiple EKUs', async () => {
      const profileData = {
        id: 'prof-tls',
        name: 'TLS Server',
        allowed_ekus: ['serverAuth', 'clientAuth'],
        max_ttl_seconds: 7776000,
        enabled: true,
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse(profileData));
      const result = await getProfile('prof-tls');
      expect(result.allowed_ekus).toHaveLength(2);
      expect(result.allowed_ekus).toContain('serverAuth');
      expect(result.allowed_ekus).toContain('clientAuth');
    });
  });

  // ─── Job Verification Fields ─────────────────────────────

  describe('Job Verification', () => {
    it('getJobs returns jobs with verification fields', async () => {
      const jobData = {
        data: [{
          id: 'job-1',
          certificate_id: 'mc-1',
          type: 'Deployment',
          status: 'Completed',
          verification_status: 'success',
          verified_at: '2026-03-28T12:00:00Z',
          verification_fingerprint: 'abc123',
          verification_error: '',
          attempts: 1,
          max_attempts: 3,
          scheduled_at: '2026-03-28T11:00:00Z',
          completed_at: '2026-03-28T11:05:00Z',
          created_at: '2026-03-28T11:00:00Z',
        }],
        total: 1,
        page: 1,
        per_page: 50,
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse(jobData));
      const result = await getJobs({ certificate_id: 'mc-1' });
      expect(result.data[0].verification_status).toBe('success');
      expect(result.data[0].verified_at).toBe('2026-03-28T12:00:00Z');
      expect(result.data[0].verification_fingerprint).toBe('abc123');
    });

    it('getJobs handles jobs without verification data', async () => {
      const jobData = {
        data: [{
          id: 'job-2',
          certificate_id: 'mc-2',
          type: 'Issuance',
          status: 'Completed',
          attempts: 1,
          max_attempts: 3,
          scheduled_at: '2026-03-28T11:00:00Z',
          completed_at: '2026-03-28T11:05:00Z',
          created_at: '2026-03-28T11:00:00Z',
        }],
        total: 1,
        page: 1,
        per_page: 50,
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse(jobData));
      const result = await getJobs({});
      expect(result.data[0].verification_status).toBeUndefined();
      expect(result.data[0].verified_at).toBeUndefined();
    });
  });

  // ─── Digest ─────────────────────────────

  describe('Digest', () => {
    it('previewDigest fetches HTML preview', async () => {
      const html = '<html><body>Digest Preview</body></html>';
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          text: () => Promise.resolve(html),
        } as Response)
      );
      const result = await previewDigest();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/digest/preview');
      expect(result).toBe(html);
    });

    it('previewDigest throws on error', async () => {
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: false,
          status: 503,
          text: () => Promise.resolve('not configured'),
        } as Response)
      );
      await expect(previewDigest()).rejects.toThrow('Digest preview failed: 503');
    });

    it('sendDigest sends POST request', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ message: 'digest sent' }));
      const result = await sendDigest();
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/digest/send');
      expect(init.method).toBe('POST');
      expect(result.message).toBe('digest sent');
    });
  });

  // ─── Job Detail ────────────────────────────

  describe('Job Detail', () => {
    it('getJob fetches single job by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'job-1', type: 'Deployment', status: 'Completed' }));
      const result = await getJob('job-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/jobs/job-1');
      expect(result.id).toBe('job-1');
      expect(result.type).toBe('Deployment');
    });

    it('getJobVerification fetches verification result', async () => {
      const verificationData = {
        job_id: 'job-1',
        target_id: 't-nginx1',
        verified: true,
        actual_fingerprint: 'abc123',
        expected_fingerprint: 'abc123',
        verified_at: '2026-03-28T12:00:00Z',
      };
      mockFetch.mockReturnValueOnce(mockJsonResponse(verificationData));
      const result = await getJobVerification('job-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/jobs/job-1/verification');
      expect(result.verified).toBe(true);
      expect(result.actual_fingerprint).toBe('abc123');
    });
  });

  // ─── Issuer Detail ─────────────────────────

  describe('Issuer Detail', () => {
    it('getIssuer fetches single issuer by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-local', name: 'Local CA', type: 'local_ca', status: 'active' }));
      const result = await getIssuer('iss-local');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/issuers/iss-local');
      expect(result.name).toBe('Local CA');
      expect(result.type).toBe('local_ca');
    });
  });

  // ─── Target Detail ─────────────────────────

  describe('Target Detail', () => {
    it('getTarget fetches single target by ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-nginx1', name: 'Web Server', type: 'nginx', hostname: 'web1.example.com' }));
      const result = await getTarget('t-nginx1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/targets/t-nginx1');
      expect(result.name).toBe('Web Server');
      expect(result.type).toBe('nginx');
    });
  });

  // ─── Prometheus Metrics ────────────────────

  describe('Prometheus Metrics', () => {
    it('getPrometheusMetrics fetches text format', async () => {
      const metricsText = '# HELP certctl_certificate_total Total certificates\ncertctl_certificate_total 10';
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          text: () => Promise.resolve(metricsText),
        } as Response)
      );
      const result = await getPrometheusMetrics();
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/metrics/prometheus');
      expect(result).toContain('certctl_certificate_total');
    });

    it('getPrometheusMetrics throws on error', async () => {
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: false,
          status: 500,
          text: () => Promise.resolve('error'),
        } as Response)
      );
      await expect(getPrometheusMetrics()).rejects.toThrow('Prometheus metrics failed: 500');
    });

    it('getPrometheusMetrics includes auth header', async () => {
      setApiKey('prom-key');
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          text: () => Promise.resolve('metrics'),
        } as Response)
      );
      await getPrometheusMetrics();
      const [, init] = mockFetch.mock.calls[0];
      expect(init.headers['Authorization']).toBe('Bearer prom-key');
    });
  });

  describe('Frontend Audit: New API Functions', () => {
    it('getCertificateDeployments sends GET with cert ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0 }));
      await getCertificateDeployments('mc-1');
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/certificates/mc-1/deployments');
    });

    // M-006: JSON CRL endpoint (`GET /api/v1/crl`) removed entirely — RFC 5280
    // defines only the DER wire format, which is now served unauthenticated at
    // `/.well-known/pki/crl/{issuer_id}` (fetched directly, no GUI wrapper).
    // OCSP likewise relocated to `/.well-known/pki/ocsp/{issuer_id}/{serial}`
    // per RFC 8615.
    it('getOCSPStatus sends GET to /.well-known/pki/ocsp with issuer and serial', async () => {
      const buf = new ArrayBuffer(8);
      mockFetch.mockReturnValueOnce(
        Promise.resolve({
          ok: true,
          status: 200,
          arrayBuffer: () => Promise.resolve(buf),
        } as Response)
      );
      await getOCSPStatus('iss-local', 'ABC123');
      expect(mockFetch.mock.calls[0][0]).toBe('/.well-known/pki/ocsp/iss-local/ABC123');
    });

    it('updateIssuer sends PUT with data', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-1', name: 'Updated' }));
      await updateIssuer('iss-1', { name: 'Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/issuers/iss-1');
      expect(init.method).toBe('PUT');
    });

    it('updateTarget sends PUT with data', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-1', name: 'Updated' }));
      await updateTarget('t-1', { name: 'Updated' });
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toBe('/api/v1/targets/t-1');
      expect(init.method).toBe('PUT');
    });

    it('getPolicy sends GET with policy ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'pol-1', name: 'Test' }));
      await getPolicy('pol-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/policies/pol-1');
    });
  });

  describe('Health Checks (M48)', () => {
    it('listHealthChecks sends GET with optional filters', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ data: [], total: 0, page: 1, per_page: 50 }));
      const result = await listHealthChecks({ status: 'degraded' });
      expect(result.total).toBe(0);
      expect(mockFetch.mock.calls[0][0]).toContain('/api/v1/health-checks');
      expect(mockFetch.mock.calls[0][0]).toContain('status=degraded');
    });

    it('getHealthCheck sends GET with health check ID', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'hc-1', endpoint: 'example.com:443' }));
      const result = await getHealthCheck('hc-1');
      expect(result.id).toBe('hc-1');
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/health-checks/hc-1');
    });

    it('createHealthCheck sends POST with data', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'hc-1', endpoint: 'example.com:443' }));
      const result = await createHealthCheck({ endpoint: 'example.com:443' });
      expect(result.id).toBe('hc-1');
      const [url, init] = mockFetch.mock.calls[0];
      expect(url).toContain('/api/v1/health-checks');
      expect(init.method).toBe('POST');
    });

    it('getHealthCheckSummary sends GET to /health-checks/summary', async () => {
      mockFetch.mockReturnValueOnce(mockJsonResponse({ healthy: 5, degraded: 1, down: 0, cert_mismatch: 0, unknown: 2, total: 8 }));
      const result = await getHealthCheckSummary();
      expect(result.healthy).toBe(5);
      expect(result.total).toBe(8);
      expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/health-checks/summary');
    });
  });
});
