<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_session\Unit\EventSubscriber;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\flowdrop_runtime\Event\PipelineCompletedEvent;
use Drupal\flowdrop_session\Constants\MessageAnnotation;
use Drupal\flowdrop_session\Entity\FlowDropSessionInterface;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Drupal\flowdrop_session\EventSubscriber\PipelineStatusSubscriber;
use Drupal\flowdrop_session\Service\SessionServiceInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Covers the nested-vs-top-level dispatch in PipelineStatusSubscriber.
 *
 * Top-level pipeline completions render as a centered `display: notice`
 * (today's behaviour, now explicit). Sub-pipelines consolidate into a
 * `display: card` row that carries hierarchy + outcome + duration tags
 * instead of a separate notice — Phase 3 of the 1.13.0 annotation work.
 *
 * @coversDefaultClass \Drupal\flowdrop_session\EventSubscriber\PipelineStatusSubscriber
 * @group flowdrop_session
 */
class PipelineStatusSubscriberTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The subscriber under test.
   */
  private PipelineStatusSubscriber $subscriber;

  /**
   * The session-service mock (captures addSystemMessage calls).
   *
   * @var \Prophecy\Prophecy\ObjectProphecy<\Drupal\flowdrop_session\Service\SessionServiceInterface>
   */
  private $sessionService;

  /**
   * The captured content from the most recent addSystemMessage call.
   *
   * Prophecy rebinds will() callbacks to its own scope, so $this inside
   * the closure is not the test instance. The setUp wiring binds these
   * properties by reference into the callback's `use` clause instead.
   */
  private string $capturedContent = '';

  /**
   * The captured metadata from the most recent addSystemMessage call.
   *
   * @var array<string, mixed>
   */
  private array $capturedMetadata = [];

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

    $sessionStorage = $this->prophesize(EntityStorageInterface::class);
    $session = $this->prophesize(FlowDropSessionInterface::class);
    $sessionStorage->load('sess-1')->willReturn($session->reveal());

    $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $entityTypeManager->getStorage('flowdrop_session')->willReturn($sessionStorage->reveal());

    $this->sessionService = $this->prophesize(SessionServiceInterface::class);
    $messageStub = $this->prophesize(FlowDropSessionMessageInterface::class)->reveal();
    // $this inside the will() callback is prophecy's scope, not the test's.
    // Bind the capture properties by reference so the closure can write to
    // them. Properties reset between tests via PHPUnit reinstantiation.
    $contentRef = &$this->capturedContent;
    $metadataRef = &$this->capturedMetadata;
    $this->sessionService
      ->addSystemMessage(Argument::any(), Argument::any(), Argument::any(), Argument::any())
      ->will(function ($args) use ($messageStub, &$contentRef, &$metadataRef) {
        $contentRef = $args[1];
        $metadataRef = $args[2];
        return $messageStub;
      });

    $this->subscriber = new PipelineStatusSubscriber(
      $entityTypeManager->reveal(),
      $this->sessionService->reveal(),
    );
  }

  /**
   * Builds an event with a given pipeline context, status, and duration.
   *
   * @param array<string, mixed> $playground
   *   Contents of the `playground` key inside the pipeline context.
   * @param string $status
   *   One of PipelineStatus::* values.
   * @param int $execMicroseconds
   *   Execution duration in microseconds.
   */
  private function event(array $playground, string $status, int $execMicroseconds): PipelineCompletedEvent {
    return new PipelineCompletedEvent(
      ['playground' => $playground],
      'exec-1',
      $status,
      $execMicroseconds,
    );
  }

  /**
   * Reads the captured content from the most recent addSystemMessage call.
   */
  private function capturedContent(): string {
    return $this->capturedContent;
  }

  /**
   * Reads the captured metadata from the most recent addSystemMessage call.
   *
   * @return array<string, mixed>
   *   The captured metadata array.
   */
  private function capturedMetadata(): array {
    return $this->capturedMetadata;
  }

  /**
   * Successful top-level run falls back to a thin notice (no card noise).
   *
   * The body is just the outcome — the playground renders the
   * hierarchy chip strip on notice rows, so including the leaf label
   * in the body would duplicate the chip.
   */
  public function testSuccessfulTopLevelRendersAsThinNotice(): void {
    $this->subscriber->onPipelineCompleted($this->event(
      [
        'session_id' => 'sess-1',
        'hierarchy' => [['id' => 'wf:calc_user', 'label' => 'Calculator User']],
      ],
      PipelineStatus::COMPLETED,
      500_000,
    ));

    $meta = $this->capturedMetadata();
    $this->assertSame(MessageAnnotation::DISPLAY_NOTICE, $meta[MessageAnnotation::METADATA_KEY_DISPLAY]);
    $this->assertArrayNotHasKey(MessageAnnotation::METADATA_KEY_TAGS, $meta);
    $this->assertSame('completed in 500ms', $this->capturedContent());
  }

  /**
   * Failed top-level run renders as a card without a kind tag.
   *
   * Failed runs grab the eye with the full layout regardless of depth;
   * top-level skips the `kind` chip because the single-crumb hierarchy
   * already signals "this is the run you launched".
   */
  public function testFailedTopLevelRendersAsCardWithoutKindTag(): void {
    $this->subscriber->onPipelineCompleted($this->event(
      [
        'session_id' => 'sess-1',
        'hierarchy' => [['id' => 'wf:calc_user', 'label' => 'Calculator User']],
      ],
      PipelineStatus::FAILED,
      500_000,
    ));

    $meta = $this->capturedMetadata();
    $this->assertSame(MessageAnnotation::DISPLAY_CARD, $meta[MessageAnnotation::METADATA_KEY_DISPLAY]);
    $tags = $meta[MessageAnnotation::METADATA_KEY_TAGS];
    $tagsByType = array_column($tags, NULL, 'type');
    $this->assertArrayNotHasKey('kind', $tagsByType);
    $this->assertSame('failed', $tagsByType['status']['label']);
    $this->assertSame(MessageAnnotation::COLOR_ERROR, $tagsByType['status']['color']);
    // Card body is just the outcome.
    $this->assertStringStartsWith('failed', $this->capturedContent());
  }

  /**
   * Pipelines with no hierarchy data fall back to the plain notice display.
   *
   * Covers older pipelines (pre-hierarchy) and any non-playground run
   * that still happens to fire through this subscriber.
   */
  public function testFallbackToNoticeWhenHierarchyMissing(): void {
    $this->subscriber->onPipelineCompleted($this->event(
      ['session_id' => 'sess-1'],
      PipelineStatus::COMPLETED,
      500_000,
    ));

    $meta = $this->capturedMetadata();
    $this->assertSame(MessageAnnotation::DISPLAY_NOTICE, $meta[MessageAnnotation::METADATA_KEY_DISPLAY]);
    $this->assertArrayNotHasKey(MessageAnnotation::METADATA_KEY_TAGS, $meta);
    $this->assertArrayNotHasKey(MessageAnnotation::METADATA_KEY_HIERARCHY, $meta);
  }

  /**
   * Successful sub-workflow falls back to a thin notice (no card noise).
   *
   * A completed sub-workflow is implied by the per-node job logs above
   * it — wrapping that in a heavyweight card would clutter the stream.
   * Body is just the outcome — the hierarchy chip strip carries the
   * workflow label on notices.
   */
  public function testSuccessfulSubWorkflowRendersAsThinNotice(): void {
    $hierarchy = [
      ['id' => 'wf:calc_user', 'label' => 'Calculator User'],
      ['id' => 'wf:calculator', 'label' => 'Calculator'],
    ];

    $this->subscriber->onPipelineCompleted($this->event(
      ['session_id' => 'sess-1', 'hierarchy' => $hierarchy],
      PipelineStatus::COMPLETED,
      230_000,
    ));

    $meta = $this->capturedMetadata();
    $this->assertSame(MessageAnnotation::DISPLAY_NOTICE, $meta[MessageAnnotation::METADATA_KEY_DISPLAY]);
    $this->assertSame($hierarchy, $meta[MessageAnnotation::METADATA_KEY_HIERARCHY]);
    $this->assertArrayNotHasKey(MessageAnnotation::METADATA_KEY_TAGS, $meta);
    $this->assertSame('completed in 230ms', $this->capturedContent());
  }

  /**
   * Failed sub-workflow uses error color on the status tag.
   */
  public function testFailedSubWorkflowCardCarriesErrorStatusTag(): void {
    $hierarchy = [
      ['id' => 'wf:parent', 'label' => 'Parent'],
      ['id' => 'wf:child', 'label' => 'Child'],
    ];

    $this->subscriber->onPipelineCompleted($this->event(
      ['session_id' => 'sess-1', 'hierarchy' => $hierarchy],
      PipelineStatus::FAILED,
      150_000,
    ));

    $tags = $this->capturedMetadata()[MessageAnnotation::METADATA_KEY_TAGS];
    $statusTag = array_column($tags, NULL, 'type')['status'];
    $this->assertSame('failed', $statusTag['label']);
    $this->assertSame(MessageAnnotation::COLOR_ERROR, $statusTag['color']);
  }

  /**
   * Paused sub-workflow uses warning color on the status tag.
   */
  public function testPausedSubWorkflowCardCarriesWarningStatusTag(): void {
    $hierarchy = [
      ['id' => 'wf:parent', 'label' => 'Parent'],
      ['id' => 'wf:child', 'label' => 'Child'],
    ];

    $this->subscriber->onPipelineCompleted($this->event(
      ['session_id' => 'sess-1', 'hierarchy' => $hierarchy],
      PipelineStatus::PAUSED,
      80_000,
    ));

    $tags = $this->capturedMetadata()[MessageAnnotation::METADATA_KEY_TAGS];
    $statusTag = array_column($tags, NULL, 'type')['status'];
    $this->assertSame('paused', $statusTag['label']);
    $this->assertSame(MessageAnnotation::COLOR_WARNING, $statusTag['color']);
  }

  /**
   * Three-deep nesting on a failure produces a card with the full hierarchy.
   *
   * Uses FAILED status because successful sub-workflows fall back to
   * notice; the deep-hierarchy check is meaningful only for the card path.
   */
  public function testDeepNestingFailureProducesCardWithFullHierarchy(): void {
    $hierarchy = [
      ['id' => 'wf:a', 'label' => 'A'],
      ['id' => 'wf:b', 'label' => 'B'],
      ['id' => 'wf:c', 'label' => 'C'],
    ];

    $this->subscriber->onPipelineCompleted($this->event(
      ['session_id' => 'sess-1', 'hierarchy' => $hierarchy],
      PipelineStatus::FAILED,
      100_000,
    ));

    $meta = $this->capturedMetadata();
    $this->assertSame(MessageAnnotation::DISPLAY_CARD, $meta[MessageAnnotation::METADATA_KEY_DISPLAY]);
    $this->assertSame($hierarchy, $meta[MessageAnnotation::METADATA_KEY_HIERARCHY]);
    // Card body is just the outcome (no leaf label prefix).
    $this->assertStringStartsWith('failed', $this->capturedContent());
  }

  /**
   * A pipeline with no playground context is ignored — no message written.
   */
  public function testNonPlaygroundPipelineIgnored(): void {
    unset($GLOBALS['_pipeline_status_test_content'], $GLOBALS['_pipeline_status_test_metadata']);

    $event = new PipelineCompletedEvent([], 'exec-1', PipelineStatus::COMPLETED, 1);
    $this->subscriber->onPipelineCompleted($event);

    $this->sessionService
      ->addSystemMessage(Argument::any(), Argument::any(), Argument::any(), Argument::any())
      ->shouldNotHaveBeenCalled();
  }

  /**
   * {@inheritdoc}
   */
  protected function tearDown(): void {
    unset($GLOBALS['_pipeline_status_test_content'], $GLOBALS['_pipeline_status_test_metadata']);
    parent::tearDown();
  }

}
