<?php

namespace Firebase\JWT;

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use UnexpectedValueException;

class JWKTest extends TestCase
{
    private static $keys;
    private static $privKey1;
    private static $privKey2;

    public function testMissingKty()
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('JWK must contain a "kty" parameter');

        $badJwk = ['kid' => 'foo'];
        $keys = JWK::parseKeySet(['keys' => [$badJwk]]);
    }

    public function testInvalidAlgorithm()
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('No supported algorithms found in JWK Set');

        $badJwk = ['kty' => 'BADTYPE', 'alg' => 'RSA256'];
        $keys = JWK::parseKeySet(['keys' => [$badJwk]]);
    }

    public function testParsePrivateKey()
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('RSA private keys are not supported');

        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
            true
        );
        $jwkSet['keys'][0]['d'] = 'privatekeyvalue';

        JWK::parseKeySet($jwkSet);
    }

    public function testParsePrivateKeyWithoutAlg()
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('JWK must contain an "alg" parameter');

        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
            true
        );
        unset($jwkSet['keys'][0]['alg']);

        JWK::parseKeySet($jwkSet);
    }

    public function testParsePrivateKeyWithoutAlgWithDefaultAlgParameter()
    {
        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
            true
        );
        unset($jwkSet['keys'][0]['alg']);

        $jwks = JWK::parseKeySet($jwkSet, 'foo');
        $this->assertSame('foo', $jwks['jwk1']->getAlgorithm());
    }

    public function testParseKeyWithEmptyDValue()
    {
        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
            true
        );

        // empty or null values are ok
        $jwkSet['keys'][0]['d'] = null;

        $keys = JWK::parseKeySet($jwkSet);
        $this->assertTrue(\is_array($keys));
    }

    public function testParseJwkKeySet()
    {
        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/rsa-jwkset.json'),
            true
        );
        $keys = JWK::parseKeySet($jwkSet);
        $this->assertTrue(\is_array($keys));
        $this->assertArrayHasKey('jwk1', $keys);
        self::$keys = $keys;
    }

    public function testParseJwkKey_empty()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('JWK must not be empty');

        JWK::parseKeySet(['keys' => [[]]]);
    }

    public function testParseJwkKeySet_empty()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('JWK Set did not contain any keys');

        JWK::parseKeySet(['keys' => []]);
    }

    /**
     * @depends testParseJwkKeySet
     */
    public function testDecodeByJwkKeySetTokenExpired()
    {
        $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem');
        $payload = ['exp' => strtotime('-1 hour')];
        $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1');

        $this->expectException(ExpiredException::class);

        JWT::decode($msg, self::$keys);
    }

    /**
     * @dataProvider provideDecodeByJwkKeySet
     */
    public function testDecodeByJwkKeySet($pemFile, $jwkFile, $alg, $keyId)
    {
        $privKey1 = file_get_contents(__DIR__ . '/data/' . $pemFile);
        $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
        $msg = JWT::encode($payload, $privKey1, $alg, $keyId);

        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/' . $jwkFile),
            true
        );

        $keys = JWK::parseKeySet($jwkSet);
        $result = JWT::decode($msg, $keys);

        $this->assertSame('foo', $result->sub);
    }

    public function provideDecodeByJwkKeySet()
    {
        return [
            ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256', 'jwk1'],
            ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256', 'jwk1'],
            ['ecdsa384-private.pem', 'ec-jwkset.json', 'ES384', 'jwk4'],
            ['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA', 'jwk1'],
        ];
    }

    /**
     * @depends testParseJwkKeySet
     */
    public function testDecodeByMultiJwkKeySet()
    {
        $privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem');
        $payload = ['sub' => 'bar', 'exp' => strtotime('+10 seconds')];
        $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2');

        $result = JWT::decode($msg, self::$keys);

        $this->assertSame('bar', $result->sub);
    }

    public function testDecodeByOctetJwkKeySet()
    {
        $jwkSet = json_decode(
            file_get_contents(__DIR__ . '/data/octet-jwkset.json'),
            true
        );
        $keys = JWK::parseKeySet($jwkSet);
        $payload = ['sub' => 'foo', 'exp' => strtotime('+10 seconds')];
        foreach ($keys as $keyId => $key) {
            $msg = JWT::encode($payload, $key->getKeyMaterial(), $key->getAlgorithm(), $keyId);
            $result = JWT::decode($msg, $keys);

            $this->assertSame('foo', $result->sub);
        }
    }

    public function testOctetJwkMissingK()
    {
        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage('k not set');

        $badJwk = ['kty' => 'oct', 'alg' => 'HS256'];
        $keys = JWK::parseKeySet(['keys' => [$badJwk]]);
    }

    public function testParseKey()
    {
        // Use a known module and exponent, and ensure it parses as expected
        $jwk = [
            'alg' => 'RS256',
            'kty' => 'RSA',
            'n' => 'hsYvCPtkUV7SIxwkOkJsJfhwV_CMdXU5i0UmY2QEs-Pa7v0-0y-s4EjEDtsQ8Yow6hc670JhkGBcMzhU4DtrqNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmXi3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth_GoyU5S8-G3OqIqLlQCwxkI5s-g2qvg_aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2JArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv-4X1CXEK8bO5VesIeRX0oDf7UoM-pVAw',
            'use' => 'sig',
            'e' => 'AQAB',
            'kid' => '838c06c62046c2d948affe137dd5310129f4d5d1'
        ];

        $key = JWK::parseKey($jwk);
        $this->assertNotNull($key);

        $openSslKey = $key->getKeyMaterial();
        $pubKey = openssl_pkey_get_public($openSslKey);
        $keyData = openssl_pkey_get_details($pubKey);

        $expectedPublicKey = <<<EOF
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhsYvCPtkUV7SIxwkOkJs
JfhwV/CMdXU5i0UmY2QEs+Pa7v0+0y+s4EjEDtsQ8Yow6hc670JhkGBcMzhU4Dtr
qNGROXebyOse5FX0m0UvWo1qXqNTf28uBKB990mY42Icr8sGjtOw8ajyT9kufbmX
i3eZKagKpG0TDGK90oBEfoGzCxoFT87F95liNth/GoyU5S8+G3OqIqLlQCwxkI5s
+g2qvg/aooALfh1rhvx2wt4EJVMSrdnxtPQSPAtZBiw5SwCnVglc6OnalVNvAB2J
ArbqC9GAzzz9pApAk28SYg5a4hPiPyqwRv+4X1CXEK8bO5VesIeRX0oDf7UoM+pV
AwIDAQAB
-----END PUBLIC KEY-----

EOF;

        $this->assertEquals($expectedPublicKey, $keyData['key']);
    }
}
