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

const { getDomain } = require('../helpers/eip712');
const { ERC4337Helper } = require('../helpers/erc4337');
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey, MultiERC7913SigningKey } = require('../helpers/signers');
const { MAX_UINT64 } = require('../helpers/constants');

const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
const { PackedUserOperation } = require('../helpers/eip712-types');

// Prepare signers in advance (RSA are long to initialize)
const signerECDSA1 = ethers.Wallet.createRandom();
const signerECDSA2 = ethers.Wallet.createRandom();
const signerECDSA3 = ethers.Wallet.createRandom();
const signerECDSA4 = ethers.Wallet.createRandom(); // Unauthorized signer
const signerP256 = new NonNativeSigner(P256SigningKey.random());
const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());

// Minimal fixture common to the different signer verifiers
async function fixture() {
  // EOAs and environment
  const [beneficiary, other] = await ethers.getSigners();
  const target = await ethers.deployContract('CallReceiverMock');

  // ERC-7913 verifiers
  const verifierP256 = await ethers.deployContract('ERC7913P256Verifier');
  const verifierRSA = await ethers.deployContract('ERC7913RSAVerifier');

  // ERC-4337 env
  const helper = new ERC4337Helper();
  await helper.wait();
  const entrypointDomain = await getDomain(entrypoint.v08);
  const domain = { name: 'AccountMultiSigner', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract

  const makeMock = (signers, threshold) =>
    helper.newAccount('$AccountMultiSignerMock', [signers, threshold, 'AccountMultiSigner', '1']).then(mock => {
      domain.verifyingContract = mock.address;
      return mock;
    });

  // Sign user operations using MultiERC7913SigningKey
  const signUserOp = function (userOp) {
    return this.signer
      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
      .then(signature => Object.assign(userOp, { signature }));
  };

  const invalidSig = function () {
    return this.signer.signMessage('invalid');
  };

  return {
    helper,
    verifierP256,
    verifierRSA,
    domain,
    target,
    beneficiary,
    other,
    makeMock,
    signUserOp,
    invalidSig,
  };
}

describe('AccountMultiSigner', function () {
  beforeEach(async function () {
    Object.assign(this, await loadFixture(fixture));
  });

  describe('Multi ECDSA signers with threshold=1', function () {
    beforeEach(async function () {
      this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1]));
      this.mock = await this.makeMock([signerECDSA1.address], 1);
    });

    shouldBehaveLikeAccountCore();
    shouldBehaveLikeAccountHolder();
    shouldBehaveLikeERC1271({ erc7739: true });
    shouldBehaveLikeERC7821();
  });

  describe('Multi ECDSA signers with threshold=2', function () {
    beforeEach(async function () {
      this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
      this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 2);
    });

    shouldBehaveLikeAccountCore();
    shouldBehaveLikeAccountHolder();
    shouldBehaveLikeERC1271({ erc7739: true });
    shouldBehaveLikeERC7821();
  });

  describe('Mixed signers with threshold=2', function () {
    beforeEach(async function () {
      // Create signers array with all three types
      signerP256.bytes = ethers.concat([
        this.verifierP256.target,
        signerP256.signingKey.publicKey.qx,
        signerP256.signingKey.publicKey.qy,
      ]);

      signerRSA.bytes = ethers.concat([
        this.verifierRSA.target,
        ethers.AbiCoder.defaultAbiCoder().encode(
          ['bytes', 'bytes'],
          [signerRSA.signingKey.publicKey.e, signerRSA.signingKey.publicKey.n],
        ),
      ]);

      this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerP256, signerRSA]));
      this.mock = await this.makeMock([signerECDSA1.address, signerP256.bytes, signerRSA.bytes], 2);
    });

    shouldBehaveLikeAccountCore();
    shouldBehaveLikeAccountHolder();
    shouldBehaveLikeERC1271({ erc7739: true });
    shouldBehaveLikeERC7821();
  });

  describe('Signer management', function () {
    beforeEach(async function () {
      this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
      this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
      await this.mock.deploy();
    });

    it('can add signers', async function () {
      const signers = [signerECDSA3.address];

      // Successfully adds a signer
      const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
      await expect(this.mock.$_addSigners(signers))
        .to.emit(this.mock, 'ERC7913SignerAdded')
        .withArgs(signerECDSA3.address);
      const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
      expect(signersArrayAfter.length).to.equal(signersArrayBefore.length + 1);
      expect(signersArrayAfter).to.include(ethers.getAddress(signerECDSA3.address));

      // Reverts if the signer was already added
      await expect(this.mock.$_addSigners(signers))
        .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913AlreadyExists')
        .withArgs(...signers.map(s => s.toLowerCase()));
    });

    it('can remove signers', async function () {
      const signers = [signerECDSA2.address];

      // Successfully removes an already added signer
      const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
      await expect(this.mock.$_removeSigners(signers))
        .to.emit(this.mock, 'ERC7913SignerRemoved')
        .withArgs(signerECDSA2.address);
      const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
      expect(signersArrayAfter.length).to.equal(signersArrayBefore.length - 1);
      expect(signersArrayAfter).to.not.include(ethers.getAddress(signerECDSA2.address));

      // Reverts removing a signer if it doesn't exist
      await expect(this.mock.$_removeSigners(signers))
        .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913NonexistentSigner')
        .withArgs(...signers.map(s => s.toLowerCase()));

      // Reverts if removing a signer makes the threshold unreachable
      await expect(this.mock.$_removeSigners([signerECDSA1.address]))
        .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
        .withArgs(0, 1);
    });

    it('can change threshold', async function () {
      // Reachable threshold is set
      await expect(this.mock.$_setThreshold(2)).to.emit(this.mock, 'ERC7913ThresholdSet');

      // Unreachable threshold reverts
      await expect(this.mock.$_setThreshold(3))
        .to.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
        .withArgs(2, 3);

      // Zero threshold reverts
      await expect(this.mock.$_setThreshold(0)).to.revertedWithCustomError(
        this.mock,
        'MultiSignerERC7913ZeroThreshold',
      );
    });

    it('rejects invalid signer format', async function () {
      const invalidSigner = '0x123456'; // Too short

      await expect(this.mock.$_addSigners([invalidSigner]))
        .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913InvalidSigner')
        .withArgs(invalidSigner);
    });

    it('can read signers and threshold', async function () {
      await expect(
        this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress)),
      ).to.eventually.have.deep.members([signerECDSA1.address, signerECDSA2.address]);

      await expect(this.mock.threshold()).to.eventually.equal(1);
    });

    it('checks if an address is a signer', async function () {
      // Should return true for authorized signers
      await expect(this.mock.isSigner(signerECDSA1.address)).to.eventually.be.true;
      await expect(this.mock.isSigner(signerECDSA2.address)).to.eventually.be.true;

      // Should return false for unauthorized signers
      await expect(this.mock.isSigner(signerECDSA3.address)).to.eventually.be.false;
      await expect(this.mock.isSigner(signerECDSA4.address)).to.eventually.be.false;
    });
  });

  describe('Signature validation', function () {
    const TEST_MESSAGE = ethers.keccak256(ethers.toUtf8Bytes('Test message'));

    beforeEach(async function () {
      // Set up mock with authorized signers
      this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
      await this.mock.deploy();
    });

    it('rejects signatures from unauthorized signers', async function () {
      // Create signatures including an unauthorized signer
      const authorizedSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
      const unauthorizedSignature = await signerECDSA4.signMessage(ethers.getBytes(TEST_MESSAGE));

      // Prepare signers and signatures arrays
      const signers = [
        signerECDSA1.address,
        signerECDSA4.address, // Unauthorized signer
      ].sort((a, b) => (ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1));

      const signatures = signers.map(signer => {
        if (signer === signerECDSA1.address) return authorizedSignature;
        return unauthorizedSignature;
      });

      // Encode the multi-signature
      const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);

      // Should fail because one signer is not authorized
      await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
    });

    it('rejects invalid signatures from authorized signers', async function () {
      // Create a valid signature and an invalid one from authorized signers
      const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
      const invalidSignature = await signerECDSA2.signMessage(ethers.toUtf8Bytes('Different message')); // Wrong message

      // Prepare signers and signatures arrays
      const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
        ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
      );

      const signatures = signers.map(signer => {
        if (signer === signerECDSA1.address) return validSignature;
        return invalidSignature;
      });

      // Encode the multi-signature
      const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);

      // Should fail because one signature is invalid
      await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
    });

    it('rejects signatures from unsorted signers', async function () {
      // Create a valid signature and an invalid one from authorized signers
      const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
      const validSignature2 = await signerECDSA2.signMessage(ethers.getBytes(TEST_MESSAGE));

      // Prepare signers and signatures arrays
      const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
        ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
      );
      const unsortedSigners = signers.reverse();
      const signatures = unsortedSigners.map(signer => {
        if (signer === signerECDSA1.address) return validSignature1;
        return validSignature2;
      });

      // Encode the multi-signature
      const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(
        ['bytes[]', 'bytes[]'],
        [unsortedSigners, signatures],
      );

      // Should fail because signers are not sorted
      await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
    });

    it('rejects signatures when signers.length != signatures.length', async function () {
      // Create a valid signature and an invalid one from authorized signers
      const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));

      // Prepare signers and signatures arrays
      const signers = [signerECDSA1.address, signerECDSA2.address];
      const signatures = [validSignature1];

      // Encode the multi-signature
      const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);

      // Should fail because signers and signatures arrays have different lengths
      await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
    });

    it('rejects duplicated signers', async function () {
      // Create a valid signature
      const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));

      // Prepare signers and signatures arrays
      const signers = [signerECDSA1.address, signerECDSA1.address];
      const signatures = [validSignature, validSignature];

      // Encode the multi-signature
      const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);

      // Should fail because of duplicated signers
      await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
    });
  });
});
