<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Paypal\Test\Unit\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\DataObject;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\HTTP\ZendClient;
use Magento\Framework\HTTP\ZendClientFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Payment\Model\Info;
use Magento\Payment\Model\InfoInterface;
use Magento\Payment\Model\Method\ConfigInterface;
use Magento\Payment\Model\Method\ConfigInterfaceFactory;
use Magento\Paypal\Model\Config;
use Magento\Paypal\Model\Payflow\Service\Gateway;
use Magento\Paypal\Model\PayflowConfig;
use Magento\Paypal\Model\Payflowpro;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class PayflowproTest extends TestCase
{
    /**
     * @var Payflowpro
     */
    protected $payflowpro;

    /**
     * @var ObjectManager
     */
    protected $helper;

    /**
     * @var ConfigInterface|MockObject
     */
    protected $configMock;

    /**
     * @var StoreManagerInterface|MockObject
     */
    protected $storeManagerMock;

    /**
     * @var Gateway|MockObject
     */
    protected $gatewayMock;

    /**
     * @var ScopeConfigInterface|MockObject
     */
    protected $scopeConfigMock;

    /**
     * @var ManagerInterface|MockObject
     */
    private $eventManager;

    protected function setUp(): void
    {
        $this->configMock = $this->getMockBuilder(PayflowConfig::class)
            ->disableOriginalConstructor()
            ->getMock();
        $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
            ->getMockForAbstractClass();
        $this->gatewayMock = $this->getMockBuilder(Gateway::class)
            ->disableOriginalConstructor()
            ->getMock();
        $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
            ->getMockForAbstractClass();

        $configFactoryMock = $this->getMockBuilder(ConfigInterfaceFactory::class)
            ->setMethods(['create'])
            ->disableOriginalConstructor()
            ->getMock();
        $configFactoryMock->method('create')
            ->willReturn($this->configMock);

        $client = $this->getMockBuilder(ZendClient::class)
            ->getMock();

        $clientFactory = $this->getMockBuilder(ZendClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock();
        $clientFactory->method('create')->willReturn($client);

        $this->eventManager = $this->getMockBuilder(ManagerInterface::class)
            ->getMockForAbstractClass();

        $this->helper = new ObjectManager($this);
        $this->payflowpro = $this->helper->getObject(
            Payflowpro::class,
            [
                'eventDispatcher' => $this->eventManager,
                'configFactory' => $configFactoryMock,
                'httpClientFactory' => $clientFactory,
                'storeManager' => $this->storeManagerMock,
                'gateway' => $this->gatewayMock,
                'scopeConfig' => $this->scopeConfigMock
            ]
        );
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::canVoid
     *
     * @param string $message
     * @param int|null $amountPaid
     * @param bool $expected
     * @dataProvider canVoidDataProvider
     */
    public function testCanVoid($message, $amountPaid, $expected)
    {
        /** @var Payment|MockObject $payment */
        $payment = $this->getMockBuilder(Payment::class)
            ->disableOriginalConstructor()
            ->getMock();
        $payment->method('getAmountPaid')->willReturn($amountPaid);
        $this->payflowpro->setInfoInstance($payment);

        $this->assertEquals($expected, $this->payflowpro->canVoid(), $message);
    }

    /**
     * @return array
     */
    public function canVoidDataProvider()
    {
        return [
            ["Can void transaction if order's paid amount not set", null, true],
            ["Can void transaction if order's paid amount equals zero", 0, true],
            ["Can't void transaction if order's paid amount greater than zero", 10, false],
        ];
    }

    public function testCanCapturePartial()
    {
        $this->assertTrue($this->payflowpro->canCapturePartial());
    }

    public function testCanRefundPartialPerInvoice()
    {
        $this->assertTrue($this->payflowpro->canRefundPartialPerInvoice());
    }

    /**
     * test for _buildBasicRequest (BDCODE)
     */
    public function testFetchTransactionInfoForBN()
    {
        $response = $this->getGatewayResponseObject();

        $this->gatewayMock->expects($this->once())
            ->method('postRequest')
            ->willReturn($response);
        $this->initStoreMock();
        $this->configMock->expects($this->once())->method('getBuildNotationCode')
            ->willReturn('BNCODE');
        $payment = $this->getMockBuilder(Info::class)
            ->addMethods(['setTransactionId'])
            ->onlyMethods(['__wakeup'])
            ->disableOriginalConstructor()
            ->getMock();
        $payment->expects($this->once())->method('setTransactionId')->willReturnSelf();
        $this->payflowpro->fetchTransactionInfo($payment, 'AD49G8N825');
    }

    /**
     * @param $response
     * @dataProvider setTransStatusDataProvider
     */
    public function testSetTransStatus($response, $paymentExpected)
    {
        $payment = $this->helper->getObject(Info::class);
        $this->payflowpro->setTransStatus($payment, $response);
        $this->assertEquals($paymentExpected->getData(), $payment->getData());
    }

    /**
     * @return array
     */
    public function setTransStatusDataProvider()
    {
        return [
            [
                'response' => new DataObject(
                    [
                        'pnref' => 'V19A3D27B61E',
                        'result_code' => Payflowpro::RESPONSE_CODE_APPROVED,
                    ]
                ),
                'paymentExpected' => new DataObject(
                    [
                        'transaction_id' => 'V19A3D27B61E',
                        'is_transaction_closed' => 0,
                    ]
                ),
            ],
            [
                'response' => new DataObject(
                    [
                        'pnref' => 'V19A3D27B61E',
                        'result_code' => Payflowpro::RESPONSE_CODE_FRAUDSERVICE_FILTER
                    ]
                ),
                'paymentExpected' => new DataObject(
                    [
                        'transaction_id' => 'V19A3D27B61E',
                        'is_transaction_closed' => 0,
                        'is_transaction_pending' => true,
                        'is_fraud_detected' => true,
                    ]
                ),
            ],
        ];
    }

    /**
     * @param array $expectsMethods
     * @param bool $result
     *
     * @dataProvider dataProviderForTestIsActive
     */
    public function testIsActive(array $expectsMethods, $result)
    {
        $storeId = 15;

        $i = 0;
        foreach ($expectsMethods as $method => $isActive) {
            $this->scopeConfigMock->expects($this->at($i++))
                ->method('getValue')
                ->with(
                    "payment/{$method}/active",
                    ScopeInterface::SCOPE_STORE,
                    $storeId
                )->willReturn($isActive);
        }

        $this->assertEquals($result, $this->payflowpro->isActive($storeId));
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::capture
     */
    public function testCaptureWithBuildPlaceRequest()
    {
        $paymentMock = $this->getPaymentMock();
        $orderMock = $this->getOrderMock();

        // test case to build basic request
        $paymentMock->expects(static::once())
            ->method('getAdditionalInformation')
            ->with('pnref')
            ->willReturn(false);
        $paymentMock->expects(static::once())
            ->method('getParentTransactionId')
            ->willReturn(false);

        $paymentMock->expects(static::exactly(2))
            ->method('getOrder')
            ->willReturn($orderMock);

        $response = $this->execGatewayRequest();
        $amount = 23.03;
        $this->payflowpro->capture($paymentMock, $amount);
        static::assertEquals($response['pnref'], $paymentMock->getTransactionId());
        static::assertFalse((bool)$paymentMock->getIsTransactionPending());
    }

    /**
     * @return array
     */
    public function dataProviderCaptureAmountRounding()
    {
        return [
            [
                'amount' => 14.13999999999999999999999999999999999999999999999999,
                'setAmount' => 49.99,
                'expectedResult' => 14.14
            ],
            [
                'amount' => 14.13199999999999999999999999999999999999999999999999,
                'setAmount' => 49.99,
                'expectedResult' => 14.13,
            ],
            [
                'amount' => 14.14,
                'setAmount' => 49.99,
                'expectedResult' => 14.14,
            ],
            [
                'amount' => 14.13999999999999999999999999999999999999999999999999,
                'setAmount' => 14.14,
                'expectedResult' => 0,
            ]
        ];
    }

    /**
     * @param float $amount
     * @param float $setAmount
     * @param float $expectedResult
     * @dataProvider dataProviderCaptureAmountRounding
     */
    public function testCaptureAmountRounding($amount, $setAmount, $expectedResult)
    {
        $paymentMock = $this->getPaymentMock();
        $orderMock = $this->getMockBuilder(Order::class)
            ->disableOriginalConstructor()
            ->getMock();

        $infoInstanceMock = $this->getMockForAbstractClass(
            InfoInterface::class,
            [],
            '',
            false,
            false,
            false,
            ['getAmountAuthorized','hasAmountPaid']
        );

        $infoInstanceMock->expects($this->once())
            ->method('getAmountAuthorized')
            ->willReturn($setAmount);
        $infoInstanceMock->expects($this->once())
            ->method('hasAmountPaid')
            ->willReturn(true);
        $this->payflowpro->setData('info_instance', $infoInstanceMock);

        // test case to build basic request
        $paymentMock->expects($this->once())
            ->method('getAdditionalInformation')
            ->with('pnref')
            ->willReturn(false);
        $paymentMock->expects($this->any())
            ->method('getParentTransactionId')
            ->willReturn(true);

        $paymentMock->expects($this->once())
            ->method('getOrder')
            ->willReturn($orderMock);

        $this->initStoreMock();
        $response = $this->getGatewayResponseObject();
        $this->gatewayMock->expects($this->once())
            ->method('postRequest')
            ->with(
                $this->callback(function ($request) use ($expectedResult) {
                    return is_callable([$request, 'getAmt']) && $request->getAmt() == $expectedResult;
                }),
                $this->isInstanceOf(PayflowConfig::class)
            )
            ->willReturn($response);

        $this->payflowpro->capture($paymentMock, $amount);

        $this->assertEquals($response['pnref'], $paymentMock->getTransactionId());
        $this->assertFalse((bool)$paymentMock->getIsTransactionPending());
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::authorize
     */
    public function testAuthorize()
    {
        $paymentMock = $this->getPaymentMock();
        $orderMock = $this->getOrderMock();

        $paymentMock->expects(static::exactly(2))
            ->method('getOrder')
            ->willReturn($orderMock);

        $response = $this->execGatewayRequest();
        $amount = 43.20;
        $this->payflowpro->authorize($paymentMock, $amount);
        static::assertEquals($response['pnref'], $paymentMock->getTransactionId());
        static::assertFalse((bool)$paymentMock->getIsTransactionPending());
    }

    /**
     * @return array
     */
    public function dataProviderForTestIsActive()
    {
        return [
            [
                'expectsMethods' => [
                    Config::METHOD_PAYFLOWPRO => 0,
                    Config::METHOD_PAYMENT_PRO => 1,
                ],
                'result' => true,
            ],
            [
                'expectsMethods' => [
                    Config::METHOD_PAYFLOWPRO => 1
                ],
                'result' => true,
            ],
            [
                'expectsMethods' => [
                    Config::METHOD_PAYFLOWPRO => 0,
                    Config::METHOD_PAYMENT_PRO => 0,
                ],
                'result' => false,
            ],
        ];
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::refund()
     */
    public function testRefund()
    {
        /** @var Payment $paymentMock */
        $paymentMock = $this->getPaymentMock();

        $response = $this->execGatewayRequest();

        $amount = 213.04;
        $this->payflowpro->refund($paymentMock, $amount);
        static::assertEquals($response['pnref'], $paymentMock->getTransactionId());
        static::assertTrue($paymentMock->getIsTransactionClosed());
    }

    /**
     * Create mock object for store model
     * @return void
     */
    protected function initStoreMock()
    {
        $storeId = 27;
        $storeMock = $this->getMockBuilder(Store::class)
            ->disableOriginalConstructor()
            ->setMethods(['getId'])
            ->getMock();
        $this->storeManagerMock->expects(static::once())
            ->method('getStore')
            ->willReturn($storeMock);
        $storeMock->expects(static::once())
            ->method('getId')
            ->willReturn($storeId);
    }

    /**
     * Create response object for Payflowpro gateway
     * @return \Magento\Framework\DataObject
     */
    protected function getGatewayResponseObject()
    {
        return new DataObject(
            [
                'result' => '0',
                'pnref' => 'V19A3D27B61E',
                'respmsg' => 'Approved',
                'authcode' => '510PNI',
                'hostcode' => 'A',
                'request_id' => 'f930d3dc6824c1f7230c5529dc37ae5e',
                'result_code' => '0',
            ]
        );
    }

    /**
     * Call payflow gateway request and return response object
     * @return \Magento\Framework\DataObject
     */
    protected function execGatewayRequest()
    {
        $this->initStoreMock();
        $response = $this->getGatewayResponseObject();
        $this->gatewayMock->expects(static::once())
            ->method('postRequest')
            ->with(
                $this->isInstanceOf(DataObject::class),
                $this->isInstanceOf(PayflowConfig::class)
            )
            ->willReturn($response);
        return $response;
    }

    /**
     * Create mock object for payment model
     * @return MockObject
     */
    protected function getPaymentMock()
    {
        $paymentMock = $this->getMockBuilder(Info::class)
            ->disableOriginalConstructor()
            ->setMethods([
                'getAdditionalInformation', 'getParentTransactionId', 'getOrder',
                'getCcNumber', 'getCcExpMonth', 'getCcExpYear', 'getCcCid'
            ])
            ->getMock();

        $cardData = [
            'number' => 4111111111111111,
            'month' => 12,
            'year' => 18,
            'cvv' => 123
        ];
        $paymentMock->expects(static::any())
            ->method('getCcNumber')
            ->willReturn($cardData['number']);
        $paymentMock->expects(static::any())
            ->method('getCcExpMonth')
            ->willReturn($cardData['month']);
        $paymentMock->expects(static::any())
            ->method('getCcExpYear')
            ->willReturn($cardData['year']);
        $paymentMock->expects(static::any())
            ->method('getCcCid')
            ->willReturn($cardData['cvv']);
        return $paymentMock;
    }

    /**
     * Create mock object for order model
     * @return MockObject
     */
    protected function getOrderMock()
    {
        $orderData = [
            'currency' => 'USD',
            'id' => 4,
            'increment_id' => '0000004'
        ];
        $orderMock = $this->getMockBuilder(Order::class)
            ->disableOriginalConstructor()
            ->setMethods(['getBaseCurrencyCode', 'getIncrementId', 'getId', 'getBillingAddress', 'getShippingAddress'])
            ->getMock();

        $orderMock->expects(static::once())
            ->method('getId')
            ->willReturn($orderData['id']);
        $orderMock->expects(static::once())
            ->method('getBaseCurrencyCode')
            ->willReturn($orderData['currency']);
        $orderMock->expects(static::atLeastOnce())
            ->method('getIncrementId')
            ->willReturn($orderData['increment_id']);
        return $orderMock;
    }

    public function testPostRequest()
    {
        $expectedResult = new DataObject();

        $request = new DataObject();

        /** @var ConfigInterface $config */
        $config = $this->getMockForAbstractClass(ConfigInterface::class);

        $this->gatewayMock->expects(static::once())
            ->method('postRequest')
            ->with($request, $config)
            ->willReturn($expectedResult);

        static::assertSame($expectedResult, $this->payflowpro->postRequest($request, $config));
    }

    public function testPostRequestException()
    {
        $this->expectException('Magento\Framework\Exception\LocalizedException');
        $this->expectExceptionMessage(
            'Payment Gateway is unreachable at the moment. Please use another payment option.'
        );
        $request = new DataObject();

        /** @var ConfigInterface $config */
        $config = $this->getMockForAbstractClass(ConfigInterface::class);

        $this->gatewayMock->expects(static::once())
            ->method('postRequest')
            ->with($request, $config)
            ->willThrowException(new \Zend_Http_Client_Exception());

        $this->payflowpro->postRequest($request, $config);
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::addRequestOrderInfo
     */
    public function testAddRequestOrderInfo()
    {
        $orderData = [
            'id' => 1,
            'increment_id' => '0000001'
        ];
        $data = [
            'ponum' => $orderData['id'],
            'custref' => $orderData['increment_id'],
            'invnum' => $orderData['increment_id'],
            'comment1' => $orderData['increment_id']
        ];
        $expectedData = new DataObject($data);
        $actualData = new DataObject();

        $orderMock = $this->getMockBuilder(Order::class)
            ->disableOriginalConstructor()
            ->setMethods(['getIncrementId', 'getId'])
            ->getMock();
        $orderMock->expects(static::once())
            ->method('getId')
            ->willReturn($orderData['id']);
        $orderMock->expects(static::atLeastOnce())
            ->method('getIncrementId')
            ->willReturn($orderData['increment_id']);

        $this->payflowpro->addRequestOrderInfo($actualData, $orderMock);

        $this->assertEquals($expectedData, $actualData);
    }

    /**
     * @covers \Magento\Paypal\Model\Payflowpro::assignData
     */
    public function testAssignData()
    {
        $data = [
            'cc_type' => 'VI',
            'cc_last_4' => 1111,
            'cc_exp_month' => 12,
            'cc_exp_year' => 2023
        ];
        $dataObject = new DataObject($data);

        $infoInstance = $this->getMockForAbstractClass(InfoInterface::class);
        $this->payflowpro->setData('info_instance', $infoInstance);

        $this->eventManager->expects(static::exactly(2))
            ->method('dispatch');

        $this->payflowpro->assignData($dataObject);
    }

    /**
     * Asserts that PayPal gateway response mapping correctly.
     *
     * @param array $postData
     * @param DataObject $expectedResponse
     * @dataProvider dataProviderMapGatewayResponse
     */
    public function testMapGatewayResponse($postData, $expectedResponse)
    {
        self::assertEquals(
            $this->payflowpro->mapGatewayResponse($postData, new DataObject()),
            $expectedResponse
        );
    }

    /**
     * @return array
     */
    public function dataProviderMapGatewayResponse()
    {
        return [
            [
                [
                    'BILLTONAME' => 'John Doe',
                    'BILLTOFIRSTNAME' => 'John',
                    'BILLTOLASTNAME' => 'Doe',
                    'BILLTOEMAIL' => 'user@magento.com',
                    'BILLTOSTREET' => '6161 West Centinela Avenue',
                    'BILLTOCITY' => 'Culver City',
                    'BILLTOSTATE' => 'CA',
                    'BILLTOZIP' => '90230',
                    'BILLTOCOUNTRY' => 'US',
                    'SHIPTOSTREET' => '6161 West Centinela Avenue',
                    'SHIPTOCITY' => 'Culver City',
                    'SHIPTOSTATE' => 'CA',
                    'SHIPTOZIP' => '90230',
                    'SHIPTOCOUNTRY' => 'US',
                    'NAMETOSHIP' => 'John Doe',
                    'ADDRESSTOSHIP' => '6161 West Centinela Avenue',
                    'CITYTOSHIP' => 'Culver City',
                    'STATETOSHIP' => 'CA',
                    'ZIPTOSHIP' => '90230',
                    'COUNTRYTOSHIP' => 'US',
                    'NAME' => 'John Doe',
                    'CVV2MATCH' => 'Y',
                    'CARDTYPE' => '0',
                    'AVSDATA' => 'NNN',
                    'AVSZIP' => 'N',
                    'AVSADDR' => 'N',
                ],
                new DataObject([
                    'billtoname' => 'John Doe',
                    'billtofirstname' => 'John',
                    'billtolastname' => 'Doe',
                    'billtoemail' => 'user@magento.com',
                    'billtostreet' => '6161 West Centinela Avenue',
                    'billtocity' => 'Culver City',
                    'billtostate' => 'CA',
                    'billtozip' => '90230',
                    'billtocountry' => 'US',
                    'shiptostreet' => '6161 West Centinela Avenue',
                    'shiptocity' => 'Culver City',
                    'shiptostate' => 'CA',
                    'shiptozip' => '90230',
                    'shiptocountry' => 'US',
                    'nametoship' => 'John Doe',
                    'addresstoship' => '6161 West Centinela Avenue',
                    'citytoship' => 'Culver City',
                    'statetoship' => 'CA',
                    'ziptoship' => '90230',
                    'countrytoship' => 'US',
                    'name' => 'John Doe',
                    'cvv2match' => 'Y',
                    'cardtype' => '0',
                    'avsdata' => 'NN',
                    'avszip' => 'N',
                    'avsaddr' => 'N',
                    'firstname' => 'John',
                    'lastname' => 'Doe',
                    'address' => '6161 West Centinela Avenue',
                    'city' => 'Culver City',
                    'state' => 'CA',
                    'zip' => '90230',
                    'country' => 'US',
                    'email' => 'user@magento.com',
                    'cscmatch' => 'Y',
                    'ccavsstatus' => 'NNN',
                    'cc_type' => 'VI',
                ]),
            ]
        ];
    }
}
