/**

 * Locked-policy parity checks for web_node (Node ESM).

 * Run: node --check js/*.js && node js/policy_tests.js

 */

import {

  Protocol,

  Payload,

  requiredConfirmations,

  confirmationDepth,

  remainingFinalitySeconds,

  effectiveBlockTimeSeconds,

  nextBlockTimestamp,

  vpsCycleSetAllowed,

} from "./protocol.js";

import { AccountState } from "./state.js";



let passed = 0;

let failed = 0;



function assert(cond, msg) {

  if (cond) {

    passed += 1;

    console.log("  ok  " + msg);

  } else {

    failed += 1;

    console.error("  FAIL " + msg);

  }

}



console.log("protocol confirmations / block time");

assert(effectiveBlockTimeSeconds(0) === 33, "block time at genesis is 33");

assert(effectiveBlockTimeSeconds(999999999) === 33, "block time stays 33 forever");

assert(nextBlockTimestamp(100) === 133, "next timestamp +33");

assert(requiredConfirmations(1, null) === 3, "transfer 1 unit needs 3");

assert(requiredConfirmations(33 * Protocol.SRMD_UNITS, null) === 3, "transfer 33 SRMD needs 3");

assert(requiredConfirmations(33 * Protocol.SRMD_UNITS + 1, null) === 6, "transfer 33 SRMD + 1 unit needs 6");

assert(requiredConfirmations(3333 * Protocol.SRMD_UNITS, null) === 6, "transfer 3333 SRMD needs 6");

assert(requiredConfirmations(3333 * Protocol.SRMD_UNITS + 1, null) === 9, "transfer >3333 SRMD needs 9");

assert(requiredConfirmations(0, Payload.encode(Payload.STAKE_CLAIM)) === 1, "contract needs 1");

assert(confirmationDepth(10, 10) === 1, "inclusion depth is 1");

assert(confirmationDepth(12, 10) === 3, "depth formula current-H+1");

assert(remainingFinalitySeconds(1, 3) === 66, "remaining time at 33s");

assert(Protocol.STAKING_CLAIM_DAYS === 29, "staking claim days 29");

assert(Protocol.ACTIVE_NODES_CLAIM_WINDOW_HOURS === 23, "activity window 23h");

assert(Protocol.INVITER_MIN_CLAIM === 10 * Protocol.SRMD_UNITS, "referral min claim 10 SRMD");
assert(Protocol.INVITER_CLAIM_WINDOW_HOURS === 7 * 24, "referral window 7d");

assert(Protocol.transferGasFee(10 * Protocol.SRMD_UNITS) === 3300, "10 SRMD send burns 0.0033 SRMD");
assert(Protocol.formatSrmd(3300) === "0.0033", "3300 units formats as 0.0033");
assert(Protocol.parseSrmd("10.5") === 10.5 * Protocol.SRMD_UNITS, "parse 10.5 SRMD");
assert(Protocol.transferGasFee(1) === 0, "1 unit floors to 0 gas");
assert(Protocol.transferGasFee(0) === 0, "zero amount is zero gas");



console.log("state: fee burn + referral/activity expiry");

{

  const s = new AccountState();

  s.updateBalance("A", 100);

  const ok = s.transfer({

    fromAddress: "A",

    toAddress: "B",

    amount: 10,

    gasFee: 1,

    signerPublicKey: "",

  });

  assert(ok, "transfer applies");

  assert(s.getBalance("A") === 89, "sender debited amount+gas");

  assert(s.getBalance("B") === 10, "receiver credited");

  assert(s.getBalance(Protocol.BURN_ADDRESS) === 1, "gas burned");



  s.inviterClaimable.set("I", 5);

  s.inviterClaimableUpdatedAt.set("I", 1000);

  s.trackedRewardAddresses.add("I");

  s.expireUnclaimedRewards(1000 + Protocol.INVITER_CLAIM_WINDOW_HOURS * 3600);

  assert(s.inviterClaimable.get("I") === 0, "referral claimable expires at 7d");

  assert(s.getBalance(Protocol.BURN_ADDRESS) === 6, "expired referral burned");



  s.setContractBalance(Protocol.ACTIVE_ADDRESS, 100);
  s.activityEpochStart = 1000;
  s.activityMinutes.set("A", 23 * 60); // 23 full hours
  s.activityMinutes.set("B", 5 * 60); // 5 full hours
  s.trackedRewardAddresses.add("A");
  s.trackedRewardAddresses.add("B");
  s.settleActivityEpochIfDue(1000 + Protocol.ACTIVE_NODES_CLAIM_WINDOW_HOURS * 3600);
  assert((s.activityMinutes.get("A") || 0) === 0, "hours reset after settle");
  assert((s.activityMinutes.get("B") || 0) === 0, "hours reset after settle");
  // shareValue = floor(100 / 28) = 3; A claimable 69, B claimable 15; remainder 16 stays
  assert((s.activityClaimable.get("A") || 0) === 69, "A claimable hours * shareValue");
  assert((s.activityClaimable.get("B") || 0) === 15, "B claimable hours * shareValue");
  assert(s.getBalance("A") === 89, "A balance unchanged until claim");
  assert(s.getBalance("B") === 10, "B balance unchanged until claim");
  assert(s.getContractBalance(Protocol.ACTIVE_ADDRESS) === 16, "remainder stays in ARSC");

  assert(s.activeClaim({
    fromAddress: "A",
    toAddress: Protocol.ACTIVE_ADDRESS,
    amount: 0,
    gasFee: 0,
  }, 1000 + Protocol.ACTIVE_NODES_CLAIM_WINDOW_HOURS * 3600), "A claims");
  assert(s.getBalance("A") === 89 + 69, "A balance after claim");
  assert((s.activityClaimable.get("A") || 0) === 0, "A claimable cleared");

  // Unclaimed B burns after 23h claim window
  s.expireUnclaimedRewards(
    1000 + Protocol.ACTIVE_NODES_CLAIM_WINDOW_HOURS * 3600 + Protocol.ACTIVE_NODES_CLAIM_WINDOW_HOURS * 3600
  );
  assert((s.activityClaimable.get("B") || 0) === 0, "B unclaimed burned");
  assert(s.getBalance(Protocol.BURN_ADDRESS) === 6 + 15, "expired referral + B burned");
}



console.log("state: staking materialize / expire / claim preserves shares");

{

  const s = new AccountState();

  const addr = "SRMD1STAKER0000000000000000000000000";

  s.stakeMatured.set(addr, true);

  s.stakingShares.set(addr, 10);

  s.maturedStakingSharesTotal = 10;

  s.trackedStakingAddresses.add(addr);

  // Seed pool so monthly_pool/share math yields a non-zero payout.

  s.setContractBalance(Protocol.STAKING_ADDRESS, 6000);

  // Materialize period 0 at its start (ts=0).

  s.materializeStakingRewards(0);

  const payout = s.stakingRewardClaimable.get(addr) || 0;

  assert(payout > 0, "monthly staking claimable materialized");

  assert(s.stakingRewardPeriod.get(addr) === 0, "period 0 set");

  assert(s.stakingShares.get(addr) === 10, "shares preserved after materialize");



  // Expire after 29 days; burn only the claimable payout.

  const poolBefore = s.getContractBalance(Protocol.STAKING_ADDRESS);

  const burnBefore = s.getBalance(Protocol.BURN_ADDRESS);

  s.expireUnclaimedRewards(Protocol.STAKING_CLAIM_DAYS * Protocol.SECONDS_PER_DAY);

  assert((s.stakingRewardClaimable.get(addr) || 0) === 0, "expired claimable cleared");

  assert(s.stakingShares.get(addr) === 10, "shares preserved after expiry burn");

  assert(

    s.getContractBalance(Protocol.STAKING_ADDRESS) === poolBefore - payout,

    "only payout burned from pool"

  );

  assert(s.getBalance(Protocol.BURN_ADDRESS) === burnBefore + payout, "payout burned to burn address");



  // Next period independent: advance into month 1.

  s.materializeStakingRewards(Protocol.STAKING_MONTH_SECONDS);

  assert(s.stakingRewardPeriod.get(addr) === 1, "next month period independent");

  assert((s.stakingRewardClaimable.get(addr) || 0) > 0, "next month claimable anew");



  // Claim path: charge gas 0, pay claimable, clear amount, keep shares.

  s.updateBalance(addr, 0);

  const claimPayout = s.stakingRewardClaimable.get(addr);

  const claimOk = s.stakeClaim(

    {

      fromAddress: addr,

      toAddress: Protocol.STAKING_ADDRESS,

      amount: 0,

      gasFee: 0,

      signerPublicKey: "",

    },

    Protocol.STAKING_MONTH_SECONDS + 60

  );

  assert(claimOk, "stake claim succeeds within window");

  assert(s.getBalance(addr) === claimPayout, "claimer received payout");

  assert((s.stakingRewardClaimable.get(addr) || 0) === 0, "claimable cleared after claim");

  assert(s.stakingShares.get(addr) === 10, "shares preserved after claim");

}



console.log("state: applyBlock order expires before reward refresh");

{

  const s = new AccountState();

  s.inviterClaimable.set("I", 3);

  s.inviterClaimableUpdatedAt.set("I", 0);

  s.trackedRewardAddresses.add("I");

  s.accounts.set("minerAddr", { publicKeyHex: "aa", inviterAddress: "I" });

  // Use a fake miner pubkey that won't resolve — inviter path burns when no miner.

  // Instead directly test expire at boundary via applyBlock with empty txs.

  const block = {

    header: { timestamp: Protocol.INVITER_CLAIM_WINDOW_HOURS * 3600, minerPublicKey: "genesis" },

    transactions: [],

  };

  assert(s.applyBlock(block, 1), "applyBlock ok");

  assert((s.inviterClaimable.get("I") || 0) === 0, "expire runs inside applyBlock");

}

console.log("referral 3 SRMD / 33 friends + producer bond height");
{
  const s = new AccountState();
  s.applyGenesis();
  const inviter = "SRMDINVITER0001";
  const poor = "SRMDPOOR0000001";
  const friend = "SRMDFRIEND00001";
  s.balances.set(inviter, Protocol.INVITER_MIN_BALANCE);
  s.balances.set(poor, Protocol.INVITER_MIN_BALANCE - 1);
  assert(s.canBindInviter(friend, inviter) === true, "inviter with 3 SRMD can refer");
  assert(s.canBindInviter(friend, poor) === false, "inviter under 3 SRMD cannot refer");
  assert(s.bindInviter(friend, inviter) === true, "first friend binds");
  assert((s.inviteeCount.get(inviter) || 0) === 1, "invitee count 1");
  assert(s.bindInviter(friend, inviter) === true, "repeat bind is no-op");
  assert((s.inviteeCount.get(inviter) || 0) === 1, "repeat does not increment");
  for (let i = 2; i <= 33; i++) {
    assert(s.bindInviter("SRMDFRIEND" + String(i).padStart(5, "0"), inviter), "friend " + i);
  }
  assert((s.inviteeCount.get(inviter) || 0) === 33, "cap 33 friends");
  assert(s.bindInviter("SRMDFRIEND00034", inviter) === false, "34th friend rejected");
  assert(Protocol.producerAdmissionRequired(3_333_332) === false, "free mining before 3333333");
  assert(Protocol.producerAdmissionRequired(3_333_333) === true, "bond required at 3333333");
  assert(s.producerAdmitted("SRMDMINER000001", 100) === true, "admitted while free era");
  assert(s.producerAdmitted("SRMDMINER000001", 3_333_333) === false, "not admitted without bond");
  const miner = "SRMDMINERBOND01";
  s.balances.set(miner, Protocol.PRODUCER_ADMISSION_BOND);
  assert(
    s.producerBond({
      fromAddress: miner,
      toAddress: Protocol.PRODUCER_BOND_ADDRESS,
      amount: Protocol.PRODUCER_ADMISSION_BOND,
      gasFee: 0,
    }) === true,
    "lock 3 SRMD bond"
  );
  assert(s.isProducerBonded(miner) === true, "bond active");
  assert(s.producerAdmitted(miner, 3_333_333) === true, "admitted after bond");
  assert(s.getBalance(miner) === 0, "bond is locked not spent to miner");
  const joiner = "SRMDJOINQUEUE01";
  s.balances.set(joiner, 0);
  assert(
    s.producerJoin({
      fromAddress: joiner,
      toAddress: Protocol.PRODUCER_BOND_ADDRESS,
      amount: 0,
      gasFee: 0,
    }) === true,
    "join queue is free"
  );
  assert(s.isProducerJoined(joiner) === true, "joined set");
  assert(s.chainProducerSet(100).includes(joiner), "free-era set is JOIN list");
  s.parkProducer(joiner);
  assert(s.isProducerParked(joiner) === true, "parked after offline skip");
  assert(s.chainProducerSet(100).includes(joiner) === false, "parked wallets leave the shuffle");
  assert(
    s.producerJoin({
      fromAddress: joiner,
      toAddress: Protocol.PRODUCER_BOND_ADDRESS,
      amount: 0,
      gasFee: 0,
    }) === true,
    "re-JOIN unparks"
  );
  assert(s.isProducerParked(joiner) === false, "JOIN clears park");
  assert(s.chainProducerSet(100).includes(joiner) === true, "unparked back in shuffle");
  assert(
    s.producerLeave({
      fromAddress: joiner,
      toAddress: Protocol.PRODUCER_BOND_ADDRESS,
      amount: 0,
      gasFee: 0,
    }) === true,
    "leave queue"
  );
  assert(s.isProducerJoined(joiner) === false, "left set");
  assert(vpsCycleSetAllowed(["A", "B"], ["A"]) === true, "VPS subset of chain set ok");
  assert(vpsCycleSetAllowed(["A"], ["A", "X"]) === false, "VPS cannot invent miners");
  assert(vpsCycleSetAllowed([], ["X"]) === true, "empty chain set allows bootstrap");
  assert(s.isProducerJoined(miner) === true, "bond also joins the queue");
  assert(s.chainProducerSet(3_333_333).includes(miner), "bond-era set is bonded wallets");
}

console.log(`\n${passed} passed, ${failed} failed`);

if (failed) process.exit(1);


