<?php

declare(strict_types=1);

namespace Drupal\Tests\native_observability_otel\Unit\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Schema;
use Drupal\Tests\UnitTestCase;
use Drupal\native_observability_otel\Service\LivePrometheusMetricsReader;
use PHPUnit\Framework\Attributes\CoversClass;

/**
 * Unit tests for LivePrometheusMetricsReader pure logic and guards.
 *
 * Private helpers are exercised via ReflectionMethod because their edge-case
 * behaviour (nearest-rank percentile, Prometheus label escaping) is clearer
 * to verify directly than through a full database mock chain.
 */
#[CoversClass(LivePrometheusMetricsReader::class)]
final class LivePrometheusMetricsReaderTest extends UnitTestCase {

  /**
   * Builds a LivePrometheusMetricsReader backed by a minimal Connection mock.
   *
   * The Connection mock only satisfies schema()->tableExists() calls; it does
   * not stub the query chain because these tests cover table-absent guard
   * paths where the query is never reached.
   *
   * @param bool $cacheTableExists
   *   Whether the cache event table reports as existing.
   * @param bool $spansTableExists
   *   Whether the spans table reports as existing.
   *
   * @return \Drupal\native_observability_otel\Service\LivePrometheusMetricsReader
   *   The configured reader instance.
   */
  private function buildReader(
    bool $cacheTableExists = FALSE,
    bool $spansTableExists = FALSE,
  ): LivePrometheusMetricsReader {
    $schema = $this->createMock(Schema::class);
    $schema->method('tableExists')
      ->willReturnCallback(
        static function (string $table) use ($cacheTableExists, $spansTableExists): bool {
          return match ($table) {
            'native_observability_cache_event' => $cacheTableExists,
            'native_observability_spans' => $spansTableExists,
            default => FALSE,
          };
        }
      );

    $connection = $this->createMock(Connection::class);
    $connection->method('schema')->willReturn($schema);

    return new LivePrometheusMetricsReader($connection);
  }

  /**
   * Returns a ReflectionMethod handle for a private method on the reader.
   *
   * @param string $name
   *   The method name.
   *
   * @return \ReflectionMethod
   *   Accessible reflection handle.
   */
  private function privateMethod(string $name): \ReflectionMethod {
    $method = new \ReflectionMethod(LivePrometheusMetricsReader::class, $name);
    return $method;
  }

  /**
   * Verifies computePercentile returns 0.0 for an empty input list.
   */
  public function testComputePercentileReturnsZeroForEmptyList(): void {
    $result = $this->privateMethod('computePercentile')
      ->invoke($this->buildReader(), [], 0.5);

    $this->assertSame(0.0, $result);
  }

  /**
   * Verifies that p50 of a single-element list equals that element.
   */
  public function testComputePercentileP50SingleElement(): void {
    $result = $this->privateMethod('computePercentile')
      ->invoke($this->buildReader(), [42.0], 0.5);

    $this->assertSame(42.0, $result);
  }

  /**
   * Verifies the nearest-rank p95 value on a 20-element sorted list.
   *
   * Nearest-rank formula: index = ceil(20 * 0.95) - 1 = 18, value = 190.
   */
  public function testComputePercentileP95MultiElement(): void {
    $values = array_map(
      static fn(int $i): float => (float) ($i * 10),
      range(1, 20),
    );

    $result = $this->privateMethod('computePercentile')
      ->invoke($this->buildReader(), $values, 0.95);

    $this->assertSame(190.0, $result);
  }

  /**
   * Verifies that a double-quote in a label value is escaped with a backslash.
   */
  public function testSanitizeLabelValueEscapesDoubleQuote(): void {
    $result = (string) $this->privateMethod('sanitizeLabelValue')
      ->invoke($this->buildReader(), 'route"name');

    $this->assertStringContainsString('\\"', $result);
  }

  /**
   * Verifies that a backslash in a label value is doubled.
   */
  public function testSanitizeLabelValueEscapesBackslash(): void {
    $result = (string) $this->privateMethod('sanitizeLabelValue')
      ->invoke($this->buildReader(), 'a\\b');

    $this->assertSame('a\\\\b', $result);
  }

  /**
   * Verifies that an alphanumeric label value passes through unchanged.
   */
  public function testSanitizeLabelValuePassesThroughAlphanumeric(): void {
    $result = $this->privateMethod('sanitizeLabelValue')
      ->invoke($this->buildReader(), 'node.view');

    $this->assertSame('node.view', $result);
  }

  /**
   * Verifies buildCacheMetrics returns '' when the cache event table is absent.
   */
  public function testBuildCacheMetricsReturnsEmptyStringWhenTableAbsent(): void {
    $this->assertSame(
      '',
      $this->buildReader(cacheTableExists: FALSE)->buildCacheMetrics(),
    );
  }

  /**
   * Verifies buildSpanMetrics returns '' when the spans table is absent.
   */
  public function testBuildSpanMetricsReturnsEmptyStringWhenTableAbsent(): void {
    $this->assertSame(
      '',
      $this->buildReader(spansTableExists: FALSE)->buildSpanMetrics(300),
    );
  }

}
