<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_trigger\Unit\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop\Service\EntitySerializer;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_trigger\Service\CronScheduleMatcher;
use Drupal\flowdrop_trigger\Service\CronTriggerStateService;
use Drupal\flowdrop_trigger\Service\EventFilterService;
use Drupal\flowdrop_trigger\Service\EventTypePluginManager;
use Drupal\flowdrop_trigger\Service\TriggerExecutor;
use Drupal\flowdrop_trigger\Service\TriggerManager;
use Drupal\flowdrop_trigger\FlowDropEventTypeInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Interface for mocking user entities with getRoles() in unit tests.
 */
// phpcs:ignore Drupal.Classes.ClassFileName
interface MockUserEntityInterface extends ContentEntityInterface {

  /**
   * Gets the roles for this user entity.
   *
   * @return array<string>
   *   The user roles.
   */
  public function getRoles(): array;

}

/**
 * Tests the TriggerManager service.
 *
 * @coversDefaultClass \Drupal\flowdrop_trigger\Service\TriggerManager
 * @group flowdrop_trigger
 */
class TriggerManagerTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The trigger manager under test.
   *
   * @var \Drupal\flowdrop_trigger\Service\TriggerManager
   */
  private TriggerManager $manager;

  /**
   * The entity type manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private $entityTypeManager;

  /**
   * The event type plugin manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\EventTypePluginManager
   */
  private $eventTypeManager;

  /**
   * The entity serializer mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop\Service\EntitySerializer
   */
  private $entitySerializer;

  /**
   * The event filter mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\EventFilterService
   */
  private $eventFilter;

  /**
   * The trigger executor mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\TriggerExecutor
   */
  private $triggerExecutor;

  /**
   * The config factory mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ConfigFactoryInterface
   */
  private $configFactory;

  /**
   * The cron schedule matcher mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\CronScheduleMatcher
   */
  private $cronScheduleMatcher;

  /**
   * The cron trigger state service mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\CronTriggerStateService
   */
  private $cronStateService;

  /**
   * The logger mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * The trigger config entity storage mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Entity\EntityStorageInterface
   */
  private $configStorage;

  /**
   * The workflow entity storage mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Entity\EntityStorageInterface
   */
  private $workflowStorage;

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

    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->eventTypeManager = $this->prophesize(EventTypePluginManager::class);
    $this->entitySerializer = $this->prophesize(EntitySerializer::class);
    $this->eventFilter = $this->prophesize(EventFilterService::class);
    $this->triggerExecutor = $this->prophesize(TriggerExecutor::class);
    $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
    $this->cronScheduleMatcher = $this->prophesize(CronScheduleMatcher::class);
    $this->cronStateService = $this->prophesize(CronTriggerStateService::class);
    $this->logger = $this->prophesize(LoggerInterface::class);

    $loggerFactory = $this->prophesize(LoggerChannelFactoryInterface::class);
    // @phpstan-ignore-next-line
    $loggerFactory->get('flowdrop_trigger')->willReturn($this->logger->reveal());

    $this->configStorage = $this->prophesize(EntityStorageInterface::class);
    $this->workflowStorage = $this->prophesize(EntityStorageInterface::class);

    $this->entityTypeManager->getStorage('flowdrop_trigger_config')
      ->willReturn($this->configStorage->reveal());
    $this->entityTypeManager->getStorage('flowdrop_workflow')
      ->willReturn($this->workflowStorage->reveal());

    $this->manager = new TriggerManager(
      $this->entityTypeManager->reveal(),
      $this->eventTypeManager->reveal(),
      $this->entitySerializer->reveal(),
      $loggerFactory->reveal(),
      $this->eventFilter->reveal(),
      $this->triggerExecutor->reveal(),
      $this->configFactory->reveal(),
      $this->cronScheduleMatcher->reveal(),
      $this->cronStateService->reveal(),
    );
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventBlockedByEventFilter(): void {
    $this->eventFilter->isEventTypeAllowed('entity.insert')->willReturn(FALSE);

    $entity = $this->prophesize(EntityInterface::class);
    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventBlockedByEntityType(): void {
    $this->eventFilter->isEventTypeAllowed('entity.insert')->willReturn(TRUE);
    $this->eventFilter->isEntityEvent('entity.insert')->willReturn(TRUE);
    $this->eventFilter->isEntityTypeAllowed('node')->willReturn(FALSE);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventBlockedByBundle(): void {
    $this->eventFilter->isEventTypeAllowed('entity.insert')->willReturn(TRUE);
    $this->eventFilter->isEntityEvent('entity.insert')->willReturn(TRUE);
    $this->eventFilter->isEntityTypeAllowed('node')->willReturn(TRUE);
    $this->eventFilter->isBundleAllowed('node', 'article')->willReturn(FALSE);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');
    $entity->bundle()->willReturn('article');

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventBlockedByUserRole(): void {
    $this->eventFilter->isEventTypeAllowed('user.login')->willReturn(TRUE);
    $this->eventFilter->isEntityEvent('user.login')->willReturn(FALSE);
    $this->eventFilter->isUserEvent('user.login')->willReturn(TRUE);
    $this->eventFilter->isUserRoleAllowed(['authenticated', 'administrator'])->willReturn(FALSE);

    $entity = $this->prophesize(MockUserEntityInterface::class);
    $entity->getEntityTypeId()->willReturn('user');
    $entity->bundle()->willReturn('user');
    $entity->getRoles()->willReturn(['authenticated', 'administrator']);
    $entity->id()->willReturn('1');

    $this->manager->processEvent('user.login', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventNoMatchingTriggers(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'article');

    $entity = $this->createMockContentEntity('node', 'article', '1');

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([]);

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processEvent
   * @covers ::loadMatchingTriggers
   */
  public function testProcessEventExecutesMatchingTriggers(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'article');

    $entity = $this->createMockContentEntity('node', 'article', '1');
    $entity->getOriginal()->willReturn(NULL);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('my_workflow')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('trigger_1', 'my_workflow', 0);
    $config->matches('entity.insert', Argument::type('array'))->willReturn(TRUE);

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->entitySerializer->serialize(Argument::any())->willReturn(['nid' => 1]);

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(
      Argument::type(FlowDropTriggerConfigInterface::class),
      'entity.insert',
      Argument::any(),
      ['nid' => 1],
      NULL
    )->shouldHaveBeenCalledOnce();
  }

  /**
   * @covers ::processEvent
   * @covers ::loadMatchingTriggers
   */
  public function testProcessEventTriggersExecuteInWeightOrder(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'page');

    $entity = $this->createMockContentEntity('node', 'page', '5');
    $entity->getOriginal()->willReturn(NULL);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf_a')->willReturn($workflow->reveal());
    $this->workflowStorage->load('wf_b')->willReturn($workflow->reveal());

    $configHigh = $this->createMockTriggerConfig('trigger_high', 'wf_a', 10);
    $configHigh->matches(Argument::cetera())->willReturn(TRUE);

    $configLow = $this->createMockTriggerConfig('trigger_low', 'wf_b', -5);
    $configLow->matches(Argument::cetera())->willReturn(TRUE);

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([$configHigh->reveal(), $configLow->reveal()]);

    $this->entitySerializer->serialize(Argument::any())->willReturn([]);

    $executionOrder = [];
    $this->triggerExecutor->executeTrigger(Argument::cetera())
      ->will(function ($args) use (&$executionOrder) {
        $executionOrder[] = $args[0]->id();
      });

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->assertEquals(['trigger_low', 'trigger_high'], $executionOrder);
  }

  /**
   * @covers ::processEvent
   * @covers ::loadMatchingTriggers
   */
  public function testProcessEventOriginalEntityPassedForUpdate(): void {
    $this->setupEventFilterPassthrough('entity.update', 'node', 'article');

    $original = $this->prophesize(ContentEntityInterface::class);
    $entity = $this->createMockContentEntity('node', 'article', '1');
    $entity->getOriginal()->willReturn($original->reveal());

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'wf', 0);
    $config->matches(Argument::cetera())->willReturn(TRUE);

    $this->configStorage->loadByProperties(['event_type' => 'entity.update', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->entitySerializer->serialize($entity->reveal())->willReturn(['title' => 'new']);
    $this->entitySerializer->serialize($original->reveal())->willReturn(['title' => 'old']);

    $this->manager->processEvent('entity.update', $entity->reveal());

    $this->triggerExecutor->executeTrigger(
      Argument::any(),
      'entity.update',
      Argument::any(),
      ['title' => 'new'],
      ['title' => 'old']
    )->shouldHaveBeenCalledOnce();
  }

  /**
   * @covers ::loadMatchingTriggers
   */
  public function testLoadMatchingTriggersSkipsOrphanedTrigger(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'page');

    $entity = $this->createMockContentEntity('node', 'page', '1');
    $entity->getOriginal()->willReturn(NULL);

    // Workflow doesn't exist.
    $this->workflowStorage->load('missing_workflow')->willReturn(NULL);

    $config = $this->createMockTriggerConfig('t1', 'missing_workflow', 0);

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
    $this->logger->warning(Argument::containingString('non-existent workflow'), Argument::any())
      ->shouldHaveBeenCalled();
  }

  /**
   * @covers ::loadMatchingTriggers
   */
  public function testLoadMatchingTriggersSkipsDisabledWorkflow(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'page');

    $entity = $this->createMockContentEntity('node', 'page', '1');
    $entity->getOriginal()->willReturn(NULL);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(FALSE);
    $this->workflowStorage->load('disabled_wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'disabled_wf', 0);

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::loadMatchingTriggers
   */
  public function testLoadMatchingTriggersSkipsNonMatchingConditions(): void {
    $this->setupEventFilterPassthrough('entity.insert', 'node', 'page');

    $entity = $this->createMockContentEntity('node', 'page', '1');
    $entity->getOriginal()->willReturn(NULL);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'wf', 0);
    $config->matches('entity.insert', Argument::type('array'))->willReturn(FALSE);

    $this->configStorage->loadByProperties(['event_type' => 'entity.insert', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->manager->processEvent('entity.insert', $entity->reveal());

    $this->triggerExecutor->executeTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processFormEvent
   */
  public function testProcessFormEventBlockedByEventFilter(): void {
    $this->eventFilter->isEventTypeAllowed('form.submit')->willReturn(FALSE);

    $this->manager->processFormEvent('form.submit', 'node_article_edit_form');

    $this->triggerExecutor->executeFormTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processFormEvent
   */
  public function testProcessFormEventBlockedByFormId(): void {
    $this->eventFilter->isEventTypeAllowed('form.submit')->willReturn(TRUE);
    $this->eventFilter->isFormIdAllowed('user_admin_settings')->willReturn(FALSE);

    $this->manager->processFormEvent('form.submit', 'user_admin_settings');

    $this->triggerExecutor->executeFormTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processFormEvent
   */
  public function testProcessFormEventPluginCreationFailureHandled(): void {
    $this->eventFilter->isEventTypeAllowed('form.submit')->willReturn(TRUE);
    $this->eventFilter->isFormIdAllowed('my_form')->willReturn(TRUE);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'wf', 0);
    $config->matches(Argument::cetera())->willReturn(TRUE);
    $this->configStorage->loadByProperties(['event_type' => 'form.submit', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $this->eventTypeManager->createInstance('form.submit')
      ->willThrow(new \Exception('Plugin not found'));

    $this->manager->processFormEvent('form.submit', 'my_form');

    $this->triggerExecutor->executeFormTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
    $this->logger->error(Argument::containingString('Failed to load event type plugin'), Argument::any())
      ->shouldHaveBeenCalled();
  }

  /**
   * @covers ::processFormEvent
   */
  public function testProcessFormEventExecutesTriggers(): void {
    $this->eventFilter->isEventTypeAllowed('form.submit')->willReturn(TRUE);
    $this->eventFilter->isFormIdAllowed('node_article_form')->willReturn(TRUE);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'wf', 0);
    $config->matches(Argument::cetera())->willReturn(TRUE);
    $this->configStorage->loadByProperties(['event_type' => 'form.submit', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    $eventTypePlugin = $this->prophesize(FlowDropEventTypeInterface::class);
    $eventTypePlugin->extractTriggerData(NULL, NULL, Argument::type('array'))
      ->willReturn(['form_values' => ['title' => 'Test']]);
    $this->eventTypeManager->createInstance('form.submit')->willReturn($eventTypePlugin->reveal());

    $this->manager->processFormEvent('form.submit', 'node_article_form');

    $this->triggerExecutor->executeFormTrigger(
      Argument::type(FlowDropTriggerConfigInterface::class),
      'form.submit',
      'node_article_form',
      ['form_values' => ['title' => 'Test']],
      Argument::type('array')
    )->shouldHaveBeenCalledOnce();
  }

  /**
   * @covers ::processCronEvent
   */
  public function testProcessCronEventBlockedByEventFilter(): void {
    $this->eventFilter->isEventTypeAllowed('cron.run')->willReturn(FALSE);

    $this->manager->processCronEvent('cron.run');

    $this->triggerExecutor->executeCronTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processCronEvent
   */
  public function testProcessCronEventScheduleFiltering(): void {
    $this->eventFilter->isEventTypeAllowed('cron.run')->willReturn(TRUE);

    $cronConfig = $this->prophesize(ImmutableConfig::class);
    $cronConfig->get('max_workflows_per_run')->willReturn(0);
    $this->configFactory->get('flowdrop_trigger.settings.cron')->willReturn($cronConfig->reveal());

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf')->willReturn($workflow->reveal());

    $config = $this->createMockTriggerConfig('t1', 'wf', 0);
    $config->matches(Argument::cetera())->willReturn(TRUE);
    $this->configStorage->loadByProperties(['event_type' => 'cron.run', 'status' => TRUE])
      ->willReturn([$config->reveal()]);

    // Schedule matcher says no.
    $this->cronScheduleMatcher->shouldTriggerRun($config->reveal())->willReturn(FALSE);

    $this->cronStateService->recordSkipped('t1', Argument::type('string'))->shouldBeCalled();

    $this->manager->processCronEvent('cron.run');

    $this->triggerExecutor->executeCronTrigger(Argument::cetera())->shouldNotHaveBeenCalled();
  }

  /**
   * @covers ::processCronEvent
   */
  public function testProcessCronEventMaxWorkflowsLimit(): void {
    $this->eventFilter->isEventTypeAllowed('cron.run')->willReturn(TRUE);

    $cronConfig = $this->prophesize(ImmutableConfig::class);
    $cronConfig->get('max_workflows_per_run')->willReturn(1);
    $this->configFactory->get('flowdrop_trigger.settings.cron')->willReturn($cronConfig->reveal());

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->status()->willReturn(TRUE);
    $this->workflowStorage->load('wf_a')->willReturn($workflow->reveal());
    $this->workflowStorage->load('wf_b')->willReturn($workflow->reveal());

    $config1 = $this->createMockTriggerConfig('t1', 'wf_a', 0);
    $config1->matches(Argument::cetera())->willReturn(TRUE);
    $config2 = $this->createMockTriggerConfig('t2', 'wf_b', 1);
    $config2->matches(Argument::cetera())->willReturn(TRUE);

    $this->configStorage->loadByProperties(['event_type' => 'cron.run', 'status' => TRUE])
      ->willReturn([$config1->reveal(), $config2->reveal()]);

    $this->cronScheduleMatcher->shouldTriggerRun(Argument::any())->willReturn(TRUE);

    $eventTypePlugin = $this->prophesize(FlowDropEventTypeInterface::class);
    $eventTypePlugin->extractTriggerData(NULL, NULL, Argument::any())->willReturn([]);
    $this->eventTypeManager->createInstance('cron.run')->willReturn($eventTypePlugin->reveal());

    $this->manager->processCronEvent('cron.run');

    // Only 1 trigger should execute (max_workflows_per_run = 1).
    $this->triggerExecutor->executeCronTrigger(Argument::cetera())->shouldHaveBeenCalledTimes(1);
  }

  /**
   * @covers ::getAllActiveTriggers
   */
  public function testGetAllActiveTriggersGroupsByEventType(): void {
    $config1 = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config1->getEventType()->willReturn('entity.insert');
    $config2 = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config2->getEventType()->willReturn('entity.update');
    $config3 = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config3->getEventType()->willReturn('entity.insert');

    $this->configStorage->loadByProperties(['status' => TRUE])
      ->willReturn([$config1->reveal(), $config2->reveal(), $config3->reveal()]);

    $result = $this->manager->getAllActiveTriggers();

    $this->assertCount(2, $result);
    $this->assertCount(2, $result['entity.insert']);
    $this->assertCount(1, $result['entity.update']);
  }

  /**
   * @covers ::getTriggersForWorkflow
   */
  public function testGetTriggersForWorkflow(): void {
    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $this->configStorage->loadByProperties(['workflow_id' => 'test_wf'])
      ->willReturn([$config->reveal()]);

    $result = $this->manager->getTriggersForWorkflow('test_wf');

    $this->assertCount(1, $result);
  }

  /**
   * @covers ::processEvent
   */
  public function testProcessEventUserEventOnNonUserEntitySkipsRoleCheck(): void {
    $this->eventFilter->isEventTypeAllowed('user.login')->willReturn(TRUE);
    $this->eventFilter->isEntityEvent('user.login')->willReturn(FALSE);
    $this->eventFilter->isUserEvent('user.login')->willReturn(TRUE);

    // Non-user entity type - role check should be skipped.
    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');
    $entity->bundle()->willReturn('node');
    $entity->id()->willReturn('1');

    $this->configStorage->loadByProperties(['event_type' => 'user.login', 'status' => TRUE])
      ->willReturn([]);

    $this->manager->processEvent('user.login', $entity->reveal());

    // Should not have called isUserRoleAllowed since entity type is not 'user'.
    $this->eventFilter->isUserRoleAllowed(Argument::any())->shouldNotHaveBeenCalled();
  }

  /**
   * Sets up event filter to allow all events for a given entity type/bundle.
   */
  private function setupEventFilterPassthrough(string $eventType, string $entityType, string $bundle): void {
    $this->eventFilter->isEventTypeAllowed($eventType)->willReturn(TRUE);
    $this->eventFilter->isEntityEvent($eventType)->willReturn(str_starts_with($eventType, 'entity.'));
    $this->eventFilter->isUserEvent($eventType)->willReturn(str_starts_with($eventType, 'user.'));
    $this->eventFilter->isEntityTypeAllowed($entityType)->willReturn(TRUE);
    $this->eventFilter->isBundleAllowed($entityType, $bundle)->willReturn(TRUE);
  }

  /**
   * Creates a mock content entity.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\Core\Entity\ContentEntityInterface>
   *   The mock content entity.
   */
  private function createMockContentEntity(string $entityType, string $bundle, string $id) {
    $entity = $this->prophesize(ContentEntityInterface::class);
    $entity->getEntityTypeId()->willReturn($entityType);
    $entity->bundle()->willReturn($bundle);
    $entity->id()->willReturn($id);
    $entity->getOriginal()->willReturn(NULL);
    return $entity;
  }

  /**
   * Creates a mock trigger config.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface>
   *   The mock trigger config.
   */
  private function createMockTriggerConfig(string $id, string $workflowId, int $weight) {
    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn($id);
    $config->getWorkflowId()->willReturn($workflowId);
    $config->getWeight()->willReturn($weight);
    return $config;
  }

}
