<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_trigger\Unit\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\flowdrop_trigger\Service\EventFilterService;
use Drupal\flowdrop_trigger\Service\EventTypePluginManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the EventFilterService.
 *
 * @coversDefaultClass \Drupal\flowdrop_trigger\Service\EventFilterService
 * @group flowdrop_trigger
 */
class EventFilterServiceTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The event filter service under test.
   *
   * @var \Drupal\flowdrop_trigger\Service\EventFilterService
   */
  private EventFilterService $service;

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

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

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

    $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
    $this->eventTypeManager = $this->prophesize(EventTypePluginManager::class);

    $this->service = new EventFilterService(
      $this->configFactory->reveal(),
      $this->eventTypeManager->reveal(),
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeNotAllowedWhenCategoryDisabled(): void {
    $this->eventTypeManager->getDefinition('entity.insert', FALSE)
      ->willReturn(['category' => 'entity']);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(FALSE);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertFalse(
      $this->service->isEventTypeAllowed('entity.insert')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeNotAllowedWhenEnabledNotExplicitlyTrue(): void {
    $this->eventTypeManager->getDefinition('entity.insert', FALSE)
      ->willReturn(['category' => 'entity']);

    $config = $this->prophesize(ImmutableConfig::class);
    // NULL (not explicitly set) should default to disabled.
    $config->get('enabled')->willReturn(NULL);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertFalse(
      $this->service->isEventTypeAllowed('entity.insert')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeAllowedWhenEnabledNoRestrictions(): void {
    $this->eventTypeManager->getDefinition('entity.insert', FALSE)
      ->willReturn(['category' => 'entity']);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(TRUE);
    $config->get('allowed_event_types')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEventTypeAllowed('entity.insert')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeAllowedWhenInRestrictedList(): void {
    $this->eventTypeManager->getDefinition('entity.insert', FALSE)
      ->willReturn(['category' => 'entity']);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(TRUE);
    $config->get('allowed_event_types')
      ->willReturn(['entity.insert', 'entity.update']);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEventTypeAllowed('entity.insert')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeNotAllowedWhenNotInRestrictedList(): void {
    $this->eventTypeManager->getDefinition('entity.delete', FALSE)
      ->willReturn(['category' => 'entity']);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(TRUE);
    $config->get('allowed_event_types')
      ->willReturn(['entity.insert', 'entity.update']);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertFalse(
      $this->service->isEventTypeAllowed('entity.delete')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeUsesCorrectCategoryConfig(): void {
    $this->eventTypeManager->getDefinition('user.login', FALSE)
      ->willReturn(['category' => 'user']);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(TRUE);
    $config->get('allowed_event_types')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.user')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEventTypeAllowed('user.login')
    );
  }

  /**
   * @covers ::isEventTypeAllowed
   */
  public function testEventTypeDefaultsCategoryToEntityWhenMissing(): void {
    $this->eventTypeManager->getDefinition('custom.event', FALSE)
      ->willReturn(NULL);

    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('enabled')->willReturn(TRUE);
    $config->get('allowed_event_types')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEventTypeAllowed('custom.event')
    );
  }

  /**
   * @covers ::isEntityEvent
   */
  public function testIsEntityEvent(): void {
    $this->assertTrue(
      $this->service->isEntityEvent('entity.insert')
    );
    $this->assertTrue(
      $this->service->isEntityEvent('entity.update')
    );
    $this->assertFalse(
      $this->service->isEntityEvent('user.login')
    );
    $this->assertFalse(
      $this->service->isEntityEvent('cron.run')
    );
  }

  /**
   * @covers ::isUserEvent
   */
  public function testIsUserEvent(): void {
    $this->assertTrue(
      $this->service->isUserEvent('user.login')
    );
    $this->assertTrue(
      $this->service->isUserEvent('user.logout')
    );
    $this->assertFalse(
      $this->service->isUserEvent('entity.insert')
    );
  }

  /**
   * @covers ::isCronEvent
   */
  public function testIsCronEvent(): void {
    $this->assertTrue($this->service->isCronEvent('cron.run'));
    $this->assertFalse(
      $this->service->isCronEvent('entity.insert')
    );
  }

  /**
   * @covers ::isEntityTypeAllowed
   */
  public function testEntityTypeAllowedNoRestrictions(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('allowed_entity_types')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEntityTypeAllowed('node')
    );
  }

  /**
   * @covers ::isEntityTypeAllowed
   */
  public function testEntityTypeAllowedInList(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('allowed_entity_types')
      ->willReturn(['node', 'taxonomy_term']);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isEntityTypeAllowed('node')
    );
    $this->assertFalse(
      $this->service->isEntityTypeAllowed('user')
    );
  }

  /**
   * @covers ::isBundleAllowed
   */
  public function testBundleAllowedNoRestrictions(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('allowed_bundles')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isBundleAllowed('node', 'article')
    );
  }

  /**
   * @covers ::isBundleAllowed
   */
  public function testBundleAllowedFormatMatching(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('allowed_bundles')
      ->willReturn(['node:article', 'node:page']);
    $this->configFactory->get('flowdrop_trigger.settings.entity')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isBundleAllowed('node', 'article')
    );
    $this->assertTrue(
      $this->service->isBundleAllowed('node', 'page')
    );
    $this->assertFalse(
      $this->service->isBundleAllowed('node', 'blog')
    );
    $this->assertFalse(
      $this->service->isBundleAllowed('taxonomy_term', 'article')
    );
  }

  /**
   * @covers ::isFormIdAllowed
   */
  public function testFormIdExcludedPatternsTakePrecedence(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_form_ids')
      ->willReturn(['user_admin_*']);
    $config->get('allowed_form_ids')
      ->willReturn(['user_admin_settings']);
    $this->configFactory->get('flowdrop_trigger.settings.form')
      ->willReturn($config->reveal());

    // Excluded pattern takes precedence over allowed list.
    $this->assertFalse(
      $this->service->isFormIdAllowed('user_admin_settings')
    );
  }

  /**
   * @covers ::isFormIdAllowed
   */
  public function testFormIdWildcardMatching(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_form_ids')->willReturn([]);
    $config->get('allowed_form_ids')
      ->willReturn(['node_*_form', 'user_login_form']);
    $this->configFactory->get('flowdrop_trigger.settings.form')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isFormIdAllowed('node_article_form')
    );
    $this->assertTrue(
      $this->service->isFormIdAllowed('node_page_form')
    );
    $this->assertTrue(
      $this->service->isFormIdAllowed('user_login_form')
    );
    $this->assertFalse(
      $this->service->isFormIdAllowed('comment_form')
    );
  }

  /**
   * @covers ::isFormIdAllowed
   */
  public function testFormIdAllowedNoRestrictions(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_form_ids')->willReturn([]);
    $config->get('allowed_form_ids')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.form')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isFormIdAllowed('any_form')
    );
  }

  /**
   * @covers ::isUserRoleAllowed
   */
  public function testUserRoleExcludedBlocksEvenIfOtherAllowed(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_roles')
      ->willReturn(['administrator']);
    $config->get('allowed_roles')
      ->willReturn(['authenticated', 'administrator']);
    $this->configFactory->get('flowdrop_trigger.settings.user')
      ->willReturn($config->reveal());

    // User has both roles, but administrator is excluded.
    $this->assertFalse(
      $this->service->isUserRoleAllowed(
        ['authenticated', 'administrator']
      )
    );
  }

  /**
   * @covers ::isUserRoleAllowed
   */
  public function testUserRoleAllowedMultipleRolesUnion(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_roles')->willReturn([]);
    $config->get('allowed_roles')->willReturn(['editor']);
    $this->configFactory->get('flowdrop_trigger.settings.user')
      ->willReturn($config->reveal());

    // User has 'editor' role among others - should be allowed.
    $this->assertTrue(
      $this->service->isUserRoleAllowed(
        ['authenticated', 'editor']
      )
    );
    // User without 'editor' role.
    $this->assertFalse(
      $this->service->isUserRoleAllowed(['authenticated'])
    );
  }

  /**
   * @covers ::isUserRoleAllowed
   */
  public function testUserRoleAllowedNoRestrictions(): void {
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('excluded_roles')->willReturn([]);
    $config->get('allowed_roles')->willReturn([]);
    $this->configFactory->get('flowdrop_trigger.settings.user')
      ->willReturn($config->reveal());

    $this->assertTrue(
      $this->service->isUserRoleAllowed(['authenticated'])
    );
  }

  /**
   * @covers ::matchWildcard
   */
  public function testMatchWildcardExactMatch(): void {
    $this->assertTrue(
      $this->service->matchWildcard('node_form', 'node_form')
    );
  }

  /**
   * @covers ::matchWildcard
   */
  public function testMatchWildcardStarPattern(): void {
    $this->assertTrue(
      $this->service->matchWildcard(
        'node_article_form',
        'node_*_form'
      )
    );
    $this->assertTrue(
      $this->service->matchWildcard(
        'node_page_form',
        'node_*_form'
      )
    );
    $this->assertFalse(
      $this->service->matchWildcard(
        'user_login_form',
        'node_*_form'
      )
    );
  }

  /**
   * @covers ::matchWildcard
   */
  public function testMatchWildcardQuestionMark(): void {
    $this->assertTrue(
      $this->service->matchWildcard('test_a', 'test_?')
    );
    $this->assertFalse(
      $this->service->matchWildcard('test_ab', 'test_?')
    );
  }

  /**
   * @covers ::matchWildcard
   */
  public function testMatchWildcardSpecialRegexChars(): void {
    // Dots, brackets, etc. should be treated as literals.
    $this->assertTrue(
      $this->service->matchWildcard(
        'form.submit',
        'form.submit'
      )
    );
    $this->assertFalse(
      $this->service->matchWildcard(
        'formXsubmit',
        'form.submit'
      )
    );
  }

  /**
   * @covers ::getCategoryConfigName
   */
  public function testGetCategoryConfigName(): void {
    $this->eventTypeManager->getDefinition('cron.run', FALSE)
      ->willReturn(['category' => 'cron']);

    $this->assertEquals(
      'flowdrop_trigger.settings.cron',
      $this->service->getCategoryConfigName('cron.run')
    );
  }

}
