<?php

namespace Drupal\Tests\eca_queue\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\eca\Entity\Eca;
use Drupal\eca_queue\Plugin\Action\EnqueueTaskDelayed;
use Drupal\eca_queue\Task;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Kernel tests for the "eca_enqueue_task_delayed" action plugin.
 */
#[Group('eca')]
#[Group('eca_queue')]
#[RunTestsInSeparateProcesses]
class EnqueueTaskDelayedTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'eca',
    'eca_queue',
    'modeler_api',
  ];

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installConfig(static::$modules);
    User::create(['uid' => 0, 'name' => 'anonymous'])->save();
    User::create(['uid' => 1, 'name' => 'admin'])->save();
  }

  /**
   * Tests EnqueueTaskDelayed.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Exception
   */
  public function testEnqueueTaskDelayed(): void {
    /** @var \Drupal\Core\Action\ActionManager $action_manager */
    $action_manager = \Drupal::service('plugin.manager.action');
    /** @var \Drupal\eca\Token\TokenInterface $token_services */
    $token_services = \Drupal::service('eca.token_services');
    $queue = \Drupal::queue('eca_task', TRUE);

    $token_services->addTokenData('entity', User::load(0));
    $token_services->addTokenData('admin', User::load(1));

    // Create an action for enqueuing a task.
    $defaults = [
      'task_name' => 'delayed_task',
      'task_value' => '',
      'tokens' => '',
      'delay_value' => '2',
      'delay_unit' => EnqueueTaskDelayed::DELAY_MINUTES,
    ];
    /** @var \Drupal\eca_queue\Plugin\Action\EnqueueTask $action */
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [] + $defaults);
    $this->assertTrue($action->access(NULL, User::load(0)), 'Access must be granted.');
    $this->assertTrue($action->access(NULL, User::load(1)), 'Access must be granted.');
    $this->assertSame(0, $queue->numberOfItems(), 'Queue must be empty before execution.');
    $action->execute();
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must have exactly one item after execution.');
    $queue->deleteItem($queue->claimItem());

    // Repeat the same thing.
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [] + $defaults);
    $this->assertTrue($action->access(NULL, User::load(0)), 'Access must be granted.');
    $this->assertTrue($action->access(NULL, User::load(1)), 'Access must be granted.');
    $this->assertSame(0, $queue->numberOfItems(), 'Queue must be empty before execution.');
    $action->execute();
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must have exactly one item after execution.');
    // One more time, without clearing beforehand.
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [] + $defaults);
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must be unchanged before execution.');
    $action->execute();
    $this->assertSame(2, $queue->numberOfItems(), 'Queue must have exactly two items after execution.');
    $queue->deleteItem($queue->claimItem());
    $queue->deleteItem($queue->claimItem());

    // Now create a task, that is to be processed via ECA. The delay of that
    // task it to be set 0 (immediately due). That ECA config will just create
    // another queue item and we assert that this item is then stored in queue.
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [
      'delay_value' => '0',
      'delay_unit' => EnqueueTaskDelayed::DELAY_SECONDS,
    ] + $defaults);
    $this->assertSame(0, $queue->numberOfItems(), 'Queue must be empty before execution.');
    $action->execute();
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must have exactly one item after execution.');

    $eca_config_values = [
      'langcode' => 'en',
      'status' => TRUE,
      'id' => 'queue_process_delayed',
      'label' => 'ECA queue worker',
      'modeller' => 'fallback',
      'version' => '1.0.0',
      'events' => [
        'event_queue' => [
          'plugin' => 'eca_queue:processing_task',
          'label' => 'ECA processing task',
          'configuration' => [
            'task_name' => 'delayed_task',
            'task_value' => '',
          ],
          'successors' => [
            ['id' => 'action_enqueue', 'condition' => ''],
          ],
        ],
      ],
      'conditions' => [],
      'gateways' => [],
      'actions' => [
        'action_enqueue' => [
          'plugin' => 'eca_enqueue_task_delayed',
          'label' => 'Enqueue another item',
          'configuration' => [
            'task_name' => 'another_task',
            'task_value' => 'delayed_task_value',
            'tokens' => '',
            'delay_value' => '100',
            'delay_unit' => EnqueueTaskDelayed::DELAY_SECONDS,
          ],
          'successors' => [],
        ],
      ],
    ];
    $ecaConfig = Eca::create($eca_config_values);
    $ecaConfig->save();

    /** @var \Drupal\Core\Queue\QueueWorkerManagerInterface $queue_worker_manager */
    $queue_worker_manager = \Drupal::service('plugin.manager.queue_worker');
    /** @var \Drupal\eca_queue\Plugin\QueueWorker\TaskWorker $queue_worker */
    $queue_worker = $queue_worker_manager->createInstance('eca_task');
    $item = $queue->claimItem();
    /** @var \Drupal\eca_queue\Task $task */
    $task = $item->data;
    $this->assertInstanceOf(Task::class, $task, 'Task item must be a task object.');
    $this->assertTrue($task->isDueForProcessing(), 'Task must be due for processing, because delay is set to 0.');
    $queue_worker->processItem($item->data);
    $queue->deleteItem($item);
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must have exactly one item (newly created by ECA configuration).');
    $item = $queue->claimItem();
    /** @var \Drupal\eca_queue\Task $task */
    $task = $item->data;
    $this->assertInstanceOf(Task::class, $task, 'Newly created queue item must be a Task object.');
    $this->assertEquals('another_task', $task->getTaskName(), 'Task name must match with the one added via ECA configuration.');
    $this->assertEquals('delayed_task_value', $task->getTaskValue(), 'Task value must match with the one added via ECA configuration.');
    $this->assertFalse($task->hasData('entity'), 'No entity data must have been passed to the task.');
    $this->assertFalse($task->hasData('admin'), 'No user data must have been passed to the task.');

    // When the queue worker works on the newly created item, the ECA config
    // will react upon that again. But it must not execute its action on that
    // event, because it has a different event name. That's why the number of
    // queue items must remain the same.
    $exception = NULL;
    try {
      $queue_worker->processItem($task);
    }
    catch (\Exception $e) {
      $exception = $e;
    }
    finally {
      $this->assertTrue(isset($exception));
      $this->assertEquals('Task is not yet due for processing.', $exception->getMessage());
    }
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must be unchanged.');
    // Now create another ECA config, that one will react upon the name of
    // the newly created task item, but it is not yet due for processing and
    // therefore the queue must remain unchanged.
    $eca_config_values['id'] .= '_2';
    $eca_config_values['events']['event_queue']['configuration']['task_name'] = 'another_task';
    Eca::create($eca_config_values)->save();
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must be unchanged.');
    $exception = NULL;
    try {
      $queue_worker->processItem($task);
    }
    catch (\Exception $e) {
      $exception = $e;
    }
    finally {
      $this->assertTrue(isset($exception));
      $this->assertEquals('Task is not yet due for processing.', $exception->getMessage());
    }
    $this->assertSame(1, $queue->numberOfItems(), 'Queue must be unchanged.');

    $queue->deleteItem($item);
    while ($item = $queue->claimItem()) {
      $queue->deleteItem($item);
    }
    $this->assertSame(0, $queue->numberOfItems(), 'Queue must be empty.');

    /** @var \Drupal\eca_queue\Plugin\Action\EnqueueTask $action */
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [
      'task_name' => 'user_task',
      'task_value' => '[entity:account-name]',
      'tokens' => "entity",
      'delay_value' => '100',
      'delay_unit' => EnqueueTaskDelayed::DELAY_SECONDS,
    ] + $defaults);
    $action->execute();
    $item = $queue->claimItem();
    /** @var \Drupal\eca_queue\Task $task */
    $task = $item->data;
    $this->assertInstanceOf(Task::class, $task, 'Newly created queue item must be a Task object.');
    $this->assertEquals('user_task', $task->getTaskName(), 'Task name must match with the one defined via plugin configuration.');
    $this->assertEquals('anonymous', $task->getTaskValue(), 'Task value must match with the one defined via plugin configuration.');
    $this->assertTrue($task->hasData('entity'), 'Entity data must have been passed to the task.');
    $this->assertInstanceOf(UserInterface::class, $task->getData('entity'), 'The passed entity Token must be a user.');
    $this->assertSame('0', (string) $task->getData('entity')->id(), 'The loaded entity be the anonymous user.');
    $this->assertFalse($task->hasData('admin'), 'No admin user data must have been passed to the task.');
    $this->assertFalse($task->isDueForProcessing(), 'Task must not be due for processing.');
    $queue->deleteItem($item);

    /** @var \Drupal\eca_queue\Plugin\Action\EnqueueTask $action */
    $action = $action_manager->createInstance('eca_enqueue_task_delayed', [
      'task_name' => 'user_task',
      'task_value' => '[admin:uid]',
      'tokens' => "entity,\nadmin",
      'delay_value' => '0',
      'delay_unit' => EnqueueTaskDelayed::DELAY_SECONDS,
    ] + $defaults);
    $action->execute();
    $item = $queue->claimItem();
    /** @var \Drupal\eca_queue\Task $task */
    $task = $item->data;
    $this->assertInstanceOf(Task::class, $task, 'Newly created queue item must be a Task object.');
    $this->assertEquals('user_task', $task->getTaskName(), 'Task name must match with the one defined via plugin configuration.');
    $this->assertEquals('1', $task->getTaskValue(), 'Task value must match with the one defined via plugin configuration.');
    $this->assertTrue($task->hasData('entity'), 'Entity data must have been passed to the task.');
    $this->assertInstanceOf(UserInterface::class, $task->getData('entity'), 'The passed entity Token must be a user.');
    $this->assertSame('0', (string) $task->getData('entity')->id(), 'The loaded entity must be the anonymous user ID.');
    $this->assertTrue($task->hasData('admin'), 'Admin user data must have been passed to the task.');
    $this->assertSame('1', (string) $task->getData('admin')->id(), 'The loaded admin user must be the admin user account, as it was defined in the Token environment.');
    $this->assertTrue($task->isDueForProcessing(), 'Task must be due for processing.');

    $queue->deleteItem($item);
  }

}
