import { beforeEach, describe, expect, it } from 'vitest';
import { handleActuateAPI } from '../../api/index.js';
import { initDB } from '../../db.js';
import { encryptSecret, encryptStringArray } from '../../security/secret-storage.js';
import { createMfaPendingToken, computeRequestFingerprint } from '../../auth/mfa-pending.js';
import { generateTOTPSecret } from '../../auth/totp.js';
/**
 * Contract test for the backup-code recovery path of POST /auth/totp/login.
 *
 * Backup codes are generated and shown at enrollment but, before this, were
 * never consumed anywhere — a lost authenticator meant permanent lockout. This
 * proves a valid code signs the user in, is single-use (consumed on success),
 * and that an unknown code is rejected.
 */
const SECRET = 'a'.repeat(48);
const ENCRYPTION_KEY = 'b'.repeat(64); // 32 bytes hex
const BACKUP_CODES = ['aaaa1111', 'bbbb2222', 'cccc3333'];
async function createMockDB(state) {
    const totpSecret = await encryptSecret(generateTOTPSecret());
    return {
        user: {
            findUnique: async () => ({
                id: 'user-1',
                email: 'admin@example.com',
                name: 'Admin',
                role: 'ADMIN',
                totpSecret,
                totpEnabled: true,
                backupCodes: await encryptStringArray(state.backupCodes),
                isActive: true,
            }),
            update: async ({ data }) => {
                // The handler re-encrypts the remaining codes; decrypt-by-count is
                // enough for the assertion (we only check how many remain).
                state.backupCodes = state.backupCodes.slice(0, data.backupCodes.length);
                return { id: 'user-1' };
            },
        },
        document: {
            findFirst: async () => ({ data: {} }),
        },
        session: {
            findMany: async () => [],
            create: async () => ({ id: 'sess-1' }),
            updateMany: async () => ({ count: 0 }),
        },
        auditLog: {
            create: async () => ({ id: 'log-1' }),
        },
    };
}
async function makePendingToken(ip, userAgent) {
    const fingerprint = await computeRequestFingerprint(ip, userAgent);
    return createMfaPendingToken({ userId: 'user-1', fingerprint }, SECRET);
}
beforeEach(() => {
    delete globalThis.__actuateConfig;
    process.env.CMS_SECRET = SECRET;
    process.env.CMS_ENCRYPTION_KEY = ENCRYPTION_KEY;
});
describe('POST /auth/totp/login — backup code recovery', () => {
    it('signs in with a valid backup code and consumes it (single-use)', async () => {
        const ip = '203.0.113.10';
        const ua = 'vitest-agent';
        const state = { backupCodes: [...BACKUP_CODES] };
        const db = await createMockDB(state);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const token = await makePendingToken(ip, ua);
        const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
            method: 'POST',
            headers: { 'x-real-ip': ip, 'user-agent': ua, 'content-type': 'application/json' },
            body: JSON.stringify({ mfaPendingToken: token, code: 'BBBB-2222' }),
        }));
        expect(res.status).toBe(200);
        expect(res.headers.get('set-cookie')).toContain('actuate_session=');
        // The used code was removed — one fewer remains.
        expect(state.backupCodes).toHaveLength(BACKUP_CODES.length - 1);
    });
    it('rejects an unknown code', async () => {
        const ip = '203.0.113.11';
        const ua = 'vitest-agent';
        const state = { backupCodes: [...BACKUP_CODES] };
        const db = await createMockDB(state);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        const token = await makePendingToken(ip, ua);
        const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
            method: 'POST',
            headers: { 'x-real-ip': ip, 'user-agent': ua, 'content-type': 'application/json' },
            body: JSON.stringify({ mfaPendingToken: token, code: 'deadbeef' }),
        }));
        expect(res.status).toBe(401);
        expect(state.backupCodes).toHaveLength(BACKUP_CODES.length);
    });
    it('rejects a stale pending token from a different device fingerprint', async () => {
        const state = { backupCodes: [...BACKUP_CODES] };
        const db = await createMockDB(state);
        initDB(db);
        const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
        // Token minted for one device, redeemed from another (different UA).
        const token = await makePendingToken('203.0.113.12', 'device-a');
        const res = await handler(new Request('https://example.com/api/cms/auth/totp/login', {
            method: 'POST',
            headers: {
                'x-real-ip': '203.0.113.12',
                'user-agent': 'device-b',
                'content-type': 'application/json',
            },
            body: JSON.stringify({ mfaPendingToken: token, code: 'aaaa1111' }),
        }));
        expect(res.status).toBe(401);
    });
});
//# sourceMappingURL=totp-login.test.js.map