<?php
declare(strict_types=1);

namespace Mdanter\Ecc\Tests\Math;

use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Math\GmpMathInterface;
use Mdanter\Ecc\Math\NumberTheory;
use Mdanter\Ecc\Tests\AbstractTestCase;

class NumberTheoryTest extends AbstractTestCase
{
    /**
     * @var
     */
    protected $compression_data;

    /**
     * @var
     */
    protected $sqrt_data;

    /**
     * @var \Mdanter\Ecc\Primitives\GeneratorPoint
     */
    protected $generator;

    protected function setUp()
    {
        // file containing a json array of {compressed=>'', decompressed=>''} values
        // of compressed and uncompressed ECDSA public keys (testing secp256k1 curve)
        $file_comp = TEST_DATA_DIR.'/compression.json';

        if (! file_exists($file_comp)) {
            $this->fail('Key compression input data not found');
        }

        $file_sqrt = TEST_DATA_DIR.'/square_root_mod_p.json';
        if (! file_exists($file_sqrt)) {
            $this->fail('Square root input data not found');
        }
        $this->generator = EccFactory::getSecgCurves()->generator256k1();
        $this->compression_data = json_decode(file_get_contents($file_comp));

        $this->sqrt_data = json_decode(file_get_contents($file_sqrt));
    }

    /**
     * @dataProvider getAdapters
     * @expectedException \LogicException
     */
    public function testSqrtDataWithNoRoots(GmpMathInterface $adapter)
    {
        $theory = $adapter->getNumberTheory();

        foreach ($this->sqrt_data->no_root as $r) {
            // todo: turn into adapter
            $a = gmp_init($r->a, 10);
            $p = gmp_init($r->p, 10);
            $theory->squareRootModP($a, $p);
        }
    }
    /**
     * @dataProvider getAdapters
     */
    public function testSqrtDataWithRoots(GmpMathInterface $adapter)
    {
        $theory = $adapter->getNumberTheory();

        foreach ($this->sqrt_data->has_root as $r) {
            $a = gmp_init($r->a, 10);
            $p = gmp_init($r->p, 10);
            $root1 = $theory->squareRootModP($a, $p);
            $root2 = $adapter->sub($p, $root1);
            $this->assertTrue(in_array(gmp_strval($root1, 10), $r->res));
            $this->assertTrue(in_array(gmp_strval($root2, 10), $r->res));
        }
    }

    /**
     * @dataProvider getAdapters
     */
    public function testCompressionConsistency(GmpMathInterface $adapter)
    {
        $theory = $adapter->getNumberTheory();
        $this->_doCompressionConsistence($adapter, $theory);
    }

    public function _doCompressionConsistence(GmpMathInterface $adapter, NumberTheory $theory)
    {
        foreach ($this->compression_data as $o) {
            // Try and regenerate the y coordinate from the parity byte
            // '04' . $x_coordinate . determined y coordinate should equal $o->decompressed
            // Tests squareRootModP which touches most functions in NumberTheory
            $y_byte = substr($o->compressed, 0, 2);
            $x_coordinate = substr($o->compressed, 2);

            $x = gmp_init($x_coordinate, 16);

            // x^3
            $x3 = $adapter->powmod($x, gmp_init(3, 10), $this->generator->getCurve()->getPrime());

            // y^2
            $y2 = $adapter->add(
                $x3,
                $this->generator->getCurve()->getB()
            );

            // y0 = sqrt(y^2)
            $y0 = $theory->squareRootModP(
                $y2,
                $this->generator->getCurve()->getPrime()
            );

            if ($y_byte == '02') {
                $y_coordinate = ($adapter->equals($adapter->mod($y0, gmp_init(2, 10)), gmp_init('0')))
                    ? gmp_strval($y0, 16)
                    : gmp_strval(gmp_sub($this->generator->getCurve()->getPrime(), $y0), 16);
            } else {
                $y_coordinate = ($adapter->equals($adapter->mod($y0, gmp_init(2, 10)), gmp_init('0')))
                    ? gmp_strval(gmp_sub($this->generator->getCurve()->getPrime(), $y0), 16)
                    : gmp_strval($y0, 16);
            }
            $y_coordinate = str_pad($y_coordinate, 64, '0', STR_PAD_LEFT);

            // Successfully regenerated uncompressed ECDSA key from the x coordinate and the parity byte.
            $this->assertTrue('04'.$x_coordinate.$y_coordinate == $o->decompressed);
        }
    }

    /**
     * @dataProvider getAdapters
     */
    public function testModFunction(GmpMathInterface $math)
    {
        // $o->compressed, $o->decompressed public key.
        // Check that we can compress a key properly (tests $math->mod())
        foreach ($this->compression_data as $o) {
            $prefix = substr($o->decompressed, 0, 2); // will be 04.

            $this->assertEquals('04', $prefix);

            // hex encoded (X,Y) coordinate of ECDSA public key.
            $x = substr($o->decompressed, 2, 64);
            $y = substr($o->decompressed, 66, 64);

            // y % 2 == 0       - true: y is even(02) / false: y is odd(03)
            $mod = $math->mod(gmp_init($y, 16), gmp_init(2, 10));
            $compressed = '0'.(($math->equals($mod, gmp_init(0))) ? '2' : '3').$x;

            // Check that the mod function reported the parity for the y value.
            $this->assertTrue($compressed === $o->compressed);
        }
    }
}
