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

use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Signifyd\Api\CaseRepositoryInterface;
use Magento\Signifyd\Api\Data\CaseInterface;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Magento\Signifyd\Model\SignifydGateway\Gateway;
use Magento\Signifyd\Model\SignifydGateway\GatewayException;
use Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilderInterface;
use Magento\Signifyd\Model\SignifydGateway\ApiClient;
use Magento\Signifyd\Model\SignifydGateway\ApiCallException;

class GatewayTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var CreateCaseBuilderInterface|MockObject
     */
    private $createCaseBuilder;

    /**
     * @var ApiClient|MockObject
     */
    private $apiClient;

    /**
     * @var Gateway
     */
    private $gateway;

    /**
     * @var OrderRepositoryInterface|MockObject
     */
    private $orderRepository;

    /**
     * @var CaseRepositoryInterface|MockObject
     */
    private $caseRepository;

    public function setUp()
    {
        $this->createCaseBuilder = $this->getMockBuilder(CreateCaseBuilderInterface::class)
            ->getMockForAbstractClass();

        $this->apiClient = $this->getMockBuilder(ApiClient::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class)
            ->getMockForAbstractClass();

        $this->caseRepository= $this->getMockBuilder(CaseRepositoryInterface::class)
            ->getMockForAbstractClass();

        $this->gateway = new Gateway(
            $this->createCaseBuilder,
            $this->apiClient,
            $this->orderRepository,
            $this->caseRepository
        );
    }

    public function testCreateCaseForSpecifiedOrder()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;
        $dummySignifydInvestigationId = 42;

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'investigationId' => $dummySignifydInvestigationId
            ]);

        $this->createCaseBuilder
            ->expects($this->atLeastOnce())
            ->method('build')
            ->with($this->equalTo($dummyOrderId))
            ->willReturn([]);

        $result = $this->gateway->createCase($dummyOrderId);
        $this->assertEquals(42, $result);
    }

    public function testCreateCaseCallsValidApiMethod()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;
        $dummySignifydInvestigationId = 42;

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->createCaseBuilder
            ->method('build')
            ->willReturn([]);

        $this->apiClient
            ->expects($this->atLeastOnce())
            ->method('makeApiCall')
            ->with(
                $this->equalTo('/cases'),
                $this->equalTo('POST'),
                $this->isType('array'),
                $this->equalTo($dummyStoreId)
            )
            ->willReturn([
                'investigationId' => $dummySignifydInvestigationId
            ]);

        $result = $this->gateway->createCase($dummyOrderId);
        $this->assertEquals(42, $result);
    }

    public function testCreateCaseNormalFlow()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;
        $dummySignifydInvestigationId = 42;

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->createCaseBuilder
            ->method('build')
            ->willReturn([]);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'investigationId' => $dummySignifydInvestigationId
            ]);

        $returnedInvestigationId = $this->gateway->createCase($dummyOrderId);
        $this->assertEquals(
            $dummySignifydInvestigationId,
            $returnedInvestigationId,
            'Method must return value specified in "investigationId" response parameter'
        );
    }

    public function testCreateCaseWithFailedApiCall()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;
        $apiCallFailureMessage = 'Api call failed';

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->createCaseBuilder
            ->method('build')
            ->willReturn([]);
        $this->apiClient
            ->method('makeApiCall')
            ->willThrowException(new ApiCallException($apiCallFailureMessage));

        $this->expectException(GatewayException::class);
        $this->expectExceptionMessage($apiCallFailureMessage);
        $this->gateway->createCase($dummyOrderId);
    }

    public function testCreateCaseWithMissedResponseRequiredData()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->createCaseBuilder
            ->method('build')
            ->willReturn([]);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'someOtherParameter' => 'foo',
            ]);

        $this->expectException(GatewayException::class);
        $this->gateway->createCase($dummyOrderId);
    }

    public function testCreateCaseWithAdditionalResponseData()
    {
        $dummyOrderId = 1;
        $dummyStoreId = 2;
        $dummySignifydInvestigationId = 42;

        $this->withOrderEntity($dummyOrderId, $dummyStoreId);
        $this->createCaseBuilder
            ->method('build')
            ->willReturn([]);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'investigationId' => $dummySignifydInvestigationId,
                'someOtherParameter' => 'foo',
            ]);

        $returnedInvestigationId = $this->gateway->createCase($dummyOrderId);
        $this->assertEquals(
            $dummySignifydInvestigationId,
            $returnedInvestigationId,
            'Method must return value specified in "investigationId" response parameter and ignore any other parameters'
        );
    }

    public function testSubmitCaseForGuaranteeCallsValidApiMethod()
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;
        $dummyDisposition = 'APPROVED';

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->expects($this->atLeastOnce())
            ->method('makeApiCall')
            ->with(
                $this->equalTo('/guarantees'),
                $this->equalTo('POST'),
                $this->equalTo([
                    'caseId' => $dummySygnifydCaseId
                ]),
                $this->equalTo($dummyStoreId)
            )->willReturn([
                'disposition' => $dummyDisposition
            ]);

        $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
        $this->assertEquals('APPROVED', $result);
    }

    public function testSubmitCaseForGuaranteeWithFailedApiCall()
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;
        $apiCallFailureMessage = 'Api call failed';

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willThrowException(new ApiCallException($apiCallFailureMessage));

        $this->expectException(GatewayException::class);
        $this->expectExceptionMessage($apiCallFailureMessage);
        $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
        $this->assertEquals('Api call failed', $result);
    }

    public function testSubmitCaseForGuaranteeReturnsDisposition()
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;
        $dummyDisposition = 'APPROVED';
        $dummyGuaranteeId = 123;
        $dummyRereviewCount = 0;

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'guaranteeId' => $dummyGuaranteeId,
                'disposition' => $dummyDisposition,
                'rereviewCount' => $dummyRereviewCount,
            ]);

        $actualDisposition = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
        $this->assertEquals(
            $dummyDisposition,
            $actualDisposition,
            'Method must return guarantee disposition retrieved in Signifyd API response as a result'
        );
    }

    public function testSubmitCaseForGuaranteeWithMissedDisposition()
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;
        $dummyGuaranteeId = 123;
        $dummyRereviewCount = 0;

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'guaranteeId' => $dummyGuaranteeId,
                'rereviewCount' => $dummyRereviewCount,
            ]);

        $this->expectException(GatewayException::class);
        $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
    }

    public function testSubmitCaseForGuaranteeWithUnexpectedDisposition()
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;
        $dummyUnexpectedDisposition = 'UNEXPECTED';

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'disposition' => $dummyUnexpectedDisposition,
            ]);

        $this->expectException(GatewayException::class);
        $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
        $this->assertEquals('UNEXPECTED', $result);
    }

    /**
     * @dataProvider supportedGuaranteeDispositionsProvider
     */
    public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpectedDisposition)
    {
        $dummySygnifydCaseId = 42;
        $dummyStoreId = 1;

        $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId);
        $this->apiClient
            ->method('makeApiCall')
            ->willReturn([
                'disposition' => $dummyExpectedDisposition,
            ]);

        try {
            $result = $this->gateway->submitCaseForGuarantee($dummySygnifydCaseId);
            $this->assertEquals($dummyExpectedDisposition, $result);
        } catch (GatewayException $e) {
            $this->fail(sprintf(
                'Expected disposition "%s" was not accepted with message "%s"',
                $dummyExpectedDisposition,
                $e->getMessage()
            ));
        }
    }

    /**
     * Checks a test case when guarantee for a case is successfully canceled
     *
     * @covers \Magento\Signifyd\Model\SignifydGateway\Gateway::cancelGuarantee
     */
    public function testCancelGuarantee()
    {
        $caseId = 123;
        $dummyStoreId = 1;

        $this->withCaseEntity($caseId, $dummyStoreId);
        $this->apiClient->expects(self::once())
            ->method('makeApiCall')
            ->with(
                '/cases/' . $caseId . '/guarantee',
                'PUT',
                ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED],
                $dummyStoreId
            )
            ->willReturn(
                ['disposition' => Gateway::GUARANTEE_CANCELED]
            );

        $result = $this->gateway->cancelGuarantee($caseId);
        self::assertEquals(Gateway::GUARANTEE_CANCELED, $result);
    }

    /**
     * Checks a case when API request returns unexpected guarantee disposition.
     *
     * @covers \Magento\Signifyd\Model\SignifydGateway\Gateway::cancelGuarantee
     * @expectedException \Magento\Signifyd\Model\SignifydGateway\GatewayException
     * @expectedExceptionMessage API returned unexpected disposition: DECLINED.
     */
    public function testCancelGuaranteeWithUnexpectedDisposition()
    {
        $caseId = 123;
        $dummyStoreId = 1;

        $this->withCaseEntity($caseId, $dummyStoreId);
        $this->apiClient->expects(self::once())
            ->method('makeApiCall')
            ->with(
                '/cases/' . $caseId . '/guarantee',
                'PUT',
                ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED],
                $dummyStoreId
            )
            ->willReturn(['disposition' => Gateway::GUARANTEE_DECLINED]);

        $result = $this->gateway->cancelGuarantee($caseId);
        $this->assertEquals(Gateway::GUARANTEE_CANCELED, $result);
    }

    /**
     * @return array
     */
    public function supportedGuaranteeDispositionsProvider()
    {
        return [
            'APPROVED' => ['APPROVED'],
            'DECLINED' => ['DECLINED'],
            'PENDING' => ['PENDING'],
            'CANCELED' => ['CANCELED'],
            'IN_REVIEW' => ['IN_REVIEW'],
            'UNREQUESTED' => ['UNREQUESTED'],
        ];
    }

    /**
     * Specifies order entity mock execution.
     *
     * @param int $orderId
     * @param int $storeId
     * @return void
     */
    private function withOrderEntity(int $orderId, int $storeId): void
    {
        $orderEntity = $this->getMockBuilder(OrderInterface::class)
            ->disableOriginalConstructor()
            ->getMock();
        $orderEntity->method('getStoreId')
            ->willReturn($storeId);
        $this->orderRepository->method('get')
            ->with($orderId)
            ->willReturn($orderEntity);
    }

    /**
     * Specifies case entity mock execution.
     *
     * @param int $caseId
     * @param int $storeId
     * @return void
     */
    private function withCaseEntity(int $caseId, int $storeId): void
    {
        $orderId = 1;

        $caseEntity = $this->getMockBuilder(CaseInterface::class)
            ->disableOriginalConstructor()
            ->getMock();
        $caseEntity->method('getOrderId')
            ->willReturn($orderId);
        $this->caseRepository->method('getByCaseId')
            ->with($caseId)
            ->willReturn($caseEntity);

        $this->withOrderEntity($orderId, $storeId);
    }
}