<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_interrupt\Kernel;

use Drupal\flowdrop_interrupt\DTO\ExecutionStamp;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptDirection;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Drupal\flowdrop_interrupt\Event\InterruptCreatedEvent;
use Drupal\flowdrop_interrupt\Message\HitlInterruptMessage;
use Drupal\flowdrop_interrupt\Message\InwardSignalMessage;
use Drupal\flowdrop_interrupt\Message\LinkedInterruptMessage;
use Drupal\flowdrop_interrupt\Service\InterruptMessageBusInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\flowdrop\Kernel\FlowDropKernelTestTrait;

/**
 * End-to-end test of the 1.4.0 interrupt message bus.
 *
 * Verifies the wiring that unit tests can't reach: container-collected
 * handlers, the compiler pass that asserts no duplicate registrations,
 * the factory's storage write, and the InterruptCreatedEvent dispatch.
 * One test per concrete message shape (HITL / Linked / InwardSignal) so a
 * future regression points at the right handler.
 *
 * @group flowdrop_interrupt
 *
 * @coversNothing
 */
class InterruptMessageBusEndToEndTest extends KernelTestBase {

  use FlowDropKernelTestTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'options',
    'text',
    'flowdrop',
    'flowdrop_workflow',
    'flowdrop_orchestration',
    'flowdrop_runtime',
    'flowdrop_pipeline',
    'flowdrop_job',
    'flowdrop_session',
    'flowdrop_node_type',
    'flowdrop_interrupt',
  ];

  /**
   * Captured InterruptCreatedEvent instances dispatched during the test.
   *
   * @var \Drupal\flowdrop_interrupt\Event\InterruptCreatedEvent[]
   */
  private array $capturedEvents = [];

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

    $this->installEntitySchema('user');
    $this->installEntitySchema('flowdrop_workflow');
    $this->installEntitySchema('flowdrop_workflow_snapshot');
    $this->installEntitySchema('flowdrop_pipeline');
    $this->installEntitySchema('flowdrop_job');
    $this->installEntitySchema('flowdrop_session');
    $this->installEntitySchema('flowdrop_session_message');
    $this->installEntitySchema('flowdrop_node_type');
    $this->installEntitySchema('flowdrop_interrupt');

    $this->capturedEvents = [];
    $this->container->get('event_dispatcher')->addListener(
      InterruptCreatedEvent::NAME,
      function (InterruptCreatedEvent $event): void {
        $this->capturedEvents[] = $event;
      },
    );
  }

  /**
   * Bus is wired and exposes all three 1.4.0 message-shape handlers.
   */
  public function testBusServiceIsWiredFromContainer(): void {
    $bus = $this->container->get('flowdrop_interrupt.message_bus');
    $this->assertInstanceOf(InterruptMessageBusInterface::class, $bus);
  }

  /**
   * HITL message → persisted entity + InterruptCreatedEvent.
   *
   * Covers the most common path: a node throws an InterruptRequiredException
   * carrying a HitlInterruptMessage; the catch-side dispatch via the bus
   * lands a SchemaForm interrupt with Outward direction and the orchestrator
   * stamp fields.
   */
  public function testHitlMessageRoundTripsThroughBusToPersistedEntity(): void {
    $bus = $this->container->get('flowdrop_interrupt.message_bus');
    $message = new HitlInterruptMessage(
      type: InterruptType::SchemaForm,
      message: 'Approve the article?',
      schema: ['type' => 'boolean', 'presentation' => 'confirmation'],
    );
    $stamp = new ExecutionStamp(
      workflowId: 'approval_workflow',
      nodeId: 'approve_node',
      pipelineId: 'pipeline-uuid-001',
      sessionId: 42,
      jobId: 7,
    );

    $interrupt = $bus->dispatch($message, $stamp);

    $this->assertPersistedAndMatches($interrupt, [
      'type' => InterruptType::SchemaForm,
      'status' => InterruptStatus::Pending,
      'direction' => InterruptDirection::Outward,
      'workflowId' => 'approval_workflow',
      'nodeId' => 'approve_node',
      'pipelineId' => 'pipeline-uuid-001',
      'sessionId' => 42,
      'jobId' => 7,
      'message' => 'Approve the article?',
    ]);
    $this->assertSame(
      ['type' => 'boolean', 'presentation' => 'confirmation'],
      $interrupt->getSchema(),
    );
    $this->assertEventDispatchedFor($interrupt, 'approval_workflow', 'approve_node');
  }

  /**
   * LinkedInterruptMessage → persisted entity stamped with parent linkage.
   *
   * Sub-workflow / parent-pipeline dispatch path. The factory must persist
   * `linked_interrupt_id` from the message and inherit
   * workflow_id / node_id / pipeline_id from the ExecutionStamp.
   */
  public function testLinkedMessageRoundTripsThroughBusToPersistedEntity(): void {
    $bus = $this->container->get('flowdrop_interrupt.message_bus');
    $message = new LinkedInterruptMessage(
      type: InterruptType::WorkflowExecution,
      message: 'Waiting on sub-workflow completion',
      linkedInterruptId: 'parent-interrupt-uuid',
      context: ['reason' => 'sub_workflow_running'],
    );
    $stamp = new ExecutionStamp(
      workflowId: 'parent_workflow',
      nodeId: 'invoke_subflow',
      pipelineId: 'parent-pipeline-uuid',
    );

    $interrupt = $bus->dispatch($message, $stamp);

    $this->assertPersistedAndMatches($interrupt, [
      'type' => InterruptType::WorkflowExecution,
      'status' => InterruptStatus::Pending,
      'direction' => InterruptDirection::Outward,
      'workflowId' => 'parent_workflow',
      'nodeId' => 'invoke_subflow',
      'pipelineId' => 'parent-pipeline-uuid',
      'message' => 'Waiting on sub-workflow completion',
    ]);
    $this->assertSame('parent-interrupt-uuid', $interrupt->getLinkedInterruptId());
    $this->assertEventDispatchedFor($interrupt, 'parent_workflow', 'invoke_subflow');
  }

  /**
   * InwardSignalMessage → persisted Inward interrupt.
   *
   * Operator-initiated cancel / pause path. Direction must be stamped as
   * Inward by the handler regardless of what the ExecutionStamp says.
   */
  public function testInwardSignalRoundTripsThroughBusToPersistedEntity(): void {
    $bus = $this->container->get('flowdrop_interrupt.message_bus');
    $message = new InwardSignalMessage(
      type: InterruptType::Cancel,
      message: 'operator cancelled',
      targetPipelineId: 'target-pipeline-uuid',
      initiatorUid: 99,
      reason: 'user requested cancel from playground',
    );
    $stamp = new ExecutionStamp(
      workflowId: 'long_running_workflow',
      nodeId: '',
      direction: InterruptDirection::Inward,
    );

    $interrupt = $bus->dispatch($message, $stamp);

    $this->assertPersistedAndMatches($interrupt, [
      'type' => InterruptType::Cancel,
      'status' => InterruptStatus::Pending,
      'direction' => InterruptDirection::Inward,
      'workflowId' => 'long_running_workflow',
    ]);
    $this->assertSame('target-pipeline-uuid', $interrupt->getTargetPipelineId());
    $this->assertSame(99, $interrupt->getInitiatorUid());
    $this->assertSame('user requested cancel from playground', $interrupt->getReason());
    $this->assertEventDispatchedFor($interrupt, 'long_running_workflow', '');
  }

  /**
   * Asserts the interrupt was saved with an id and matches expected fields.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt
   *   The persisted interrupt entity returned by the bus.
   * @param array<string, mixed> $expected
   *   Keyed expectations; supports the InterruptType / InterruptStatus /
   *   InterruptDirection enum-typed fields plus scalar identity fields.
   */
  private function assertPersistedAndMatches(
    FlowDropInterruptInterface $interrupt,
    array $expected,
  ): void {
    $this->assertNotNull($interrupt->id(), 'Interrupt must have an id (was persisted).');
    $this->assertNotEmpty($interrupt->uuid(), 'Interrupt must have a UUID.');

    if (isset($expected['type'])) {
      $this->assertSame($expected['type'], $interrupt->getType());
    }
    if (isset($expected['status'])) {
      $this->assertSame($expected['status'], $interrupt->getStatus());
    }
    if (isset($expected['direction'])) {
      $this->assertSame($expected['direction'], $interrupt->getDirection());
    }
    if (isset($expected['workflowId'])) {
      $this->assertSame($expected['workflowId'], $interrupt->getWorkflowId());
    }
    if (isset($expected['nodeId'])) {
      $this->assertSame($expected['nodeId'], $interrupt->getNodeId());
    }
    if (array_key_exists('pipelineId', $expected)) {
      $this->assertSame($expected['pipelineId'], $interrupt->getPipelineId());
    }
    if (array_key_exists('sessionId', $expected)) {
      $this->assertSame($expected['sessionId'], $interrupt->getSessionId());
    }
    if (array_key_exists('jobId', $expected)) {
      $this->assertSame($expected['jobId'], $interrupt->getJobId());
    }
    if (isset($expected['message'])) {
      $this->assertSame($expected['message'], $interrupt->getMessage());
    }
  }

  /**
   * Asserts an InterruptCreatedEvent fired for the given interrupt.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt
   *   The interrupt the event should reference.
   * @param string $workflowId
   *   Expected workflow id on the dispatched event.
   * @param string $nodeId
   *   Expected node id on the dispatched event.
   */
  private function assertEventDispatchedFor(
    FlowDropInterruptInterface $interrupt,
    string $workflowId,
    string $nodeId,
  ): void {
    $matched = array_filter(
      $this->capturedEvents,
      static fn (InterruptCreatedEvent $e): bool =>
        $e->interrupt->id() === $interrupt->id()
        && $e->workflowId === $workflowId
        && $e->nodeId === $nodeId,
    );
    $this->assertCount(
      1,
      $matched,
      'Exactly one InterruptCreatedEvent must fire for the persisted interrupt.',
    );
  }

}
