#!/usr/bin/env node
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

const root = new URL('./', import.meta.url);
const record = JSON.parse(await readFile(new URL('bos-presale-wallet-flows.json', root), 'utf8'));
const sources = JSON.parse(await readFile(new URL('sources.json', root), 'utf8'));

function decimal(value, scale) {
  assert.match(value, /^\d+(?:\.\d+)?$/, `invalid decimal: ${value}`);
  const [whole, fraction = ''] = value.split('.');
  assert.ok(fraction.length <= scale, `${value} exceeds scale ${scale}`);
  return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
}

function format(value, scale, places = scale) {
  const negative = value < 0n;
  const absolute = negative ? -value : value;
  const padded = absolute.toString().padStart(scale + 1, '0');
  const whole = scale ? padded.slice(0, -scale) : padded;
  const fraction = scale ? padded.slice(-scale).slice(0, places) : '';
  return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`;
}

const sourceIds = new Set(sources.map((source) => source.id));
const referencedSourceIds = [];
const collectSourceIds = (value) => {
  if (Array.isArray(value)) {
    value.forEach(collectSourceIds);
    return;
  }
  if (!value || typeof value !== 'object') return;
  for (const [key, entry] of Object.entries(value)) {
    if (key === 'sourceIds') {
      assert.ok(Array.isArray(entry));
      referencedSourceIds.push(...entry);
    } else {
      collectSourceIds(entry);
    }
  }
};
collectSourceIds(record);
assert.ok(referencedSourceIds.length >= 20, 'expected source coverage for collector, destination, and distribution records');
for (const id of referencedSourceIds) assert.ok(sourceIds.has(id), `missing source ${id}`);

assert.equal(record.ethereumCollectors.length, 3);
assert.equal(new Set(record.ethereumCollectors.map((collector) => collector.address.toLowerCase())).size, 3);
assert.deepEqual(
  record.ethereumCollectors.map((collector) => collector.exactPurchaseSweepLowerBound.rows),
  [79, 27, 33]
);
assert.deepEqual(
  record.ethereumCollectors.map((collector) => collector.exactPurchaseSweepLowerBound.uniqueDepositAddresses),
  [68, 26, 29]
);
assert.equal(record.associatedEthereumCollectorBranches.length, 1);
const branch60c = record.associatedEthereumCollectorBranches[0];
assert.equal(branch60c.id, 'origins-linked-60c');
assert.equal(branch60c.exactPurchaseSweepLowerBound.rows, 5);
assert.equal(branch60c.exactPurchaseSweepLowerBound.uniqueDepositAddresses, 4);
assert.equal(branch60c.allObservedCanonicalReceipts.stablecoinFaceValue, '305.915601');
assert.equal(branch60c.payerBeneficiaryCrossmatch.uniquePayers, 1);
assert.equal(branch60c.payerBeneficiaryCrossmatch.scheduledBos, '110856.229015747261564859');
assert.equal(
  record.ethereumCollectors.reduce((total, collector) => total + collector.exactPurchaseSweepLowerBound.rows, 0)
    + branch60c.exactPurchaseSweepLowerBound.rows,
  144
);
assert.equal(
  record.ethereumCollectors.reduce((total, collector) => total + collector.exactPurchaseSweepLowerBound.uniqueDepositAddresses, 0)
    + branch60c.exactPurchaseSweepLowerBound.uniqueDepositAddresses,
  127
);

const collectorMatches = record.ethereumCollectors.reduce(
  (total, collector) => total + collector.payerBeneficiaryCrossmatch.uniquePayers,
  0
) + branch60c.payerBeneficiaryCrossmatch.uniquePayers;
assert.equal(collectorMatches, 45);
assert.equal(record.crossmatchUnion.uniquePayerBeneficiaries, 43);
assert.equal(
  decimal(record.crossmatchUnion.scheduledBos, 18),
  decimal('7371964.297651759941706702', 18)
);

const routeById = Object.fromEntries(record.downstreamRoutes.map((route) => [route.id, route]));
const early = routeById['early-binance-endpoint'];
const middle = routeById['middle-operations-wallet'];
const post = routeById['post-cutover-509'];
const rootstock = routeById['rootstock-to-exchequer'];
const relayReuse = routeById['later-collectors-reuse-binance-relay'];
assert.ok(early && middle && post && rootstock && relayReuse);

const stablecoinTotal = (route) => ['USDT', 'USDC', 'DAI']
  .filter((symbol) => route.canonicalTokenTotalsDelivered[symbol])
  .reduce((sum, symbol) => sum + decimal(route.canonicalTokenTotalsDelivered[symbol], 6), 0n);

assert.equal(format(stablecoinTotal(early), 6), early.stablecoinFaceValue);
assert.equal(format(stablecoinTotal(middle), 6), middle.stablecoinFaceValue);
assert.equal(format(stablecoinTotal(post), 6), post.stablecoinFaceValue);
assert.equal(early.stablecoinFaceValue, '102167.018527');
assert.equal(middle.stablecoinFaceValue, '43585.792331');
assert.equal(post.stablecoinFaceValue, '234160.095765');
assert.equal(
  format(
    decimal(middle.directTo509.USDT, 6)
      + decimal(middle.directTo509.USDC, 6)
      + decimal(middle.directTo509.DAI, 6),
    6
  ),
  middle.directTo509.stablecoinFaceValue
);
assert.equal(middle.directTo509.stablecoinFaceValue, '4937.393862');
assert.equal(relayReuse.amounts['1160_DAI_to_relay'], relayReuse.amounts['relay_DAI_to_Binance']);

assert.equal(record.partialMultichainFootprint.length, 3);
const chainFootprint = Object.fromEntries(record.partialMultichainFootprint.map((entry) => [entry.chain, entry]));
assert.equal(chainFootprint['BNB Chain'].directTo509.stablecoinFaceValue, '7857.87166544');
assert.equal(chainFootprint['BNB Chain'].binanceRoute.stablecoinFaceValue, '20638.700273170727414066');
assert.equal(chainFootprint['Arbitrum One'].received.ARB, chainFootprint['Arbitrum One'].onward.ARB);
assert.equal(chainFootprint.Polygon.received.USDT, '1334.922732');

const rbtcScale = 18;
const rbtcGas = decimal(rootstock.receivedFromDepositSweeps, rbtcScale)
  - decimal(rootstock.amountSentToExchequer, rbtcScale);
assert.equal(format(rbtcGas, rbtcScale, 11), rootstock.collectorTransactionGas);
assert.equal(rootstock.path.at(-1).toLowerCase(), '0x924f5ad34698fd20c90fe5d5a8a0abd3b42dc711');

const distribution = record.communitySaleDistribution;
assert.equal(distribution.createdSchedules, 1162);
assert.equal(distribution.cancelledSchedules.count, 2);
assert.equal(distribution.cancelledSchedules.activeScheduleCount, 1160);
assert.equal(distribution.uniqueBeneficiaries - distribution.cancelledSchedules.count, distribution.cancelledSchedules.activeUniqueBeneficiaries);
assert.equal(
  decimal(distribution.createdPrincipalBos, 18) - decimal(distribution.cancelledSchedules.principalBos, 18),
  decimal(distribution.cancelledSchedules.activePrincipalBos, 18)
);

const saleTokens = decimal(distribution.saleReconciliation.originsTokensSold, 18);
const scheduledTokens = decimal(distribution.saleReconciliation.createdScheduleBos, 18);
assert.equal(
  saleTokens - scheduledTokens,
  decimal(distribution.saleReconciliation.differenceBos, 18)
);
const scheduledPercentScaled = (scheduledTokens * 100_000_000_000_000n) / saleTokens;
assert.equal(format(scheduledPercentScaled, 12, 12), distribution.saleReconciliation.scheduledPercentOfTokensSold);

const canonicalContracts = new Set(record.canonicalEthereumAssets.map((asset) => asset.contract.toLowerCase()));
assert.deepEqual(canonicalContracts, new Set([
  '0xdac17f958d2ee523a2206206994597c13d831ec7',
  '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
  '0x6b175474e89094c44da98b954eedeac495271d0f',
  '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
]));

assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /Fireblocks/.test(entry)));
assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /Binance/.test(entry)));
assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /intercompany/.test(entry)));

process.stdout.write('BOS presale wallet-flow verification PASS\n');
process.stdout.write('- reconstructed Ethereum collectors: 3 primary + 1 associated branch\n');
process.stdout.write('- exact purchase-sweep lower bound: 144 rows across 127 deposit addresses\n');
process.stdout.write('- payer/vesting-beneficiary crossmatch: 43 unique addresses\n');
process.stdout.write(`- early Binance route: ${early.stablecoinFaceValue} stablecoin units + ${early.canonicalTokenTotalsDelivered.WBTC} WBTC + ${early.ETH} ETH\n`);
process.stdout.write(`- middle operations route: ${middle.stablecoinFaceValue} stablecoin units + ${middle.ETH} ETH\n`);
process.stdout.write(`- middle collector direct to 0x509: ${middle.directTo509.stablecoinFaceValue} stablecoin units + ${middle.directTo509.WBTC} WBTC + ${middle.directTo509.ETH} ETH\n`);
process.stdout.write(`- post-cutover 0x509 route: ${post.stablecoinFaceValue} stablecoin units + ${post.ETH} ETH\n`);
process.stdout.write(`- Rootstock collector to Exchequer: ${rootstock.amountSentToExchequer} RBTC\n`);
process.stdout.write(`- Community Sale schedules: ${distribution.createdSchedules}; ${distribution.createdPrincipalBos} BOS\n`);
