import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
  base32Encode, base32Decode, generateSecret, generateToken,
  verifyToken, otpauthURI, generateRecoveryCodes,
} from '../../src/auth/totp.js';

// ── base32 round-trips ──────────────────────────────────────────────────────
test('base32 encode/decode round-trips arbitrary bytes', () => {
  for (const s of ['', 'f', 'fo', 'foo', 'foob', 'fooba', 'foobar']) {
    const buf = Buffer.from(s);
    assert.deepEqual(base32Decode(base32Encode(buf)), buf);
  }
});

// ── RFC 6238 Appendix B test vectors (SHA-1, 8 digits in the RFC; we use the
// low 6 here, so compare the last 6 digits of each published value). ──────────
// The RFC uses the ASCII secret "12345678901234567890". We base32-encode it and
// check the 6-digit code at each published timestamp.
test('matches RFC 6238 SHA-1 vectors (low 6 digits)', () => {
  const secret = base32Encode(Buffer.from('12345678901234567890'));
  // [unix seconds, full 8-digit code from the RFC] → expect last 6 digits.
  const vectors = [
    [59,          '94287082'],
    [1111111109,  '07081804'],
    [1111111111,  '14050471'],
    [1234567890,  '89005924'],
    [2000000000,  '69279037'],
    [20000000000, '65353130'],
  ];
  for (const [secs, full8] of vectors) {
    const got = generateToken(secret, secs * 1000);
    assert.equal(got, full8.slice(-6), `t=${secs}`);
  }
});

// ── verify with drift window ────────────────────────────────────────────────
// verifyToken returns the matched counter (truthy) or null (falsy).
test('verifyToken accepts the current code and ±1 step of drift', () => {
  const secret = generateSecret();
  const now = 1_700_000_000_000;
  const code = generateToken(secret, now);
  const baseCounter = Math.floor(now / 1000 / 30);
  assert.equal(verifyToken(secret, code, now), baseCounter);
  // 30s earlier / later still inside ±1 window — the *issued* code matches the
  // baseCounter, but at now+30s we're in step baseCounter+1, so the issued
  // code matches as drift = -1 step → returns baseCounter.
  assert.equal(verifyToken(secret, code, now + 30_000), baseCounter);
  assert.equal(verifyToken(secret, code, now - 30_000), baseCounter);
  // 2 steps away → rejected.
  assert.equal(verifyToken(secret, code, now + 90_000), null);
});

test('verifyToken rejects malformed / empty input without throwing', () => {
  const secret = generateSecret();
  assert.equal(verifyToken(secret, ''), null);
  assert.equal(verifyToken(secret, 'abcdef'), null);
  assert.equal(verifyToken(secret, '12345'), null);   // too short
  assert.equal(verifyToken(secret, '1234567'), null);  // too long
  assert.equal(verifyToken('', '123456'), null);
});

test('verifyToken tolerates spaces in the user-entered code', () => {
  const secret = generateSecret();
  const now = 1_700_000_000_000;
  const code = generateToken(secret, now);
  const expected = Math.floor(now / 1000 / 30);
  assert.equal(verifyToken(secret, code.slice(0, 3) + ' ' + code.slice(3), now), expected);
});

test('verifyToken returns the matched counter (for replay protection)', () => {
  const secret = generateSecret();
  const now = 1_700_000_000_000;
  const code = generateToken(secret, now);
  const counter = verifyToken(secret, code, now);
  assert.ok(typeof counter === 'number' && counter > 0);
  assert.equal(counter, Math.floor(now / 1000 / 30));
});

// ── otpauth URI ─────────────────────────────────────────────────────────────
test('otpauthURI embeds secret, issuer, and account', () => {
  const uri = otpauthURI('JBSWY3DPEHPK3PXP', 'alice', 'Dragonflight');
  assert.match(uri, /^otpauth:\/\/totp\/Dragonflight%3Aalice\?/);
  assert.match(uri, /secret=JBSWY3DPEHPK3PXP/);
  assert.match(uri, /issuer=Dragonflight/);
  assert.match(uri, /digits=6/);
  assert.match(uri, /period=30/);
});

// ── recovery codes ──────────────────────────────────────────────────────────
test('generateRecoveryCodes returns N distinct formatted codes', () => {
  const codes = generateRecoveryCodes(10);
  assert.equal(codes.length, 10);
  assert.equal(new Set(codes).size, 10);
  for (const c of codes) assert.match(c, /^[0-9a-f]{5}-[0-9a-f]{5}$/);
});
