<?php

namespace TbiBank\FusionPay\Test\Unit\Controller\Api;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Quote\Model\Quote\AddressFactory;
use Magento\Quote\Model\QuoteFactory;
use Magento\Quote\Model\QuoteManagement;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\TestCase;
use TbiBank\FusionPay\Controller\Api\Gateway;
use TbiBank\FusionPay\Helper\Data as FusionPayHelper;
use TbiBank\FusionPay\Logger\FusionPayLogger;
use TbiBank\FusionPay\Model\Cryptor;
use TbiBank\FusionPay\Model\ProgrammaticOrderContext;
use TbiBank\FusionPay\Model\TbiStatusUpdate;

/**
 * Behaviour under test: TbiBank\FusionPay\Controller\Api\Gateway::execute()
 *
 * CKT-3844 replaces the deprecated Magento\Framework\Registry with the injected
 * ProgrammaticOrderContext service (see ProgrammaticOrderContextTest and
 * PaymentMethodVisibilityTest for the collaborator-level behaviour). This test exercises the
 * controller itself: the ONE call site (`$this->programmaticOrderContext->activate()`) that used
 * to be `$this->registry->register('is_programmatic_order', true)`, in its real place inside the
 * `create_order` flow, plus the sibling `update_status` command path that drives
 * TbiStatusUpdate — both already-tested at the unit level, exercised here end-to-end through the
 * controller to prove the wiring is correct.
 *
 * Given a valid Gateway API POST payload
 * When  the command is 'create_order'
 * Then  the programmatic order context is activated BEFORE the quote is submitted as an order —
 *       so that PaymentMethodVisibility's afterIsActive() (invoked further down the same
 *       request, e.g. while collecting payment info) sees it active and bypasses FusionPay's own
 *       visibility rules for this server-side order.
 */
class GatewayTest extends TestCase
{
    private const ENCRYPTION_KEY = 'unit-test-encryption-key';

    private function encryptedPayload(array $data): string
    {
        return Cryptor::Encrypt(json_encode($data), self::ENCRYPTION_KEY);
    }

    private function makeRequest(array $decryptedPayload): RequestInterface
    {
        $request = $this->createMock(RequestInterface::class);
        $request->method('isPost')->willReturn(true);
        $request->method('getContent')->willReturn(json_encode([
            'data' => $this->encryptedPayload($decryptedPayload),
        ]));
        $request->method('getHeader')->willReturn(null);
        $request->method('getClientIp')->willReturn('127.0.0.1');
        return $request;
    }

    private function makeHelper(string $encryptionKey = self::ENCRYPTION_KEY): FusionPayHelper
    {
        // getConstValue() is `static` on the real Data helper — PHPUnit mocks can't stub static
        // methods, so we leave it un-stubbed (it falls through to the real implementation,
        // which just reads a class constant; Gateway.php's only uses of it are in commented-out
        // origin/IP checks, so this has no bearing on the behaviour under test).
        $helper = $this->getMockBuilder(FusionPayHelper::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['getConfigValue'])
            ->getMock();
        $helper->method('getConfigValue')->willReturnCallback(
            fn ($path) => $path === 'tbi_settings/general_settings/encryption_key' ? $encryptionKey : null
        );
        return $helper;
    }

    private function makeGateway(
        RequestInterface $request,
        FusionPayHelper $helper,
        OrderRepositoryInterface $orderRepository,
        ProgrammaticOrderContext $programmaticOrderContext,
        TbiStatusUpdate $tbiStatusUpdate,
        ?QuoteManagement $quoteManagement = null
    ): Gateway {
        $storeManager = $this->createMock(StoreManagerInterface::class);
        $storeManager->method('getStore')->willReturn(new Store());

        $quoteManagement = $quoteManagement ?? $this->createMock(QuoteManagement::class);

        $productRepository = $this->createMock(ProductRepositoryInterface::class);
        $productRepository->method('getById')->willReturn(new \stdClass());

        $customerRepository = $this->createMock(CustomerRepositoryInterface::class);

        $logger = $this->getMockBuilder(FusionPayLogger::class)
            ->disableOriginalConstructor()
            ->getMock();

        return new Gateway(
            new Context($request),
            new JsonFactory(),
            $helper,
            new Cryptor(),
            $storeManager,
            $quoteManagement,
            new QuoteFactory(),
            $productRepository,
            $customerRepository,
            new AddressFactory(),
            $orderRepository,
            $programmaticOrderContext,
            $tbiStatusUpdate,
            $logger
        );
    }

    public function testCreateOrderActivatesProgrammaticOrderContextBeforeSubmit(): void
    {
        $payload = [
            'command' => 'create_order',
            'email' => 'buyer@example.com',
            'firstname' => 'Ada',
            'lastname' => 'Lovelace',
            'items' => [
                (object) ['params' => (object) ['product' => 42], 'qty' => 1],
            ],
            'deliveryaddress' => (object) [
                'streetname' => '1 Main St',
                'city' => 'Bucharest',
                'postalcode' => '010101',
            ],
            'phone' => '+40700000000',
        ];

        $submittedOrder = new Order();
        $submittedOrder->setIncrementId('100000300');

        $context = new ProgrammaticOrderContext();
        $this->assertFalse($context->isActive(), 'sanity: context starts inactive');

        $quoteManagement = $this->createMock(QuoteManagement::class);
        $quoteManagement->expects($this->once())
            ->method('submit')
            ->willReturnCallback(function () use ($context, $submittedOrder) {
                // Then: by the time the quote is submitted, activate() has already run.
                $this->assertTrue($context->isActive());
                return $submittedOrder;
            });

        $orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $orderRepository->expects($this->once())->method('save')->with($submittedOrder);

        $tbiStatusUpdate = $this->getMockBuilder(TbiStatusUpdate::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['updateStatus'])
            ->getMock();
        $tbiStatusUpdate->expects($this->once())
            ->method('updateStatus')
            ->with('100000300', $this->anything());

        $gateway = $this->makeGateway(
            $this->makeRequest($payload),
            $this->makeHelper(),
            $orderRepository,
            $context,
            $tbiStatusUpdate,
            $quoteManagement
        );

        $result = $gateway->execute();

        // Then: the context is (still) active after execute() returns...
        $this->assertTrue($context->isActive());
        // ...and the controller reports success with the new order's increment id.
        $this->assertSame(['success' => true, 'order_id' => '100000300'], $result->getData());
    }

    /**
     * Given an 'update_status' command payload
     * When  the controller executes
     * Then  it maps the numeric status code to the corresponding tbi_status label and forwards
     *       it to TbiStatusUpdate — this path does not touch ProgrammaticOrderContext at all.
     */
    public function testUpdateStatusMapsNumericStatusAndDelegatesToTbiStatusUpdate(): void
    {
        $payload = [
            'command' => 'update_status',
            'order_id' => '100000123',
            'status' => '1', // Approved
        ];

        $context = new ProgrammaticOrderContext();

        $tbiStatusUpdate = $this->getMockBuilder(TbiStatusUpdate::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['updateStatus'])
            ->getMock();
        $tbiStatusUpdate->expects($this->once())
            ->method('updateStatus')
            ->with('100000123', $this->callback(fn ($status) => (string) $status === 'Approved'), null);

        $orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $orderRepository->expects($this->never())->method('save');

        $gateway = $this->makeGateway(
            $this->makeRequest($payload),
            $this->makeHelper(),
            $orderRepository,
            $context,
            $tbiStatusUpdate
        );

        $result = $gateway->execute();

        $this->assertSame(['success' => true, 'order_id' => '100000123'], $result->getData());
        $this->assertFalse($context->isActive(), 'update_status must never touch the programmatic order context');
    }

    public function testUpdateStatusForwardsContractIdWhenProvided(): void
    {
        $payload = [
            'command' => 'update_status',
            'order_id' => '100000124',
            'status' => '1',
            'contract_id' => '20266032281',
        ];

        $context = new ProgrammaticOrderContext();

        $tbiStatusUpdate = $this->getMockBuilder(TbiStatusUpdate::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['updateStatus'])
            ->getMock();
        $tbiStatusUpdate->expects($this->once())
            ->method('updateStatus')
            ->with(
                '100000124',
                $this->callback(fn ($status) => (string) $status === 'Approved'),
                '20266032281'
            );

        $orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $orderRepository->expects($this->never())->method('save');

        $gateway = $this->makeGateway(
            $this->makeRequest($payload),
            $this->makeHelper(),
            $orderRepository,
            $context,
            $tbiStatusUpdate
        );

        $result = $gateway->execute();

        $this->assertSame(['success' => true, 'order_id' => '100000124'], $result->getData());
        $this->assertFalse($context->isActive());
    }

    /**
     * Given a non-POST request
     * When  the controller executes
     * Then  it short-circuits with an access-denied error and never reaches the decrypt/command
     *       dispatch logic at all.
     */
    public function testNonPostRequestIsDeniedWithoutDecrypting(): void
    {
        $request = $this->createMock(RequestInterface::class);
        $request->method('isPost')->willReturn(false);
        $request->method('getHeader')->willReturn(null);
        $request->method('getClientIp')->willReturn('127.0.0.1');
        $request->expects($this->never())->method('getContent');

        $context = new ProgrammaticOrderContext();
        $tbiStatusUpdate = $this->getMockBuilder(TbiStatusUpdate::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['updateStatus'])
            ->getMock();
        $tbiStatusUpdate->expects($this->never())->method('updateStatus');

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

        $gateway = $this->makeGateway($request, $this->makeHelper(), $orderRepository, $context, $tbiStatusUpdate);

        $result = $gateway->execute();

        $this->assertSame(['error' => 1, 'message' => 'Access denied.'], $result->getData());
    }
}
