import { describe, it, expect } from 'vitest';
import { generatePassword } from './password-gen';

const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&*+-?@';

describe('generatePassword', () => {
	it('rejects lengths under 8', () => {
		expect(() => generatePassword(7)).toThrowError(/at least 8/);
	});

	it('respects the requested length', () => {
		for (const len of [8, 16, 32, 64]) {
			expect(generatePassword(len)).toHaveLength(len);
		}
	});

	it('uses only characters from the documented charset', () => {
		const set = new Set(CHARSET);
		for (let i = 0; i < 1000; i++) {
			for (const c of generatePassword(32)) {
				expect(set.has(c)).toBe(true);
			}
		}
	});

	// Rejection-sampling sanity. With N = 71 the expected count per
	// char over 100k samples is ~1408 (σ ≈ 37). A 6σ band catches
	// any byte-level bias (biased modulo would push the first 38
	// chars by ~16 ppm — too small for this band to flag on its
	// own, but a regression to `% N` over Uint16/Uint32 with a
	// non-power-of-two charset would still produce visible drift in
	// pathological codepaths). Mostly this guards against
	// fundamental mistakes (off-by-one in the loop, returning the
	// same byte stream every time, etc.).
	it('distribution stays within a wide tolerance band', () => {
		const samples = 100_000;
		const counts = new Map<string, number>();
		for (let i = 0; i < samples; i++) {
			const c = generatePassword(8)[0];
			counts.set(c, (counts.get(c) ?? 0) + 1);
		}
		const expected = samples / CHARSET.length;
		const sigma = Math.sqrt(expected);
		const band = 6 * sigma;
		for (const c of CHARSET) {
			const observed = counts.get(c) ?? 0;
			const drift = Math.abs(observed - expected);
			expect(
				drift,
				`char "${c}": observed ${observed}, expected ~${Math.round(expected)} (drift ${drift.toFixed(0)} > ${band.toFixed(0)})`
			).toBeLessThan(band);
		}
	});
});
