<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Paypal\Model;

use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Creditmemo;
use Magento\Sales\Model\Order\Invoice;
use Magento\TestFramework\Helper\Bootstrap;

/**
 * @magentoAppArea frontend
 */
class IpnTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $_objectManager;

    protected function setUp(): void
    {
        $this->_objectManager = Bootstrap::getObjectManager();
    }

    /**
     * @param string $currencyCode
     * @dataProvider currencyProvider
     * @magentoDataFixture Magento/Paypal/_files/order_express.php
     * @magentoConfigFixture current_store payment/paypal_direct/active 1
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestExpressCurrency($currencyCode)
    {
        $this->_processIpnRequestCurrency($currencyCode);
    }

    /**
     * Refund full order amount by Paypal Express IPN message service.
     *
     * @magentoDataFixture Magento/Paypal/_files/order_express_with_invoice_and_shipping.php
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestFullRefund()
    {
        $ipnData = require __DIR__ . '/../_files/ipn_refund.php';
        $ipnFactory = $this->_objectManager->create(IpnFactory::class);
        $ipnModel = $ipnFactory->create(
            [
                'data' => $ipnData,
                'curlFactory' => $this->_createMockedHttpAdapter()
            ]
        );

        $ipnModel->processIpnRequest();

        $order = $this->_objectManager->create(Order::class);
        $order->loadByIncrementId('100000001');

        $creditmemoItems = $order->getCreditmemosCollection()->getItems();
        $creditmemo = current($creditmemoItems);

        $this->assertEquals(Order::STATE_CLOSED, $order->getState()) ;
        $this->assertCount(1, $creditmemoItems);
        $this->assertEquals(Creditmemo::STATE_REFUNDED, $creditmemo->getState());
        $this->assertEquals(10, $order->getSubtotalRefunded());
        $this->assertEquals(10, $order->getBaseSubtotalRefunded());
        $this->assertEquals(20, $order->getShippingRefunded());
        $this->assertEquals(20, $order->getBaseShippingRefunded());
        $this->assertEquals(30, $order->getTotalRefunded());
        $this->assertEquals(30, $order->getBaseTotalRefunded());
        $this->assertEquals(30, $order->getTotalOnlineRefunded());
        $this->assertEmpty($order->getTotalOfflineRefunded());
    }

    /**
     * Refund full order amount by Paypal Express IPN message service with concurrent requests.
     *
     * @magentoDataFixture Magento/Paypal/_files/order_express_with_invoice_and_shipping.php
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestFullRefundConcurrent()
    {
        $ipnData = require __DIR__ . '/../_files/ipn_refund.php';
        $ipnFactory = $this->_objectManager->create(IpnFactory::class);
        $ipnModel = $ipnFactory->create(
            [
                'data' => $ipnData,
                'curlFactory' => $this->_createMockedHttpAdapter()
            ]
        );

        $ipnModel->processIpnRequest();
        $ipnModel->processIpnRequest();

        $order = $this->_objectManager->create(Order::class);
        $order->loadByIncrementId('100000001');

        $creditmemoItems = $order->getCreditmemosCollection()->getItems();
        $creditmemo = current($creditmemoItems);

        $this->assertEquals(Order::STATE_CLOSED, $order->getState()) ;
        $this->assertCount(1, $creditmemoItems);
        $this->assertEquals(Creditmemo::STATE_REFUNDED, $creditmemo->getState());
        $this->assertEquals(10, $order->getSubtotalRefunded());
        $this->assertEquals(10, $order->getBaseSubtotalRefunded());
        $this->assertEquals(20, $order->getShippingRefunded());
        $this->assertEquals(20, $order->getBaseShippingRefunded());
        $this->assertEquals(30, $order->getTotalRefunded());
        $this->assertEquals(30, $order->getBaseTotalRefunded());
        $this->assertEquals(30, $order->getTotalOnlineRefunded());
        $this->assertEmpty($order->getTotalOfflineRefunded());
    }

    /**
     * Partial refund of order amount by Paypal Express IPN message service.
     *
     * @magentoDataFixture Magento/Paypal/_files/order_express_with_invoice_and_shipping.php
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestPartialRefund()
    {
        $ipnData = require __DIR__ . '/../_files/ipn_refund.php';

        $refundAmount = -15;
        $ipnData['mc_gross'] = $refundAmount;

        $ipnFactory = $this->_objectManager->create(IpnFactory::class);
        $ipnModel = $ipnFactory->create(
            [
                'data' => $ipnData,
                'curlFactory' => $this->_createMockedHttpAdapter()
            ]
        );

        $ipnModel->processIpnRequest();

        $order = $this->_objectManager->create(Order::class);
        $order->loadByIncrementId('100000001');

        $creditmemoItems = $order->getCreditmemosCollection()->getItems();
        $comments = $order->load($order->getId())->getAllStatusHistory();
        $commentData = reset($comments);
        $commentOrigin = sprintf(
            'IPN "Refunded". Refund issued by merchant. Registered notification about refunded amount of $%d.00. '.
            'Transaction ID: "%s". Credit Memo has not been created. Please create offline Credit Memo.',
            abs($refundAmount),
            $ipnData['txn_id']
        );

        $this->assertEquals(Order::STATE_PROCESSING, $order->getState()) ;
        $this->assertEmpty(count($creditmemoItems));
        $this->assertCount(1, $comments);
        $this->assertEquals($commentOrigin, $commentData->getComment());
    }

    /**
     * Refund rest of order amount by Paypal Express IPN message service.
     *
     * @magentoDataFixture Magento/Paypal/_files/order_express_with_invoice_and_shipping.php
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestRestRefund()
    {
        $ipnData = require __DIR__ . '/../_files/ipn_refund.php';

        $ipnFactory = $this->_objectManager->create(IpnFactory::class);
        $ipnModel = $ipnFactory->create(
            [
                'data' => $ipnData,
                'curlFactory' => $this->_createMockedHttpAdapter()
            ]
        );

        $ipnModel->processIpnRequest();

        $order = $this->_objectManager->create(Order::class);
        $order->loadByIncrementId('100000001');

        $creditmemoItems = $order->getCreditmemosCollection()->getItems();

        $this->assertEquals(Order::STATE_CLOSED, $order->getState()) ;
        $this->assertCount(1, $creditmemoItems);
        $this->assertEquals(10, $order->getSubtotalRefunded());
        $this->assertEquals(10, $order->getBaseSubtotalRefunded());
        $this->assertEquals(20, $order->getShippingRefunded());
        $this->assertEquals(20, $order->getBaseShippingRefunded());
        $this->assertEquals(30, $order->getTotalRefunded());
        $this->assertEquals(30, $order->getBaseTotalRefunded());
        $this->assertEquals(30, $order->getTotalOnlineRefunded());
        $this->assertEmpty($order->getTotalOfflineRefunded());
    }

    /**
     * Verifies canceling an order that was in payment review state by PayPal Express IPN message service.
     *
     * @magentoDataFixture Magento/Paypal/_files/order_express_with_invoice_payment_review.php
     * @magentoConfigFixture current_store payment/paypal_express/active 1
     * @magentoConfigFixture current_store paypal/general/merchant_country US
     */
    public function testProcessIpnRequestWithFailedStatus()
    {
        $ipnData = require __DIR__ . '/../_files/ipn_failed.php';

        /** @var IpnFactory $ipnFactory */
        $ipnFactory = $this->_objectManager->create(IpnFactory::class);
        $ipnModel = $ipnFactory->create(
            [
                'data' => $ipnData,
                'curlFactory' => $this->_createMockedHttpAdapter()
            ]
        );

        $ipnModel->processIpnRequest();

        $order = $this->getOrder($ipnData['invoice']);
        $invoiceItems = $order->getInvoiceCollection()
            ->getItems();
        /** @var Invoice $invoice */
        $invoice = array_pop($invoiceItems);
        $invoice->getState();

        $this->assertEquals(Order::STATE_CANCELED, $order->getState());
        $this->assertEquals(Invoice::STATE_CANCELED, $invoice->getState());
    }

    /**
     * Test processIpnRequest() currency check for paypal_express and paypal_standard payment methods
     *
     * @param string $currencyCode
     */
    protected function _processIpnRequestCurrency($currencyCode)
    {
        $ipnData = require __DIR__ . '/../_files/ipn.php';
        $ipnData['mc_currency'] = $currencyCode;

        /** @var  $ipnFactory \Magento\Paypal\Model\IpnFactory */
        $ipnFactory = $this->_objectManager->create(\Magento\Paypal\Model\IpnFactory::class);

        $model = $ipnFactory->create(['data' => $ipnData, 'curlFactory' => $this->_createMockedHttpAdapter()]);
        $model->processIpnRequest();

        $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class);
        $order->loadByIncrementId('100000001');
        $this->_assertOrder($order, $currencyCode);
    }

    /**
     * Perform order state and status assertions depending on currency code
     *
     * @param \Magento\Sales\Model\Order $order
     * @param string $currencyCode
     */
    protected function _assertOrder($order, $currencyCode)
    {
        if ($currencyCode == 'USD') {
            $this->assertEquals('complete', $order->getState());
            $this->assertEquals('complete', $order->getStatus());
        } else {
            $this->assertEquals('payment_review', $order->getState());
            $this->assertEquals('fraud', $order->getStatus());
        }
    }

    /**
     * Data provider for currency check tests
     *
     * @static
     * @return array
     */
    public static function currencyProvider()
    {
        return [['USD'], ['EUR']];
    }

    /**
     * Mocked HTTP adapter to get VERIFIED PayPal IPN postback result
     *
     * @return \Magento\Framework\HTTP\Adapter\Curl
     */
    protected function _createMockedHttpAdapter()
    {
        $factory = $this->createPartialMock(\Magento\Framework\HTTP\Adapter\CurlFactory::class, ['create']);
        $adapter = $this->createPartialMock(\Magento\Framework\HTTP\Adapter\Curl::class, ['read', 'write']);

        $adapter->expects($this->any())->method('read')->with()->willReturn("\nVERIFIED");

        $adapter->expects($this->any())->method('write');

        $factory->expects($this->any())->method('create')->with()->willReturn($adapter);
        return $factory;
    }

    /**
     * Get stored order.
     *
     * @param string $incrementId
     * @return OrderInterface
     */
    private function getOrder(string $incrementId)
    {
        /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
        $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class);
        $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId)
            ->create();

        $orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class);
        $orders = $orderRepository->getList($searchCriteria)
            ->getItems();

        /** @var OrderInterface $order */
        return array_pop($orders);
    }
}
