<?php
declare(strict_types=1);

namespace Tests\Unit;

use App\Modules\Cron\CronRepository;
use App\Modules\Settings\AllegroApiClient;
use App\Modules\Settings\AllegroIntegrationRepository;
use App\Modules\Settings\AllegroOrderImportService;
use App\Modules\Settings\AllegroOrderSyncStateRepository;
use App\Modules\Settings\AllegroStatusMappingRepository;
use App\Modules\Settings\AllegroStatusSyncService;
use App\Modules\Settings\AllegroTokenManager;
use PDO;
use PDOStatement;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use RuntimeException;

final class AllegroStatusSyncServiceTest extends TestCase
{
    private CronRepository&MockObject $cronRepository;
    private AllegroOrderImportService&MockObject $orderImportService;
    private AllegroApiClient&MockObject $apiClient;
    private AllegroTokenManager&MockObject $tokenManager;
    private AllegroStatusMappingRepository&MockObject $statusMappings;
    private AllegroOrderSyncStateRepository&MockObject $syncStateRepository;
    private AllegroIntegrationRepository&MockObject $integrationRepository;

    protected function setUp(): void
    {
        $this->cronRepository = $this->createMock(CronRepository::class);
        $this->orderImportService = $this->createMock(AllegroOrderImportService::class);
        $this->apiClient = $this->createMock(AllegroApiClient::class);
        $this->tokenManager = $this->createMock(AllegroTokenManager::class);
        $this->statusMappings = $this->createMock(AllegroStatusMappingRepository::class);
        $this->syncStateRepository = $this->createMock(AllegroOrderSyncStateRepository::class);
        $this->integrationRepository = $this->createMock(AllegroIntegrationRepository::class);
    }

    public function testPushDirectionProcessesMappedOrdersAndSkipsUnmapped(): void
    {
        $this->cronRepository
            ->method('getStringSetting')
            ->willReturn('orderpro_to_allegro');

        $this->integrationRepository
            ->method('getActiveIntegrationId')
            ->willReturn(5);

        $this->statusMappings
            ->method('buildOrderproToAllegroMap')
            ->willReturn([
                'processing' => 'ready_for_processing',
            ]);

        $this->tokenManager
            ->method('resolveToken')
            ->willReturn(['token-1', 'sandbox']);

        $this->syncStateRepository
            ->method('getLastStatusPushedAt')
            ->willReturn(null);

        $this->syncStateRepository
            ->expects($this->once())
            ->method('updateLastStatusPushedAt')
            ->with(5, '2026-03-28 10:10:00');

        $this->apiClient
            ->expects($this->exactly(2))
            ->method('updateCheckoutFormFulfillment')
            ->with(
                'sandbox',
                'token-1',
                $this->logicalOr('A1', 'A3'),
                'ready_for_processing'
            )
            ->willReturn([]);

        $service = $this->createServiceWithPushRows([
            ['source_order_id' => 'A1', 'orderpro_status_code' => 'processing', 'latest_change' => '2026-03-28 10:00:00'],
            ['source_order_id' => 'A2', 'orderpro_status_code' => 'unknown', 'latest_change' => '2026-03-28 10:05:00'],
            ['source_order_id' => 'A3', 'orderpro_status_code' => 'processing', 'latest_change' => '2026-03-28 10:10:00'],
        ]);

        $result = $service->sync();

        $this->assertTrue($result['ok']);
        $this->assertSame('orderpro_to_allegro', $result['direction']);
        $this->assertSame(2, $result['pushed']);
        $this->assertSame(1, $result['skipped']);
        $this->assertSame(0, $result['failed']);
    }

    public function testPushDirectionReturnsEarlyWhenNoMappingsExist(): void
    {
        $this->cronRepository
            ->method('getStringSetting')
            ->willReturn('orderpro_to_allegro');

        $this->integrationRepository
            ->method('getActiveIntegrationId')
            ->willReturn(5);

        $this->statusMappings
            ->method('buildOrderproToAllegroMap')
            ->willReturn([]);

        $this->apiClient
            ->expects($this->never())
            ->method('updateCheckoutFormFulfillment');

        $service = $this->createServiceWithPushRows([]);
        $result = $service->sync();

        $this->assertTrue($result['ok']);
        $this->assertSame('orderpro_to_allegro', $result['direction']);
        $this->assertSame(0, $result['pushed']);
        $this->assertSame(0, $result['failed']);
        $this->assertStringContainsString('Brak mapowan', (string) ($result['message'] ?? ''));
    }

    public function testPushDirectionCollectsFailureAndContinuesProcessing(): void
    {
        $this->cronRepository
            ->method('getStringSetting')
            ->willReturn('orderpro_to_allegro');

        $this->integrationRepository
            ->method('getActiveIntegrationId')
            ->willReturn(7);

        $this->statusMappings
            ->method('buildOrderproToAllegroMap')
            ->willReturn([
                'processing' => 'ready_for_processing',
            ]);

        $this->tokenManager
            ->method('resolveToken')
            ->willReturn(['token-fail-test', 'sandbox']);

        $this->syncStateRepository
            ->method('getLastStatusPushedAt')
            ->willReturn(null);

        $this->syncStateRepository
            ->expects($this->once())
            ->method('updateLastStatusPushedAt')
            ->with(7, '2026-03-28 10:10:00');

        $calls = 0;
        $this->apiClient
            ->method('updateCheckoutFormFulfillment')
            ->willReturnCallback(function () use (&$calls): array {
                $calls++;
                if ($calls === 1) {
                    throw new RuntimeException('API Allegro HTTP 422');
                }

                return [];
            });

        $service = $this->createServiceWithPushRows([
            ['source_order_id' => 'X1', 'orderpro_status_code' => 'processing', 'latest_change' => '2026-03-28 10:00:00'],
            ['source_order_id' => 'X2', 'orderpro_status_code' => 'processing', 'latest_change' => '2026-03-28 10:10:00'],
        ]);

        $result = $service->sync();

        $this->assertTrue($result['ok']);
        $this->assertSame(1, $result['pushed']);
        $this->assertSame(1, $result['failed']);
        $this->assertCount(1, $result['errors']);
        $this->assertSame('X1', $result['errors'][0]['source_order_id'] ?? null);
    }

    public function testPushDirectionRetriesOnceAfter401(): void
    {
        $this->cronRepository
            ->method('getStringSetting')
            ->willReturn('orderpro_to_allegro');

        $this->integrationRepository
            ->method('getActiveIntegrationId')
            ->willReturn(9);

        $this->statusMappings
            ->method('buildOrderproToAllegroMap')
            ->willReturn([
                'processing' => 'ready_for_processing',
            ]);

        $this->tokenManager
            ->method('resolveToken')
            ->willReturnOnConsecutiveCalls(
                ['token-old', 'sandbox'],
                ['token-new', 'sandbox']
            );

        $this->syncStateRepository
            ->method('getLastStatusPushedAt')
            ->willReturn(null);

        $this->syncStateRepository
            ->expects($this->once())
            ->method('updateLastStatusPushedAt')
            ->with(9, '2026-03-28 11:00:00');

        $calls = 0;
        $this->apiClient
            ->method('updateCheckoutFormFulfillment')
            ->willReturnCallback(function () use (&$calls): array {
                $calls++;
                if ($calls === 1) {
                    throw new RuntimeException('ALLEGRO_HTTP_401');
                }

                return [];
            });

        $service = $this->createServiceWithPushRows([
            ['source_order_id' => 'R1', 'orderpro_status_code' => 'processing', 'latest_change' => '2026-03-28 11:00:00'],
        ]);
        $result = $service->sync();

        $this->assertTrue($result['ok']);
        $this->assertSame(1, $result['pushed']);
        $this->assertSame(0, $result['failed']);
        $this->assertSame(0, $result['skipped']);
        $this->assertSame(2, $calls);
    }

    /**
     * @param array<int, array<string, mixed>> $rows
     */
    private function createServiceWithPushRows(array $rows): AllegroStatusSyncService
    {
        $statement = $this->createMock(PDOStatement::class);
        $statement
            ->method('execute')
            ->willReturn(true);
        $statement
            ->method('fetchAll')
            ->willReturn($rows);

        $pdo = $this->createMock(PDO::class);
        $pdo
            ->method('prepare')
            ->willReturn($statement);

        return new AllegroStatusSyncService(
            $this->cronRepository,
            $this->orderImportService,
            $this->apiClient,
            $this->tokenManager,
            $this->statusMappings,
            $this->syncStateRepository,
            $this->integrationRepository,
            $pdo
        );
    }
}
