import { wasm as wasmTester } from 'circom_tester';
import * as crypto from 'crypto';
import { initElliptic } from '@selfxyz/common/utils/certificate_parsing/elliptic';
import * as path from 'path';
import { splitToWords } from '@selfxyz/common/utils/bytes';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const elliptic = initElliptic();

const testSuite = [
  {
    hash: 'sha512',
    curve: 'brainpoolP256r1',
    n: 64,
    k: 4,
    reason: 'when hash is greater than curve bits',
  },
];

const fullTestSuite = [
  {
    hash: 'sha1',
    curve: 'brainpoolP224r1',
    n: 32,
    k: 7,
    reason: 'when hash is lesser than curve bits',
  },
  {
    hash: 'sha512',
    curve: 'brainpoolP256r1',
    n: 64,
    k: 4,
    reason: 'when hash is greater than curve bits',
  },
  {
    hash: 'sha384',
    curve: 'brainpoolP384r1',
    n: 64,
    k: 6,
    reason: 'when hash bits are the same as curve bits',
  },
  {
    hash: 'sha512',
    curve: 'brainpoolP512r1',
    n: 64,
    k: 8,
    reason: 'when hash bits are the same as curve bits',
  },
  {
    hash: 'sha256',
    curve: 'p256',
    n: 64,
    k: 4,
    reason: 'when hash bits are the same as curve bits',
  },
  {
    hash: 'sha384',
    curve: 'p384',
    n: 64,
    k: 6,
    reason: 'when hash bits are the same as curve bits',
  },
  {
    hash: 'sha512',
    curve: 'p521',
    n: 66,
    k: 8,
    reason: 'when hash bits are less than the curve bits',
  },
];

describe('ecdsa', () => {
  testSuite.forEach(({ hash, curve, n, k, reason }) => {
    const message = crypto.randomBytes(32);
    (
      [
        [true, 'should verify'],
        [false, 'should not verify'],
      ] as [boolean, string][]
    ).forEach(([shouldVerify, shouldVerifyReason]) => {
      describe(shouldVerifyReason, function () {
        this.timeout(0);
        const inputs = sign(message, curve, hash, k, n);
        if (!shouldVerify) {
          inputs.hashParsed.map((x) => 0);
        }

        it(reason, async () => {
          const circuit = await wasmTester(
            path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_${curve}.circom`),
            {
              include: ['../node_modules', '../node_modules/@zk-kit/binary-merkle-root.circom/src'],
            }
          );

          try {
            const witness = await circuit.calculateWitness(inputs);
            await circuit.checkConstraints(witness);
            if (!shouldVerify) {
              throw new Error('Test failed: Invalid signature was verified.');
            }
          } catch (error) {
            if (shouldVerify) {
              console.log(error);
              throw new Error('Test failed: Valid signature was not verified.');
            }
          }
        });
      });
    });
    it('should not verify if either signature component is greater than the order', async function () {
      this.timeout(0);
      // takes way too long to find a valid input for these
      if (['p256', 'p384'].includes(curve)) {
        return;
      }
      const circuit = await wasmTester(
        path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_${curve}.circom`),
        {
          include: ['../node_modules', '../node_modules/@zk-kit/binary-merkle-root.circom/src'],
        }
      );

      for (const item of [true, false]) {
        try {
          let inputs;
          while (true) {
            try {
              inputs = signOverflow(message, curve, hash, k, n, item);
              break;
            } catch (err) {}
          }
          const witness = await circuit.calculateWitness(inputs);
          await circuit.checkConstraints(witness);
          throw new Error('Test failed: Invalid signature was verified.');
        } catch (error) {}
      }
    });
  });
  it('should not accept invalid chunks in the signature', async function () {
    this.timeout(0);
    const circuit = await wasmTester(
      path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_p256.circom`),
      {
        include: ['../node_modules', '../node_modules/@zk-kit/binary-merkle-root.circom/src'],
      }
    );

    const inputs = {
      signature: [
        [
          '11897043862654108222',
          '6687976630675743167',
          '6842677606991059234',
          '3933303995770833589',
        ],
        [
          '10364704208062614840',
          '21394470794141451286901280378935131115',
          '0',
          '15812853153589603704',
        ],
      ],
      pubKey: [
        [
          '1647443686294582730',
          '7524809848328723651',
          '2690299118416708846',
          '2230381215521625212',
        ],
        [
          '12063856007545978738',
          '2856046104882309217',
          '14084651496056034469',
          '2603012891351374004',
        ],
      ],
      hashParsed: [
        0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
        1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1,
        0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0,
        1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0,
        0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0,
        1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1,
        0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0,
        1, 0, 1, 1, 1, 0, 1, 0,
      ],
    };

    try {
      const witness = await circuit.calculateWitness(inputs);
      await circuit.checkConstraints(witness);
      throw new Error('Test failed: Invalid signature was verified.');
    } catch (err) {
      if (!(err as Error).message.includes('Num2Bits')) {
        throw err;
      }
    }
  });

  it('should reduce the final signature addition mod n', async function () {
    this.timeout(0);
    const circuit = await wasmTester(
      path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_p256.circom`),
      {
        include: ['../node_modules', '../node_modules/@zk-kit/binary-merkle-root.circom/src'],
      }
    );

    const inputs = {
      signature: [
        ['884452912994769579', '4834901530490986875', '0', '0'],
        [
          '17562291160714782030',
          '13611842547513532036',
          '18446744073709551615',
          '18446744069414584320',
        ],
      ],
      pubKey: [
        [
          '12004473255778836739',
          '5567425807485590512',
          '4612562821672420442',
          '781819838238377577',
        ],
        [
          '2517678904895060574',
          '13415238991415823444',
          '5824794594647846510',
          '14195660962316692941',
        ],
      ],
      hashParsed: [
        1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0,
        0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0,
        0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
        1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
        0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0,
        0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0,
        0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1,
        1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
        0, 0, 1, 0, 0, 0, 1, 1,
      ],
    };
    try {
      const witness = await circuit.calculateWitness(inputs);
      await circuit.checkConstraints(witness);
    } catch (err) {
      throw err;
    }
  });
});

function sign(message: Uint8Array, curve: string, hash: string, n: number, k: number) {
  const ec = new elliptic.ec(curve);

  const key = ec.genKeyPair();

  const messageHash = crypto.createHash(hash).update(message).digest();

  const signature = key.sign(messageHash, 'hex');
  const pubkey = key.getPublic();
  const hashParsed = [];
  Array.from(messageHash).forEach((x) =>
    hashParsed.push(...x.toString(2).padStart(8, '0').split(''))
  );

  return {
    signature: [
      ...splitToWords(BigInt(signature.r.toString()), k, n),
      ...splitToWords(BigInt(signature.s.toString()), k, n),
    ],
    pubKey: [
      splitToWords(BigInt(pubkey.getX().toString()), k, n),
      splitToWords(BigInt(pubkey.getY().toString()), k, n),
    ],
    hashParsed,
  };
}

function signOverflow(
  message: Uint8Array,
  curve: string,
  hash: string,
  n: number,
  k: number,
  overflowS: boolean
) {
  const ec = new elliptic.ec(curve);

  const key = ec.genKeyPair();

  const messageHash = crypto.createHash(hash).update(message).digest();

  const signature = key.sign(messageHash, 'hex');
  const pubkey = key.getPublic();
  const hashParsed = [];
  Array.from(messageHash).forEach((x) =>
    hashParsed.push(...x.toString(2).padStart(8, '0').split(''))
  );

  let r = BigInt(signature.r.toString(10));
  let s = BigInt(signature.s.toString(10));

  if (overflowS) {
    s = s + BigInt(ec.n.toString());
  } else {
    r = r + BigInt(ec.n.toString());
  }

  return {
    signature: [...splitToWords(r, k, n), ...splitToWords(s, k, n)],
    pubKey: [
      splitToWords(BigInt(pubkey.getX().toString()), k, n),
      splitToWords(BigInt(pubkey.getY().toString()), k, n),
    ],
    hashParsed,
  };
}
