const {
  time,
  loadFixture,
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
const { ethers } = require('hardhat');

describe('DAO', () => {
  async function deploy() {
    const [account1, account2] = await ethers.getSigners();
    const DAO = await ethers.getContractFactory('DAO');
    const dao = await DAO.deploy();
    return { dao, account1, account2 };
  }

  it('Should deploy', async () => {
    const { dao } = await loadFixture(deploy);
    expect(dao).to.exist;
    expect(await dao.totalSupply()).to.equal(0);
  });

  describe('Post', () => {
    it('should be able to add a post', async () => {
      const { dao, account1 } = await loadFixture(deploy);
      const contentId = 'some-id';
      await expect(dao.addPost(account1, contentId)).to.emit(dao, 'PostAdded').withArgs(0);
      const post = await dao.posts(0);
      expect(post.author).to.equal(account1);
      expect(post.sender).to.equal(account1);
      expect(post.contentId).to.equal(contentId);
    });

    it('should be able to add a post on behalf of another account', async () => {
      const { dao, account1, account2 } = await loadFixture(deploy);
      const contentId = 'some-id';
      await dao.addPost(account2, contentId);
      const post = await dao.posts(0);
      expect(post.author).to.equal(account2);
      expect(post.sender).to.equal(account1);
      expect(post.contentId).to.equal(contentId);
    });
  });

  describe('Validation Pool', () => {
    let dao;
    let account1;
    let account2;
    const POOL_DURATION = 3600; // 1 hour
    const POOL_FEE = 100;
    const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);

    const initiateValidationPool = ({
      postIndex, duration,
      quorumNumerator, quorumDenominator, bindingPercent,
      redistributeLosingStakes, callbackOnValidate,
      callbackData, fee,
    } = {}) => dao.initiateValidationPool(
      postIndex ?? 0,
      duration ?? POOL_DURATION,
      quorumNumerator ?? 1,
      quorumDenominator ?? 3,
      bindingPercent ?? 100,
      redistributeLosingStakes ?? true,
      callbackOnValidate ?? false,
      callbackData ?? emptyCallbackData,
      { value: fee ?? POOL_FEE },
    );

    beforeEach(async () => {
      ({ dao, account1, account2 } = await loadFixture(deploy));
      await dao.addPost(account1, 'content-id');
      const init = () => initiateValidationPool({ fee: POOL_FEE });
      await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
      expect(await dao.validationPoolCount()).to.equal(1);
      expect(await dao.memberCount()).to.equal(0);
      expect(await dao.balanceOf(account1)).to.equal(0);
      expect(await dao.totalSupply()).to.equal(POOL_FEE);
    });

    describe('Initiate', () => {
      it('should not be able to initiate a validation pool without a fee', async () => {
        const init = () => initiateValidationPool({ fee: 0 });
        await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool');
      });

      it('should not be able to initiate a validation pool with a quorum below the minimum', async () => {
        const init = () => initiateValidationPool({ quorumNumerator: 1, quorumDenominator: 11 });
        await expect(init()).to.be.revertedWith('Quorum is below minimum');
      });

      it('should not be able to initiate a validation pool with a quorum greater than 1', async () => {
        const init = () => initiateValidationPool({ quorumNumerator: 11, quorumDenominator: 10 });
        await expect(init()).to.be.revertedWith('Quorum is greater than one');
      });

      it('should not be able to initiate a validation pool with duration below minimum', async () => {
        const init = () => initiateValidationPool({ duration: 0 });
        await expect(init()).to.be.revertedWith('Duration is too short');
      });

      it('should not be able to initiate a validation pool with duration above maximum', async () => {
        const init = () => initiateValidationPool({ duration: 40000000000000 });
        await expect(init()).to.be.revertedWith('Duration is too long');
      });

      it('should not be able to initiate a validation pool with bindingPercent above 100', async () => {
        const init = () => initiateValidationPool({ bindingPercent: 101 });
        await expect(init()).to.be.revertedWith('Binding percent must be <= 100');
      });

      it('should be able to initiate a second validation pool', async () => {
        const init = () => initiateValidationPool();
        await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
        expect(await dao.validationPoolCount()).to.equal(2);
      });

      it('Should be able to fetch pool instance', async () => {
        const pool = await dao.validationPools(0);
        expect(pool).to.exist;
        expect(pool.duration).to.equal(POOL_DURATION);
        expect(pool.postIndex).to.equal(0);
        expect(pool.resolved).to.be.false;
        expect(pool.sender).to.equal(account1);
      });
    });

    describe('Stake', async () => {
      beforeEach(async () => {
        time.increase(POOL_DURATION + 1);
        console.log('evaluating first pool');
        await dao.evaluateOutcome(0);
        expect(await dao.balanceOf(account1)).to.equal(100);
        expect(await dao.balanceOf(dao.target)).to.equal(0);
        console.log('initiating second pool');
        await initiateValidationPool();
        expect(await dao.balanceOf(dao.target)).to.equal(100);
      });

      it('should be able to stake before validation pool has elapsed', async () => {
        console.log('staking on second pool');
        await dao.stake(1, 10, true);
        expect(await dao.balanceOf(account1)).to.equal(90);
        expect(await dao.balanceOf(dao.target)).to.equal(110);
        time.increase(POOL_DURATION + 1);
        console.log('evaluating second pool');
        await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
        expect(await dao.balanceOf(dao.target)).to.equal(0);
        expect(await dao.balanceOf(account1)).to.equal(200);
      });

      it('should not be able to stake after validation pool has elapsed', async () => {
        time.increase(POOL_DURATION + 1);
        await expect(dao.stake(1, 10, true)).to.be.revertedWith('Pool end time has passed');
      });

      it('should be able to stake against a validation pool', async () => {
        await dao.stake(1, 10, false);
        expect(await dao.balanceOf(account1)).to.equal(90);
        expect(await dao.balanceOf(dao.target)).to.equal(110);
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true);
        expect(await dao.balanceOf(dao.target)).to.equal(0);
        expect(await dao.balanceOf(account1)).to.equal(200);
        const pool = await dao.validationPools(1);
        expect(pool.outcome).to.be.false;
      });
    });

    describe('Evaluate outcome', () => {
      it('should not be able to evaluate outcome before duration has elapsed', async () => {
        await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool end time has not yet arrived');
      });

      it('should be able to evaluate outcome after duration has elapsed', async () => {
        expect(await dao.balanceOf(dao.target)).to.equal(100);
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
        expect(await dao.memberCount()).to.equal(1);
        expect(await dao.balanceOf(account1)).to.equal(100);
        const pool = await dao.validationPools(0);
        expect(pool.resolved).to.be.true;
        expect(pool.outcome).to.be.true;
      });

      it('should not be able to evaluate outcome more than once', async () => {
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
        await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved');
      });

      it('should be able to evaluate outcome of second validation pool', async () => {
        const init = () => dao.initiateValidationPool(
          0,
          POOL_DURATION,
          1,
          3,
          100,
          true,
          false,
          emptyCallbackData,
          { value: POOL_FEE },
        );
        await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
        expect(await dao.validationPoolCount()).to.equal(2);
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
        expect(await dao.balanceOf(account1)).to.equal(100);
        await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
        expect(await dao.balanceOf(account1)).to.equal(200);
      });

      it('should not be able to evaluate outcome if quorum is not met', async () => {
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);

        const init = () => initiateValidationPool({ quorumNumerator: 1, quorumDenominator: 1 });
        await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
        expect(await dao.validationPoolCount()).to.equal(2);
        time.increase(POOL_DURATION + 1);
        await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false);
      });

      describe('Validation pool options', () => {
        beforeEach(async () => {
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(0);
          await dao.addPost(account2, 'content-id');
          const init = () => initiateValidationPool({ postIndex: 1 });
          await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(1);
        });

        it('Binding validation pool should redistribute stakes', async () => {
          const init = () => initiateValidationPool();
          await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
          await dao.connect(account1).stake(2, 10, true);
          await dao.connect(account2).stake(2, 10, false);
          expect(await dao.balanceOf(account1)).to.equal(90);
          expect(await dao.balanceOf(account2)).to.equal(90);
          expect(await dao.balanceOf(dao.target)).to.equal(120);
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(2);
          expect(await dao.balanceOf(account1)).to.equal(210);
          expect(await dao.balanceOf(account2)).to.equal(90);
          expect(await dao.balanceOf(dao.target)).to.equal(0);
        });

        it('Non binding validation pool should not redistribute stakes', async () => {
          const init = () => initiateValidationPool({ bindingPercent: 0 });
          await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
          await dao.connect(account1).stake(2, 10, true);
          await dao.connect(account2).stake(2, 10, false);
          expect(await dao.balanceOf(account1)).to.equal(90);
          expect(await dao.balanceOf(account2)).to.equal(90);
          expect(await dao.balanceOf(dao.target)).to.equal(120);
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(2);
          expect(await dao.balanceOf(account1)).to.equal(200);
          expect(await dao.balanceOf(account2)).to.equal(100);
          expect(await dao.balanceOf(dao.target)).to.equal(0);
        });

        it('Partially binding validation pool should redistribute some stakes', async () => {
          const init = () => initiateValidationPool({ bindingPercent: 50 });
          await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
          await dao.connect(account1).stake(2, 10, true);
          await dao.connect(account2).stake(2, 10, false);
          expect(await dao.balanceOf(account1)).to.equal(90);
          expect(await dao.balanceOf(account2)).to.equal(90);
          expect(await dao.balanceOf(dao.target)).to.equal(120);
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(2);
          expect(await dao.balanceOf(account1)).to.equal(205);
          expect(await dao.balanceOf(account2)).to.equal(95);
          expect(await dao.balanceOf(dao.target)).to.equal(0);
          expect(await dao.totalSupply()).to.equal(300);
        });

        it('If redistributeLosingStakes is false, validation pool should burn binding portion of losing stakes', async () => {
          const init = () => initiateValidationPool({
            bindingPercent: 50,
            redistributeLosingStakes: false,
          });
          await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
          await dao.connect(account1).stake(2, 10, true);
          await dao.connect(account2).stake(2, 10, false);
          expect(await dao.balanceOf(account1)).to.equal(90);
          expect(await dao.balanceOf(account2)).to.equal(90);
          expect(await dao.balanceOf(dao.target)).to.equal(120);
          time.increase(POOL_DURATION + 1);
          await dao.evaluateOutcome(2);
          expect(await dao.balanceOf(account1)).to.equal(200);
          expect(await dao.balanceOf(account2)).to.equal(95);
          expect(await dao.balanceOf(dao.target)).to.equal(0);
          expect(await dao.totalSupply()).to.equal(295);
        });
      });
    });
  });
});