import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import hre from 'hardhat';

import { Chronos, EpochStorage } from '../../typechain';

type EpochStorageFixture = {
  accounts: SignerWithAddress[];
  EpochStorage: EpochStorage;
  Chronos: Chronos;
};

describe('@unit EpochStorage', () => {
  let accounts: SignerWithAddress[];
  let EpochStorage: EpochStorage;
  let Chronos: Chronos;
  // Resolved at fixture load time; mirrors Chronos.epochLength() so that
  // "advance one epoch" test primitives track whatever cadence the
  // deployment parameters currently specify (30 days on mainnet/dev post
  // V10 alignment, historically 1h on legacy dev profiles).
  let epochSeconds: number;

  async function deployEpochStorageFixture(): Promise<EpochStorageFixture> {
    await hre.deployments.fixture(['EpochStorage', 'Chronos']);
    EpochStorage = await hre.ethers.getContract<EpochStorage>('EpochStorageV8');
    Chronos = await hre.ethers.getContract<Chronos>('Chronos');
    epochSeconds = Number(await Chronos.epochLength());
    accounts = await hre.ethers.getSigners();
    return { accounts, EpochStorage, Chronos };
  }

  beforeEach(async () => {
    hre.helpers.resetDeploymentsJson();
    ({ accounts, EpochStorage, Chronos } = await loadFixture(
      deployEpochStorageFixture,
    ));
  });

  it('Should have correct name and version', async () => {
    expect(await EpochStorage.name()).to.equal('EpochStorage');
    expect(await EpochStorage.version()).to.equal('10.0.4');
  });

  it('Add knowledge value for single epoch, verify totals and running node max', async () => {
    const epoch = 10;
    await EpochStorage.addEpochPublishingAllocation(123, epoch, 500);
    expect(await EpochStorage.getEpochPublishingAllocation(epoch)).to.equal(
      500,
    );
    expect(
      await EpochStorage.getNodeEpochPublishingAllocation(123, epoch),
    ).to.equal(500);
    // `addEpochPublishingAllocation` maintains the per-epoch node max so its
    // public getters stay consistent (Slither uninitialized-state). A single
    // node with 500 → the running max is 500.
    expect(
      await EpochStorage.getEpochNodeMaxPublishingAllocation(epoch),
    ).to.equal(500);
  });

  it('Add knowledge values to multiple epochs, check accumulated node stats', async () => {
    await EpochStorage.addEpochPublishingAllocation(42, 1, 1000);
    await EpochStorage.addEpochPublishingAllocation(42, 2, 250);
    await EpochStorage.addEpochPublishingAllocation(42, 3, 500);
    expect(await EpochStorage.getEpochPublishingAllocation(1)).to.equal(1000);
    expect(await EpochStorage.getEpochPublishingAllocation(2)).to.equal(250);
    expect(await EpochStorage.getEpochPublishingAllocation(3)).to.equal(500);
    expect(
      await EpochStorage.getNodeEpochPublishingAllocation(42, 1),
    ).to.equal(1000);
    expect(
      await EpochStorage.getNodeEpochPublishingAllocation(42, 2),
    ).to.equal(250);
    expect(
      await EpochStorage.getNodeEpochPublishingAllocation(42, 3),
    ).to.equal(500);
  });

  it('Get node knowledge value percentage returns zero if total is zero', async () => {
    expect(
      await EpochStorage.getNodeEpochPublishingAllocationPercentage(
        9999,
        100,
      ),
    ).to.equal(0);
    await EpochStorage.addEpochPublishingAllocation(777, 100, 0);
    expect(
      await EpochStorage.getNodeEpochPublishingAllocationPercentage(777, 100),
    ).to.equal(0);
  });

  it('Add tokens to epoch range, check if distributed pool is correct', async () => {
    await EpochStorage.addTokensToEpochRange(1, 100, 109, 100);

    expect(await EpochStorage.getEpochPool(1, 99)).to.be.equal(0);
    expect(await EpochStorage.getEpochPool(1, 101)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 102)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 103)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 104)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 105)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 106)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 107)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 108)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 109)).to.be.equal(10);
    expect(await EpochStorage.getEpochPool(1, 110)).to.be.equal(0);
    expect(await EpochStorage.getEpochPool(1, 111)).to.be.equal(0); // expect(await EpochStorage.getEpochPool(1, 9)).to.be.gt(0);
  });

  it('Pay out epoch tokens, verify distribution and nodePaidOut', async () => {
    await EpochStorage.addTokensToEpochRange(2, 1, 1, 1000);
    await EpochStorage.payOutEpochTokens(2, 1, 500, 300);
    expect(await EpochStorage.getEpochDistributedPool(2, 1)).to.equal(300);
    expect(await EpochStorage.getNodeEpochPaidOut(2, 500, 1)).to.equal(300);
    await EpochStorage.payOutEpochTokens(2, 1, 500, 200);
    expect(await EpochStorage.getEpochDistributedPool(2, 1)).to.equal(500);
    expect(await EpochStorage.getNodeEpochPaidOut(2, 500, 1)).to.equal(500);
  });

  it('Add tokens to multiple epochs, finalize, then check correct cumulative values', async () => {
    await EpochStorage.addTokensToEpochRange(3, 2, 5, 5000);
    expect(await EpochStorage.getEpochPool(3, 2)).to.be.gt(0);
    expect(await EpochStorage.getEpochPool(3, 6)).to.be.equal(0);
    expect(await EpochStorage.getEpochDistributedPool(3, 4)).to.equal(0);
  });

  it('Simulate paying out across epochs and nodes, random checks', async () => {
    await EpochStorage.addTokensToEpochRange(4, 5, 5, 6000);
    await EpochStorage.payOutEpochTokens(4, 5, 111, 200);
    await EpochStorage.payOutEpochTokens(4, 5, 222, 300);
    expect(await EpochStorage.getEpochDistributedPool(4, 5)).to.equal(500);
    expect(await EpochStorage.getNodeEpochPaidOut(4, 111, 5)).to.equal(200);
    expect(await EpochStorage.getNodeEpochPaidOut(4, 222, 5)).to.equal(300);
  });

  it('Add large range tokens, partial remainders, finalize, verify pools', async () => {
    await EpochStorage.addTokensToEpochRange(5, 1, 10, 9999);
    await EpochStorage.addTokensToEpochRange(5, 1, 5, 555);
    expect(await EpochStorage.getEpochPool(5, 10)).to.be.closeTo(1000, 1);
    expect(await EpochStorage.accumulatedRemainder(5)).to.be.gt(0);
  });

  it('Add big knowledge values for multiple identities in same epoch — node max tracks the largest', async () => {
    await EpochStorage.addEpochPublishingAllocation(101, 20, 1500);
    await EpochStorage.addEpochPublishingAllocation(202, 20, 3000);
    await EpochStorage.addEpochPublishingAllocation(303, 20, 2800);
    // The epoch total accumulates all three; the per-epoch node max tracks the
    // single largest per-node allocation = max(1500, 3000, 2800) = 3000.
    expect(await EpochStorage.getEpochPublishingAllocation(20)).to.equal(7300);
    expect(
      await EpochStorage.getEpochNodeMaxPublishingAllocation(20),
    ).to.equal(3000);
  });

  it('Verify getCurrentEpochPool with no tokens added, expecting zero', async () => {
    const shardId = 7;
    expect(await EpochStorage.getCurrentEpochPool(shardId)).to.equal(0);
  });

  it('Verify getCurrentEpochPool after adding tokens to current epoch', async () => {
    const shardId = 8;
    const currentEpoch = await hre.ethers.getContractAt(
      'Chronos',
      await EpochStorage.chronos(),
    );
    const epochNum = await currentEpoch.getCurrentEpoch();
    await EpochStorage.addTokensToEpochRange(shardId, epochNum, epochNum, 2000);
    expect(await EpochStorage.getCurrentEpochPool(shardId)).to.be.equal(2000);
  });

  it('Check getEpochRangePool for partially overlapping epochs, ensuring correct totals', async () => {
    const shardId = 9;
    await EpochStorage.addTokensToEpochRange(shardId, 2, 4, 3000);
    await EpochStorage.addTokensToEpochRange(shardId, 3, 5, 2000);
    expect(
      (await EpochStorage.getEpochRangePool(shardId, 2, 5)) +
        (await EpochStorage.accumulatedRemainder(shardId)),
    ).to.be.equal(5000);
  });

  it('Validate getPreviousEpochPool when current epoch is 1, expecting zero', async () => {
    const shardId = 10;
    expect(await EpochStorage.getPreviousEpochPool(shardId)).to.equal(0);
  });

  it('Add tokens to partially overlapping ranges, finalize them, verify getEpochPool returns expected values', async () => {
    const shardId = 11;
    await EpochStorage.addTokensToEpochRange(shardId, 2, 5, 4000);
    await EpochStorage.addTokensToEpochRange(shardId, 4, 6, 3000);

    const epoch4Pool = await EpochStorage.getEpochPool(shardId, 4);
    const epoch5Pool = await EpochStorage.getEpochPool(shardId, 5);
    const epoch6Pool = await EpochStorage.getEpochPool(shardId, 6);
    expect(epoch4Pool).to.be.equal(2000);
    expect(epoch5Pool).to.be.equal(2000);
    expect(epoch6Pool).to.be.equal(1000);
  });

  it('Add tokens to multiple ranges, then partially pay out tokens, check getEpochPool remains correct', async () => {
    const shardId = 12;
    await EpochStorage.addTokensToEpochRange(shardId, 3, 3, 1000);
    await EpochStorage.addTokensToEpochRange(shardId, 3, 4, 2000);

    const oldPool4 = await EpochStorage.getEpochPool(shardId, 4);

    // Pay out some tokens in epoch 3
    await EpochStorage.payOutEpochTokens(shardId, 3, 99, 500);
    const dist3 = await EpochStorage.getEpochDistributedPool(shardId, 3);
    const pool3 = await EpochStorage.getEpochPool(shardId, 3);
    const remainingPool3 = await EpochStorage.getEpochRemainingPool(shardId, 3);
    expect(dist3).to.equal(500);
    expect(pool3).to.be.equal(2000); // Still the same as we don't automatically decrement paid out
    expect(remainingPool3).to.be.equal(1500);

    // Check next epoch is still unaffected
    const newPool4 = await EpochStorage.getEpochPool(shardId, 4);
    expect(newPool4).to.be.equal(oldPool4);
  });

  it('Add tokens across multiple ranges, verify getEpochRangePool sums up properly', async () => {
    const shardId = 13;
    await EpochStorage.addTokensToEpochRange(shardId, 5, 7, 2100);
    await EpochStorage.addTokensToEpochRange(shardId, 6, 8, 1800);
    const fullRangePool = await EpochStorage.getEpochRangePool(shardId, 5, 8);
    expect(fullRangePool).to.be.equal(3900);
    expect(await EpochStorage.getEpochRangePool(shardId, 6, 7)).to.be.equal(
      2600,
    );
  });

  it('Check getPreviousEpochPool after adding tokens, then increment epoch to finalize', async () => {
    const shardId = 14;
    await EpochStorage.addTokensToEpochRange(shardId, 1, 1, 2000);
    // Move to next epoch (resolved from Chronos so it tracks current epoch cadence)
    await time.increase(epochSeconds + 1);
    expect(await EpochStorage.getPreviousEpochPool(shardId)).to.equal(2000);
  });

  it('Complex overlapping ranges: verify expected epoch pools and compare getEpochPool vs getEpochRangePool', async () => {
    const shardId = 15;

    // Add two overlapping ranges: epochs 10-20 => 50000 tokens, epochs 15-25 => 60000 tokens
    await EpochStorage.addTokensToEpochRange(shardId, 10, 20, 50000);
    await EpochStorage.addTokensToEpochRange(shardId, 15, 25, 60000);

    // Check pools for a few key epochs in the overlapping region
    const pool15 = await EpochStorage.getEpochPool(shardId, 15);
    const pool20 = await EpochStorage.getEpochPool(shardId, 20);
    const pool25 = await EpochStorage.getEpochPool(shardId, 25);

    // Since epoch 15 and 20 are in both ranges, and epoch 25 is only in the second range,
    // we expect pool25 to be strictly less than pool20
    expect(pool15).to.be.gt(0);
    expect(pool20).to.be.gte(pool15);
    expect(pool25).to.be.lt(pool20);

    // Compare getEpochRangePool(15, 25) vs sum of getEpochPool for each epoch in [15..25]
    let summedEpochPools = 0;
    for (let e = 15; e <= 25; e++) {
      summedEpochPools += Number(await EpochStorage.getEpochPool(shardId, e));
    }
    const rangePool = Number(
      await EpochStorage.getEpochRangePool(shardId, 15, 25),
    );
    // Expect them to be reasonably close (allowing minor remainder differences)
    expect(rangePool).to.be.closeTo(summedEpochPools, 1);
  });

  it('Add tokens to a single long range, then retrieve getEpochPool vs getEpochRangePool for smaller sub-ranges', async () => {
    const shardId = 16;

    // Add a large range: 5-15 => 30000 tokens
    await EpochStorage.addTokensToEpochRange(shardId, 5, 15, 30000);

    // Pick a sub-range [7..10] and compare direct pool sums with getEpochRangePool
    let sumSubRange = 0;
    for (let e = 7; e <= 10; e++) {
      sumSubRange += Number(await EpochStorage.getEpochPool(shardId, e));
    }
    const subRangePool = Number(
      await EpochStorage.getEpochRangePool(shardId, 7, 10),
    );
    expect(subRangePool).to.be.closeTo(sumSubRange, 1);

    // Also check an epoch outside the range, expecting zero
    expect(await EpochStorage.getEpochPool(shardId, 20)).to.equal(0);
  });

  it('Verify getEpochPool remains correct after partial payouts', async () => {
    const shardId = 17;

    // Range: 2-4 => 6000 tokens total
    await EpochStorage.addTokensToEpochRange(shardId, 2, 4, 6000);

    // Pay out 100 tokens in epoch 3
    await EpochStorage.payOutEpochTokens(shardId, 3, 909, 100);
    const dist3 = await EpochStorage.getEpochDistributedPool(shardId, 3);
    expect(dist3).to.equal(100);

    const pool3 = await EpochStorage.getEpochPool(shardId, 3);
    // Pool 3 should still be >= distributed
    expect(pool3).to.be.gte(dist3);

    // Check that getEpochRangePool(2,4) is at least equal to sum of pay outs
    const rangePool = Number(
      await EpochStorage.getEpochRangePool(shardId, 2, 4),
    );
    expect(rangePool).to.be.gte(dist3);
  });

  it('Multiple non-overlapping ranges with partial remainders, ensuring getEpochPool and getEpochRangePool logic stays consistent', async () => {
    const shardId = 18;

    // Ranges: 5-6 => 1000, 7-8 => 2000, 9-10 => 3000
    await EpochStorage.addTokensToEpochRange(shardId, 5, 6, 1000);
    await EpochStorage.addTokensToEpochRange(shardId, 7, 8, 2000);
    await EpochStorage.addTokensToEpochRange(shardId, 9, 10, 3000);

    // Check each epoch's pool
    const p5 = await EpochStorage.getEpochPool(shardId, 5);
    const p6 = await EpochStorage.getEpochPool(shardId, 6);
    const p7 = await EpochStorage.getEpochPool(shardId, 7);
    const p8 = await EpochStorage.getEpochPool(shardId, 8);
    const p9 = await EpochStorage.getEpochPool(shardId, 9);
    const p10 = await EpochStorage.getEpochPool(shardId, 10);

    expect(p5).to.be.equal(500);
    expect(p6).to.be.equal(500);
    expect(p7).to.be.equal(1000);
    expect(p8).to.be.equal(1000);
    expect(p9).to.be.equal(1500);
    expect(p10).to.be.equal(1500);

    // Compare getEpochRangePool(5,10) with sum of individual getEpochPool(5..10)
    const totalInd =
      Number(p5) +
      Number(p6) +
      Number(p7) +
      Number(p8) +
      Number(p9) +
      Number(p10);
    const totalRange = Number(
      await EpochStorage.getEpochRangePool(shardId, 5, 10),
    );
    expect(totalRange).to.be.closeTo(totalInd, 1);
  });

  it('getCurrentEpochPublishingAllocation, getPreviousEpochPublishingAllocation', async () => {
    await EpochStorage.addEpochPublishingAllocation(10, 1, 200);
    await time.increase(epochSeconds);
    await EpochStorage.addEpochPublishingAllocation(20, 2, 300);
    expect(await EpochStorage.getCurrentEpochPublishingAllocation()).to.equal(
      300,
    );
    expect(
      await EpochStorage.getPreviousEpochPublishingAllocation(),
    ).to.equal(200);
  });

  it('getNodeCurrentEpochPublishingAllocation, getNodePreviousEpochPublishingAllocation', async () => {
    await EpochStorage.addEpochPublishingAllocation(123, 1, 500);
    await time.increase(epochSeconds);
    await EpochStorage.addEpochPublishingAllocation(123, 2, 700);
    expect(
      await EpochStorage.getNodeCurrentEpochPublishingAllocation(123),
    ).to.equal(700);
    expect(
      await EpochStorage.getNodePreviousEpochPublishingAllocation(123),
    ).to.equal(500);
  });

  it('getCurrentEpochNodeMaxPublishingAllocation, getPreviousEpochNodeMaxPublishingAllocation track the per-epoch max', async () => {
    await EpochStorage.addEpochPublishingAllocation(1, 1, 1000);
    await EpochStorage.addEpochPublishingAllocation(2, 1, 500);
    await time.increase(epochSeconds);
    await EpochStorage.addEpochPublishingAllocation(1, 2, 300);
    await EpochStorage.addEpochPublishingAllocation(2, 2, 800);
    // `addEpochPublishingAllocation` maintains the per-epoch node max. Epoch 1
    // max = max(1000, 500) = 1000; epoch 2 max = max(300, 800) = 800. After the
    // time.increase, the current epoch is 2 and the previous is 1.
    expect(
      await EpochStorage.getCurrentEpochNodeMaxPublishingAllocation(),
    ).to.equal(800);
    expect(
      await EpochStorage.getPreviousEpochNodeMaxPublishingAllocation(),
    ).to.equal(1000);
  });

  it('getNodeCurrentEpochPublishingAllocationPercentage, getNodePreviousEpochPublishingAllocationPercentage', async () => {
    await EpochStorage.addEpochPublishingAllocation(1111, 1, 500);
    await EpochStorage.addEpochPublishingAllocation(2222, 1, 1500);
    await time.increase(epochSeconds);
    await EpochStorage.addEpochPublishingAllocation(1111, 2, 1000);
    await EpochStorage.addEpochPublishingAllocation(2222, 2, 2000);
    expect(
      await EpochStorage.getNodePreviousEpochPublishingAllocationPercentage(
        1111,
      ),
    ).to.equal((500n * 1000000000000000000n) / 2000n);
    expect(
      await EpochStorage.getNodeCurrentEpochPublishingAllocationPercentage(
        1111,
      ),
    ).to.equal((1000n * 1000000000000000000n) / 3000n);
  });
});
