## Task 2: Fingerprint del contenuto DVR (+ setup vitest)

**Files:**
- Create: `app/src/lib/server/pdf/fingerprint.ts`
- Create: `app/vitest.config.ts`
- Create: `app/src/lib/server/pdf/fingerprint.test.ts`
- Modify: `app/package.json` (devDep `vitest`, script `test`)

**Interfaces:**
- Produces:
  - `export const TEMPLATE_VERSION = 'v11'`
  - `export type FingerprintInput = { templateVersion: string; agenzia: Record<string, unknown>; dvr: Record<string, unknown>; pericoli: unknown[]; valutazioni: unknown[]; misure: unknown[]; incendio: unknown[]; piano: unknown[]; attivita: unknown[]; allegati: unknown[]; settings: Record<string, unknown> }`
  - `export function hashFingerprintInput(input: FingerprintInput): string` (sha256 hex, deterministico, indipendente dall'ordine delle chiavi)
  - `export async function loadFingerprintInput(client: Client, agenziaId: number): Promise<FingerprintInput>` (`client` = `@libsql/client` Client; risolve la gemella via la stessa logica di `resolveDvrAgenziaId`)
  - `export async function computeDvrFingerprint(client: Client, agenziaId: number): Promise<string>`
- Consumes: `@libsql/client` `Client`, modulo `node:crypto`.

- [ ] **Step 1: Installa vitest e aggiungi lo script**

```bash
cd app && npm install -D vitest
```
In `app/package.json` aggiungi a `scripts`: `"test": "vitest run"`.

- [ ] **Step 2: Config vitest**

Crea `app/vitest.config.ts`:

```ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
	test: {
		include: ['src/**/*.test.ts'],
		environment: 'node'
	}
});
```

- [ ] **Step 3: Scrivi il test della funzione pura `hashFingerprintInput`**

Crea `app/src/lib/server/pdf/fingerprint.test.ts`:

```ts
import { describe, it, expect } from 'vitest';
import { hashFingerprintInput, type FingerprintInput } from './fingerprint';

const base: FingerprintInput = {
	templateVersion: 'v11',
	agenzia: { id: 1, codiceFisico: '22399', nomeAgenzia: 'ABANO TERME' },
	dvr: { revCorrente: 2 },
	pericoli: [{ id: 1, colD: 'S' }],
	valutazioni: [], misure: [], incendio: [], piano: [], attivita: [],
	allegati: [{ id: 9, tipo: 'foto_immobile' }],
	settings: { rspp_nome: 'X' }
};

describe('hashFingerprintInput', () => {
	it('è deterministico per lo stesso input', () => {
		expect(hashFingerprintInput(base)).toBe(hashFingerprintInput(structuredClone(base)));
	});
	it('non dipende dall’ordine delle chiavi', () => {
		const reordered = { ...base, settings: { rspp_nome: 'X' }, agenzia: { nomeAgenzia: 'ABANO TERME', codiceFisico: '22399', id: 1 } };
		expect(hashFingerprintInput(reordered as FingerprintInput)).toBe(hashFingerprintInput(base));
	});
	it('cambia se cambia un valore del DVR', () => {
		const changed = structuredClone(base); changed.dvr.revCorrente = 3;
		expect(hashFingerprintInput(changed)).not.toBe(hashFingerprintInput(base));
	});
	it('cambia se cambia il template version', () => {
		const changed = structuredClone(base); changed.templateVersion = 'v12';
		expect(hashFingerprintInput(changed)).not.toBe(hashFingerprintInput(base));
	});
});
```

- [ ] **Step 4: Esegui il test e verifica che fallisca**

Run: `cd app && npx vitest run src/lib/server/pdf/fingerprint.test.ts`
Expected: FAIL (modulo `./fingerprint` inesistente).

- [ ] **Step 5: Implementa `fingerprint.ts`**

Crea `app/src/lib/server/pdf/fingerprint.ts`:

```ts
import { createHash } from 'node:crypto';
import type { Client } from '@libsql/client';

export const TEMPLATE_VERSION = 'v11';

export type FingerprintInput = {
	templateVersion: string;
	agenzia: Record<string, unknown>;
	dvr: Record<string, unknown>;
	pericoli: unknown[];
	valutazioni: unknown[];
	misure: unknown[];
	incendio: unknown[];
	piano: unknown[];
	attivita: unknown[];
	allegati: unknown[];
	settings: Record<string, unknown>;
};

// Serializzazione stabile (chiavi ordinate) → hash riproducibile a prescindere dall'ordine.
function stableStringify(value: unknown): string {
	if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
	if (value && typeof value === 'object') {
		const keys = Object.keys(value as Record<string, unknown>).sort();
		return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify((value as Record<string, unknown>)[k])).join(',') + '}';
	}
	return JSON.stringify(value ?? null);
}

export function hashFingerprintInput(input: FingerprintInput): string {
	return createHash('sha256').update(stableStringify(input)).digest('hex');
}

async function rows(client: Client, sql: string, args: unknown[]): Promise<Record<string, unknown>[]> {
	const r = await client.execute({ sql, args: args as never });
	return r.rows as unknown as Record<string, unknown>[];
}

// Stessa regola di $lib/server/dvr.resolveDvrAgenziaId, ma via client libsql diretto.
async function resolveDvrAgenziaId(client: Client, agenziaId: number): Promise<number> {
	const [a] = await rows(client, 'SELECT dvr_condiviso_da_id AS shared FROM agenzie WHERE id = ?', [agenziaId]);
	return (a?.shared as number) ?? agenziaId;
}

export async function loadFingerprintInput(client: Client, agenziaId: number): Promise<FingerprintInput> {
	const dvrAgenziaId = await resolveDvrAgenziaId(client, agenziaId);
	const [agenzia] = await rows(client,
		`SELECT codice_fisico, codice_immobile, codice_agenzia, tipologia, nome_agenzia, nome_sede,
		        indirizzo, cap, citta, provincia, latitudine, longitudine, piano, mq,
		        datore_lavoro, rspp, zona_sismica_id, rischio_frane_id, rischio_alluvioni_id, dvr_condiviso_da_id
		 FROM agenzie WHERE id = ?`, [agenziaId]);
	const [dvr] = await rows(client, `SELECT * FROM dvr WHERE agenzia_id = ?`, [dvrAgenziaId]);
	const dvrId = (dvr?.id as number) ?? -1;
	const pericoli = await rows(client, `SELECT id, punto_id, col_c_lookup_id, col_c_override, col_d, col_e, col_f_lookup_id, col_f_override FROM dvr_pericoli WHERE dvr_id = ? ORDER BY id`, [dvrId]);
	const valutazioni = await rows(client, `SELECT id, elemento_id, figure_esposte, rischi_descr, misure_prevenzione_lookup_id, misure_prevenzione_override, p, g FROM dvr_valutazione WHERE dvr_id = ? ORDER BY id`, [dvrId]);
	const misure = await rows(client, `SELECT id, valutazione_id, lookup_id, testo_override, p_var, g_var, r_var, ordine FROM dvr_valutazione_misure WHERE valutazione_id IN (SELECT id FROM dvr_valutazione WHERE dvr_id = ?) ORDER BY id`, [dvrId]);
	const incendio = await rows(client, `SELECT id, documento_id, stato_lookup_id, stato_override, note FROM dvr_incendio_doc WHERE dvr_id = ? ORDER BY id`, [dvrId]);
	const piano = await rows(client, `SELECT id, attivita_lookup_id, attivita_override, responsabile, data_avvio, data_conclusione, ordine FROM dvr_piano_miglioramento WHERE dvr_id = ? ORDER BY id`, [dvrId]);
	const attivita = await rows(client, `SELECT id, source_type, source_id, famiglia_id, variante_scelta_id, stato FROM dvr_attivita_miglioramento WHERE dvr_id = ? ORDER BY id`, [dvrId]);
	const allegati = await rows(client, `SELECT id, tipo, storage_path, excluded FROM agenzie_allegati WHERE agenzia_id = ? ORDER BY id`, [agenziaId]);
	const [settings] = await rows(client, `SELECT datore_lavoro_nome, datore_lavoro_qualifica, rspp_nome, rspp_qualifica FROM app_settings WHERE id = 1`, []);
	return {
		templateVersion: TEMPLATE_VERSION,
		agenzia: agenzia ?? {}, dvr: dvr ?? {}, pericoli, valutazioni, misure,
		incendio, piano, attivita, allegati, settings: settings ?? {}
	};
}

export async function computeDvrFingerprint(client: Client, agenziaId: number): Promise<string> {
	return hashFingerprintInput(await loadFingerprintInput(client, agenziaId));
}
```

> Nota: gli allegati entrano nell'hash via `id`+`tipo`+`storage_path`+`excluded`. La sostituzione di un'immagine cambia `storage_path` (vedi `buildKey`), quindi l'hash cambia. Se in futuro una sostituzione riusasse la stessa chiave, aggiungere alla SELECT una colonna di versione/sha dell'allegato.

- [ ] **Step 6: Esegui i test e verifica che passino**

Run: `cd app && npx vitest run src/lib/server/pdf/fingerprint.test.ts`
Expected: 4 test PASS.

- [ ] **Step 7: Commit**

```bash
git add app/src/lib/server/pdf/fingerprint.ts app/src/lib/server/pdf/fingerprint.test.ts app/vitest.config.ts app/package.json app/package-lock.json
git commit -m "PDF: fingerprint del contenuto DVR + setup vitest"
```

---

