<?php

declare(strict_types=1);

namespace Drupal\Tests\native_observability\Unit\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Database\Connection;
use Drupal\Tests\UnitTestCase;
use Drupal\native_observability\Service\RequestLogExclusionMatcher;
use Drupal\native_observability\Service\RequestLogExclusionRepository;
use Drupal\native_observability\Service\RequestLogExclusionRuleNormalizer;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Component\HttpFoundation\Request;

/**
 * Tests runtime request-log exclusion matching.
 */
#[CoversClass(RequestLogExclusionMatcher::class)]
final class RequestLogExclusionMatcherTest extends UnitTestCase {

  /**
   * Tests exact path and query value matching.
   */
  public function testMatchesExactPathAndQueryValue(): void {
    $matcher = new RequestLogExclusionMatcher($this->buildRepositoryWithCachedRules(
      [
        [
          'id' => 7,
          'enabled' => TRUE,
          'method' => 'GET',
          'path_match_type' => 'exact',
          'path_pattern' => '/foo/bar',
          'query_conditions' => [
            ['key' => 'preview', 'operator' => 'equals', 'value' => '1', 'value_to' => ''],
          ],
        ],
      ],
    ));
    $request = Request::create('/foo/bar?preview=1', 'GET');

    $this->assertTrue($matcher->isExcluded($request));
    $this->assertTrue((bool) $request->attributes->get(RequestLogExclusionMatcher::ATTR_EXCLUDED));
    $this->assertSame(7, $request->attributes->get(RequestLogExclusionMatcher::ATTR_MATCHED_RULE_ID));
  }

  /**
   * Tests comparable range matching for query values.
   */
  public function testMatchesBetweenCondition(): void {
    $matcher = new RequestLogExclusionMatcher($this->buildRepositoryWithCachedRules(
      [
        [
          'id' => 3,
          'enabled' => TRUE,
          'method' => 'GET',
          'path_match_type' => 'exact',
          'path_pattern' => '/reports',
          'query_conditions' => [
            ['key' => 'from', 'operator' => 'between', 'value' => '2026-01-01', 'value_to' => '2026-01-31'],
          ],
        ],
      ],
    ));
    $request = Request::create('/reports?from=2026-01-15', 'GET');

    $this->assertTrue($matcher->isExcluded($request));
  }

  /**
   * Tests that invalid comparable values fail safely.
   */
  public function testInvalidComparableValuesDoNotExclude(): void {
    $matcher = new RequestLogExclusionMatcher($this->buildRepositoryWithCachedRules(
      [
        [
          'id' => 4,
          'enabled' => TRUE,
          'method' => 'GET',
          'path_match_type' => 'exact',
          'path_pattern' => '/reports',
          'query_conditions' => [
            ['key' => 'from', 'operator' => 'gte', 'value' => 'not-a-date', 'value_to' => ''],
          ],
        ],
      ],
    ));
    $request = Request::create('/reports?from=2026-01-15', 'GET');

    $this->assertFalse($matcher->isExcluded($request));
  }

  /**
   * Builds a repository whose runtime cache already contains rules.
   *
   * @param array<int, array<string, mixed>> $rules
   *   Rules exposed by cache.
   *
   * @return \Drupal\native_observability\Service\RequestLogExclusionRepository
   *   Repository instance.
   */
  private function buildRepositoryWithCachedRules(array $rules): RequestLogExclusionRepository {
    $cache = $this->createMock(CacheBackendInterface::class);
    $cache->method('get')
      ->with(RequestLogExclusionRepository::CACHE_ID)
      ->willReturn((object) ['data' => $rules]);

    return new RequestLogExclusionRepository(
      $this->createMock(Connection::class),
      $cache,
      $this->createMock(CacheTagsInvalidatorInterface::class),
      new RequestLogExclusionRuleNormalizer(),
    );
  }

}
