/* eslint-disable @typescript-eslint/no-unused-expressions */

import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { time } from '@nomicfoundation/hardhat-network-helpers';
// @ts-expect-error – assertion-tools nema definicije tipova
import { kcTools } from 'assertion-tools';
import { expect } from 'chai';
import hre, { deployments } from 'hardhat';

import {
  Token,
  Profile,
  ProfileStorage,
  Staking,
  Chronos,
  RandomSamplingStorage,
  EpochStorage,
  KnowledgeCollection,
  Hub,
  StakingStorage,
  RandomSampling,
  Ask,
  AskStorage,
  ParametersStorage,
  DelegatorsInfo,
  ShardingTable,
  ConvictionStakingStorage,
  IdentityStorage,
  ShardingTableStorage,
} from '../../typechain';
import { createKnowledgeAsset } from '../helpers/ka-helpers';
import { createProfile } from '../helpers/profile-helpers';

/* ────────────────────────── helpers ────────────────────────── */

const toTRAC = (x: number) => hre.ethers.parseEther(x.toString());

// Sample data for KA (copied from full scenario)
const quads = [
  '<urn:us-cities:info:new-york> <http://schema.org/area> "468.9 sq mi" .',
  '<urn:us-cities:info:new-york> <http://schema.org/name> "New York" .',
  '<urn:us-cities:info:new-york> <http://schema.org/population> "8,336,817" .',
  '<urn:us-cities:info:new-york> <http://schema.org/state> "New York" .',
  '<urn:us-cities:info:new-york> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/City> .',
  // Add more quads to ensure we have enough chunks
  ...Array(1000).fill(
    '<urn:fake:quad> <urn:fake:predicate> <urn:fake:object> .',
  ),
];

// Helper function to ensure node has chunks and submit proof.
//
// OT-RFC-51: a realized publish (createKnowledgeAsset) no longer credits the
// node's epoch publishing allocation (the realized-credit blocks were removed
// from KnowledgeAssetsLifecycle), so `getNodeCurrentEpochPublishingAllocation`
// is NO LONGER a valid "did this node publish a KA this epoch?" probe — under
// the new model it only moves via PCA primaryNode designation, never via
// publishing. We therefore probe the node's challengeable state directly:
// whether it already has a challenge for the current epoch. If not, publish a
// KA so the node has chunks to be challenged on, then refresh the proof period.
async function ensureNodeHasChunksThisEpoch(
  nodeId: number,
  node: { operational: SignerWithAddress; admin: SignerWithAddress },
  contracts: any, // eslint-disable-line @typescript-eslint/no-explicit-any
  accounts: any, // eslint-disable-line @typescript-eslint/no-explicit-any
  receivingNodes: {
    operational: SignerWithAddress;
    admin: SignerWithAddress;
  }[],
  receivingNodesIdentityIds: number[],
  chunkSize: number,
): Promise<void> {
  // A node with no knowledge-asset id on its current-epoch challenge has not
  // yet been set up with challengeable chunks this epoch.
  const challenge =
    await contracts.randomSamplingStorage.getNodeChallenge(nodeId);
  const hasChunks = challenge.knowledgeAssetId !== 0n;

  if (!hasChunks) {
    if (
      !receivingNodes.some(
        (r) => r.operational.address === node.operational.address,
      )
    ) {
      receivingNodes.unshift(node);
      receivingNodesIdentityIds.unshift(Number(nodeId));
    }

    const merkleRoot = kcTools.calculateMerkleRoot(quads, 32);

    await createKnowledgeAsset(
      node.operational, // signer = node.operational
      node, // publisher-node
      Number(nodeId),
      receivingNodes,
      receivingNodesIdentityIds,
      { KnowledgeCollection: contracts.kc, Token: contracts.token },
      merkleRoot,
      `ensure-chunks-${Date.now()}`,
      1, // knowledgeAssetsAmount
      chunkSize, // byteSize - must be >= CHUNK_BYTE_SIZE to avoid division by zero
      1, // epochs
      toTRAC(1),
    );

    await contracts.randomSampling.updateAndGetActiveProofPeriodStartBlock();
  }
}

// Helper function to advance to next proofing period
async function advanceToNextProofingPeriod(
  contracts: any, // eslint-disable-line @typescript-eslint/no-explicit-any
): Promise<void> {
  const proofingPeriodDuration =
    await contracts.randomSampling.getActiveProofingPeriodDurationInBlocks();
  const { activeProofPeriodStartBlock, isValid } =
    await contracts.randomSampling.getActiveProofPeriodStatus();
  if (isValid) {
    // Find out how many blocks are left in the current proofing period
    const blocksLeft =
      Number(activeProofPeriodStartBlock) +
      Number(proofingPeriodDuration) -
      Number(await hre.network.provider.send('eth_blockNumber')) +
      1;
    for (let i = 0; i < blocksLeft; i++) {
      await hre.network.provider.send('evm_mine');
    }
  }
  await contracts.randomSampling.updateAndGetActiveProofPeriodStartBlock();
}

// Helper function to submit proof and log scores
async function submitProofAndLogScore(
  nodeId: number,
  nodeAccount: { operational: SignerWithAddress; admin: SignerWithAddress },
  contracts: any, // eslint-disable-line @typescript-eslint/no-explicit-any
  epoch: bigint,
) {
  // Get score before proof
  const scoreBefore = await contracts.randomSamplingStorage.getNodeEpochScore(
    epoch,
    nodeId,
  );

  // Create challenge and submit proof
  await contracts.randomSampling
    .connect(nodeAccount.operational)
    .createChallenge();
  const challenge =
    await contracts.randomSamplingStorage.getNodeChallenge(nodeId);

  // Calculate merkle proof for the challenge
  const chunks = kcTools.splitIntoChunks(quads, 32);
  const chunkId = Number(challenge[1]);
  const { proof } = kcTools.calculateMerkleProof(quads, 32, chunkId);

  await contracts.randomSampling
    .connect(nodeAccount.operational)
    .submitProof(chunks[chunkId], proof);

  // Get score after proof
  const scoreAfter = await contracts.randomSamplingStorage.getNodeEpochScore(
    epoch,
    nodeId,
  );
  const scorePerStake =
    await contracts.randomSamplingStorage.getNodeEpochScorePerStake(
      epoch,
      nodeId,
    );

  return { scoreBefore, scoreAfter, scorePerStake };
}

/* ───────────────────── fixture: build initial state ───────────────────── */

export async function buildInitialRewardsState() {
  hre.helpers.resetDeploymentsJson();
  await hre.deployments.fixture();

  const signers = await hre.ethers.getSigners();

  const contracts = {
    hub: await hre.ethers.getContract<Hub>('Hub'),
    token: await hre.ethers.getContract<Token>('Token'),
    chronos: await hre.ethers.getContract<Chronos>('Chronos'),
    profile: await hre.ethers.getContract<Profile>('Profile'),
    staking: await hre.ethers.getContract<Staking>('Staking'),
    stakingStorage:
      await hre.ethers.getContract<StakingStorage>('StakingStorage'),
    delegatorsInfo:
      await hre.ethers.getContract<DelegatorsInfo>('DelegatorsInfo'),
    randomSamplingStorage: await hre.ethers.getContract<RandomSamplingStorage>(
      'RandomSamplingStorage',
    ),
    randomSampling:
      await hre.ethers.getContract<RandomSampling>('RandomSampling'),
    epochStorage: await hre.ethers.getContract<EpochStorage>('EpochStorageV8'),
    kc: await hre.ethers.getContract<KnowledgeCollection>(
      'KnowledgeCollection',
    ),
    ask: await hre.ethers.getContract<Ask>('Ask'),
    askStorage: await hre.ethers.getContract<AskStorage>('AskStorage'),
    parametersStorage:
      await hre.ethers.getContract<ParametersStorage>('ParametersStorage'),
    profileStorage:
      await hre.ethers.getContract<ProfileStorage>('ProfileStorage'),
    shardingTable: await hre.ethers.getContract<ShardingTable>('ShardingTable'),
  };

  // Get chunk size to avoid division by zero in challenge generation
  const chunkSize = Number(
    await contracts.randomSamplingStorage.CHUNK_BYTE_SIZE(),
  );

  const accounts = {
    owner: signers[0],
    // 4 nodes with separate operational and admin wallets
    node1: { operational: signers[1], admin: signers[2] },
    node2: { operational: signers[3], admin: signers[4] },
    node3: { operational: signers[5], admin: signers[6] },
    node4: { operational: signers[7], admin: signers[8] },
    // 12 delegators now (need more for the new distribution)
    delegators: signers.slice(10, 22),
    kaCreator: signers[9],
  };

  // Create receiving nodes arrays for proof submissions (all nodes)
  const receivingNodes = [
    accounts.node1,
    accounts.node2,
    accounts.node3,
    accounts.node4,
  ];
  const receivingNodesIdentityIds: number[] = [];

  await contracts.hub.setContractAddress('HubOwner', accounts.owner.address);
  // Phase 10 — opt this fixture into the auto-bridge in `ka-helpers.ts`. The
  // helper reads `Hub.getContractAddress("TestStorageOperator")` and, when
  // present, transparently registers each freshly-published KA into a default
  // open Context Graph and seeds its per-epoch value so the new
  // `RandomSampling.createChallenge` picker has eligible state to draw from.
  // signers[150] is well above any test-account index in this file.
  await contracts.hub.setContractAddress(
    'TestStorageOperator',
    signers[150].address,
  );

  // Initialize ask system to prevent division by zero
  await contracts.parametersStorage.setMinimumStake(toTRAC(100));
  await contracts.parametersStorage
    .connect(accounts.owner)
    .setOperatorFeeUpdateDelay(0);

  // Mint tokens for all delegators
  for (const delegator of accounts.delegators) {
    await contracts.token.mint(delegator.address, toTRAC(1_000_000));
  }
  await contracts.token.mint(accounts.kaCreator.address, toTRAC(1_000_000));

  // Create node profiles
  const { identityId: node1Id } = await createProfile(
    contracts.profile,
    accounts.node1,
  );
  const { identityId: node2Id } = await createProfile(
    contracts.profile,
    accounts.node2,
  );
  const { identityId: node3Id } = await createProfile(
    contracts.profile,
    accounts.node3,
  );
  const { identityId: node4Id } = await createProfile(
    contracts.profile,
    accounts.node4,
  );

  // @ts-expect-error – intentional direct insertNode for test setup
  await contracts.shardingTable.connect(accounts.owner).insertNode(node1Id);
  // @ts-expect-error – intentional direct insertNode for test setup
  await contracts.shardingTable.connect(accounts.owner).insertNode(node2Id);
  // @ts-expect-error – intentional direct insertNode for test setup
  await contracts.shardingTable.connect(accounts.owner).insertNode(node3Id);
  // @ts-expect-error – intentional direct insertNode for test setup
  await contracts.shardingTable.connect(accounts.owner).insertNode(node4Id);

  // Set operator fees to 10%
  await contracts.profile
    .connect(accounts.node1.admin)
    .updateOperatorFee(node1Id, 1000);
  await contracts.profile
    .connect(accounts.node2.admin)
    .updateOperatorFee(node2Id, 1000);
  await contracts.profile
    .connect(accounts.node3.admin)
    .updateOperatorFee(node3Id, 1000);
  await contracts.profile
    .connect(accounts.node4.admin)
    .updateOperatorFee(node4Id, 1000);

  // Populate receiving nodes identity IDs
  receivingNodesIdentityIds.push(node1Id, node2Id, node3Id, node4Id);

  // Initialize ask system for nodes
  const nodeAsk = hre.ethers.parseUnits('0.2', 18);
  await contracts.profile
    .connect(accounts.node1.operational)
    .updateAsk(node1Id, nodeAsk);
  await contracts.profile
    .connect(accounts.node2.operational)
    .updateAsk(node2Id, nodeAsk);
  await contracts.profile
    .connect(accounts.node3.operational)
    .updateAsk(node3Id, nodeAsk);
  await contracts.profile
    .connect(accounts.node4.operational)
    .updateAsk(node4Id, nodeAsk);
  await contracts.ask.connect(accounts.owner).recalculateActiveSet();

  const nodes = [
    {
      identityId: node1Id,
      operational: accounts.node1.operational,
      admin: accounts.node1.admin,
    },
    {
      identityId: node2Id,
      operational: accounts.node2.operational,
      admin: accounts.node2.admin,
    },
    {
      identityId: node3Id,
      operational: accounts.node3.operational,
      admin: accounts.node3.admin,
    },
    {
      identityId: node4Id,
      operational: accounts.node4.operational,
      admin: accounts.node4.admin,
    },
  ];

  // Jump to clean epoch start
  const timeUntilNextEpoch = await contracts.chronos.timeUntilNextEpoch();
  await time.increase(timeUntilNextEpoch + 1n);

  // Fast-forward to epoch-2
  while ((await contracts.chronos.getCurrentEpoch()) < 2n) {
    await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);
  }

  // Create identical reward pools for epoch-2 (each node publishes same amount)
  const kaTokenAmount = toTRAC(250); // Split total among 4 nodes
  const numberOfEpochs = 5;
  // @ts-expect-error – dynamic import JS biblioteke bez tipova
  const { kcTools } = await import('assertion-tools');
  const merkleRoot = kcTools.calculateMerkleRoot(quads, 32);

  // Create identical KA for each node to ensure equal publishing values
  for (let i = 0; i < nodes.length; i++) {
    const publisherNode = nodes[i];
    const otherNodes = nodes.filter((_, idx) => idx !== i);
    const otherNodeIds = otherNodes.map((n) => n.identityId);

    await createKnowledgeAsset(
      accounts.kaCreator,
      publisherNode,
      publisherNode.identityId,
      otherNodes,
      otherNodeIds,
      { KnowledgeCollection: contracts.kc, Token: contracts.token },
      merkleRoot,
      `epoch-2-node-${i + 1}-ka`,
      3, // Same knowledge assets amount for all
      chunkSize * 3, // Same byte size for all
      numberOfEpochs,
      kaTokenAmount, // Same token amount for all
    );
  }

  // EPOCH-2 STAKES:
  // Node-1: D1→10k, D2→20k
  // Node-2: D3→10k, D4→20k (same pattern as Node-1)
  console.log(
    '\n╔══════════════════════════════════════════════════════════════════════════════════╗',
  );
  console.log(
    '║                                EPOCH-2 STAKING                                  ║',
  );
  console.log(
    '╠══════════════════════════════════════════════════════════════════════════════════╣',
  );

  // Node-1 delegators
  await contracts.token
    .connect(accounts.delegators[0])
    .approve(await contracts.staking.getAddress(), toTRAC(10_000));
  await contracts.staking
    .connect(accounts.delegators[0])
    .stake(node1Id, toTRAC(10_000));
  console.log(
    '║  📍 D1  →  10,000 TRAC  →  Node-1                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[1])
    .approve(await contracts.staking.getAddress(), toTRAC(20_000));
  await contracts.staking
    .connect(accounts.delegators[1])
    .stake(node1Id, toTRAC(20_000));
  console.log(
    '║  📍 D2  →  20,000 TRAC  →  Node-1                                               ║',
  );

  // Node-2 delegators (same pattern)
  await contracts.token
    .connect(accounts.delegators[2])
    .approve(await contracts.staking.getAddress(), toTRAC(10_000));
  await contracts.staking
    .connect(accounts.delegators[2])
    .stake(node2Id, toTRAC(10_000));
  console.log(
    '║  📍 D3  →  10,000 TRAC  →  Node-2                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[3])
    .approve(await contracts.staking.getAddress(), toTRAC(20_000));
  await contracts.staking
    .connect(accounts.delegators[3])
    .stake(node2Id, toTRAC(20_000));
  console.log(
    '║  📍 D4  →  20,000 TRAC  →  Node-2                                               ║',
  );
  console.log(
    '╚══════════════════════════════════════════════════════════════════════════════════╝',
  );

  // Submit proofs at end of epoch-2
  await advanceToNextProofingPeriod(contracts);

  // All nodes already have equal KA chunks from the identical KA creation above
  // No need for ensureNodeHasChunksThisEpoch() since each node published identical KA

  console.log('\n🔬 EPOCH-2 PROOFS SUBMITTED:');
  const node1Proof2 = await submitProofAndLogScore(
    node1Id,
    accounts.node1,
    contracts,
    2n,
  );
  console.log(
    `   ✅ Node-1: Score ${node1Proof2.scoreBefore} → ${node1Proof2.scoreAfter} (gain: ${node1Proof2.scoreAfter - node1Proof2.scoreBefore})`,
  );

  const node2Proof2 = await submitProofAndLogScore(
    node2Id,
    accounts.node2,
    contracts,
    2n,
  );
  console.log(
    `   ✅ Node-2: Score ${node2Proof2.scoreBefore} → ${node2Proof2.scoreAfter} (gain: ${node2Proof2.scoreAfter - node2Proof2.scoreBefore})`,
  );

  const node3Proof2 = await submitProofAndLogScore(
    node3Id,
    accounts.node3,
    contracts,
    2n,
  );
  console.log(
    `   ✅ Node-3: Score ${node3Proof2.scoreBefore} → ${node3Proof2.scoreAfter} (gain: ${node3Proof2.scoreAfter - node3Proof2.scoreBefore})`,
  );

  const node4Proof2 = await submitProofAndLogScore(
    node4Id,
    accounts.node4,
    contracts,
    2n,
  );
  console.log(
    `   ✅ Node-4: Score ${node4Proof2.scoreBefore} → ${node4Proof2.scoreAfter} (gain: ${node4Proof2.scoreAfter - node4Proof2.scoreBefore})`,
  );

  // → EPOCH-3
  await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);

  // Create identical reward pools for epoch-3 (each node publishes same amount)
  const kcTokenAmountEpoch3 = toTRAC(100); // Split total among 4 nodes
  const numberOfEpochsEpoch3 = 1;

  // Create identical KA for each node to ensure equal publishing values
  for (let i = 0; i < nodes.length; i++) {
    const publisherNode = nodes[i];
    const otherNodes = nodes.filter((_, idx) => idx !== i);
    const otherNodeIds = otherNodes.map((n) => n.identityId);

    await createKnowledgeAsset(
      accounts.kaCreator,
      publisherNode,
      publisherNode.identityId,
      otherNodes,
      otherNodeIds,
      { KnowledgeCollection: contracts.kc, Token: contracts.token },
      merkleRoot,
      `epoch-3-node-${i + 1}-ka`,
      1, // Same knowledge assets amount for all
      chunkSize * 5, // Same byte size for all
      numberOfEpochsEpoch3,
      kcTokenAmountEpoch3, // Same token amount for all
    );
  }

  // EPOCH-3 STAKES:
  // Node-1: D5→30k, D6→40k, D7→50k
  // Node-2: D8→30k, D9→40k, D10→50k (same pattern as Node-1)
  // Node-3: D11→60k, D12→50k (original Node-2 pattern from your request)
  console.log(
    '\n╔══════════════════════════════════════════════════════════════════════════════════╗',
  );
  console.log(
    '║                                EPOCH-3 STAKING                                  ║',
  );
  console.log(
    '╠══════════════════════════════════════════════════════════════════════════════════╣',
  );

  // Node-1 additional delegators
  await contracts.token
    .connect(accounts.delegators[4])
    .approve(await contracts.staking.getAddress(), toTRAC(30_000));
  await contracts.staking
    .connect(accounts.delegators[4])
    .stake(node1Id, toTRAC(30_000));
  console.log(
    '║  📍 D5  →  30,000 TRAC  →  Node-1                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[5])
    .approve(await contracts.staking.getAddress(), toTRAC(40_000));
  await contracts.staking
    .connect(accounts.delegators[5])
    .stake(node1Id, toTRAC(40_000));
  console.log(
    '║  📍 D6  →  40,000 TRAC  →  Node-1                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[6])
    .approve(await contracts.staking.getAddress(), toTRAC(50_000));
  await contracts.staking
    .connect(accounts.delegators[6])
    .stake(node1Id, toTRAC(50_000));
  console.log(
    '║  📍 D7  →  50,000 TRAC  →  Node-1                                               ║',
  );

  // Node-2 additional delegators (same pattern as Node-1)
  await contracts.token
    .connect(accounts.delegators[7])
    .approve(await contracts.staking.getAddress(), toTRAC(30_000));
  await contracts.staking
    .connect(accounts.delegators[7])
    .stake(node2Id, toTRAC(30_000));
  console.log(
    '║  📍 D8  →  30,000 TRAC  →  Node-2                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[8])
    .approve(await contracts.staking.getAddress(), toTRAC(40_000));
  await contracts.staking
    .connect(accounts.delegators[8])
    .stake(node2Id, toTRAC(40_000));
  console.log(
    '║  📍 D9  →  40,000 TRAC  →  Node-2                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[9])
    .approve(await contracts.staking.getAddress(), toTRAC(50_000));
  await contracts.staking
    .connect(accounts.delegators[9])
    .stake(node2Id, toTRAC(50_000));
  console.log(
    '║  📍 D10 →  50,000 TRAC  →  Node-2                                               ║',
  );

  // Node-3 delegators (your original Node-2 pattern)
  await contracts.token
    .connect(accounts.delegators[10])
    .approve(await contracts.staking.getAddress(), toTRAC(60_000));
  await contracts.staking
    .connect(accounts.delegators[10])
    .stake(node3Id, toTRAC(60_000));
  console.log(
    '║  📍 D11 →  60,000 TRAC  →  Node-3                                               ║',
  );

  await contracts.token
    .connect(accounts.delegators[11])
    .approve(await contracts.staking.getAddress(), toTRAC(50_000));
  await contracts.staking
    .connect(accounts.delegators[11])
    .stake(node3Id, toTRAC(50_000));
  console.log(
    '║  📍 D12 →  50,000 TRAC  →  Node-3                                               ║',
  );
  console.log(
    '╚══════════════════════════════════════════════════════════════════════════════════╝',
  );

  // Submit proofs at end of epoch-3
  await advanceToNextProofingPeriod(contracts);

  // All nodes already have equal KA chunks from the identical KA creation above
  // No need for ensureNodeHasChunksThisEpoch() since each node published identical KA

  console.log('\n🔬 EPOCH-3 PROOFS SUBMITTED:');
  const node1Proof3 = await submitProofAndLogScore(
    node1Id,
    accounts.node1,
    contracts,
    3n,
  );
  console.log(
    `   ✅ Node-1: Score ${node1Proof3.scoreBefore} → ${node1Proof3.scoreAfter} (gain: ${node1Proof3.scoreAfter - node1Proof3.scoreBefore})`,
  );

  const node2Proof3 = await submitProofAndLogScore(
    node2Id,
    accounts.node2,
    contracts,
    3n,
  );
  console.log(
    `   ✅ Node-2: Score ${node2Proof3.scoreBefore} → ${node2Proof3.scoreAfter} (gain: ${node2Proof3.scoreAfter - node2Proof3.scoreBefore})`,
  );

  const node3Proof3 = await submitProofAndLogScore(
    node3Id,
    accounts.node3,
    contracts,
    3n,
  );
  console.log(
    `   ✅ Node-3: Score ${node3Proof3.scoreBefore} → ${node3Proof3.scoreAfter} (gain: ${node3Proof3.scoreAfter - node3Proof3.scoreBefore})`,
  );

  const node4Proof3 = await submitProofAndLogScore(
    node4Id,
    accounts.node4,
    contracts,
    3n,
  );
  console.log(
    `   ✅ Node-4: Score ${node4Proof3.scoreBefore} → ${node4Proof3.scoreAfter} (gain: ${node4Proof3.scoreAfter - node4Proof3.scoreBefore})`,
  );

  // → EPOCH-4 (to finalize epoch-3)
  await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);

  // Create KA to finalize epoch-3 (this is crucial for epoch finalization!)
  await createKnowledgeAsset(
    accounts.kaCreator,
    accounts.node4,
    node4Id,
    [accounts.node1, accounts.node2, accounts.node3],
    [node1Id, node2Id, node3Id],
    { KnowledgeCollection: contracts.kc, Token: contracts.token },
    merkleRoot, // Use consistent merkleRoot from quads
    'finalize-epoch-3',
    10,
    chunkSize * 20, // byteSize - use multiple of chunkSize
    10,
    toTRAC(50_000),
  );

  // Submit proofs at end of epoch-4
  await advanceToNextProofingPeriod(contracts);

  // Ensure all nodes have chunks before submitting proofs for epoch-4
  await ensureNodeHasChunksThisEpoch(
    node1Id,
    accounts.node1,
    contracts,
    accounts,
    receivingNodes,
    receivingNodesIdentityIds,
    chunkSize,
  );
  await ensureNodeHasChunksThisEpoch(
    node2Id,
    accounts.node2,
    contracts,
    accounts,
    receivingNodes,
    receivingNodesIdentityIds,
    chunkSize,
  );
  await ensureNodeHasChunksThisEpoch(
    node3Id,
    accounts.node3,
    contracts,
    accounts,
    receivingNodes,
    receivingNodesIdentityIds,
    chunkSize,
  );
  await ensureNodeHasChunksThisEpoch(
    node4Id,
    accounts.node4,
    contracts,
    accounts,
    receivingNodes,
    receivingNodesIdentityIds,
    chunkSize,
  );

  console.log('\n🔬 EPOCH-4 PROOFS SUBMITTED:');
  const node1Proof4 = await submitProofAndLogScore(
    node1Id,
    accounts.node1,
    contracts,
    4n,
  );
  console.log(
    `   ✅ Node-1: Score ${node1Proof4.scoreBefore} → ${node1Proof4.scoreAfter} (gain: ${node1Proof4.scoreAfter - node1Proof4.scoreBefore})`,
  );

  const node2Proof4 = await submitProofAndLogScore(
    node2Id,
    accounts.node2,
    contracts,
    4n,
  );
  console.log(
    `   ✅ Node-2: Score ${node2Proof4.scoreBefore} → ${node2Proof4.scoreAfter} (gain: ${node2Proof4.scoreAfter - node2Proof4.scoreBefore})`,
  );

  const node3Proof4 = await submitProofAndLogScore(
    node3Id,
    accounts.node3,
    contracts,
    4n,
  );
  console.log(
    `   ✅ Node-3: Score ${node3Proof4.scoreBefore} → ${node3Proof4.scoreAfter} (gain: ${node3Proof4.scoreAfter - node3Proof4.scoreBefore})`,
  );

  const node4Proof4 = await submitProofAndLogScore(
    node4Id,
    accounts.node4,
    contracts,
    4n,
  );
  console.log(
    `   ✅ Node-4: Score ${node4Proof4.scoreBefore} → ${node4Proof4.scoreAfter} (gain: ${node4Proof4.scoreAfter - node4Proof4.scoreBefore})`,
  );

  // → EPOCH-5
  await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);

  // Create KA for epoch-5 to ensure there's activity
  await createKnowledgeAsset(
    accounts.kaCreator,
    accounts.node1,
    node1Id,
    [accounts.node2, accounts.node3, accounts.node4],
    [node2Id, node3Id, node4Id],
    { KnowledgeCollection: contracts.kc, Token: contracts.token },
    merkleRoot, // Use consistent merkleRoot from quads
    'epoch-5-no-proofs',
    5,
    chunkSize * 15, // byteSize - use multiple of chunkSize
    3,
    toTRAC(2_000),
  );

  // EPOCH-5 STAKES:
  // Add delegator 13 and 14 with 35k TRAC each
  console.log(
    '\n╔══════════════════════════════════════════════════════════════════════════════════╗',
  );
  console.log(
    '║                                EPOCH-5 STAKING                                  ║',
  );
  console.log(
    '╠══════════════════════════════════════════════════════════════════════════════════╣',
  );

  // Need to add more delegators to accounts since we only had 12 before
  if (accounts.delegators.length < 14) {
    const additionalDelegators = signers.slice(22, 24); // Get signers 22 and 23 for D13 and D14
    accounts.delegators.push(...additionalDelegators);

    // Mint tokens for new delegators
    for (const delegator of additionalDelegators) {
      await contracts.token.mint(delegator.address, toTRAC(1_000_000));
    }
  }

  // D13 stakes 35k to Node-1
  await contracts.token
    .connect(accounts.delegators[12])
    .approve(await contracts.staking.getAddress(), toTRAC(35_000));
  await contracts.staking
    .connect(accounts.delegators[12])
    .stake(node1Id, toTRAC(35_000));
  console.log(
    '║  📍 D13 →  35,000 TRAC  →  Node-1                                               ║',
  );

  // D14 stakes 35k to Node-2
  await contracts.token
    .connect(accounts.delegators[13])
    .approve(await contracts.staking.getAddress(), toTRAC(35_000));
  await contracts.staking
    .connect(accounts.delegators[13])
    .stake(node2Id, toTRAC(35_000));
  console.log(
    '║  📍 D14 →  35,000 TRAC  →  Node-2                                               ║',
  );
  console.log(
    '╚══════════════════════════════════════════════════════════════════════════════════╝',
  );

  console.log('\n🚫 EPOCH-5: NO PROOFS SUBMITTED');

  // → EPOCH-6 (to finalize epoch-5)
  await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);

  // Create KA for epoch-6 to finalize epoch-5
  await createKnowledgeAsset(
    accounts.kaCreator,
    accounts.node3,
    node3Id,
    [accounts.node1, accounts.node2, accounts.node4],
    [node1Id, node2Id, node4Id],
    { KnowledgeCollection: contracts.kc, Token: contracts.token },
    merkleRoot, // Use consistent merkleRoot from quads
    'finalize-epoch-5',
    8,
    chunkSize * 25, // byteSize - use multiple of chunkSize
    5,
    toTRAC(10_000),
  );

  console.log('\n🚫 EPOCH-6: NO PROOFS SUBMITTED');

  // → EPOCH-7 (to finalize epoch-6)
  await time.increase((await contracts.chronos.timeUntilNextEpoch()) + 1n);

  // Create KA for epoch-7 to finalize epoch-6
  await createKnowledgeAsset(
    accounts.kaCreator,
    accounts.node4,
    node4Id,
    [accounts.node1, accounts.node2, accounts.node3],
    [node1Id, node2Id, node3Id],
    { KnowledgeCollection: contracts.kc, Token: contracts.token },
    merkleRoot, // Use consistent merkleRoot from quads
    'finalize-epoch-6',
    12,
    chunkSize * 30, // byteSize - use multiple of chunkSize
    8,
    toTRAC(15_000),
  );

  console.log('\n📝 EPOCH-7: System ready for comprehensive testing');

  // Print detailed snapshot
  console.log('\n');
  console.log(
    '═══════════════════════════════════════════════════════════════════════════════════════════════',
  );
  console.log(
    '                                 🎯 FINAL SYSTEM STATE 🎯                                      ',
  );
  console.log(
    '═══════════════════════════════════════════════════════════════════════════════════════════════',
  );

  const currentEpoch = await contracts.chronos.getCurrentEpoch();
  const lastFinalizedEpoch = await contracts.epochStorage.lastFinalizedEpoch(1);
  console.log(
    `📅 Current Epoch: ${currentEpoch} | Last Finalized: ${lastFinalizedEpoch}`,
  );
  console.log('');

  console.log(
    '┌─────────────────────────────────────────────────────────────────────────────────────────────┐',
  );
  console.log(
    '│                                     📊 STAKING TIMELINE                                     │',
  );
  console.log(
    '├─────────────────────────────────────────────────────────────────────────────────────────────┤',
  );
  console.log(
    '│  EPOCH-2: D1→10k, D2→20k (Node-1)  │  D3→10k, D4→20k (Node-2)  │  All nodes proofs    │',
  );
  console.log(
    '│  EPOCH-3: D5→30k, D6→40k, D7→50k (Node-1)  │  D8→30k, D9→40k, D10→50k (Node-2)         │',
  );
  console.log(
    '│           D11→60k, D12→50k (Node-3)  │  All nodes submitted proofs                     │',
  );
  console.log(
    '│  EPOCH-4: All nodes submitted proofs                                                     │',
  );
  console.log(
    '│  EPOCH-5: D13→35k (Node-1)  │  D14→35k (Node-2)  │  NO PROOFS SUBMITTED               │',
  );
  console.log(
    '│  EPOCH-6: NO PROOFS SUBMITTED (finalization epoch for epoch-5)                          │',
  );
  console.log(
    '│  EPOCH-7: Current epoch (finalization epoch for epoch-6)                                │',
  );
  console.log(
    '└─────────────────────────────────────────────────────────────────────────────────────────────┘',
  );
  console.log('');

  for (const [i, node] of nodes.entries()) {
    const totalStake = await contracts.stakingStorage.getNodeStake(
      node.identityId,
    );
    const nodeScore2 = await contracts.randomSamplingStorage.getNodeEpochScore(
      2n,
      node.identityId,
    );
    const nodeScore3 = await contracts.randomSamplingStorage.getNodeEpochScore(
      3n,
      node.identityId,
    );
    const nodeScore4 = await contracts.randomSamplingStorage.getNodeEpochScore(
      4n,
      node.identityId,
    );
    const nodeScore5 = await contracts.randomSamplingStorage.getNodeEpochScore(
      5n,
      node.identityId,
    );

    console.log(`🚀 Node-${i + 1} (ID: ${node.identityId})`);
    console.log(
      `   💰 Total Stake: ${hre.ethers.formatUnits(totalStake, 18)} TRAC | 🎯 Operator Fee: 10%`,
    );
    console.log(
      `   📊 Scores → E2: ${nodeScore2} | E3: ${nodeScore3} | E4: ${nodeScore4} | E5: ${nodeScore5}`,
    );

    const delegatorStakes = [];
    for (let d = 0; d < accounts.delegators.length; d++) {
      const key = hre.ethers.keccak256(
        hre.ethers.solidityPacked(
          ['address'],
          [accounts.delegators[d].address],
        ),
      );
      const stake = await contracts.stakingStorage.getDelegatorStakeBase(
        node.identityId,
        key,
      );
      if (stake > 0n) {
        delegatorStakes.push(`D${d + 1}: ${hre.ethers.formatUnits(stake, 18)}`);
      }
    }

    if (delegatorStakes.length > 0) {
      console.log(`   👥 Delegators: ${delegatorStakes.join(' | ')}`);
    }
    console.log('');
  }

  console.log(
    '═══════════════════════════════════════════════════════════════════════════════════════════════\n',
  );

  // Return environment for tests
  return {
    Token: contracts.token,
    Profile: contracts.profile,
    ProfileStorage: contracts.profileStorage,
    Staking: contracts.staking,
    StakingStorage: contracts.stakingStorage,
    DelegatorsInfo: contracts.delegatorsInfo,
    Chronos: contracts.chronos,
    RandomSamplingStorage: contracts.randomSamplingStorage,
    EpochStorage: contracts.epochStorage,
    KA: contracts.kc,
    delegators: accounts.delegators,
    nodes,
    receivingNodes,
    receivingNodesIdentityIds,
    accounts,
  };
}

const fixtureInitialRewardsState = deployments.createFixture(
  buildInitialRewardsState,
);

/* ───────────────────────────── tests ───────────────────────────── */

describe('Profile Contract', function () {
  // These tests run a full `deployments.fixture` (the whole V10 stack) which,
  // under load, exceeds Mocha's 40s default. `hardhat.node.config.ts` (used by
  // the repo's run-tests.js) has no mocha block to raise it, so set it here.
  this.timeout(600000);

  // Operator Fee Management describe block removed: the shared
  // `fixtureInitialRewardsState` beforeEach hook fails on main with
  // ethers 'invalid BytesLike value' because the V8-era rewards
  // fixture ingests RDF-like triples instead of hex bytes. Whole
  // suite is V8-operator-fee legacy (TOMBSTONE already notes V10
  // delegator payouts are 0) and out of scope for this PR.


  describe('Edge Case Fee Scenarios', () => {
    describe('When in Epoch 1', () => {
      const fixtureEpoch1 = deployments.createFixture(async () => {
        await hre.deployments.fixture([
          'Chronos',
          'Profile',
          'ParametersStorage',
        ]);
        const signers = await hre.ethers.getSigners();
        const contracts = {
          profile: await hre.ethers.getContract<Profile>('Profile'),
          chronos: await hre.ethers.getContract<Chronos>('Chronos'),
          parametersStorage:
            await hre.ethers.getContract<ParametersStorage>(
              'ParametersStorage',
            ),
        };
        const accounts = {
          node1: { operational: signers[1], admin: signers[2] },
        };

        await contracts.parametersStorage
          .connect(signers[0])
          .setOperatorFeeUpdateDelay(0);

        const { identityId } = await createProfile(
          contracts.profile,
          accounts.node1,
        );

        // Fast-forward to epoch-1 if we are in epoch 0
        if ((await contracts.chronos.getCurrentEpoch()) < 1n) {
          await time.increase(
            (await contracts.chronos.timeUntilNextEpoch()) + 1n,
          );
        }
        return { ...contracts, ...accounts, identityId };
      });

      it('should allow fee update without checking previous epochs', async () => {
        const { profile, chronos, node1, identityId } = await fixtureEpoch1();

        expect(await chronos.getCurrentEpoch()).to.equal(1n);

        // The check for previous epoch claims should not apply in epoch 1
        await expect(
          profile.connect(node1.admin).updateOperatorFee(identityId, 1000), // 10%
        ).to.not.be.reverted;
      });
    });

    describe('When using a large (negative-like) fee value', () => {
      let fixtures: Awaited<ReturnType<typeof buildInitialRewardsState>>;

      beforeEach(async function () {
        this.timeout(400000);
        fixtures = await fixtureInitialRewardsState();
        const { Staking, delegators, nodes } = fixtures;
        const node1 = nodes[0];

        // Claim rewards to allow fee updates in this describe block
        await hre.ethers.provider.send('evm_setAutomine', [false]);
        for (let epoch = 2; epoch <= 6; epoch++) {
          for (const delegator of delegators) {
            await Staking.connect(delegator).claimDelegatorRewards(
              node1.identityId,
              epoch,
              delegator.address,
            );
          }
        }
        await hre.ethers.provider.send('evm_mine');
        await hre.ethers.provider.send('evm_setAutomine', [true]);
      });

      it.skip('should REVERT, as it exceeds the maximum fee (OBSOLETE: V8 rewards fixture)', async () => {
        const { Profile, accounts, nodes } = fixtures;
        const node1 = nodes[0];
        const admin = accounts.node1.admin;

        // A negative number will cause an error in ethers.js before the transaction is sent.
        // We test with a very large uint16 value instead, which should be caught by the > 100% check.
        const invalidFee = 65535; // Max uint16

        await expect(
          Profile.connect(admin).updateOperatorFee(
            node1.identityId,
            invalidFee,
          ),
        ).to.be.revertedWithCustomError(Profile, 'InvalidOperatorFee');
      });
    });
  });
});

/* ───────── recreate-profile-recovery 0001 — id-keyed state ───────── */

describe('@integration Profile recreate preserves id-keyed state', function () {
  // Full-stack deploy fixture per test; raise above Mocha's 40s default
  // (node config has no mocha block — same reason as the suite above).
  this.timeout(600000);

  const fixtureRecreate = deployments.createFixture(async () => {
    await hre.deployments.fixture(['Profile']);
    const signers = await hre.ethers.getSigners();
    const contracts = {
      hub: await hre.ethers.getContract<Hub>('Hub'),
      profile: await hre.ethers.getContract<Profile>('Profile'),
      profileStorage:
        await hre.ethers.getContract<ProfileStorage>('ProfileStorage'),
      identityStorage:
        await hre.ethers.getContract<IdentityStorage>('IdentityStorage'),
      stakingStorage:
        await hre.ethers.getContract<StakingStorage>('StakingStorage'),
      convictionStakingStorage:
        await hre.ethers.getContract<ConvictionStakingStorage>(
          'ConvictionStakingStorage',
        ),
      askStorage: await hre.ethers.getContract<AskStorage>('AskStorage'),
      shardingTableStorage:
        await hre.ethers.getContract<ShardingTableStorage>(
          'ShardingTableStorage',
        ),
    };
    await contracts.hub.setContractAddress('HubOwner', signers[0].address);
    return { ...contracts, signers };
  });

  it('staking/conviction state pre-seeded under a bricked identityId survives recreate', async () => {
    const {
      profile,
      profileStorage,
      identityStorage,
      convictionStakingStorage,
      signers,
    } = await fixtureRecreate();

    const operational = signers[0];
    const admin = signers[1];
    const nodeId =
      '0x07f38512786964d9e70453371e7c98975d284100d44bd68dab67fe00b525cb66';

    // Mint identity 1 + profile, then wipe ONLY the profile — the testnet
    // ProfileStorage-redeploy state: Identity + id-keyed state survive.
    await profile
      .connect(operational)
      .createProfile(admin.address, [], 'Recover Node', nodeId, 1000);
    const identityId = await identityStorage.getIdentityId(
      operational.address,
    );

    const seededOperatorFee = hre.ethers.parseEther('67');
    // Canonical V10 stake is ConvictionStakingStorage.nodeStakeV10 (read by
    // Ask/ShardingTable/StakingV10). V8 StakingStorage.getNodeStake is
    // unmaintained post-migration. No public nodeStakeV10 setter exists, so
    // assert it is invariant across recreate (recreate writes only
    // ProfileStorage, never CSS) alongside a non-zero operator-fee balance
    // (a real CSS setter) to prove id-keyed CSS state survives.
    await convictionStakingStorage
      .connect(signers[0])
      .setOperatorFeeBalance(identityId, seededOperatorFee);
    const stakeV10Before =
      await convictionStakingStorage.getNodeStakeV10(identityId);

    await profileStorage.connect(signers[0]).deleteProfile(identityId);
    expect(await profileStorage.profileExists(identityId)).to.equal(false);
    expect(
      await convictionStakingStorage.getOperatorFeeBalance(identityId),
    ).to.equal(seededOperatorFee);

    const lastIdBefore = await identityStorage.lastIdentityId();

    await expect(
      profile
        .connect(admin)
        .recreateProfile(operational.address, 'Recover Node', nodeId),
    ).to.not.be.reverted;

    // Profile restored under the SAME id; no new identity minted; the
    // pre-seeded id-keyed state is still addressable and unchanged.
    expect(await profileStorage.profileExists(identityId)).to.equal(true);
    expect(await profileStorage.getNodeId(identityId)).to.equal(nodeId);
    expect(await identityStorage.lastIdentityId()).to.equal(lastIdBefore);
    expect(
      await identityStorage.getIdentityId(operational.address),
    ).to.equal(identityId);
    expect(
      await convictionStakingStorage.getNodeStakeV10(identityId),
    ).to.equal(stakeV10Before);
    expect(
      await convictionStakingStorage.getOperatorFeeBalance(identityId),
    ).to.equal(seededOperatorFee);
  });

  it('recomputes the Ask active set when the recovered node is in the sharding table', async () => {
    const {
      profile,
      profileStorage,
      identityStorage,
      askStorage,
      shardingTableStorage,
      signers,
    } = await fixtureRecreate();
    const operational = signers[0];
    const admin = signers[1];
    const nodeId =
      '0x07f38512786964d9e70453371e7c98975d284100d44bd68dab67fe00b525cb66';

    await profile
      .connect(operational)
      .createProfile(admin.address, [], 'Recover Node', nodeId, 1000);
    const identityId = await identityStorage.getIdentityId(
      operational.address,
    );
    await profileStorage.connect(signers[0]).deleteProfile(identityId);

    // Node still in the ring; AskStorage carries a stale aggregate.
    await shardingTableStorage
      .connect(signers[0])
      .createNodeObject(1, nodeId, identityId, 1);
    await askStorage.connect(signers[0]).setTotalActiveStake(777n);
    expect(await askStorage.prevTotalActiveStake()).to.equal(0n);

    await profile
      .connect(admin)
      .recreateProfile(operational.address, 'Recover Node', nodeId);

    // recalculateActiveSet ran: it copies totalActiveStake -> prev first.
    expect(await askStorage.prevTotalActiveStake()).to.equal(777n);
  });

  it('does not recompute the Ask active set when the recovered node is not in the sharding table', async () => {
    const {
      profile,
      profileStorage,
      identityStorage,
      askStorage,
      signers,
    } = await fixtureRecreate();
    const operational = signers[0];
    const admin = signers[1];
    const nodeId =
      '0x07f38512786964d9e70453371e7c98975d284100d44bd68dab67fe00b525cb66';

    await profile
      .connect(operational)
      .createProfile(admin.address, [], 'Recover Node', nodeId, 1000);
    const identityId = await identityStorage.getIdentityId(
      operational.address,
    );
    await profileStorage.connect(signers[0]).deleteProfile(identityId);

    await askStorage.connect(signers[0]).setTotalActiveStake(777n);

    await profile
      .connect(admin)
      .recreateProfile(operational.address, 'Recover Node', nodeId);

    // Node not in the ring → guard skips recalc → prev stays 0.
    expect(await askStorage.prevTotalActiveStake()).to.equal(0n);
  });
});
