import { describe, test, expect } from 'bun:test';
import { KeyManager } from '../src/crypto/key-manager.js';
import {
  DEFAULT_ARGON2ID,
  deriveMasterKeyArgon2id,
  type Argon2idParams,
} from '../src/crypto/kdf.js';

const FAST_ARGON: Argon2idParams = { m: 256, t: 1, p: 1, dkLen: 32 };

function randBytes(n: number): Uint8Array {
  const b = new Uint8Array(n);
  globalThis.crypto.getRandomValues(b);
  return b;
}

describe('argon2id source', () => {
  const salt = new Uint8Array(16).fill(0x33);

  test('deriveMasterKeyArgon2id is deterministic', async () => {
    const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON);
    const b = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON);
    expect(a).toEqual(b);
    expect(a.length).toBe(32);
  });

  test('different secret → different key', async () => {
    const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON);
    const b = await deriveMasterKeyArgon2id('1235', salt, FAST_ARGON);
    expect(a).not.toEqual(b);
  });

  test('different salt → different key', async () => {
    const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON);
    const b = await deriveMasterKeyArgon2id('1234', new Uint8Array(16).fill(0x44), FAST_ARGON);
    expect(a).not.toEqual(b);
  });

  test('rejects empty secret', async () => {
    await expect(deriveMasterKeyArgon2id('', salt, FAST_ARGON)).rejects.toThrow(/non-empty/);
  });

  test('rejects too-short salt', async () => {
    await expect(deriveMasterKeyArgon2id('p', new Uint8Array(8), FAST_ARGON))
      .rejects.toThrow(/at least 16/);
  });

  test('KeyManager.open opens with argon2id source', async () => {
    const km = await KeyManager.open({
      kind: 'argon2id',
      secret: '123456',
      salt,
      params: FAST_ARGON,
    });
    expect(km.fieldKey('t', 'c').length).toBe(32);
    km.destroy();
  });

  test('DEFAULT_ARGON2ID is exposed and sensible', () => {
    expect(DEFAULT_ARGON2ID.dkLen).toBe(32);
    expect(DEFAULT_ARGON2ID.m).toBeGreaterThanOrEqual(8 * 1024);
    expect(DEFAULT_ARGON2ID.t).toBeGreaterThanOrEqual(1);
  });

  test('accepts Uint8Array secret', async () => {
    const secretBytes = new TextEncoder().encode('1234');
    const a = await deriveMasterKeyArgon2id(secretBytes, salt, FAST_ARGON);
    const b = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON);
    expect(a).toEqual(b);
  });
});

describe('composite source — multi-factor unlock', () => {
  const pwSalt = new Uint8Array(16).fill(0x11);
  const pinSalt = new Uint8Array(16).fill(0x22);
  const FAST_SCRYPT = { N: 1 << 10, r: 8, p: 1, dkLen: 32 };

  function pwSource(passphrase: string) {
    return { kind: 'passphrase' as const, passphrase, salt: pwSalt, params: FAST_SCRYPT };
  }
  function pinSource(secret: string) {
    return { kind: 'argon2id' as const, secret, salt: pinSalt, params: FAST_ARGON };
  }

  test('same factors → same masterKey', async () => {
    const a = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('correct horse'), pinSource('1234')],
    });
    const b = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('correct horse'), pinSource('1234')],
    });
    expect(a.storageKeyFingerprint()).toEqual(b.storageKeyFingerprint());
    a.destroy();
    b.destroy();
  });

  test('wrong PIN → different masterKey (same shape as wrong-passphrase)', async () => {
    const right = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('correct horse'), pinSource('1234')],
    });
    const wrongPin = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('correct horse'), pinSource('9999')],
    });
    expect(right.storageKeyFingerprint()).not.toEqual(wrongPin.storageKeyFingerprint());
    right.destroy();
    wrongPin.destroy();
  });

  test('wrong passphrase → different masterKey', async () => {
    const right = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('correct horse'), pinSource('1234')],
    });
    const wrongPwd = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('wrong horse'), pinSource('1234')],
    });
    expect(right.storageKeyFingerprint()).not.toEqual(wrongPwd.storageKeyFingerprint());
    right.destroy();
    wrongPwd.destroy();
  });

  test('order is significant by design', async () => {
    const ab = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('horse'), pinSource('1234')],
    });
    const ba = await KeyManager.open({
      kind: 'composite',
      sources: [pinSource('1234'), pwSource('horse')],
    });
    expect(ab.storageKeyFingerprint()).not.toEqual(ba.storageKeyFingerprint());
    ab.destroy();
    ba.destroy();
  });

  test('explicit info string changes masterKey (domain separation)', async () => {
    const a = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('horse'), pinSource('1234')],
    });
    const b = await KeyManager.open({
      kind: 'composite',
      sources: [pwSource('horse'), pinSource('1234')],
      info: 'my-app-v1',
    });
    expect(a.storageKeyFingerprint()).not.toEqual(b.storageKeyFingerprint());
    a.destroy();
    b.destroy();
  });

  test('rejects empty source list', async () => {
    await expect(KeyManager.open({ kind: 'composite', sources: [] }))
      .rejects.toThrow(/at least one/);
  });

  test('rejects nested composite', async () => {
    await expect(KeyManager.open({
      kind: 'composite',
      sources: [
        { kind: 'composite', sources: [pwSource('a')] },
        pinSource('1234'),
      ],
    })).rejects.toThrow(/cannot be nested/);
  });

  test('composite of three sources works', async () => {
    const km = await KeyManager.open({
      kind: 'composite',
      sources: [
        pwSource('horse'),
        pinSource('1234'),
        { kind: 'injected', key: randBytes(32) },
      ],
    });
    expect(km.fieldKey('t', 'c').length).toBe(32);
    km.destroy();
  });
});
