<?php

declare(strict_types=1);

namespace Drupal\Tests\native_observability_spans\Unit\Service;

use Drupal\Tests\UnitTestCase;
use Drupal\native_observability\EventSubscriber\TraceSubscriber;
use Drupal\native_observability\Service\RequestIdGeneratorInterface;
use Drupal\native_observability_spans\Service\ActiveSpanContextStack;
use Drupal\native_observability_spans\Service\SpanRecorder;
use Drupal\native_observability_spans\Service\SpanStorageInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Covers the default span recorder orchestration contract.
 *
 * These tests validate that the recorder correlates spans with the current
 * request, persists start/end data through the storage interface and keeps the
 * active span stack coherent.
 */
final class SpanRecorderTest extends UnitTestCase {

  /**
   * Verifies that startSpan inserts a correlated payload and pushes context.
   */
  public function testStartSpanPersistsCorrelatedSpanAndPushesContext(): void {
    $request = new Request();
    $request->attributes->set(TraceSubscriber::ATTR_REQUEST_ID, 'req-123');

    $requestStack = new RequestStack();
    $requestStack->push($request);

    $idGenerator = $this->createMock(RequestIdGeneratorInterface::class);
    $idGenerator->method('generate')->willReturn('span-001');

    $storage = $this->createMock(SpanStorageInterface::class);
    $storage->expects($this->once())
      ->method('insert')
      ->with($this->callback(static function (array $span): bool {
        return $span['request_id'] === 'req-123'
          && $span['span_id'] === 'span-001'
          && $span['parent_span_id'] === NULL
          && $span['name'] === 'controller.render'
          && $span['category'] === 'render'
          && is_int($span['started_at_ms'])
          && $span['payload'] === '{"route":"entity.node.canonical"}';
      }))
      ->willReturn(1);

    $activeStack = new ActiveSpanContextStack($requestStack);
    $recorder = new SpanRecorder($idGenerator, $storage, $requestStack, $activeStack);

    $spanId = $recorder->startSpan('controller.render', 'render', NULL, ['route' => 'entity.node.canonical']);

    $this->assertSame('span-001', $spanId);
    $this->assertSame('span-001', $activeStack->getCurrentSpanId());
    $this->assertNotNull($request->attributes->get('native_observability.span_started.span-001'));
  }

  /**
   * Verifies that endSpan forwards end data and clears active context.
   */
  public function testEndSpanUpdatesStorageAndPopsContext(): void {
    $request = new Request();
    $request->attributes->set(TraceSubscriber::ATTR_REQUEST_ID, 'req-456');

    $requestStack = new RequestStack();
    $requestStack->push($request);

    $idGenerator = $this->createMock(RequestIdGeneratorInterface::class);
    $idGenerator->method('generate')->willReturn('span-002');

    $storage = $this->createMock(SpanStorageInterface::class);
    $storage->expects($this->once())->method('insert')->willReturn(1);
    $storage->expects($this->once())
      ->method('end')
      ->with(
        'span-002',
        $this->greaterThanOrEqual(1000),
        $this->greaterThanOrEqual(0),
        '{"status":"ok"}'
      );

    $activeStack = new ActiveSpanContextStack($requestStack);
    $recorder = new SpanRecorder($idGenerator, $storage, $requestStack, $activeStack);

    $recorder->startSpan('service.call', 'service');
    $request->attributes->set('native_observability.span_started.span-002', 1000);

    $recorder->endSpan('span-002', ['status' => 'ok']);

    $this->assertNull($activeStack->getCurrentSpanId());
  }

}
