/**
 * @file Unit tests for the seedblend library.
 * @summary These tests are a direct port of the unit tests from the
 * 'dice_mix_interactive.py' script. Their purpose is to verify that the
 * TypeScript/Web Crypto implementation is 100% logic-compliant with the
 * Python reference script, producing identical, deterministic outputs for
 * the same inputs.
 */

import { describe, test, expect } from "bun:test";
import {
  xorBytes,
  hkdfExtractExpand,
  mnemonicToEntropy,
  entropyToMnemonic,
  blendMnemonicsAsync,
  mixWithDiceAsync,
  diceToBytes,
  detectBadPatterns,
  calculateDiceStats,
} from './seedblend';

// Helper to convert hex strings to Uint8Array
const fromHex = (hex: string): Uint8Array => {
  const bytes = hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16));
  const buffer = new ArrayBuffer(bytes.length);
  new Uint8Array(buffer).set(bytes);
  return new Uint8Array(buffer);
};

describe('SeedBlend Logic Compliance Tests (Ported from Python)', () => {

  test('should ensure XOR blending is order-independent (commutative)', () => {
    const ent1 = fromHex("a1".repeat(16));
    const ent2 = fromHex("b2".repeat(16));
    const ent3 = fromHex("c3".repeat(16));

    const blended1 = xorBytes(xorBytes(ent1, ent2), ent3);
    const blended2 = xorBytes(xorBytes(ent3, ent2), ent1);

    expect(blended1).toEqual(blended2);
  });

  test('should handle XOR of different length inputs correctly', () => {
    const ent128 = fromHex("a1".repeat(16)); // 12-word seed
    const ent256 = fromHex("b2".repeat(32)); // 24-word seed

    const blended = xorBytes(ent128, ent256);
    expect(blended.length).toBe(32);

    // Verify cycling: first 16 bytes should be a1^b2, last 16 should also be a1^b2

    expect(blended.slice(0, 16) as Uint8Array).toEqual(xorBytes(fromHex("a1".repeat(16)), fromHex("b2".repeat(16))) as Uint8Array);
    expect(blended.slice(16, 32) as Uint8Array).toEqual(xorBytes(fromHex("a1".repeat(16)), fromHex("b2".repeat(16))) as Uint8Array);
  });

  test('should perform a basic round-trip and validation for mnemonics', async () => {
    const valid12 = "army van defense carry jealous true garbage claim echo media make crunch";
    const ent12 = await mnemonicToEntropy(valid12);
    expect(ent12.length).toBe(16);

    const mnBack = await entropyToMnemonic(ent12);
    const entBack = await mnemonicToEntropy(mnBack);
    expect(ent12).toEqual(entBack);

    const valid24 = "zone huge rather sad stomach ostrich real decline laptop glimpse gasp reunion garbage rain reopen furnace catch hire feed charge cheese liquid earn exchange";
    const ent24 = await mnemonicToEntropy(valid24);
    expect(ent24.length).toBe(32);
  });

  test('should be deterministic for the same HKDF inputs', async () => {
    const data = new Uint8Array(64).fill(0x01);
    const info1 = new TextEncoder().encode('test');
    const info2 = new TextEncoder().encode('different');

    const out1 = await hkdfExtractExpand(data, 32, info1);
    const out2 = await hkdfExtractExpand(data, 32, info1);
    const out3 = await hkdfExtractExpand(data, 32, info2);

    expect(out1).toEqual(out2);
    expect(out1).not.toEqual(out3);
  });
  
  test('should produce correct HKDF lengths and match prefixes', async () => {
    const data = fromHex('ab'.repeat(32));
    const info = new TextEncoder().encode('len-test');

    const out16 = await hkdfExtractExpand(data, 16, info);
    const out32 = await hkdfExtractExpand(data, 32, info);
    
    expect(out16.length).toBe(16);
    expect(out32.length).toBe(32);
    expect(out16).toEqual(out32.slice(0, 16));
  });

  test('should detect bad dice patterns', () => {
    expect(detectBadPatterns("1111111111").bad).toBe(true);
    expect(detectBadPatterns("123456123456").bad).toBe(true);
    expect(detectBadPatterns("222333444555").bad).toBe(true);
    expect(detectBadPatterns("314159265358979323846264338327950").bad).toBe(false);
  });
  
  test('should calculate dice stats correctly', () => {
    const rolls = "123456".repeat(10); // 60 rolls, perfectly uniform
    const stats = calculateDiceStats(rolls);
    
    expect(stats.length).toBe(60);
    expect(stats.distribution).toEqual({ 1: 10, 2: 10, 3: 10, 4: 10, 5: 10, 6: 10 });
    expect(stats.mean).toBeCloseTo(3.5);
    expect(stats.chiSquare).toBe(0); // Perfect uniformity
  });

  test('should convert dice to bytes using integer math', () => {
    const rolls = "123456".repeat(17); // 102 rolls
    const bytes = diceToBytes(rolls);
    
    // Based on python script: `(102 * 2584965 // 1000000 + 7) // 8` = 33 bytes
    expect(bytes.length).toBe(33); 
  });

  // --- Crucial Integration Tests ---

  test('[CRITICAL] must reproduce the exact blended mnemonic for 4 seeds', async () => {
    const sessionMnemonics = [
      // 2x 24-word seeds
      "dog guitar hotel random owner gadget salute riot patrol work advice panic erode leader pass cross section laundry elder asset soul scale immune scatter",
      "unable point minimum sun peanut habit ready high nothing cherry silver eagle pen fabric list collect impact loan casual lyrics pig train middle screen",
      // 2x 12-word seeds
      "ethics super fog off merge misery atom sail domain bullet rather lamp",
      "life repeat play screen initial slow run stumble vanish raven civil exchange"
    ];

    const expectedMnemonic = "gasp question busy coral shrug jacket sample return main issue finish truck cage task tiny nerve desk treat feature balance idea timber dose crush";

    const { blendedMnemonic24 } = await blendMnemonicsAsync(sessionMnemonics);
    
    expect(blendedMnemonic24).toBe(expectedMnemonic);
  });

  test('[CRITICAL] must reproduce the exact final mixed output with 4 seeds and dice', async () => {
    const sessionMnemonics = [
        "dog guitar hotel random owner gadget salute riot patrol work advice panic erode leader pass cross section laundry elder asset soul scale immune scatter",
        "unable point minimum sun peanut habit ready high nothing cherry silver eagle pen fabric list collect impact loan casual lyrics pig train middle screen",
        "ethics super fog off merge misery atom sail domain bullet rather lamp",
        "life repeat play screen initial slow run stumble vanish raven civil exchange"
    ];
    const diceRolls = "3216534562134256361653421342634265362163523652413643616523462134652431625362543";
    
    const expectedFinalMnemonic = "satisfy sphere banana negative blood divide force crime window fringe private market sense enjoy diet talent super abuse toss miss until visa inform dignity";

    // Stage 1: Blend
    const { blendedEntropy } = await blendMnemonicsAsync(sessionMnemonics);

    // Stage 2: Mix
    const { finalMnemonic } = await mixWithDiceAsync(blendedEntropy, diceRolls, 256);
    
    expect(finalMnemonic).toBe(expectedFinalMnemonic);
  });
});
