<?php

namespace Drupal\Tests\visitors\Unit\Service;

use Drupal\Tests\UnitTestCase;
use Drupal\visitors\Service\PageAttachmentsService;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Tests the PageAttachmentsService class.
 *
 * @coversDefaultClass \Drupal\visitors\Service\PageAttachmentsService
 *
 * @group visitors
 */
class PageAttachmentsServiceTest extends UnitTestCase {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $configFactory;

  /**
   * The settings.
   *
   * @var \Drupal\Core\Config\ImmutableConfig|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $settings;

  /**
   * The service.
   *
   * @var \Drupal\visitors\Service\PageAttachmentsService
   */
  protected $service;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $currentUser;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $moduleHandler;

  /**
   * The current route match.
   *
   * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $currentRouteMatch;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $requestStack;

  /**
   * The visitors visibility service.
   *
   * @var \Drupal\visitors\VisitorsVisibilityInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $visibilityService;

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $logger;

  /**
   * The cache contexts manager.
   *
   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $cacheContextsManager;

  /**
   * The visitors event plugin manager.
   *
   * @var \Drupal\visitors\VisitorsEventPluginManager|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $eventPluginManager;

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

    $container = new ContainerBuilder();

    $this->configFactory = $this->createMock('Drupal\Core\Config\ConfigFactoryInterface');
    $container->set('config.factory', $this->configFactory);

    $this->currentUser = $this->createMock('Drupal\Core\Session\AccountInterface');
    $container->set('current_user', $this->currentUser);

    $this->moduleHandler = $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface');
    $container->set('module_handler', $this->moduleHandler);

    $this->currentRouteMatch = $this->createMock('Drupal\Core\Routing\ResettableStackedRouteMatchInterface');
    $container->set('current_route_match', $this->currentRouteMatch);

    $this->requestStack = $this->createMock('Symfony\Component\HttpFoundation\RequestStack');
    $container->set('request_stack', $this->requestStack);

    $this->visibilityService = $this->createMock('Drupal\visitors\VisitorsVisibilityInterface');
    $container->set('visitors.visibility', $this->visibilityService);

    $this->logger = $this->createMock('Psr\Log\LoggerInterface');
    $container->set('logger.factory', $this->logger);

    $this->cacheContextsManager = $this->createMock('Drupal\Core\Cache\Context\CacheContextsManager');
    $container->set('cache_contexts_manager', $this->cacheContextsManager);

    $this->eventPluginManager = $this->createMock('Drupal\visitors\VisitorsEventPluginManager');
    $container->set('visitors.event_plugin_manager', $this->eventPluginManager);

    \Drupal::setContainer($container);

    $this->settings = $this->createMock('Drupal\Core\Config\ImmutableConfig');

    $this->service = new PageAttachmentsService($this->configFactory, $this->currentUser, $this->moduleHandler, $this->currentRouteMatch, $this->requestStack, $this->visibilityService, $this->logger, $this->eventPluginManager);

  }

  /**
   * Tests the constructor.
   *
   * @covers ::__construct
   */
  public function testConstructor() {
    $service = new PageAttachmentsService($this->configFactory, $this->currentUser, $this->moduleHandler, $this->currentRouteMatch, $this->requestStack, $this->visibilityService, $this->logger, $this->eventPluginManager);
    $this->assertInstanceOf(PageAttachmentsService::class, $service);
  }

  /**
   * Tests the pageAttachments method.
   *
   * @covers ::pageAttachments
   */
  public function testPageAttachments() {

    $this->cacheContextsManager->expects($this->once())
      ->method('assertValidTokens')
      ->with(['user.permissions'])
      ->willReturn(TRUE);

    $this->visibilityService->expects($this->once())
      ->method('isVisible')
      ->willReturn(TRUE);

    $request = $this->createMock('Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getBasePath')
      ->willReturn('/');
    $this->requestStack->expects($this->exactly(2))
      ->method('getCurrentRequest')
      ->willReturn($request);

    $module = $this->createMock('Drupal\Core\Extension\Extension');
    $module->expects($this->once())
      ->method('getPath')
      ->willReturn('modules/custom/visitors');
    $this->moduleHandler->expects($this->once())
      ->method('getModule')
      ->with('visitors')
      ->willReturn($module);

    $this->currentRouteMatch->expects($this->exactly(3))
      ->method('getRouteName')
      ->willReturn('visitors.index');

    $this->eventPluginManager->expects($this->once())
      ->method('getDefinitions')
      ->willReturn([]);

    $page = [];
    $this->service->pageAttachments($page);

    $this->assertCount(2, $page);

    $this->assertArrayHasKey('#cache', $page);
  }

  /**
   * Tests the pageAttachments method when visibility is false.
   *
   * @covers ::pageAttachments
   */
  public function testPageAttachmentsWithException() {
    $this->cacheContextsManager->expects($this->once())
      ->method('assertValidTokens')
      ->with(['user.permissions'])
      ->willReturn(TRUE);

    $this->visibilityService->expects($this->once())
      ->method('isVisible')
      ->willThrowException(new \Exception('Visibility check failed'));

    $page = [];
    $this->service->pageAttachments($page);

    $this->assertArrayHasKey('#cache', $page);
  }

  /**
   * Tests the attachToolbar method.
   *
   * @covers ::attachToolbar
   */
  public function testAttachToolbar() {
    $this->cacheContextsManager->expects($this->once())
      ->method('assertValidTokens')
      ->with(['user.permissions'])
      ->willReturn(TRUE);

    $this->currentUser->expects($this->exactly(2))
      ->method('hasPermission')
      ->willReturnMap([
        ['access visitors', TRUE],
        ['access toolbar', TRUE],
      ]);

    $reflection = new \ReflectionMethod($this->service, 'attachToolbar');
    $reflection->setAccessible(TRUE);

    $page = [];
    $access = $reflection->invokeArgs($this->service, [&$page]);

    $this->assertArrayHasKey('#attached', $page);
    $this->assertArrayHasKey('library', $page['#attached']);
    $this->assertContains('visitors/toolbar', $page['#attached']['library']);
  }

  /**
   * Tests the attachToolbar method.
   *
   * @covers ::attachToolbar
   */
  public function testAttachToolbarNoVisitorsAccess() {
    $this->cacheContextsManager->expects($this->once())
      ->method('assertValidTokens')
      ->with(['user.permissions'])
      ->willReturn(TRUE);

    $this->currentUser->expects($this->once())
      ->method('hasPermission')
      ->willReturnMap([
        ['access visitors', FALSE],
        ['access toolbar', TRUE],
      ]);

    $reflection = new \ReflectionMethod($this->service, 'attachToolbar');
    $reflection->setAccessible(TRUE);

    $page = [];
    $access = $reflection->invokeArgs($this->service, [&$page]);

    $this->assertCount(0, $page);
  }

  /**
   * Tests the attachToolbar method.
   *
   * @covers ::attachToolbar
   */
  public function testAttachToolbarNoToolbar() {
    $this->cacheContextsManager->expects($this->once())
      ->method('assertValidTokens')
      ->with(['user.permissions'])
      ->willReturn(TRUE);

    $this->currentUser->expects($this->exactly(2))
      ->method('hasPermission')
      ->willReturnMap([
        ['access visitors', TRUE],
        ['access toolbar', FALSE],
      ]);

    $reflection = new \ReflectionMethod($this->service, 'attachToolbar');
    $reflection->setAccessible(TRUE);

    $page = [];
    $access = $reflection->invokeArgs($this->service, [&$page]);

    $this->assertCount(0, $page);
  }

  /**
   * Tests the attachMetaData method.
   *
   * @covers ::attachMetaData
   */
  public function testAttachMetaData() {

    $request = $this->createMock('Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getBasePath')
      ->willReturn('');

    $this->requestStack->expects($this->once())
      ->method('getCurrentRequest')
      ->willReturn($request);

    $module = $this->createMock('Drupal\Core\Extension\Extension');
    $module->expects($this->once())
      ->method('getPath')
      ->willReturn('modules/custom/visitors');
    $this->moduleHandler->expects($this->once())
      ->method('getModule')
      ->with('visitors')
      ->willReturn($module);

    $this->currentRouteMatch->expects($this->exactly(1))
      ->method('getRouteName')
      ->willReturn('visitors.index');

    // Make attachMetaData public for testing.
    $reflection = new \ReflectionMethod($this->service, 'attachMetaData');
    $reflection->setAccessible(TRUE);

    $page = [];
    $reflection->invokeArgs($this->service, [&$page]);

    $this->assertArrayHasKey('#attached', $page);
    $this->assertArrayHasKey('drupalSettings', $page['#attached']);
    $this->assertArrayHasKey('visitors', $page['#attached']['drupalSettings']);
    $this->assertArrayHasKey('module', $page['#attached']['drupalSettings']['visitors']);
    $this->assertEquals('/modules/custom/visitors', $page['#attached']['drupalSettings']['visitors']['module']);
  }

  /**
   * Tests the attachEntityCounter method.
   *
   * @covers ::attachEntityCounter
   */
  public function testAttachEntityCounter() {

    $this->currentRouteMatch->expects($this->once())
      ->method('getRouteName')
      ->willReturn('entity.node.canonical');

    $this->configFactory->expects($this->once())
      ->method('get')
      ->with('visitors.settings')
      ->willReturn($this->settings);
    $this->settings->expects($this->exactly(2))
      ->method('get')
      ->willReturnMap([
        ['counter.entity_types', ['node']],
        ['counter.enabled', TRUE],
      ]);

    $node = $this->createMock('Drupal\node\NodeInterface');
    $node->expects($this->once())
      ->method('id')
      ->willReturn(1);
    $this->currentRouteMatch->expects($this->once())
      ->method('getParameter')
      ->with('node')
      ->willReturn($node);

    // Make attachEntityCounter public for testing.
    $reflection = new \ReflectionMethod($this->service, 'attachEntityCounter');
    $reflection->setAccessible(TRUE);

    $page = [];
    $reflection->invokeArgs($this->service, [&$page]);

    $this->assertArrayHasKey('#attached', $page);
    $this->assertArrayHasKey('drupalSettings', $page['#attached']);
    $this->assertArrayHasKey('visitors', $page['#attached']['drupalSettings']);
    $this->assertArrayHasKey('counter', $page['#attached']['drupalSettings']['visitors']);
    $this->assertEquals('node:1', $page['#attached']['drupalSettings']['visitors']['counter']);
  }

  /**
   * Tests the attachEntityCounter method.
   *
   * @covers ::attachEntityCounter
   */
  public function testAttachEntityCounterNotConfigured() {

    $this->currentRouteMatch->expects($this->once())
      ->method('getRouteName')
      ->willReturn('entity.user.canonical');

    $this->configFactory->expects($this->once())
      ->method('get')
      ->with('visitors.settings')
      ->willReturn($this->settings);
    $this->settings->expects($this->exactly(2))
      ->method('get')
      ->willReturnMap([
        ['counter.entity_types', ['node']],
        ['counter.enabled', TRUE],
      ]);

    // Make attachEntityCounter public for testing.
    $reflection = new \ReflectionMethod($this->service, 'attachEntityCounter');
    $reflection->setAccessible(TRUE);

    $page = [];
    $reflection->invokeArgs($this->service, [&$page]);

    $this->assertCount(0, $page);
  }

  /**
   * Tests the processEvent method.
   *
   * @covers ::processEvent
   */
  public function testProcessEvent() {
    // Mock the current route.
    $this->currentRouteMatch->expects($this->once())
      ->method('getRouteName')
      ->willReturn('test.route');

    // Mock the request.
    $request = $this->createMock('Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getPathInfo')
      ->willReturn('/test/path');
    $request->expects($this->once())
      ->method('getUri')
      ->willReturn('http://example.com/test/path');
    $this->requestStack->expects($this->once())
      ->method('getCurrentRequest')
      ->willReturn($request);

    // Mock the current user ID.
    $this->currentUser->expects($this->once())
      ->method('id')
      ->willReturn(42);

    // Create mock plugin definitions sorted by weight.
    $definitions = [
      'plugin1' => ['id' => 'plugin1', 'weight' => 10],
      'plugin2' => ['id' => 'plugin2', 'weight' => -10],
      'plugin3' => ['id' => 'plugin3', 'weight' => 0],
    ];

    // Mock the event plugin manager to return our definitions.
    $this->eventPluginManager->expects($this->once())
      ->method('getDefinitions')
      ->willReturn($definitions);

    // Create mock plugins.
    $plugin1 = $this->createMock('Drupal\visitors\VisitorsEventPluginInterface');
    $plugin1->expects($this->never())
      ->method('process');

    $plugin2 = $this->createMock('Drupal\visitors\VisitorsEventPluginInterface');
    $plugin2->expects($this->once())
      ->method('process')
      ->willReturn([
        'event' => 'test_event',
        'variables' => ['key' => 'value'],
      ]);
    $plugin2->expects($this->once())
      ->method('getPluginId')
      ->willReturn('plugin2');

    $plugin3 = $this->createMock('Drupal\visitors\VisitorsEventPluginInterface');
    $plugin3->expects($this->never())
      ->method('process');

    // Mock createInstance to return our plugins.
    $this->eventPluginManager->expects($this->once())
      ->method('createInstance')
      ->willReturnMap([
        ['plugin2', [], $plugin2],
        ['plugin1', [], $plugin1],
      ]);

    // Make processEvent public for testing.
    $reflection = new \ReflectionMethod($this->service, 'processEvent');
    $reflection->setAccessible(TRUE);

    $page = [];
    $reflection->invokeArgs($this->service, [&$page]);

    // Assert the expected drupalSettings were attached.
    $this->assertArrayHasKey('#attached', $page);
    $this->assertArrayHasKey('drupalSettings', $page['#attached']);
    $this->assertArrayHasKey('visitors', $page['#attached']['drupalSettings']);
    $this->assertArrayHasKey('event', $page['#attached']['drupalSettings']['visitors']);
    $this->assertEquals([
      'plugin' => 'plugin2',
      'event' => 'test_event',
      'variables' => ['key' => 'value'],
    ], $page['#attached']['drupalSettings']['visitors']['event']);
  }

}
