import { describe, expect, test } from 'bun:test';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import {
  AezeedError,
  birthdayToDate,
  decipherMnemonic,
  isAezeedPhrase,
  mnemonicToCipherSeedBytes,
} from './cipher-seed';

// Fixtures were produced by the `aezeed` npm package (an INDEPENDENT implementation, different author
// and codebase, itself matching lnd/aezeed). Deterministic entropy and salt make them reproducible, so
// agreement here is genuine cross-implementation evidence rather than this port agreeing with itself.
//
// entropy 000102...0f, salt 0102030405, internalVersion 0, birthday 5593
const ENTROPY_HEX = '000102030405060708090a0b0c0d0e0f';
const DEFAULT_PW =
  'about wisdom spawn awkward catalog large teach salad large phrase drip caught coral snake chief mountain manage gym dog alcohol doctor valley evolve citizen';
const CUSTOM_PW =
  'above shop hair museum laugh diamond win skin habit inspire hero box category crime boss black nest useless dog alcohol doctor radar urban split';
const CUSTOM_PASSPHRASE = 'correct horse battery staple';
// entropy ffee...00, salt a1b2c3d4e5, internalVersion 1, birthday 0
const BIRTHDAY_ZERO =
  'abstract three say vacant about cable tent neck other jeans certain accident arrest market couch front inflict laptop hole marble exact okay owner number';

const split = (phrase: string): string[] => phrase.split(' ');
const hex = (b: Uint8Array): string => Buffer.from(b).toString('hex');

describe('isAezeedPhrase', () => {
  test('accepts aezeed cipher seeds', () => {
    expect(isAezeedPhrase(split(DEFAULT_PW))).toBe(true);
    expect(isAezeedPhrase(split(CUSTOM_PW))).toBe(true);
    expect(isAezeedPhrase(split(BIRTHDAY_ZERO))).toBe(true);
  });

  test('is passphrase-independent — structure only, no scrypt', () => {
    // Both fixtures hold the SAME entropy under different passphrases. Detection must not care.
    expect(isAezeedPhrase(split(DEFAULT_PW))).toBe(isAezeedPhrase(split(CUSTOM_PW)));
  });

  test('rejects genuine BIP39 mnemonics', () => {
    // The whole point: a 24-word BIP39 phrase is also 33 bytes, so only the CRC separates them.
    for (let i = 0; i < 25; i++) {
      expect(isAezeedPhrase(split(generateMnemonic(wordlist, 256)))).toBe(false);
    }
  });

  test('rejects wrong lengths and non-wordlist words', () => {
    expect(isAezeedPhrase(split(DEFAULT_PW).slice(0, 12))).toBe(false);
    expect(isAezeedPhrase([...split(DEFAULT_PW).slice(0, 23), 'notaword'])).toBe(false);
    expect(isAezeedPhrase([])).toBe(false);
  });

  test('rejects a single transposed word', () => {
    const words = split(DEFAULT_PW);
    const swapped = [...words];
    swapped[3] = words[4]!;
    swapped[4] = words[3]!;
    expect(isAezeedPhrase(swapped)).toBe(false);
  });
});

describe('decipherMnemonic', () => {
  test('recovers entropy under the default passphrase', async () => {
    const seed = await decipherMnemonic(split(DEFAULT_PW));
    expect(hex(seed.entropy)).toBe(ENTROPY_HEX);
    expect(seed.entropy).toHaveLength(16);
    expect(seed.internalVersion).toBe(0);
    expect(seed.birthday).toBe(5593);
  });

  test('recovers the same entropy under a custom passphrase', async () => {
    const seed = await decipherMnemonic(split(CUSTOM_PW), CUSTOM_PASSPHRASE);
    expect(hex(seed.entropy)).toBe(ENTROPY_HEX);
  });

  test('decodes internalVersion 1 and a zero birthday', async () => {
    const seed = await decipherMnemonic(split(BIRTHDAY_ZERO));
    expect(hex(seed.entropy)).toBe('ffeeddccbbaa99887766554433221100');
    expect(seed.internalVersion).toBe(1);
    expect(seed.birthday).toBe(0);
  });

  test('a wrong passphrase fails the AEZ tag rather than returning junk', async () => {
    // This is the property that makes the whole port safe: a bad key CANNOT yield plausible-looking
    // entropy, so we can never silently derive the wrong wallet.
    const err = await decipherMnemonic(split(CUSTOM_PW), 'not the passphrase').catch((e) => e);
    expect(err).toBeInstanceOf(AezeedError);
    expect(err.code).toBe('BAD_PASSPHRASE');
  });

  test('the default passphrase is not silently accepted for a custom-passphrase seed', async () => {
    const err = await decipherMnemonic(split(CUSTOM_PW)).catch((e) => e);
    expect(err.code).toBe('BAD_PASSPHRASE');
  });

  test('a corrupted checksum is caught before scrypt', async () => {
    const bytes = mnemonicToCipherSeedBytes(split(DEFAULT_PW))!;
    bytes[32] = bytes[32]! ^ 0xff;
    const { decipherCipherSeed } = await import('./cipher-seed');
    const err = await decipherCipherSeed(bytes, 'aezeed').catch((e) => e);
    expect(err.code).toBe('BAD_CHECKSUM');
  });

  test('rejects a non-aezeed phrase', async () => {
    const err = await decipherMnemonic(split(generateMnemonic(wordlist, 256))).catch((e) => e);
    expect(err).toBeInstanceOf(AezeedError);
  });
});

describe('framing', () => {
  test('24 words pack into exactly 33 bytes', () => {
    const bytes = mnemonicToCipherSeedBytes(split(DEFAULT_PW));
    expect(bytes).toHaveLength(33);
    expect(bytes![0]).toBe(0); // aezeed version
  });

  test('birthday maps to a real date', () => {
    // 5593 days after the genesis block.
    expect(birthdayToDate(5593).toISOString().slice(0, 10)).toBe('2024-04-27');
    expect(birthdayToDate(0).toISOString().slice(0, 10)).toBe('2009-01-03');
  });
});
