import { describe, expect, it } from 'vitest';
import type {
  AuditPublisher,
  CustodyAdapter,
  MintJobSink,
  OutstandingPolicy,
} from './custody-adapter';
import { NativeBitcoinWatcher } from './native-bitcoin-watcher';
import type { AuditRecord, BitcoinDepositEvent, MintJob } from './types';

class FakeCustodyAdapter implements CustodyAdapter {
  reserveBalanceSats = 0;
  private events: BitcoinDepositEvent[] = [];
  private cursor = 0;

  async allocateDepositAddress(input: { instructionId: string }): Promise<string> {
    return `bc1q${input.instructionId.replace(/-/g, '').slice(0, 20)}`;
  }

  queueEvent(event: BitcoinDepositEvent): void {
    this.events.push(event);
  }

  async listDepositEvents(cursor?: string): Promise<{ events: BitcoinDepositEvent[]; cursor?: string }> {
    const start = cursor ? Number(cursor) : this.cursor;
    const nextEvents = this.events.slice(start);
    const nextCursor = String(this.events.length);
    this.cursor = this.events.length;
    return { events: nextEvents, cursor: nextCursor };
  }

  async getConfirmedReserveBalanceSats(): Promise<number> {
    return this.reserveBalanceSats;
  }
}

class InMemoryMintSink implements MintJobSink {
  readonly jobs: MintJob[] = [];

  async enqueue(job: MintJob): Promise<void> {
    this.jobs.push(job);
  }
}

class InMemoryAuditPublisher implements AuditPublisher {
  readonly records: AuditRecord[] = [];

  async publish(record: AuditRecord): Promise<void> {
    this.records.push(record);
  }
}

class StaticOutstandingPolicy implements OutstandingPolicy {
  constructor(
    private readonly currentOutstandingSats: number,
    private readonly maxOutstandingSats: number,
  ) {}

  async getCurrentOutstandingSats(): Promise<number> {
    return this.currentOutstandingSats;
  }

  async getMaxOutstandingSats(): Promise<number> {
    return this.maxOutstandingSats;
  }
}

describe('NativeBitcoinWatcher', () => {
  it('allocates unique deposit addresses', async () => {
    const adapter = new FakeCustodyAdapter();
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-1',
        clientId: 'client-1',
        chain138VaultAddress: '0x1111111111111111111111111111111111111111',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
    });

    expect(instruction.depositAddress.startsWith('bc1q')).toBe(true);
    expect(audit.records[0]?.category).toBe('deposit_instruction');
  });

  it('auto-mints after 6 confirmations and emits one mint job', async () => {
    const adapter = new FakeCustodyAdapter();
    adapter.reserveBalanceSats = 300_000_000;
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-2',
        clientId: 'client-2',
        chain138VaultAddress: '0x2222222222222222222222222222222222222222',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
      expectedAmountSats: 250_000_000,
    });

    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-1',
      confirmations: 6,
      amountSats: 250_000_000,
      type: 'deposit',
    });

    await watcher.sync();

    expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
    expect(mintSink.jobs).toHaveLength(1);
    expect(mintSink.jobs[0]).toMatchObject({
      instructionId: instruction.id,
      amountSats: 250_000_000,
      canonicalSymbol: 'cBTC',
    });
  });

  it('ignores duplicate finality events', async () => {
    const adapter = new FakeCustodyAdapter();
    adapter.reserveBalanceSats = 150_000_000;
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-3',
        clientId: 'client-3',
        chain138VaultAddress: '0x3333333333333333333333333333333333333333',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
    });

    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-dup',
      confirmations: 6,
      amountSats: 125_000_000,
      type: 'deposit',
    });
    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-dup',
      confirmations: 6,
      amountSats: 125_000_000,
      type: 'deposit',
    });

    await watcher.sync();

    expect(mintSink.jobs).toHaveLength(1);
    expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
  });

  it('handles reorgs before finality without minting early', async () => {
    const adapter = new FakeCustodyAdapter();
    adapter.reserveBalanceSats = 500_000_000;
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-4',
        clientId: 'client-4',
        chain138VaultAddress: '0x4444444444444444444444444444444444444444',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
    });

    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-reorg',
      confirmations: 5,
      amountSats: 100_000_000,
      type: 'deposit',
    });
    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-reorg',
      confirmations: 2,
      amountSats: 100_000_000,
      type: 'reorg',
    });
    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-reorg',
      confirmations: 6,
      amountSats: 100_000_000,
      type: 'deposit',
    });

    await watcher.sync();

    expect(mintSink.jobs).toHaveLength(1);
    expect(watcher.getInstruction(instruction.id)?.status).toBe('minted');
  });

  it('freezes when reserve reconciliation fails', async () => {
    const adapter = new FakeCustodyAdapter();
    adapter.reserveBalanceSats = 99_999_999;
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-5',
        clientId: 'client-5',
        chain138VaultAddress: '0x5555555555555555555555555555555555555555',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
    });

    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-deficit',
      confirmations: 6,
      amountSats: 100_000_000,
      type: 'deposit',
    });

    await watcher.sync();

    expect(mintSink.jobs).toHaveLength(0);
    expect(watcher.getInstruction(instruction.id)?.status).toBe('frozen');
    expect(audit.records[audit.records.length - 1]?.category).toBe('freeze');
  });

  it('freezes when outstanding bridge capacity would be exceeded', async () => {
    const adapter = new FakeCustodyAdapter();
    adapter.reserveBalanceSats = 500_000_000;
    const mintSink = new InMemoryMintSink();
    const audit = new InMemoryAuditPublisher();
    const outstanding = new StaticOutstandingPolicy(450_000_000, 500_000_000);
    const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit, outstanding);

    const instruction = await watcher.createDepositInstruction({
      basketMandate: {
        id: 'basket-6',
        clientId: 'client-6',
        chain138VaultAddress: '0x6666666666666666666666666666666666666666',
        allocations: [{ symbol: 'cBTC', targetWeightBps: 10_000 }],
      },
    });

    adapter.queueEvent({
      instructionId: instruction.id,
      txId: 'tx-cap',
      confirmations: 6,
      amountSats: 75_000_000,
      type: 'deposit',
    });

    await watcher.sync();

    expect(mintSink.jobs).toHaveLength(0);
    expect(watcher.getInstruction(instruction.id)?.status).toBe('frozen');
    expect(watcher.getInstruction(instruction.id)?.freezeReason).toContain('Bridge outstanding limit');
  });
});
