<?php

declare(strict_types=1);

namespace ComerciaGlobalPayments\Tests\AddonPayments\SDK\Api\Unit;

use ComerciaGlobalPayments\AddonPayments\SDK\Api\EPGJs;
use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\Aes256Cbc;
use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\Aes256Ecb;
use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\CypherInterface;
use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\JWT\PrivatePublicKeysEncoder;
use ComerciaGlobalPayments\Tests\AddonPayments\SDK\Api\TestUtilityTrait;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use PHPUnit\Framework\TestCase;

/**
 * Unit testing of API clients common methods.
 *
 * @uses \ComerciaGlobalPayments\AddonPayments\SDK\Utility\NestedArray
 * @uses \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient
 * @uses \ComerciaGlobalPayments\AddonPayments\SDK\Api\EPGJs
 * @uses \ComerciaGlobalPayments\AddonPayments\SDK\Encryption\JWT\PrivatePublicKeysEncoder
 *
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.StaticAccess)
 */
class ApiClientTest extends TestCase
{
    use TestUtilityTrait;

    /**
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\EPGJs::getEndpoints
     * @dataProvider getApiClients
     */
    public function testGetEndpoints(string $class): void
    {
        $stagingEndpoints = \call_user_func([$class, 'getEndpoints']);
        $liveEndpoints = \call_user_func([$class, 'getEndpoints'], true);
        self::assertNotEmpty(
            $stagingEndpoints,
            'Tests API client returns staging endpoints'
        );
        self::assertNotEmpty(
            $liveEndpoints,
            'Tests API client returns live endpoints'
        );
        self::assertEqualsCanonicalizing(
            array_keys($stagingEndpoints),
            array_keys($liveEndpoints),
            'Tests API client returns same endpoints operations for each mode'
        );
        self::assertNotEqualsCanonicalizing(
            $stagingEndpoints,
            $liveEndpoints,
            'Tests API client returns different endpoints for each mode'
        );
    }

    /**
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\EPGJs::getAllowedEncryptionMethods
     * @dataProvider getApiClients
     */
    public function testGetAllowedEncryptionMethods(string $class): void
    {
        $methods = \call_user_func([$class, 'getAllowedEncryptionMethods']);
        self::assertNotEmpty(
            $methods,
            'Test API client returns encryption methods'
        );
        $expected = [
            Aes256Cbc::NAME => Aes256Cbc::class,
            Aes256Ecb::NAME => Aes256Ecb::class,
        ];
        self::assertEqualsCanonicalizing(
            array_keys($expected),
            array_keys($methods),
            'Test API client returns expected encryption keys'
        );
        self::assertEqualsCanonicalizing(
            array_values($expected),
            array_values($methods),
            'Test API client returns expected encryption classes'
        );
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @dataProvider getApiClients
     */
    public function testConstructWithNoArguments(string $class): void
    {
        $this->expectException(\ArgumentCountError::class);
        $this->expectExceptionMessageRegExp(
            '/^Too few arguments to function (.+)::__construct\(\).+/'
        );
        new $class();
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @dataProvider getApiClients
     */
    public function testConstructWithNoMerchantId(
        string $class,
        array $configuration
    ): void {
        $parameter = 'merchantId';
        $this->assertConstructorInvalidConfiguration(
            $class,
            $configuration,
            $parameter
        );
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @dataProvider getApiClients
     */
    public function testConstructWithNoMerchantKey(
        string $class,
        array $configuration
    ): void {
        $parameter = 'merchantKey';
        $this->assertConstructorInvalidConfiguration(
            $class,
            $configuration,
            $parameter
        );
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @dataProvider getApiClients
     */
    public function testConstructWithNoMerchantSecret(
        string $class,
        array $configuration
    ): void {
        $parameter = 'merchantSecret';
        $this->assertConstructorInvalidConfiguration(
            $class,
            $configuration,
            $parameter
        );
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @dataProvider getApiClients
     */
    public function testConstructWithInvalidEncryptionMethod(
        string $class,
        array $configuration
    ): void {
        $parameter = 'encryption';
        $this->assertConstructorInvalidConfiguration(
            $class,
            $configuration,
            $parameter
        );
    }

    /**
     * @depends      testGetEndpoints
     * @depends      testGetAllowedEncryptionMethods
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::__construct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::getEndpoint
     * @dataProvider getApiClients
     */
    public function testInvalidOperation(
        string $class,
        array $configuration
    ): void {
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration);
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessageRegExp(
            '/^Invalid operation \'spectro-patronum\', allowed operations are.+/'
        );
        $apiClient->getEndpoint('spectro-patronum');
    }

    /**
     * @depends      testInvalidOperation
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient
     * @dataProvider getApiClients
     */
    public function testConstruct(string $class, array $configuration): void
    {
        $stagingEndpoints = \call_user_func([$class, 'getEndpoints']);
        $liveEndpoints = \call_user_func([$class, 'getEndpoints'], true);
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient $stagingClient */
        $stagingClient = new $class($configuration);
        self::assertFalse(
            $stagingClient->isLive(),
            'Test created client in staging mode'
        );
        $liveConfiguration = array_merge([], $configuration);
        $liveConfiguration['live'] = true;
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient $liveClient */
        $liveClient = new $class($liveConfiguration);
        self::assertTrue(
            $liveClient->isLive(),
            'Test created client in live mode'
        );
        $randomOperation = current(
            \array_slice(
                array_values(array_flip($stagingEndpoints)),
                random_int(0, \count($stagingEndpoints) - 1),
                1
            )
        );
        self::assertNotEmpty(
            $randomOperation,
            'Test is posible to get a random operation'
        );
        self::assertContains(
            $stagingClient->getEndpoint($randomOperation),
            $stagingEndpoints,
            'Test that an operation in staging returns actual staging endpoint'
        );
        self::assertContains(
            $liveClient->getEndpoint($randomOperation),
            $liveEndpoints,
            'Test that an operation in staging returns actual live endpoint'
        );
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration);
        self::assertInstanceOf(
            CypherInterface::class,
            $apiClient->getEncryption(),
            'Test client returns a default encryption class'
        );
        $altConfiguration = array_merge([], $configuration);
        $altConfiguration['encryption'] = Aes256Cbc::NAME;
        $apiClient = new $class($altConfiguration);
        self::assertInstanceOf(
            Aes256Cbc::class,
            $apiClient->getEncryption(),
            'Test client returns CBC encryption class'
        );
        $altConfiguration['encryption'] = Aes256Ecb::NAME;
        $apiClient = new $class($altConfiguration);
        self::assertInstanceOf(
            Aes256Ecb::class,
            $apiClient->getEncryption(),
            'Test client returns ECB encryption class'
        );
    }

    /**
     * @depends      testConstruct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::generateTransactionToken
     * @dataProvider getApiClients
     */
    public function testGenerateTransactionsToken(
        string $class,
        array $configuration
    ): void {
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration);
        $txRandomNumber = random_int(1, 10);
        $transactions = [];
        do {
            $transactions[] = (string) random_int(100, 1000);
        } while (\count($transactions) < $txRandomNumber);
        $merchantId = $configuration['merchantId'];
        $merchantSecret = $configuration['merchantSecret'];
        $expected = md5(
            sprintf(
                '%s.%s.%s',
                $merchantId,
                implode(';', $transactions),
                $merchantSecret
            )
        );
        $actual = $apiClient->generateTransactionToken($transactions);
        self::assertEquals(
            $expected,
            $actual,
            'Test transaction token is generated properly'
        );
    }

    /**
     * @depends      testConstruct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::validate
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::getValidator
     * @dataProvider getApiClients
     */
    public function testValidate(string $class, array $configuration): void
    {
        $request = new SampleRequest();
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration);
        $request->setFoo(['bar']);
        self::assertEmpty(
            $apiClient->validate($request),
            'Test request passes validation'
        );
        $request->setFoo(['invalid-string']);
        self::assertNotEmpty(
            $apiClient->validate($request),
            'Test request do not passes validation'
        );
    }

    /**
     * @depends      testConstruct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::sendRequest
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::getHttpClient
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::getLogger
     * @dataProvider getApiClients
     */
    public function testSendRequest(string $class, array $configuration): void
    {
        $httpClientMock = $this->mockServer(200, ['foo' => 'bar']);
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration, $httpClientMock);
        $response = $apiClient->sendRequest('POST', 'http://mock.api.net/flip', ['X-Header' => 'baz'], ['bar' => 'foo']);
        self::assertEquals(200, $response->getStatusCode(), 'Test response is 200 OK');
        $httpClientMock = $this->mockServer(200);
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration, $httpClientMock);
        $response = $apiClient->sendRequest('GET', 'http://mock.api.net/none');
        self::assertEquals(200, $response->getStatusCode(), 'Test response is 200 OK with empty body');
    }

    /**
     * @depends      testConstruct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::sendRequest
     * @dataProvider getApiClients
     */
    public function testSendRequest4XXException(string $class, array $configuration): void
    {
        $randomCode = random_int(400, 499);
        $this->expectException(ClientException::class);
        $this->expectExceptionCode($randomCode);
        $httpClientMock = $this->mockServer($randomCode);
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration, $httpClientMock);
        $apiClient->sendRequest('POST', 'http://mock.api.net/post');
    }

    /**
     * @depends      testConstruct
     * @covers       \ComerciaGlobalPayments\AddonPayments\SDK\Api\AbstractApiClient::sendRequest
     * @dataProvider getApiClients
     */
    public function testSendRequest5XXException(string $class, array $configuration): void
    {
        $randomCode = random_int(500, 599);
        $this->expectException(ServerException::class);
        $this->expectExceptionCode($randomCode);
        $httpClientMock = $this->mockServer($randomCode);
        /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Api\ApiClientInterface $apiClient */
        $apiClient = new $class($configuration, $httpClientMock);
        $apiClient->sendRequest('POST', 'http://mock.api.net/post');
    }

    /**
     * @return array[]
     */
    public function getApiClients(): array
    {
        $keysDirectory = \dirname(__DIR__, 3).'/fixtures/keys/';

        return [
            [
                EPGJs::class,
                [
                    'merchantId' => 3500,
                    'merchantKey' => '498289b3-1cb5-4ae9-ad57-66d92f77d7b3',
                    // @fixme Confirm key and secret are not the same.
                    'merchantSecret' => md5(
                        '498289b3-1cb5-4ae9-ad57-66d92f77d7b3'
                    ),
                    'defaultProductId' => 35000001,
                    'privateKeyFile' => $keysDirectory.'good.pem',
                    'publicKeyFile' => $keysDirectory.'good.pub.pem',
                    'privateKeyAlgorithm' => PrivatePublicKeysEncoder::DEFAULT_ALGORITHM,
                ],
            ],
        ];
    }
}
