<?php

declare(strict_types=1);

namespace Drupal\Tests\native_observability_otel\Unit\EventSubscriber;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\native_observability_otel\EventSubscriber\OpenTelemetryTraceSubscriber;
use Drupal\native_observability_otel\Service\ExporterRegistry;
use Psr\Log\LoggerInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
 * Unit tests for OpenTelemetryTraceSubscriber::isOwnRouteExcluded().
 *
 * IsOwnRouteExcluded() is private and reads capture.exclude_own_routes directly
 * from native_observability.settings via ConfigFactoryInterface. The null-safe
 * default (TRUE when the key is absent) is the key contract under test.
 *
 * The onKernelResponse() smoke test verifies that an excluded route causes the
 * method to return early. ExporterRegistry is final so it is tested via
 * getMockBuilder with disableOriginalConstructor(); if the route is excluded
 * the get() method is never reached.
 */
#[CoversClass(OpenTelemetryTraceSubscriber::class)]
final class OpenTelemetryTraceSubscriberRouteExclusionTest extends UnitTestCase {

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

    // ---------------------------------------------------------------------------
    // Ensure the .install file is loaded for hook_requirements().
    // In unit tests, Drupal does NOT automatically include .install files,
    // so we must require it manually to make the procedural hook available.
    // ---------------------------------------------------------------------------
    require_once dirname(__DIR__, 4) . '/native_observability_otel.install';
  }

  /**
   * Builds an OpenTelemetryTraceSubscriber with a controlled config value.
   *
   * @param bool|null $excludeValue
   *   The raw capture.exclude_own_routes value; NULL simulates an absent key.
   *
   * @return \Drupal\native_observability_otel\EventSubscriber\OpenTelemetryTraceSubscriber
   *   The configured subscriber.
   */
  private function buildSubscriber(
    bool|null $excludeValue,
  ): OpenTelemetryTraceSubscriber {
    $config = $this->createMock(ImmutableConfig::class);
    $config->method('get')
      ->with('capture.exclude_own_routes')
      ->willReturn($excludeValue);

    $factory = $this->createMock(ConfigFactoryInterface::class);
    $factory->method('get')
      ->with('native_observability.settings')
      ->willReturn($config);

    $exporterRegistry = $this->getMockBuilder(ExporterRegistry::class)
      ->disableOriginalConstructor()
      ->getMock();

    return new OpenTelemetryTraceSubscriber(
      $exporterRegistry,
      $this->createMock(AccountProxyInterface::class),
      $this->createMock(LoggerInterface::class),
      $factory,
    );
  }

  /**
   * Invokes the private isOwnRouteExcluded() via reflection.
   *
   * @param \Drupal\native_observability_otel\EventSubscriber\OpenTelemetryTraceSubscriber $subscriber
   *   The subscriber.
   * @param string $routeName
   *   The route machine name.
   *
   * @return bool
   *   The exclusion verdict.
   */
  private function callIsOwnRouteExcluded(
    OpenTelemetryTraceSubscriber $subscriber,
    string $routeName,
  ): bool {
    $method = new \ReflectionMethod(
      OpenTelemetryTraceSubscriber::class,
      'isOwnRouteExcluded',
    );

    return (bool) $method->invoke($subscriber, $routeName);
  }

  /**
   * Verifies TRUE default when the config key is NULL and route prefix matches.
   */
  public function testDefaultsTrueWhenConfigKeyNullAndOwnRoute(): void {
    $subscriber = $this->buildSubscriber(NULL);

    $this->assertTrue(
      $this->callIsOwnRouteExcluded($subscriber, 'native_observability.dashboard'),
    );
  }

  /**
   * Verifies FALSE for a foreign route even when the config key is NULL.
   */
  public function testReturnsFalseForForeignRouteWhenConfigNull(): void {
    $subscriber = $this->buildSubscriber(NULL);

    $this->assertFalse(
      $this->callIsOwnRouteExcluded($subscriber, 'user.login'),
    );
  }

  /**
   * Verifies FALSE when exclusion is explicitly disabled via config.
   */
  public function testReturnsFalseWhenExplicitlyDisabled(): void {
    $subscriber = $this->buildSubscriber(FALSE);

    $this->assertFalse(
      $this->callIsOwnRouteExcluded($subscriber, 'native_observability.dashboard'),
    );
  }

  /**
   * Verifies onKernelResponse() returns early without calling ExporterRegistry.
   *
   * When the route is excluded the subscriber must return before reaching
   * exporterRegistry->get(). The ExporterRegistry mock is configured to fail
   * if get() is called, so passing this test proves the early-return path.
   */
  public function testOnKernelResponseSkipsExporterForExcludedRoute(): void {
    $config = $this->createMock(ImmutableConfig::class);
    $config->method('get')
      ->with('capture.exclude_own_routes')
      ->willReturn(TRUE);

    $factory = $this->createMock(ConfigFactoryInterface::class);
    $factory->method('get')
      ->with('native_observability.settings')
      ->willReturn($config);

    $exporterRegistry = $this->getMockBuilder(ExporterRegistry::class)
      ->disableOriginalConstructor()
      ->getMock();

    $exporterRegistry->expects($this->never())->method('get');

    $subscriber = new OpenTelemetryTraceSubscriber(
      $exporterRegistry,
      $this->createMock(AccountProxyInterface::class),
      $this->createMock(LoggerInterface::class),
      $factory,
    );

    $request = new Request();
    $request->attributes->set('_route', 'native_observability_otel.prometheus');

    $kernel = $this->createMock(HttpKernelInterface::class);
    $event = new ResponseEvent(
      $kernel,
      $request,
      HttpKernelInterface::MAIN_REQUEST,
      new Response(),
    );

    $subscriber->onKernelResponse($event);
  }

}
