<?php

declare(strict_types=1);

namespace Drupal\Tests\native_observability_otel\Unit\Service;

use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Schema;
use Drupal\Tests\UnitTestCase;
use Drupal\native_observability_otel\Service\LivePrometheusMetricsReader;
use Drupal\native_observability_otel\Service\PrometheusMetricsBuilder;
use PHPUnit\Framework\Attributes\CoversClass;

/**
 * Unit tests for PrometheusMetricsBuilder window resolution and section output.
 *
 * ResolveWindowSeconds() is private; it is exercised via ReflectionMethod for
 * the config-fallback branches where the value is visible without a full DB
 * round-trip. Section-concatenation tests use a real
 * LivePrometheusMetricsReader backed by a Connection mock whose schema
 * reports tables as absent, so the
 * optional sections (cache, spans) produce empty strings and the concatenation
 * logic can be verified against the HTTP section alone.
 */
#[CoversClass(PrometheusMetricsBuilder::class)]
final class PrometheusMetricsBuildTest 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 a ConfigFactoryInterface mock returning a given window value.
   *
   * @param int|null $windowValue
   *   The value to return for prometheus_window_seconds, or NULL to simulate
   *   a missing config key.
   *
   * @return \Drupal\Core\Config\ConfigFactoryInterface
   *   Mock config factory.
   */
  private function buildConfigFactory(int|null $windowValue): ConfigFactoryInterface {
    $config = $this->createMock(ImmutableConfig::class);
    $config->method('get')
      ->with('prometheus_window_seconds')
      ->willReturn($windowValue);

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

    return $factory;
  }

  /**
   * Builds a Connection mock whose schema reports both optional tables absent.
   *
   * The select() chain also returns an empty statement so buildHttpMetrics()
   * completes without throwing even though no real DB is present.
   *
   * @return \Drupal\Core\Database\Connection
   *   Mock database connection.
   */
  private function buildConnectionWithNoTables(): Connection {
    $schema = $this->createMock(Schema::class);
    $schema->method('tableExists')->willReturn(FALSE);

    $stmt = $this->createMock(StatementInterface::class);
    $stmt->method('fetchAll')->willReturn([]);

    $query = $this->createMock(SelectInterface::class);
    $query->method('fields')->willReturnSelf();
    $query->method('addExpression')->willReturnSelf();
    $query->method('condition')->willReturnSelf();
    $query->method('groupBy')->willReturnSelf();
    $query->method('orderBy')->willReturnSelf();
    $query->method('isNotNull')->willReturnSelf();
    $query->method('execute')->willReturn($stmt);

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

    return $connection;
  }

  /**
   * Builds a PrometheusMetricsBuilder with the given window config value.
   *
   * @param int|null $windowValue
   *   The prometheus_window_seconds config value (NULL = key absent).
   *
   * @return \Drupal\native_observability_otel\Service\PrometheusMetricsBuilder
   *   The configured builder.
   */
  private function buildBuilder(int|null $windowValue): PrometheusMetricsBuilder {
    $reader = new LivePrometheusMetricsReader($this->buildConnectionWithNoTables());
    return new PrometheusMetricsBuilder($reader, $this->buildConfigFactory($windowValue));
  }

  /**
   * Invokes the private resolveWindowSeconds() method.
   *
   * @param \Drupal\native_observability_otel\Service\PrometheusMetricsBuilder $builder
   *   The builder instance.
   *
   * @return int
   *   The resolved window in seconds.
   */
  private function resolveWindowSeconds(PrometheusMetricsBuilder $builder): int {
    $method = new \ReflectionMethod(PrometheusMetricsBuilder::class, 'resolveWindowSeconds');
    return (int) $method->invoke($builder);
  }

  /**
   * Verifies that a positive config value is used as-is.
   */
  public function testResolveWindowSecondsUsesConfiguredValue(): void {
    $this->assertSame(600, $this->resolveWindowSeconds($this->buildBuilder(600)));
  }

  /**
   * Verifies that a NULL config value falls back to the 300 s default.
   */
  public function testResolveWindowSecondsFallsBackToDefaultWhenNull(): void {
    $this->assertSame(300, $this->resolveWindowSeconds($this->buildBuilder(NULL)));
  }

  /**
   * Verifies that a zero config value falls back to the 300 s default.
   */
  public function testResolveWindowSecondsFallsBackToDefaultWhenZero(): void {
    $this->assertSame(300, $this->resolveWindowSeconds($this->buildBuilder(0)));
  }

  /**
   * Verifies build() returns a non-empty string when optional tables absent.
   *
   * When both optional tables (cache_event, spans) are absent their sections
   * return '' and are omitted; the HTTP section is always included.
   */
  public function testBuildReturnsHttpSectionWhenOptionalTablesAbsent(): void {
    $output = $this->buildBuilder(300)->build();

    $this->assertNotEmpty($output);
    $this->assertStringContainsString('drupal_http_requests_total', $output);
  }

  /**
   * Verifies build() does not insert a blank section for an absent cache table.
   *
   * When buildCacheMetrics() returns '' it must not be appended; the output
   * must not contain a doubled newline that would indicate an empty segment.
   */
  public function testBuildSkipsEmptyCacheSectionWithoutDoubledNewline(): void {
    $output = $this->buildBuilder(300)->build();

    $this->assertStringNotContainsString("\n\n\n", $output);
  }

}
