<?php

declare(strict_types=1);

namespace Drupal\Tests\acquia_dam\Unit\Service;

use Drupal\acquia_dam\Service\IntegrationLinksQueueService;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Queue\QueueWorkerInterface;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Tests\UnitTestCase;

/**
 * Unit tests for IntegrationLinksQueueService.
 *
 * @group acquia_dam
 * @coversDefaultClass \Drupal\acquia_dam\Service\IntegrationLinksQueueService
 */
class IntegrationLinksQueueServiceTest extends UnitTestCase {

  /**
   * The queue factory mock.
   *
   * @var \Drupal\Core\Queue\QueueFactory|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $queueFactory;

  /**
   * The queue worker manager mock.
   *
   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $queueWorkerManager;

  /**
   * The logger factory mock.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $loggerFactory;

  /**
   * The queue service under test.
   *
   * @var \Drupal\acquia_dam\Service\IntegrationLinksQueueService
   */
  protected $queueService;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->queueFactory = $this->createMock(QueueFactory::class);
    $this->queueWorkerManager = $this->createMock(QueueWorkerManagerInterface::class);
    $this->loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);

    // Setup logger.
    $logger = $this->createMock(LoggerChannelInterface::class);
    $this->loggerFactory->method('get')->willReturn($logger);

    $this->queueService = new IntegrationLinksQueueService(
      $this->queueFactory,
      $this->queueWorkerManager,
      $this->loggerFactory
    );
  }

  /**
   * @covers ::processQueue
   * @covers ::getQueueCount
   */
  public function testProcessQueueEmpty(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')->willReturn(0);
    $queue->expects($this->never())->method('claimItem');

    $this->queueFactory->method('get')->willReturn($queue);

    $results = $this->queueService->processQueue();

    $this->assertSame(0, $results['processed']);
    $this->assertSame(0, $results['failed']);
    $this->assertSame(0, $results['skipped']);
    $this->assertSame(0, $results['remaining']);
    $this->assertFalse($results['limit_reached']);
  }

  /**
   * @covers ::processQueue
   */
  public function testProcessQueueSuccess(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')
      ->willReturnOnConsecutiveCalls(2, 1, 0);

    // Create mock queue items.
    $item1 = (object) ['item_id' => 1, 'data' => ['asset_id' => 'test1']];
    $item2 = (object) ['item_id' => 2, 'data' => ['asset_id' => 'test2']];

    $queue->method('claimItem')
      ->willReturnOnConsecutiveCalls($item1, $item2, FALSE);

    $queue->expects($this->exactly(2))->method('deleteItem');

    $this->queueFactory->method('get')->willReturn($queue);

    $queue_worker = $this->createMock(QueueWorkerInterface::class);
    $queue_worker->expects($this->exactly(2))->method('processItem');

    $this->queueWorkerManager->method('createInstance')->willReturn($queue_worker);

    $results = $this->queueService->processQueue();

    $this->assertSame(2, $results['processed']);
    $this->assertSame(0, $results['failed']);
    $this->assertSame(0, $results['skipped']);
    $this->assertFalse($results['limit_reached']);
  }

  /**
   * @covers ::processQueue
   */
  public function testProcessQueueWithLimit(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')->willReturn(5);

    $item = (object) ['item_id' => 1, 'data' => ['asset_id' => 'test']];
    $queue->method('claimItem')->willReturn($item, FALSE);
    $queue->expects($this->once())->method('deleteItem');
    // After hitting limit, we break without claiming another item.
    $queue->expects($this->never())->method('releaseItem');

    $this->queueFactory->method('get')->willReturn($queue);

    $queue_worker = $this->createMock(QueueWorkerInterface::class);
    $queue_worker->expects($this->once())->method('processItem');

    $this->queueWorkerManager->method('createInstance')->willReturn($queue_worker);

    $results = $this->queueService->processQueue(1);

    $this->assertSame(1, $results['processed']);
    $this->assertTrue($results['limit_reached']);
  }

  /**
   * @covers ::processQueue
   */
  public function testProcessQueueWithFailedItem(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')->willReturn(1);

    $item = (object) ['item_id' => 1, 'data' => ['asset_id' => 'test']];
    $queue->method('claimItem')
      ->willReturnOnConsecutiveCalls($item, FALSE);

    $queue->expects($this->once())->method('releaseItem');
    $queue->expects($this->never())->method('deleteItem');

    $this->queueFactory->method('get')->willReturn($queue);

    $queue_worker = $this->createMock(QueueWorkerInterface::class);
    $queue_worker->method('processItem')
      ->willThrowException(new \Exception('Processing failed'));

    $this->queueWorkerManager->method('createInstance')->willReturn($queue_worker);

    $results = $this->queueService->processQueue();

    $this->assertSame(0, $results['processed']);
    $this->assertSame(1, $results['failed']);
  }

  /**
   * @covers ::processQueue
   */
  public function testProcessQueueWithProgressCallback(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')->willReturn(1);

    $item = (object) ['item_id' => 1, 'data' => ['asset_id' => 'test']];
    // Return item 10 times, then FALSE.
    $returns = array_fill(0, 10, $item);
    $returns[] = FALSE;
    $queue->method('claimItem')
      ->willReturnOnConsecutiveCalls(...$returns);

    $queue->expects($this->exactly(10))->method('deleteItem');

    $this->queueFactory->method('get')->willReturn($queue);

    $queue_worker = $this->createMock(QueueWorkerInterface::class);
    $this->queueWorkerManager->method('createInstance')->willReturn($queue_worker);

    $callback_count = 0;
    $callback = function () use (&$callback_count) {
      $callback_count++;
    };

    $results = $this->queueService->processQueue(0, $callback);

    $this->assertSame(10, $results['processed']);
    $this->assertSame(1, $callback_count, 'Callback should be called once (every 10 items)');
  }

  /**
   * @covers ::getQueueCount
   */
  public function testGetQueueCount(): void {
    $queue = $this->createMock(QueueInterface::class);
    $queue->method('numberOfItems')->willReturn(42);

    $this->queueFactory->method('get')->willReturn($queue);

    $count = $this->queueService->getQueueCount();
    $this->assertSame(42, $count);
  }

}
