<?php

namespace TbiBank\FusionPay\Test\Unit\Observer;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Event\Event;
use Magento\Framework\Event\Observer;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
use Psr\Log\NullLogger;
use PHPUnit\Framework\TestCase;
use TbiBank\FusionPay\Model\TbiStatusUpdate;
use TbiBank\FusionPay\Observer\SetDefaultTbiStatus;

/**
 * Behaviour under test: TbiBank\FusionPay\Observer\SetDefaultTbiStatus::execute()
 *
 * CKT-3844 changes two things about this observer:
 *  1. It stops pulling ScopeConfigInterface off Magento\Framework\App\ObjectManager::getInstance()
 *     (a service-locator anti-pattern) and instead takes it as a constructor dependency.
 *  2. After saving the order, it now also calls TbiStatusUpdate::updateStatus() so the
 *     denormalized sales_order_grid.tbi_status column stays in sync — the order-save path alone
 *     no longer reaches the grid table (AddTbiStatusToOrderGridCollection, which used to
 *     compute tbi_status for the grid via a runtime JOIN, was deleted in this same change).
 *
 * Given a sales_order_place_after event for an order paid with FusionPay
 * When  the observer executes
 * Then  it defaults tbi_status, applies the configured order status/state, saves the order,
 *       AND propagates the same increment id / tbi_status pair into the grid table via
 *       TbiStatusUpdate — all sourced from the injected ScopeConfigInterface, not ObjectManager.
 */
class SetDefaultTbiStatusTest extends TestCase
{
    private function makeObserverWithOrder(Order $order): Observer
    {
        $event = new Event();
        $event->setOrder($order);
        $observer = new Observer();
        $observer->setEvent($event);
        return $observer;
    }

    /**
     * Real TbiStatusUpdate wired to mocked DB primitives, so we assert on real SQL-shaped calls.
     * Returns [TbiStatusUpdate, $recorder] where $recorder->calls is filled in-place (a plain
     * object, not an array destructure, so the caller keeps a live view without PHP reference
     * gymnastics across a return boundary).
     */
    private function makeTbiStatusUpdate(): array
    {
        $connection = $this->createMock(AdapterInterface::class);
        $resource = $this->createMock(ResourceConnection::class);
        $resource->method('getConnection')->willReturn($connection);
        $resource->method('getTableName')->willReturnArgument(0);

        $recorder = new class {
            public array $calls = [];
        };
        $connection->method('update')->willReturnCallback(function ($table, $data, $where) use ($recorder) {
            $recorder->calls[] = [$table, $data, $where];
            return 1;
        });

        return [new TbiStatusUpdate($resource), $recorder];
    }

    public function testFusionPayOrderGetsDefaultStatusAndSyncsToGrid(): void
    {
        $order = new Order();
        $order->setPayment((new Payment())->setMethod('fusionpay'));
        $order->setIncrementId('100000200');

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

        $scopeConfig = $this->createMock(ScopeConfigInterface::class);
        $scopeConfig->method('getValue')
            ->with('payment/fusionpay/default_order_status', 'stores')
            ->willReturn('processing');

        [$tbiStatusUpdate, $recorder] = $this->makeTbiStatusUpdate();

        $observerImpl = new SetDefaultTbiStatus($orderRepository, new NullLogger(), $scopeConfig, $tbiStatusUpdate);
        $observerImpl->execute($this->makeObserverWithOrder($order));

        // Then: default tbi_status was applied (was empty before)
        $this->assertSame('In Progress', (string) $order->getData('tbi_status'));

        // Then: configured status/state were applied
        $this->assertSame('processing', $order->getStatus());
        $this->assertSame(Order::STATE_PROCESSING, $order->getState());

        // Then: the grid sync ran (TbiStatusUpdate writes both sales_order and
        // sales_order_grid — see TbiStatusUpdateTest), keyed by the order's increment id and
        // its (defaulted) tbi_status.
        $this->assertCount(2, $recorder->calls);
        $gridCall = $recorder->calls[1];
        $this->assertSame('sales_order_grid', $gridCall[0]);
        $this->assertSame(['increment_id = ?' => 100000200], $gridCall[2]);
        // The observer passes $order->getData('tbi_status') straight through — when the
        // "set default" branch fires that value is still the Phrase __() returned (never cast
        // to a plain string before it reaches the DB layer). Not a regression introduced by
        // this diff, just documenting the real shape of the data crossing this boundary.
        $this->assertSame('In Progress', (string) $gridCall[1]['tbi_status']);
    }

    /**
     * Given an order whose tbi_status was already set upstream (e.g. re-triggered event)
     * When the observer executes
     * Then it does NOT overwrite the existing tbi_status default, but STILL propagates whatever
     * value is present to the grid table — the sync must reflect the order's actual current
     * value, not silently skip because the "set default" branch didn't fire.
     */
    public function testDoesNotOverwriteExistingTbiStatusButStillSyncsGrid(): void
    {
        $order = new Order();
        $order->setPayment((new Payment())->setMethod('fusionpay'));
        $order->setIncrementId('100000201');
        $order->setData('tbi_status', 'Approved');

        $orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $scopeConfig = $this->createMock(ScopeConfigInterface::class);
        $scopeConfig->method('getValue')->willReturn(null);

        [$tbiStatusUpdate, $recorder] = $this->makeTbiStatusUpdate();

        (new SetDefaultTbiStatus($orderRepository, new NullLogger(), $scopeConfig, $tbiStatusUpdate))
            ->execute($this->makeObserverWithOrder($order));

        $this->assertSame('Approved', $order->getData('tbi_status'));
        $this->assertCount(2, $recorder->calls);
        $this->assertSame('sales_order_grid', $recorder->calls[1][0]);
        $this->assertSame('Approved', $recorder->calls[1][1]['tbi_status']);
    }

    /**
     * Given an order NOT paid via FusionPay
     * When the observer executes
     * Then it does nothing at all — no save, no grid sync. Non-FusionPay orders must never gain
     * a tbi_status or trigger writes to sales_order_grid.
     */
    public function testNonFusionPayOrdersAreIgnoredEntirely(): void
    {
        $order = new Order();
        $order->setPayment((new Payment())->setMethod('checkmo'));

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

        $scopeConfig = $this->createMock(ScopeConfigInterface::class);
        $scopeConfig->expects($this->never())->method('getValue');

        [$tbiStatusUpdate, $recorder] = $this->makeTbiStatusUpdate();

        (new SetDefaultTbiStatus($orderRepository, new NullLogger(), $scopeConfig, $tbiStatusUpdate))
            ->execute($this->makeObserverWithOrder($order));

        $this->assertCount(0, $recorder->calls);
    }

    /**
     * Regression guard for the ObjectManager::getInstance() removal: the observer must resolve
     * scope config exclusively through the constructor-injected ScopeConfigInterface. We prove
     * this behaviourally (not via reflection) by making the mock return a distinctive value and
     * asserting it drives the resulting order state — if the class silently fell back to a
     * globally-resolved config instead, this mock's return value would never be observed.
     */
    public function testDefaultOrderStatusComesFromInjectedScopeConfigNotObjectManager(): void
    {
        $order = new Order();
        $order->setPayment((new Payment())->setMethod('fusionpay'));
        $order->setIncrementId('100000202');

        $orderRepository = $this->createMock(OrderRepositoryInterface::class);
        $scopeConfig = $this->createMock(ScopeConfigInterface::class);
        $scopeConfig->expects($this->once())
            ->method('getValue')
            ->with('payment/fusionpay/default_order_status', 'stores')
            ->willReturn('complete');

        [$tbiStatusUpdate,] = $this->makeTbiStatusUpdate();

        (new SetDefaultTbiStatus($orderRepository, new NullLogger(), $scopeConfig, $tbiStatusUpdate))
            ->execute($this->makeObserverWithOrder($order));

        $this->assertSame('complete', $order->getStatus());
        $this->assertSame(Order::STATE_COMPLETE, $order->getState());
    }
}
