<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_session\Unit\Service;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistryInterface;
use Drupal\flowdrop_session\Entity\FlowDropSessionInterface;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Drupal\flowdrop_session\Service\SessionExecutionService;
use Drupal\flowdrop_session\Service\SessionService;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Covers the hierarchy-building logic in SessionExecutionService.
 *
 * The construction site is private but produces the single piece of new
 * state introduced by 1.13.0 annotation threading — the hierarchy trail
 * that the playground UI renders as a chevron-separated chip strip. Tests
 * exercise it via reflection rather than spinning up the full orchestrator
 * stack because the surrounding flow already has integration coverage.
 *
 * @coversDefaultClass \Drupal\flowdrop_session\Service\SessionExecutionService
 * @group flowdrop_session
 */
class SessionExecutionServiceHierarchyTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The service under test.
   */
  private SessionExecutionService $service;

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

    $loggerChannel = $this->prophesize(LoggerChannelInterface::class);
    $loggerFactory = $this->prophesize(LoggerChannelFactoryInterface::class);
    $loggerFactory->get('flowdrop_session')->willReturn($loggerChannel->reveal());

    $orchestratorHelper = $this->prophesize(OrchestratorHelperInterface::class);
    $orchestratorHelper->isStateGraph(Argument::any())->willReturn(FALSE);

    $this->service = new SessionExecutionService(
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $loggerFactory->reveal(),
      $orchestratorHelper->reveal(),
      $this->prophesize(SessionService::class)->reveal(),
      $this->prophesize(PipelineStorageRegistryInterface::class)->reveal(),
      $this->prophesize(TimeInterface::class)->reveal(),
      $this->prophesize(EventDispatcherInterface::class)->reveal(),
    );
  }

  /**
   * Builds a stub workflow with the given id and label.
   */
  private function stubWorkflow(string $id, ?string $label): FlowDropWorkflowInterface {
    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn($id);
    $workflow->label()->willReturn($label);
    return $workflow->reveal();
  }

  /**
   * Invokes the private buildHierarchy helper.
   *
   * @param array<string, mixed> $parentContext
   *   Inherited execution context.
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow to append.
   *
   * @return array<int, array{id: string, label: string, icon?: string}>
   *   The combined trail.
   */
  private function invokeBuildHierarchy(array $parentContext, FlowDropWorkflowInterface $workflow): array {
    $ref = new \ReflectionMethod(SessionExecutionService::class, 'buildHierarchy');
    return $ref->invoke($this->service, $parentContext, $workflow);
  }

  /**
   * Top-level invocation seeds the trail with one entry for this workflow.
   */
  public function testTopLevelSeedsSingleCrumb(): void {
    $workflow = $this->stubWorkflow('calc_user', 'Calculator User');

    $trail = $this->invokeBuildHierarchy([], $workflow);

    $this->assertSame([
      ['id' => 'wf:calc_user', 'label' => 'Calculator User'],
    ], $trail);
  }

  /**
   * A sub-workflow appends itself to the inherited parent trail.
   */
  public function testSubWorkflowAppendsToInheritedTrail(): void {
    $parent = [
      'hierarchy' => [
        ['id' => 'wf:calc_user', 'label' => 'Calculator User'],
      ],
    ];
    $workflow = $this->stubWorkflow('calculator', 'Calculator');

    $trail = $this->invokeBuildHierarchy($parent, $workflow);

    $this->assertSame([
      ['id' => 'wf:calc_user', 'label' => 'Calculator User'],
      ['id' => 'wf:calculator', 'label' => 'Calculator'],
    ], $trail);
  }

  /**
   * Three-deep nesting preserves the full trail.
   */
  public function testThreeDeepNesting(): void {
    $parent = [
      'hierarchy' => [
        ['id' => 'wf:a', 'label' => 'A'],
        ['id' => 'wf:b', 'label' => 'B', 'icon' => 'mdi:cog'],
      ],
    ];
    $workflow = $this->stubWorkflow('c', 'C');

    $trail = $this->invokeBuildHierarchy($parent, $workflow);

    $this->assertSame([
      ['id' => 'wf:a', 'label' => 'A'],
      ['id' => 'wf:b', 'label' => 'B', 'icon' => 'mdi:cog'],
      ['id' => 'wf:c', 'label' => 'C'],
    ], $trail);
  }

  /**
   * Malformed inherited items are dropped; the current workflow is still added.
   */
  public function testMalformedInheritedItemsAreDropped(): void {
    $parent = [
      'hierarchy' => [
        // Valid.
        ['id' => 'wf:ok', 'label' => 'Valid'],
        // Missing label — dropped.
        ['id' => 'wf:bad'],
        // Non-array — dropped.
        'string',
      ],
    ];
    $workflow = $this->stubWorkflow('current', 'Current');

    $trail = $this->invokeBuildHierarchy($parent, $workflow);

    $this->assertSame([
      ['id' => 'wf:ok', 'label' => 'Valid'],
      ['id' => 'wf:current', 'label' => 'Current'],
    ], $trail);
  }

  /**
   * Workflow without a label falls back to its id as the display label.
   */
  public function testFallsBackToIdWhenLabelMissing(): void {
    $workflow = $this->stubWorkflow('no_label_wf', NULL);

    $trail = $this->invokeBuildHierarchy([], $workflow);

    $this->assertSame([
      ['id' => 'wf:no_label_wf', 'label' => 'no_label_wf'],
    ], $trail);
  }

  /**
   * A non-array hierarchy in parentContext is ignored — seeds at depth 1.
   */
  public function testNonArrayInheritedHierarchyIsIgnored(): void {
    $workflow = $this->stubWorkflow('wf', 'WF');

    $trail = $this->invokeBuildHierarchy(['hierarchy' => 'oops'], $workflow);

    $this->assertSame([['id' => 'wf:wf', 'label' => 'WF']], $trail);
  }

  /**
   * Includes the hierarchy under playground options when supplied.
   *
   * Sanity check that the helper output reaches PlaygroundContext's read path.
   */
  public function testBuildOrchestrationOptionsCarriesHierarchy(): void {
    $session = $this->prophesize(FlowDropSessionInterface::class);
    $session->id()->willReturn('42');
    $message = $this->prophesize(FlowDropSessionMessageInterface::class);
    $message->id()->willReturn('99');
    $message->getSequenceNumber()->willReturn(3);

    $hierarchy = [
      ['id' => 'wf:a', 'label' => 'A'],
      ['id' => 'wf:b', 'label' => 'B'],
    ];

    $ref = new \ReflectionMethod(SessionExecutionService::class, 'buildOrchestrationOptions');
    $options = $ref->invoke(
      $this->service,
      [],
      $session->reveal(),
      $message->reveal(),
      $hierarchy,
    );

    $this->assertSame($hierarchy, $options['playground']['hierarchy']);
    $this->assertSame('42', $options['playground']['session_id']);
  }

  /**
   * Empty hierarchy is omitted from playground options entirely.
   */
  public function testEmptyHierarchyOmittedFromOptions(): void {
    $session = $this->prophesize(FlowDropSessionInterface::class);
    $session->id()->willReturn('42');
    $message = $this->prophesize(FlowDropSessionMessageInterface::class);
    $message->id()->willReturn('99');
    $message->getSequenceNumber()->willReturn(3);

    $ref = new \ReflectionMethod(SessionExecutionService::class, 'buildOrchestrationOptions');
    $options = $ref->invoke(
      $this->service,
      [],
      $session->reveal(),
      $message->reveal(),
      [],
    );

    $this->assertArrayNotHasKey('hierarchy', $options['playground']);
  }

}
