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

describe('Proposal', () => {
  async function deploy() {
    // Contracts are deployed using the first signer/account by default
    const [account1, account2] = await ethers.getSigners();

    const DAO = await ethers.getContractFactory('DAO');
    const dao = await DAO.deploy();
    const Proposals = await ethers.getContractFactory('Proposals');
    const proposals = await Proposals.deploy(dao.target);

    await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
    await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
    const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
    await dao.initiateValidationPool(
      'some-content-id',
      60,
      [1, 3],
      [1, 2],
      100,
      true,
      false,
      callbackData,
      { value: 1000 },
    );
    await dao.initiateValidationPool(
      'some-other-content-id',
      60,
      [1, 3],
      [1, 2],
      100,
      true,
      false,
      callbackData,
      { value: 1000 },
    );
    await time.increase(61);
    await dao.evaluateOutcome(0);
    await dao.evaluateOutcome(1);

    return {
      dao, proposals, account1, account2,
    };
  }

  it('Should deploy', async () => {
    const {
      dao, proposals, account1, account2,
    } = await loadFixture(deploy);
    expect(dao).to.exist;
    expect(proposals).to.exist;
    expect(await dao.memberCount()).to.equal(2);
    expect(await dao.balanceOf(account1)).to.equal(1000);
    expect(await dao.balanceOf(account2)).to.equal(1000);
    expect(await dao.totalSupply()).to.equal(2000);
    expect(await proposals.proposalCount()).to.equal(0);
  });

  describe('Attestation', () => {
    let dao;
    let proposals;
    let account1;
    let account2;
    let proposal;

    beforeEach(async () => {
      ({
        dao,
        proposals,
        account1,
        account2,
      } = await loadFixture(deploy));

      const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
      await dao.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
      await proposals.propose('proposal-content-id', [20, 20, 20], false, emptyCallbackData, { value: 100 });
      expect(await proposals.proposalCount()).to.equal(1);
      proposal = await proposals.proposals(0);
      expect(proposal.postId).to.equal('proposal-content-id');
      expect(proposal.stage).to.equal(0);
    });

    it('Can submit a proposal', async () => {
      const postAuthors = await dao.getPostAuthors('proposal-content-id');
      expect(postAuthors).to.have.length(1);
      expect(postAuthors[0].weightPPM).to.equal(1000000);
      expect(postAuthors[0].authorAddress).to.equal(account1);
    });

    it('Can attest for a proposal', async () => {
      await proposals.connect(account1).attest(0, 200);
      // Nonbinding, non-encumbering
      expect(await dao.balanceOf(account1)).to.equal(1000);
    });

    describe('Evaluate attestation', () => {
      it('when threshold is met, advance to referendum 0% binding', async () => {
        await proposals.attest(0, 200);
        await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);
      });

      it('threshold may be met by accumulation of attestations', async () => {
        await proposals.connect(account1).attest(0, 100);
        await proposals.connect(account2).attest(0, 100);
        await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);
      });

      it('when threshold is not met, and duration has not elapsed, do nothing', async () => {
        await proposals.evaluateAttestation(0);
        expect(proposal.stage).to.equal(0);
      });

      it('when threshold is not met, and duration has elapsed, close the proposal', async () => {
        await time.increase(365 * 86400 + 1); // 1 year + 1 second
        await proposals.evaluateAttestation(0);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4); // Stage.Failed
      });
    });

    describe('Referendum 0% binding', () => {
      beforeEach(async () => {
        await proposals.attest(0, 200);
        await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);
      });

      it('proposal dies if it fails to meet quorum', async () => {
        await time.increase(21);
        await expect(dao.evaluateOutcome(2)).to.emit(dao, 'ValidationPoolResolved').withArgs(2, false, false);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4); // Stage.Failed
      });

      it('referendum retries if it fails to meet participation rate', async () => {
        await dao.stakeOnValidationPool(2, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);
      });

      it('referendum retries if it fails to meet win ratio', async () => {
        await dao.stakeOnValidationPool(2, 1000, false);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, false, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);

        const pools = await proposals.getPools(0);
        expect(pools[0][0].started).to.be.true;
        expect(pools[0][1].started).to.be.true;
        expect(pools[0][2].started).to.be.false;
        expect(pools[0][0].completed).to.be.true;
        expect(pools[0][1].completed).to.be.false;
        expect(pools[0][2].completed).to.be.false;
      });

      it('proposal fails if a referendum fails to meet participation rate 3 times', async () => {
        await dao.stakeOnValidationPool(2, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);

        await dao.stakeOnValidationPool(3, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(1);

        await dao.stakeOnValidationPool(4, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4);
      });

      it('advances to next referendum if it meets participation rate and win ratio', async () => {
        await dao.stakeOnValidationPool(2, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);
      });
    });

    describe('Referendum 1% binding', () => {
      beforeEach(async () => {
        await proposals.attest(0, 200);
        await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
        await dao.stakeOnValidationPool(2, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);
      });

      afterEach(async () => {
        const pool = await dao.validationPools(3);
        expect(pool.resolved).to.be.true;
      });

      it('proposal dies if it fails to meet quorum', async () => {
        await time.increase(21);
        await expect(dao.evaluateOutcome(3)).to.emit(dao, 'ValidationPoolResolved').withArgs(3, false, false);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4); // Stage.Failed
      });

      it('referendum retries if it fails to meet participation rate', async () => {
        await dao.stakeOnValidationPool(3, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);
      });

      it('referendum retries if it fails to meet win ratio', async () => {
        await dao.stakeOnValidationPool(3, 1000, false);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, false, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);
      });

      it('proposal fails if a referendum fails to meet participation rate 3 times', async () => {
        await dao.stakeOnValidationPool(3, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);

        await dao.stakeOnValidationPool(4, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(5);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(2);

        await dao.stakeOnValidationPool(5, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(5))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(5, true, true);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4);
      });

      it('advances to next referendum if it meets participation rate and win ratio', async () => {
        await dao.stakeOnValidationPool(3, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);
      });
    });

    describe('Referendum 100% binding', () => {
      beforeEach(async () => {
        await proposals.attest(0, 200);
        await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
        await dao.stakeOnValidationPool(2, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(2))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
        await dao.stakeOnValidationPool(3, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(3))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(4);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);
      });

      afterEach(async () => {
        const pool = await dao.validationPools(4);
        expect(pool.resolved).to.be.true;
      });

      it('proposal dies if it fails to meet quorum', async () => {
        await time.increase(21);
        await expect(dao.evaluateOutcome(4)).to.emit(dao, 'ValidationPoolResolved').withArgs(4, false, false);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4); // Stage.Failed
      });

      it('referendum retries if it fails to meet participation rate', async () => {
        await dao.stakeOnValidationPool(4, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(5);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);
      });

      it('referendum retries if it fails to meet win ratio', async () => {
        await dao.stakeOnValidationPool(4, 1000, false);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, false, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(5);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);
      });

      it('proposal fails if a referendum fails to meet participation rate 3 times', async () => {
        await dao.stakeOnValidationPool(4, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(5);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);

        await dao.stakeOnValidationPool(5, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(5))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(5, true, true)
          .to.emit(dao, 'ValidationPoolInitiated').withArgs(6);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(3);

        await dao.stakeOnValidationPool(6, 200, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(6))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(6, true, true);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(4);
      });

      it('advances to accepted stage if it meets participation rate and win ratio', async () => {
        await dao.connect(account1).stakeOnValidationPool(4, 1000, true);
        await dao.connect(account2).stakeOnValidationPool(4, 1000, true);
        await time.increase(21);
        await expect(dao.evaluateOutcome(4))
          .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true);
        proposal = await proposals.proposals(0);
        expect(proposal.stage).to.equal(5);
      });
    });
  });
});