import { describe, test, expect, beforeEach, mock } from 'bun:test';
import { MemoryStorage } from '@shade/crypto-web';
import { FingerprintNotVerifiedError } from '@shade/core';
import { FingerprintGateRegistry } from '../src/gates.js';

describe('FingerprintGateRegistry — unit', () => {
  let storage: MemoryStorage;
  let gates: FingerprintGateRegistry;

  beforeEach(() => {
    storage = new MemoryStorage();
    gates = new FingerprintGateRegistry(storage);
  });

  test('default first-large-file threshold is 10 MiB', () => {
    expect(gates.getFirstLargeFileThreshold()).toBe(10 * 1024 * 1024);
  });

  test('threshold becomes the registered value', () => {
    gates.registerFirstLargeFile(2048, () => true);
    expect(gates.getFirstLargeFileThreshold()).toBe(2048);
  });

  test('rejects negative thresholds', () => {
    expect(() => gates.registerFirstLargeFile(-1, () => true)).toThrow();
  });

  test('checkFirstLargeFile is a no-op when size < threshold', async () => {
    const handler = mock(() => true);
    gates.registerFirstLargeFile(10_000, handler);
    await gates.checkFirstLargeFile('bob', 'fp', 1_000);
    expect(handler).not.toHaveBeenCalled();
  });

  test('checkFirstLargeFile invokes handler and approves on true', async () => {
    let called = false;
    gates.registerFirstLargeFile(10, (ctx) => {
      called = true;
      expect(ctx.gate).toBe('first-large-file');
      expect(ctx.fileSize).toBe(100);
      expect(ctx.peerAddress).toBe('bob');
      expect(ctx.fingerprint).toBe('FP');
      return true;
    });

    await gates.checkFirstLargeFile('bob', 'FP', 100);
    expect(called).toBe(true);

    // Subsequent calls: peer is verified, handler not consulted.
    let secondCalled = false;
    gates.registerFirstLargeFile(10, () => {
      secondCalled = true;
      return false;
    });
    await gates.checkFirstLargeFile('bob', 'FP', 100);
    expect(secondCalled).toBe(false);
  });

  test('handler false → throws FingerprintNotVerifiedError', async () => {
    gates.registerFirstLargeFile(10, () => false);
    await expect(gates.checkFirstLargeFile('bob', 'FP', 100)).rejects.toBeInstanceOf(
      FingerprintNotVerifiedError,
    );
  });

  test('handler throw → throws FingerprintNotVerifiedError', async () => {
    gates.registerFirstLargeFile(10, () => {
      throw new Error('user closed modal');
    });
    await expect(gates.checkFirstLargeFile('bob', 'FP', 100)).rejects.toBeInstanceOf(
      FingerprintNotVerifiedError,
    );
  });

  test('no handler registered → TOFU + warn + persists verification', async () => {
    const originalWarn = console.warn;
    let warnings = 0;
    console.warn = () => {
      warnings += 1;
    };
    try {
      // backup-import always fires (no threshold)
      await gates.checkBackupImport('bob', 'FP');
    } finally {
      console.warn = originalWarn;
    }
    expect(warnings).toBe(1);

    // The peer is now considered verified at FP under the
    // tofu-after-warning source.
    expect(await gates.isVerified('bob', 'FP')).toBe(true);
    const v = await storage.getPeerVerification('bob');
    expect(v?.verifiedBy).toBe('tofu-after-warning');
  });

  test('warn fires only once per peer', async () => {
    const originalWarn = console.warn;
    let warnings = 0;
    console.warn = () => {
      warnings += 1;
    };
    try {
      await gates.checkBackupImport('bob', 'FP');
      await gates.checkBackupImport('bob', 'FP');
      await gates.checkBackupImport('bob', 'FP');
    } finally {
      console.warn = originalWarn;
    }
    expect(warnings).toBe(1);
  });

  test('isVerified is fingerprint-sensitive', async () => {
    await gates.markVerified('bob', 'FP_OLD');
    expect(await gates.isVerified('bob', 'FP_OLD')).toBe(true);
    expect(await gates.isVerified('bob', 'FP_NEW')).toBe(false);
  });

  test('identity-version bump invalidates verification', async () => {
    await gates.markVerified('bob', 'FP');
    expect(await gates.isVerified('bob', 'FP')).toBe(true);

    await storage.bumpPeerIdentityVersion('bob');
    expect(await gates.isVerified('bob', 'FP')).toBe(false);
  });

  test('revoke removes saved verification', async () => {
    await gates.markVerified('bob', 'FP');
    expect(await gates.isVerified('bob', 'FP')).toBe(true);
    await gates.revoke('bob');
    expect(await gates.isVerified('bob', 'FP')).toBe(false);
  });

  test('backup-import and new-device are minimum-gates (no threshold bypass)', async () => {
    let backupCalled = false;
    let newDeviceCalled = false;
    gates.registerBackupImport(() => {
      backupCalled = true;
      return true;
    });
    gates.registerNewDeviceTrust(() => {
      newDeviceCalled = true;
      return true;
    });
    await gates.checkBackupImport('me', 'FP1');
    await gates.checkNewDeviceTrust('bob', 'FP2');
    expect(backupCalled).toBe(true);
    expect(newDeviceCalled).toBe(true);
  });
});
