import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

const here = new URL('./', import.meta.url);
const transactions = JSON.parse(await readFile(new URL('curated-transactions.json', here), 'utf8'));
const snapshots = JSON.parse(await readFile(new URL('snapshots.json', here), 'utf8'));
const loanRecords = JSON.parse(await readFile(new URL('loan-records.json', here), 'utf8'));
const feeRecords = JSON.parse(await readFile(new URL('fee-authority-records.json', here), 'utf8'));
const assetMap = JSON.parse(await readFile(new URL('asset-map.json', here), 'utf8'));
const governanceAttribution = JSON.parse(await readFile(new URL('governance-attribution.json', here), 'utf8'));
const poolModelInputs = JSON.parse(await readFile(new URL('pool-model-inputs.json', here), 'utf8'));
const bosLaunchpadSale = JSON.parse(await readFile(new URL('bos-launchpad-sale.json', here), 'utf8'));

const SCALE = 18;
const WAD = 10n ** BigInt(SCALE);

function decimal(value, scale = SCALE) {
  const source = String(value);
  const negative = source.startsWith('-');
  const unsigned = negative ? source.slice(1) : source;
  const [whole = '0', fraction = ''] = unsigned.split('.');
  const normalized = `${whole}${fraction.padEnd(scale, '0').slice(0, scale)}`;
  const parsed = BigInt(normalized || '0');
  return negative ? -parsed : parsed;
}

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

function percent(numerator, denominator, places = 15) {
  const factor = 10n ** BigInt(places);
  const scaled = numerator * 100n * factor;
  const rounded = (scaled + denominator / 2n) / denominator;
  return format(rounded, places, places);
}

function ceilDivide(numerator, denominator) {
  return (numerator + denominator - 1n) / denominator;
}

function equalWeightConverterQuote(amountIn, model) {
  assert.equal(model.sourceReserveWeightPPM, 500000);
  assert.equal(model.destinationReserveWeightPPM, 500000);
  const reserveIn = decimal(model.reserveSOV);
  const reserveOut = decimal(model.reserveWRBTC);
  const ppm = 1000000n;
  const gross = reserveOut - reserveIn * reserveOut / (reserveIn + amountIn);
  const fee = ceilDivide(gross * BigInt(model.conversionFeePPM), ppm);
  return gross - fee;
}

function assertPin(pin, label) {
  assert.ok(Number.isSafeInteger(pin.block) && pin.block > 0, `${label} block`);
  assert.match(pin.blockHash, /^0x[0-9a-f]{64}$/i, `${label} block hash`);
  assert.match(pin.dateUTC, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, `${label} UTC timestamp`);
  assert.ok(Number.isFinite(Date.parse(pin.dateUTC)), `${label} parseable timestamp`);
}

for (const [label, pin] of [
  ['treasury opening', snapshots.treasury.opening],
  ['treasury liquidity', snapshots.treasury.liquidity],
  ['treasury staking', snapshots.treasury.staking],
  ['FeeSharing', snapshots.feeSharing],
  ['loans', snapshots.loans],
  ['pool', snapshots.pool]
]) assertPin(pin, label);
assertPin({ block: snapshots.governance.snapshotBlock, blockHash: snapshots.governance.snapshotBlockHash, dateUTC: snapshots.governance.snapshotDateUTC }, 'governance');
assert.deepEqual(loanRecords.snapshot, { block: snapshots.loans.block, blockHash: snapshots.loans.blockHash, dateUTC: snapshots.loans.dateUTC });
assert.equal(feeRecords.snapshot.block, snapshots.feeSharing.block);
assert.equal(feeRecords.snapshot.blockHash, snapshots.feeSharing.blockHash);
assert.equal(feeRecords.snapshot.dateUTC, snapshots.feeSharing.dateUTC);

const report = {};

assert.equal(transactions.operations.length, 27, 'curated operation count');
report.operationCount = transactions.operations.length;
assert.equal(new Set(transactions.operations.map((entry) => entry.id)).size, 27, 'unique Exchequer operation IDs');
assert.equal(new Set(transactions.operations.map((entry) => entry.tx)).size, 27, 'unique Exchequer execution hashes');
assert.deepEqual(
  [...transactions.operations].map((entry) => entry.utc),
  [...transactions.operations].map((entry) => entry.utc).sort(),
  'Exchequer operations are chronological'
);

const signerCounts = Object.fromEntries(Object.keys(transactions.signers).map((signer) => [signer, 0]));
for (const operation of transactions.operations) {
  assert.equal(operation.approvers.length, transactions.threshold, `threshold approvals on Exchequer multisig operation ${operation.id}`);
  assert.equal(new Set(operation.approvers).size, transactions.threshold, `distinct approvals on Exchequer multisig operation ${operation.id}`);
  assert.match(operation.tx, /^0x[0-9a-f]{64}$/i, `execution hash on Exchequer multisig operation ${operation.id}`);
  assert.ok(Number.isSafeInteger(operation.block) && operation.block > 0, `block on Exchequer multisig operation ${operation.id}`);
  assert.match(operation.destination, /^0x[0-9a-f]{40}$/i, `destination on Exchequer multisig operation ${operation.id}`);
  assert.match(operation.utc, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, `UTC timestamp on Exchequer multisig operation ${operation.id}`);
  for (const signer of operation.approvers) {
    assert.ok(transactions.signers[signer], `known signer ${signer} on Exchequer multisig operation ${operation.id}`);
    signerCounts[signer] += 1;
  }
}
assert.deepEqual(signerCounts, { F: 27, N: 21, A: 20, E: 13 });
report.signerCounts = signerCounts;

const computedAssetTotals = {};
for (const operation of transactions.operations) {
  computedAssetTotals[operation.asset] = (computedAssetTotals[operation.asset] || 0n) + decimal(operation.amount);
}
const formattedAssetTotals = Object.fromEntries(Object.entries(computedAssetTotals).map(([asset, amount]) => [asset, format(amount)]));
assert.deepEqual(formattedAssetTotals, transactions.assetTotals, 'per-asset Exchequer operation totals');
report.assetTotals = formattedAssetTotals;

const rbtcIds = new Set([2099, 2157, 2164]);
const exchequerRbtc = transactions.operations
  .filter((entry) => rbtcIds.has(entry.id))
  .reduce((total, entry) => total + decimal(entry.amount), 0n);
assert.equal(format(exchequerRbtc), snapshots.treasury.rbtcRoute.exchequerToD9);
report.exchequerToD9RBTC = format(exchequerRbtc);
const rbtcRoute = snapshots.treasury.rbtcRoute;
assert.ok(decimal(rbtcRoute.exchequerToD9) > decimal(rbtcRoute.d9ToPowPeg));
assert.ok(decimal(rbtcRoute.d9ToPowPeg) > decimal(rbtcRoute.bitcoinRelease));
assert.ok(decimal(rbtcRoute.bitcoinRelease) > decimal(rbtcRoute.exchangeStyleClusterReceipt));
assert.notEqual(rbtcRoute.exchequerToD9, snapshots.treasury.fastBtcNovember.intoD9, 'Exchequer and November FastBTC routes are distinct');
assert.equal(assetMap.treasuryRbtcRoute.totals.exchequerToD9RBTC, rbtcRoute.exchequerToD9);
assert.equal(assetMap.treasuryRbtcRoute.totals.d9ToPowPegRBTC, rbtcRoute.d9ToPowPeg);
assert.equal(assetMap.treasuryRbtcRoute.totals.bitcoinReleasedBTC, rbtcRoute.bitcoinRelease);
assert.equal(assetMap.treasuryRbtcRoute.totals.terminalDepositBTC, rbtcRoute.exchangeStyleClusterReceipt);
assert.equal(assetMap.treasuryRbtcRoute.routes.length, 3);
const mappedTreasuryExchequerToD9 = assetMap.treasuryRbtcRoute.routes.reduce((total, route) => total + decimal(route.steps[0].amount), 0n);
const mappedTreasuryD9ToPowPeg = assetMap.treasuryRbtcRoute.routes.reduce((total, route) => total + decimal(route.steps[1].amount), 0n);
const mappedTreasuryBitcoinRelease = assetMap.treasuryRbtcRoute.routes.reduce((total, route) => total + decimal(route.steps[2].amount), 0n);
const mappedTreasuryTerminal = assetMap.treasuryRbtcRoute.routes.reduce((total, route) => total + decimal(route.steps[3].amount), 0n);
assert.equal(format(mappedTreasuryExchequerToD9), rbtcRoute.exchequerToD9);
assert.equal(format(mappedTreasuryD9ToPowPeg), rbtcRoute.d9ToPowPeg);
assert.equal(format(mappedTreasuryBitcoinRelease), rbtcRoute.bitcoinRelease);
assert.equal(format(mappedTreasuryTerminal), rbtcRoute.exchangeStyleClusterReceipt);
for (const route of assetMap.treasuryRbtcRoute.routes) {
  assert.equal(route.steps.length, 4, `${route.id} has a four-step cross-chain route`);
  for (const step of route.steps) {
    assert.ok(Number.isSafeInteger(step.block) && step.block > 0, `${route.id} ${step.chain} block`);
    assert.ok(Number.isFinite(Date.parse(step.utc)), `${route.id} ${step.chain} UTC timestamp`);
    assert.match(step.tx, step.chain === 'Bitcoin' ? /^[0-9a-f]{64}$/i : /^0x[0-9a-f]{64}$/i, `${route.id} ${step.chain} transaction`);
  }
}
const transferValuation = assetMap.treasuryRbtcRoute.transferTimeUsdValuation;
assert.equal(transferValuation.transfers.length, assetMap.treasuryRbtcRoute.routes.length, 'one USD valuation per Exchequer RBTC route');
let transferTimeUsdTotal = 0n;
for (const valuation of transferValuation.transfers) {
  const route = assetMap.treasuryRbtcRoute.routes.find((candidate) => candidate.id === valuation.routeId);
  assert.ok(route, `known RBTC route for ${valuation.routeId} valuation`);
  const exchequerStep = route.steps[0];
  assert.equal(valuation.executionUTC, exchequerStep.utc, `${valuation.routeId} execution timestamp`);
  assert.equal(valuation.amountRBTC, exchequerStep.amount, `${valuation.routeId} Exchequer amount`);
  assert.match(valuation.sourceId, /^coinbase-btc-exchequer-(?:dec4|jan22|jan24)$/, `${valuation.routeId} Coinbase source`);
  assert.equal(
    valuation.candleStartUTC,
    valuation.executionUTC.replace(/:\d{2}Z$/, ':00Z'),
    `${valuation.routeId} one-minute candle contains execution`
  );
  for (const field of ['low', 'high', 'open', 'close', 'volumeBTC']) {
    assert.ok(decimal(valuation.candle[field]) > 0n, `${valuation.routeId} positive candle ${field}`);
  }
  assert.ok(decimal(valuation.candle.low, 2) <= decimal(valuation.candle.close, 2), `${valuation.routeId} close above candle low`);
  assert.ok(decimal(valuation.candle.close, 2) <= decimal(valuation.candle.high, 2), `${valuation.routeId} close below candle high`);
  const usdExactAtScale20 = decimal(valuation.amountRBTC) * decimal(valuation.candle.close, 2);
  const usdCents = (usdExactAtScale20 + 5n * 10n ** 17n) / 10n ** 18n;
  assert.equal(usdExactAtScale20, decimal(valuation.estimatedUSDExact, 20), `${valuation.routeId} exact transfer-time USD value`);
  assert.equal(usdCents, decimal(valuation.estimatedUSDRounded, 2), `${valuation.routeId} rounded transfer-time USD value`);
  transferTimeUsdTotal += usdExactAtScale20;
}
const transferTimeUsdCents = (transferTimeUsdTotal + 5n * 10n ** 17n) / 10n ** 18n;
assert.equal(transferTimeUsdTotal, decimal(transferValuation.totalEstimatedUSDExact, 20), 'combined exact transfer-time USD value');
assert.equal(transferTimeUsdCents, decimal(transferValuation.totalEstimatedUSDRounded, 2), 'combined rounded transfer-time USD value');
assert.equal(transferValuation.displayTotalUSD, '$2.12 million');
report.treasuryRbtcTransferTimeValue = {
  rbtc: rbtcRoute.exchequerToD9,
  usd: transferValuation.totalEstimatedUSDRounded,
  display: transferValuation.displayTotalUSD,
  priceMethod: 'Coinbase Exchange one-minute BTC-USD candle close for each execution'
};
const fastBtcSourceTotal = assetMap.fastBtcNovember.sourceTransfersToD9.reduce((total, entry) => total + decimal(entry.amountRBTC), 0n);
const fastBtcPegOutTotal = assetMap.fastBtcNovember.pegOutAndBitcoinSteps
  .filter((entry) => entry.chain === 'Rootstock' && entry.to === 'PowPeg')
  .reduce((total, entry) => total + decimal(entry.amountRBTC), 0n);
const fastBtcTerminalTotal = assetMap.fastBtcNovember.pegOutAndBitcoinSteps
  .filter((entry) => entry.chain === 'Bitcoin' && entry.to === assetMap.addresses.bitcoinTerminalDeposit)
  .reduce((total, entry) => total + decimal(entry.amountBTC), 0n);
assert.equal(format(fastBtcSourceTotal), assetMap.fastBtcNovember.totals.sourcesIntoD9RBTC);
assert.equal(format(fastBtcPegOutTotal), assetMap.fastBtcNovember.totals.d9PegOutRBTC);
assert.equal(format(fastBtcTerminalTotal), assetMap.fastBtcNovember.totals.terminalDepositBTC);
assert.equal(assetMap.fastBtcNovember.totals.sourcesIntoD9RBTC, snapshots.treasury.fastBtcNovember.intoD9);
assert.equal(assetMap.fastBtcNovember.totals.d9PegOutRBTC, snapshots.treasury.fastBtcNovember.pegOut);
for (const snapshot of assetMap.exchequerBalanceSnapshots) {
  assert.ok(Number.isSafeInteger(snapshot.block) && snapshot.block > 0, 'asset-map balance block');
  assert.match(snapshot.blockHash, /^0x[0-9a-f]{64}$/i, 'asset-map balance block hash');
  assert.ok(Number.isFinite(Date.parse(snapshot.utc)), 'asset-map balance UTC timestamp');
}
const openingBalances = assetMap.exchequerBalanceSnapshots[0].balances;
const currentBalances = assetMap.exchequerBalanceSnapshots.at(-1).balances;
const openingPositiveNonSov = Object.keys(openingBalances).filter((asset) => asset !== 'SOV' && decimal(openingBalances[asset]) > 0n);
const openingAssetsNowZero = openingPositiveNonSov.filter((asset) => decimal(currentBalances[asset]) === 0n);
assert.deepEqual(openingPositiveNonSov.filter((asset) => decimal(currentBalances[asset]) > 0n).sort(), ['RBTC', 'XUSD']);
assert.equal(openingAssetsNowZero.length, openingPositiveNonSov.length - 2);
report.assetMap = {
  openingBlock: assetMap.exchequerBalanceSnapshots[0].block,
  currentBlock: assetMap.exchequerBalanceSnapshots.at(-1).block,
  openingPositiveNonSovAssets: openingPositiveNonSov.length,
  openingAssetsNowZero: openingAssetsNowZero.length,
  remainingOpeningAssets: ['RBTC', 'XUSD'],
  distinctFastBtcIntoD9RBTC: assetMap.fastBtcNovember.totals.sourcesIntoD9RBTC
};
const residualExecutable = decimal(snapshots.treasury.liquidity.rbtc) + decimal(snapshots.treasury.liquidity.fullSovSaleQuoteWRBTC);
assert.equal(format(residualExecutable), snapshots.treasury.liquidity.combinedExecutableWRBTC);
report.residualExecutableWRBTC = format(residualExecutable);

const loan = snapshots.loans;
assert.equal(loanRecords.openings.length, 4);
assert.equal(new Set(loanRecords.openings.map((entry) => entry.borrower)).size, 4);
assert.equal(
  (Date.parse(loanRecords.openings.at(-1).utc) - Date.parse(loanRecords.openings[0].utc)) / 1000,
  15 * 60 + 12,
  'four linked openings span 15m12s'
);
const openingPrincipal = loanRecords.openings.reduce((total, entry) => total + decimal(entry.principalWRBTC), 0n);
const openingCollateral = loanRecords.openings.reduce((total, entry) => total + decimal(entry.collateralSOV), 0n);
const liquidationRepayment = loanRecords.liquidations.reduce((total, entry) => total + decimal(entry.repayWRBTC), 0n);
const liquidationSeizures = loanRecords.liquidations.reduce((total, entry) => total + decimal(entry.seizedSOV), 0n);
const currentPrincipal = loanRecords.current.reduce((total, entry) => total + decimal(entry.principalWRBTC), 0n);
const currentCollateral = loanRecords.current.reduce((total, entry) => total + decimal(entry.collateralSOV), 0n);
assert.equal(format(openingPrincipal), loan.eventPrincipalWRBTC);
assert.equal(format(openingCollateral), loan.openingCollateralSOV);
assert.equal(loanRecords.liquidations.length, 9);
assert.equal(format(liquidationRepayment), loan.liquidatorRepaymentWRBTC);
assert.equal(format(liquidationSeizures), loan.liquidationSeizuresSOV);
assert.equal(format(currentPrincipal), loan.remainingFourLoanPrincipalWRBTC);
assert.equal(format(currentCollateral), loan.remainingFourLoanCollateralSOV);
assert.ok(loanRecords.current.every((entry) => entry.active && entry.unsafe));
const reconciledCollateral = decimal(loan.openingCollateralSOV)
  - decimal(loan.liquidationSeizuresSOV)
  - decimal(loan.rolloverReductionSOV);
assert.equal(format(reconciledCollateral), loan.remainingFourLoanCollateralSOV);
const reconciledPrincipal = decimal(loan.eventPrincipalWRBTC) - decimal(loan.liquidatorRepaymentWRBTC);
assert.equal(format(reconciledPrincipal), loan.remainingFourLoanPrincipalWRBTC);
report.fourLoanCollateralReconciliationSOV = format(reconciledCollateral);
report.fourLoanPrincipalReconciliationWRBTC = format(reconciledPrincipal);

const votes = snapshots.governance.sip0088Voters
  .reduce((total, voter) => total + decimal(voter.votes), 0n);
assert.equal(format(votes), snapshots.governance.sip0088Against);
report.sip0088AgainstVotes = format(votes);

const governance = snapshots.governance;
assert.deepEqual(governanceAttribution.snapshot, {
  block: governance.snapshotBlock,
  blockHash: governance.snapshotBlockHash,
  dateUTC: governance.snapshotDateUTC
});
assert.equal(governanceAttribution.methodology.accountCount, 70);
assert.ok(Number.isSafeInteger(governanceAttribution.methodology.excludedTruncatedHistoryCount));
assert.ok(governanceAttribution.methodology.excludedTruncatedHistoryCount > 0);
assert.equal(governanceAttribution.accounts.length, 70);
assert.equal(new Set(governanceAttribution.accounts.map((account) => account.address)).size, 70);
assert.equal(governanceAttribution.accounts.filter((account) => account.seedRoot).length, 1);

const governanceRoot = governanceAttribution.methodology.root.toLowerCase();
const governanceAccountTotals = {
  liquidSOV: 0n,
  activeStakedSOV: 0n,
  currentVotingPower: 0n,
  sourceWeightedVotingPower: 0n
};
for (const account of governanceAttribution.accounts) {
  assert.match(account.address, /^0x[0-9a-f]{40}$/i, `strict B7 account ${account.address}`);
  assert.equal(account.address, account.address.toLowerCase());
  assert.equal(account.fundingPathAddressToRoot[0], account.address);
  assert.equal(account.fundingPathAddressToRoot.at(-1), governanceRoot);
  assert.equal(new Set(account.fundingPathAddressToRoot).size, account.fundingPathAddressToRoot.length);
  assert.equal(account.firstFundingHops.length, account.fundingPathAddressToRoot.length - 1);
  assert.equal(account.seedRoot, account.address === governanceRoot);
  assert.equal(account.directB7Recipient, account.fundingPathAddressToRoot.length === 2);
  for (const [index, hop] of account.firstFundingHops.entries()) {
    assert.equal(hop.recipient, account.fundingPathAddressToRoot[index]);
    assert.equal(hop.funder, account.fundingPathAddressToRoot[index + 1]);
    assert.match(hop.transaction, /^0x[0-9a-f]{64}$/i, `first-funding transaction for ${hop.recipient}`);
    assert.ok(Number.isSafeInteger(hop.block) && hop.block > 0, `first-funding block for ${hop.recipient}`);
    assert.ok(Number.isFinite(Date.parse(hop.dateUTC)), `first-funding UTC timestamp for ${hop.recipient}`);
    assert.equal(hop.valueWei, decimal(hop.valueRBTC).toString());
    assert.equal(hop.proofType, 'recipient first native incoming transaction');
    assert.equal(typeof hop.localNodeStatus, 'string');
  }
  for (const field of Object.keys(governanceAccountTotals)) {
    governanceAccountTotals[field] += decimal(account[field]);
  }
}
for (const [field, total] of Object.entries(governanceAccountTotals)) {
  assert.equal(format(total), governanceAttribution.totals[field], `strict B7 ${field} sum`);
}
assert.equal(
  format(governanceAccountTotals.liquidSOV + governanceAccountTotals.activeStakedSOV),
  governanceAttribution.totals.liquidPlusActiveStakedSOV
);
assert.equal(governanceAttribution.totals.sourceWeightedVotingPower, governance.sourceAttributedVotingPower);
assert.equal(governanceAttribution.totals.totalVotingPower, governance.totalVotingPower);
assert.equal(
  percent(governanceAccountTotals.currentVotingPower, decimal(governanceAttribution.totals.totalVotingPower), 18),
  governanceAttribution.totals.currentAddressSharePercent
);
assert.equal(
  percent(governanceAccountTotals.sourceWeightedVotingPower, decimal(governanceAttribution.totals.totalVotingPower), 18),
  governanceAttribution.totals.sourceWeightedSharePercent
);
assert.equal(governanceAttribution.totals.sourceWeightedSharePercent, '67.663810381608164728');
assert.equal(
  percent(decimal(governance.sourceAttributedVotingPower), decimal(governance.totalVotingPower), 18),
  governance.votingPowerPercent
);
const sip85 = governance.sip0085;
const sip85NonB7For = decimal(sip85.for) - decimal(sip85.b7LinkedFor);
const sip85NonB7Against = decimal(sip85.against) - decimal(sip85.b7LinkedAgainst);
const sip85ActualTotal = decimal(sip85.for) + decimal(sip85.against);
const sip85NonB7Total = sip85NonB7For + sip85NonB7Against;
const sip85ActualSupport = percent(decimal(sip85.for), sip85ActualTotal, 18);
const sip85NonB7Support = percent(sip85NonB7For, sip85NonB7Total, 18);
assert.equal(sip85.quorumPercentageVotes, '20');
assert.equal(sip85.majorityPercentageVotes, '70');
assert.ok(Number.isSafeInteger(sip85.ruleReadBlock) && sip85.ruleReadBlock === sip85.endBlock + 1);
assert.match(sip85.ruleReadBlockHash, /^0x[0-9a-f]{64}$/i);
assert.match(sip85.ruleReadDateUTC, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
assert.equal(format(sip85NonB7For), sip85.withoutB7LinkedFor);
assert.equal(format(sip85NonB7Against), sip85.withoutB7LinkedAgainst);
assert.equal(format(sip85NonB7Total), sip85.withoutB7LinkedTotal);
assert.equal(sip85ActualSupport, sip85.supportPercent);
assert.equal(sip85NonB7Support, sip85.withoutB7LinkedSupportPercent);
assert.ok(sip85ActualTotal > decimal(sip85.storedQuorumVotes), 'SIP-0085 actual vote met stored quorum');
assert.ok(decimal(sip85.supportPercent) <= decimal(sip85.majorityPercentageVotes), 'SIP-0085 actual support did not exceed Owner Governor threshold');
assert.ok(sip85NonB7Total > decimal(sip85.storedQuorumVotes), 'SIP-0085 counterfactual still meets stored quorum');
assert.ok(decimal(sip85.withoutB7LinkedSupportPercent) > decimal(sip85.majorityPercentageVotes), 'SIP-0085 counterfactual exceeds Owner Governor support threshold');
const sip88Signal = governance.sip0088.signalProposal;
assert.equal(sip88Signal.against, '0');
assert.equal(sip88Signal.for, '50211182.149156205159025471');
assert.equal(sip88Signal.state, 'Expired');
for (const proposal of [governance.sip0088.ownerProposal, governance.sip0088.adminProposal]) {
  assert.equal(decimal(proposal.against), votes);
  assert.equal(percent(votes, decimal(proposal.for) + votes, 18), proposal.b7AgainstSharePercent);
  assert.ok(votes > decimal(proposal.for), `${proposal.proposal} defeat arithmetic`);
}
const sip89 = governance.sip0089;
assert.equal(percent(decimal(governance.sip0089FVote), decimal(sip89.for), 18), sip89.fShareOfForPercent);
assert.equal(percent(decimal(governance.sip0089FVote), decimal(sip89.for) + decimal(sip89.against), 18), sip89.fShareOfAllPercent);
report.governance = {
  strictFirstFundedViaB7Accounts: governanceAttribution.accounts.length,
  strictCurrentAddressVotingPower: governanceAttribution.totals.currentVotingPower,
  strictSourceWeightedVotingPower: governanceAttribution.totals.sourceWeightedVotingPower,
  sourceAttributedVotingPowerPercent: governance.votingPowerPercent,
  sip0085: {
    actualSupportPercent: sip85.supportPercent,
    storedQuorumVotes: sip85.storedQuorumVotes,
    supportThresholdPercent: sip85.majorityPercentageVotes,
    withoutB7LinkedTotalVotes: sip85.withoutB7LinkedTotal,
    withoutB7LinkedSupportPercent: sip85.withoutB7LinkedSupportPercent,
    outcomeDeterminativeUnderActualRules: true
  },
  sip0088SignalForVotes: sip88Signal.for,
  sip0088OwnerAgainstSharePercent: governance.sip0088.ownerProposal.b7AgainstSharePercent,
  sip0088AdminAgainstSharePercent: governance.sip0088.adminProposal.b7AgainstSharePercent,
  sip0089FShareOfForPercent: sip89.fShareOfForPercent
};

const pool = snapshots.pool;
assert.deepEqual(poolModelInputs.snapshot, {
  block: pool.block,
  blockHash: pool.blockHash,
  dateUTC: pool.dateUTC,
  protocol: poolModelInputs.snapshot.protocol,
  priceFeeds: poolModelInputs.snapshot.priceFeeds
});
assert.equal(poolModelInputs.loans.length, 11);
assert.equal(poolModelInputs.pool.activeSOVLoanCount, 11);
assert.equal(poolModelInputs.pool.unsafeSOVLoanCount, 11);
assert.equal(new Set(poolModelInputs.loans.map((entry) => entry.loanId)).size, 11);
assert.equal(new Set(poolModelInputs.loans.map((entry) => entry.borrower)).size, 11);
assert.equal(poolModelInputs.loans.filter((entry) => entry.originalFourBorrower).length, 4);

const amm = poolModelInputs.amm;
assert.equal(amm.sourceToken, '0xefc78fc7d48b64958315949279ba181c2114abbd');
assert.equal(amm.destinationToken, poolModelInputs.pool.underlying);
assert.equal(amm.conversionFeePPM, 3500);
assert.equal(amm.conversionFeePercent, '0.35');
const ammCall = (name, argument) => amm.calls.find(
  (entry) => entry.function === name && (argument === undefined || entry.argument === argument)
);
assert.equal(BigInt(ammCall('conversionFee()').returnData), BigInt(amm.conversionFeePPM));
assert.equal(
  BigInt(ammCall('reserveBalance(address)', amm.sourceToken).returnData),
  decimal(amm.reserveSOV)
);
assert.equal(
  BigInt(ammCall('reserveBalance(address)', amm.destinationToken).returnData),
  decimal(amm.reserveWRBTC)
);
assert.equal(
  BigInt(ammCall('reserveWeight(address)', amm.sourceToken).returnData),
  BigInt(amm.sourceReserveWeightPPM)
);
assert.equal(
  BigInt(ammCall('reserveWeight(address)', amm.destinationToken).returnData),
  BigInt(amm.destinationReserveWeightPPM)
);

let modelPrincipal = 0n;
let modelCollateral = 0n;
let modelOracleValue = 0n;
let modelOracleShortfall = 0n;
for (const loanInput of poolModelInputs.loans) {
  assert.match(loanInput.loanId, /^0x[0-9a-f]{64}$/i, `canonical loan ${loanInput.loanId}`);
  assert.match(loanInput.borrower, /^0x[0-9a-f]{40}$/i, `canonical borrower ${loanInput.borrower}`);
  assert.equal(loanInput.lender, poolModelInputs.pool.lender);
  assert.equal(loanInput.maintenanceMarginPercent, '15');
  assert.equal(loanInput.currentMarginPercent, '0');
  assert.equal(loanInput.unsafe, true);

  const principal = decimal(loanInput.principalWRBTC);
  const collateral = decimal(loanInput.collateralSOV);
  const oracleValue = decimal(loanInput.oracleCollateralValueWRBTC);
  const standaloneQuote = equalWeightConverterQuote(collateral, amm);
  assert.equal(format(standaloneQuote), loanInput.standaloneAmmQuoteWRBTC, `standalone quote ${loanInput.loanId}`);
  assert.equal(format(principal - standaloneQuote), loanInput.standaloneAmmShortfallWRBTC, `standalone shortfall ${loanInput.loanId}`);
  assert.equal(decimal(percent(standaloneQuote, principal, 18)), decimal(loanInput.standaloneAmmCoveragePercent), `standalone coverage ${loanInput.loanId}`);
  assert.equal(format(principal - oracleValue), loanInput.oracleShortfallWRBTC, `oracle shortfall ${loanInput.loanId}`);
  assert.equal(decimal(percent(oracleValue, principal, 18)), decimal(loanInput.oracleCoveragePercent), `oracle coverage ${loanInput.loanId}`);
  assert.equal(loanInput.maxLiquidatableWRBTC, loanInput.principalWRBTC);
  assert.equal(loanInput.maxSeizableSOV, loanInput.collateralSOV);

  modelPrincipal += principal;
  modelCollateral += collateral;
  modelOracleValue += oracleValue;
  modelOracleShortfall += decimal(loanInput.oracleShortfallWRBTC);
}
assert.equal(format(modelPrincipal), poolModelInputs.pool.sovBackedPrincipalRBTC);
assert.equal(format(modelCollateral), poolModelInputs.pool.sovCollateralSOV);
assert.equal(format(modelOracleValue), poolModelInputs.pool.oracleCollateralValueWRBTC);
assert.equal(format(modelOracleShortfall), poolModelInputs.pool.oracleShortfallWRBTC);
const aggregateAmmQuote = equalWeightConverterQuote(modelCollateral, amm);
assert.equal(format(aggregateAmmQuote), poolModelInputs.pool.aggregateAmmQuoteWRBTC);
assert.equal(format(modelPrincipal - aggregateAmmQuote), poolModelInputs.pool.aggregateAmmShortfallWRBTC);

assert.equal(poolModelInputs.pool.lenderClaimsRBTC, pool.lenderClaimsRBTC);
assert.equal(poolModelInputs.pool.cashRBTC, pool.cashRBTC);
assert.equal(poolModelInputs.pool.allActivePrincipalRBTC, pool.allActivePrincipalRBTC);
assert.equal(poolModelInputs.pool.sovBackedPrincipalRBTC, pool.sovBackedPrincipalRBTC);
assert.equal(poolModelInputs.pool.aggregateAmmQuoteWRBTC, pool.aggregateAmmRecoveryRBTC);
assert.equal(poolModelInputs.pool.aggregateAmmShortfallWRBTC, pool.aggregateAmmShortfallRBTC);

const claims = decimal(poolModelInputs.pool.lenderClaimsRBTC);
const cash = decimal(poolModelInputs.pool.cashRBTC);
const borrow = decimal(poolModelInputs.pool.allActivePrincipalRBTC);
const sovPrincipal = modelPrincipal;
const ammShortfall = decimal(poolModelInputs.pool.aggregateAmmShortfallWRBTC);
const poolHaircut = percent(ammShortfall, claims, 18);
assert.equal(poolHaircut, pool.poolWideAmmHaircutPercent);
assert.equal(poolHaircut, poolModelInputs.pool.poolWideAmmHaircutPercent);

const remainingClaims = claims - cash;
const netInterest = claims - cash - borrow;
const healthyPrincipal = borrow - sovPrincipal;
const lateRecoverable = healthyPrincipal + netInterest;
const lateHaircut = percent(remainingClaims - lateRecoverable, remainingClaims, 18);
const lateRecovery = percent(lateRecoverable, remainingClaims, 18);
assert.equal(format(remainingClaims), pool.lateLenderStress.remainingClaimsRBTC);
assert.equal(format(lateRecoverable), pool.lateLenderStress.recoverableRBTC);
assert.equal(lateHaircut, pool.lateLenderStress.haircutPercent);
assert.equal(lateRecovery, pool.lateLenderStress.recoveryPercent);
report.canonicalPoolAmmHaircutPercent = poolHaircut;
report.canonicalPoolModel = {
  activeSOVLoanCount: poolModelInputs.loans.length,
  sovBackedPrincipalRBTC: format(modelPrincipal),
  sovCollateralSOV: format(modelCollateral),
  aggregateAmmQuoteWRBTC: format(aggregateAmmQuote),
  aggregateAmmShortfallWRBTC: format(modelPrincipal - aggregateAmmQuote),
  reserveSOV: amm.reserveSOV,
  reserveWRBTC: amm.reserveWRBTC,
  conversionFeePPM: amm.conversionFeePPM
};
report.lateLenderStress = {
  remainingClaimsRBTC: format(remainingClaims),
  recoverableRBTC: format(lateRecoverable),
  recoveryPercent: lateRecovery,
  haircutPercent: lateHaircut
};

assert.equal(snapshots.feeSharing.withdrawalEvents, 0);
assert.equal(snapshots.feeSharing.accumulationEvents, 136);
assert.equal(feeRecords.withheld.rbtcEvents + feeRecords.withheld.zusdEvents, snapshots.feeSharing.accumulationEvents);
assert.equal(feeRecords.withheld.rbtc, snapshots.feeSharing.withheldRBTC);
assert.equal(feeRecords.withheld.zusd, snapshots.feeSharing.withheldZUSD);
assert.equal(feeRecords.withheld.withdrawalEvents, snapshots.feeSharing.withdrawalEvents);
assert.equal(feeRecords.snapshot.owner.toLowerCase(), feeRecords.exchequer.toLowerCase());
assert.equal(feeRecords.snapshot.proxyOwner.toLowerCase(), feeRecords.exchequer.toLowerCase());
assert.equal(feeRecords.ownership.length, 3);
assert.equal(feeRecords.januaryFreeze.length, 5);
assert.equal(feeRecords.februaryWithholding.length, 7);
const feeTimeline = [...feeRecords.ownership, ...feeRecords.januaryFreeze, ...feeRecords.februaryWithholding];
assert.equal(new Set(feeTimeline.map((entry) => entry.tx)).size, 15, 'unique FeeSharing transaction spine');
assert.deepEqual(feeTimeline.map((entry) => entry.utc), feeTimeline.map((entry) => entry.utc).sort(), 'FeeSharing chronology is ordered');
for (const entry of feeTimeline) {
  assert.match(entry.tx, /^0x[0-9a-f]{64}$/i, `FeeSharing transaction at ${entry.utc}`);
  assert.ok(Number.isSafeInteger(entry.block) && entry.block > 0, `FeeSharing block at ${entry.utc}`);
}
const freezeExecution = feeRecords.januaryFreeze[2];
const revertExecution = feeRecords.januaryFreeze[4];
assert.equal((Date.parse(revertExecution.utc) - Date.parse(freezeExecution.utc)) / 1000, 44 * 3600 + 20 * 60 + 1, 'claim-freeze duration');
report.feeSharing = {
  accumulatedRBTC: snapshots.feeSharing.withheldRBTC,
  accumulatedZUSD: snapshots.feeSharing.withheldZUSD,
  withdrawalEvents: snapshots.feeSharing.withdrawalEvents,
  transactionSpineCount: feeTimeline.length,
  claimFreezeDuration: '44h20m01s'
};

assert.equal(bosLaunchpadSale.periods.length, 2, 'two completed BOS launchpad backend periods');
assert.ok(bosLaunchpadSale.periods.every((period) => period.isCompleted === true), 'BOS launchpad periods are complete');
assert.equal(bosLaunchpadSale.periods[0].end, bosLaunchpadSale.periods[1].start, 'BOS launchpad periods are contiguous');
const bosTokensSold = bosLaunchpadSale.periods.reduce(
  (total, period) => total + decimal(period.tokensSold, 8),
  0n
);
const bosUsdCollected = bosLaunchpadSale.periods.reduce(
  (total, period) => total + decimal(period.usdCollected, 20),
  0n
);
assert.equal(format(bosTokensSold, 8), bosLaunchpadSale.combined.tokensSold, 'combined BOS launchpad token allocations');
assert.equal(format(bosUsdCollected, 20), bosLaunchpadSale.combined.usdCollected, 'combined BOS launchpad usdCollected counter');
assert.deepEqual(
  bosLaunchpadSale.calculation.addendPhaseIds,
  bosLaunchpadSale.periods.map((period) => period.phaseId),
  'BOS launchpad calculation uses exactly the two backend periods'
);
assert.deepEqual(bosLaunchpadSale.calculation.excludedFromAddends, ['phaseOneSubset']);
report.bosLaunchpadSale = {
  completedBackendPeriods: bosLaunchpadSale.periods.length,
  tokensSold: format(bosTokensSold, 8),
  usdCollected: format(bosUsdCollected, 20),
  phaseOneSubsetDoubleCounted: false
};

if (process.argv.includes('--verify')) {
  process.stdout.write(`${JSON.stringify({ status: 'verified', ...report }, null, 2)}\n`);
} else {
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
