// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import type { BsiCatalogState } from '../../src/compliance/bsi-catalog-management';
import { BsiCatalogLibrary } from '../../src/compliance/ui/BsiCatalogLibrary';
import { useRegulatoryDecisionRevision } from '../../src/compliance/ui/decision-revision';
// Data Hub BSI tile (user decision 2026-10-08): like the MITRE matrix tile, it names the active catalog and
// mapping and offers `Update from BSI`; `Use bundled catalog` appears only after an update. Synthetic states.
const bundled: BsiCatalogState = { version: 1, origin: 'bundled', catalogVersion: '2026-09-24T07:12:39.131983+00:00', crosswalkVersion: '1.0.1-qa',
requirementCount: 901, crosswalkEntryCount: 83, catalogSha256: 'a'.repeat(64), crosswalkSha256: 'b'.repeat(64), missingTargets: ['BER.6.8'] };
const updated: BsiCatalogState = { ...bundled, origin: 'bsi', updatedAt: '2026-10-08T12:00:00.000Z', catalogVersion: '2026-10-01T00:00:00+00:00',
crosswalkVersion: '1.0.2', missingTargets: ['BER.6.8', 'GEB.5.4'] };
let host: HTMLDivElement;
let root: Root;
let revisions: number[];
function Revision() { revisions.push(useRegulatoryDecisionRevision()); return null; }
const render = () => act(async () => {
root.render(<>>);
await new Promise(resolve => setTimeout(resolve, 0));
});
const badge = () => host.querySelector('[data-bsi-catalog] .datahub-rack-badge')?.textContent;
const button = (label: string) => [...host.querySelectorAll('[data-bsi-catalog] button')].find(item => item.textContent === label);
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
localStorage.clear();
resetEnginePreferencesForTests();
revisions = [];
host = document.createElement('div');
document.body.append(host);
root = createRoot(host);
});
afterEach(async () => {
await act(async () => root.unmount());
host.remove();
vi.restoreAllMocks();
vi.unstubAllGlobals();
resetEnginePreferencesForTests();
});
describe('Data Hub BSI catalog tile', () => {
it('names the bundled catalog and mapping, updates from BSI and reloads the derived BSI views', async () => {
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
const refresh = vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...updated, updated: true });
await render();
expect(host.querySelector('[data-bsi-catalog] .datahub-rack-header strong')?.textContent).toBe('BSI GRUNDSCHUTZ++ KERNEL');
expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled');
expect(button('Use bundled catalog')).toBeUndefined();
const before = revisions.at(-1)!;
await act(async () => button('Update from BSI')!.click());
expect(refresh).toHaveBeenCalledTimes(1);
expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated');
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓Updated to catalog 2026-10-01 and mapping 1.0.2. 2 mapping targets are not in the catalog.');
// Client Info and Work reload their derived BSI requirements.
expect(revisions.at(-1)).toBe(before + 1);
expect(button('Use bundled catalog')).toBeDefined();
});
it('says when the catalog is up to date without reloading other views', async () => {
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...bundled, updated: false });
await render();
const before = revisions.at(-1);
await act(async () => button('Update from BSI')!.click());
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The BSI catalog is up to date.');
expect(revisions.at(-1)).toBe(before);
});
it('returns to the bundled catalog and shows an update failure without changing the badge', async () => {
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(updated);
const reset = vi.spyOn(AssessorApiClient.prototype, 'resetBsiCatalog').mockResolvedValue(bundled);
vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockRejectedValue(new Error('BSI could not be reached. Please try again later.'));
await render();
await act(async () => button('Update from BSI')!.click());
expect(host.querySelector('[role="alert"]')?.textContent).toBe('BSI could not be reached. Please try again later.');
expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated');
await act(async () => button('Use bundled catalog')!.click());
expect(reset).toHaveBeenCalledTimes(1);
expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled');
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The bundled BSI catalog is active again.');
expect(button('Use bundled catalog')).toBeUndefined();
});
it('renders and requests nothing while the Compliance Engine is off', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
setEngineEnabled('compliance', false);
await render();
expect(host.querySelector('[data-bsi-catalog]')).toBeNull();
expect(request).not.toHaveBeenCalled();
});
it('sits in the Data Hub reference row directly after the ISO corpus tile', () => {
const source = readFileSync(join(process.cwd(), 'src/features/project-import/DataHub.tsx'), 'utf8');
expect(source).toContain(' \n \n');
const styles = readFileSync(join(process.cwd(), 'src/styles/datahub-redesign.css'), 'utf8');
expect(styles).toContain('.datahub-reference-row > .bsi-catalog-library { grid-area: 4 / 1 / 5 / -1; }');
});
});