<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow_executor\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\CancellableNodeProcessorInterface;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Service\InterruptManagerInterface;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Drupal\flowdrop_workflow\Service\WorkflowSchemaResolverInterface;
use Drupal\flowdrop_workflow_executor\Plugin\FlowDropNodeProcessor\WorkflowNode;
use Drupal\flowdrop_workflow_executor\Service\WorkflowExecutionServiceInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests WorkflowNode's Phase 5 cascade cancel hook.
 *
 * @coversDefaultClass \Drupal\flowdrop_workflow_executor\Plugin\FlowDropNodeProcessor\WorkflowNode
 * @group flowdrop_workflow_executor
 */
class WorkflowNodeCancelTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The node opts into the Phase 4 cancel hook.
   */
  public function testImplementsCancellableInterface(): void {
    $node = new WorkflowNode(
      [],
      'flowdrop_workflow:foo',
      [],
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $this->prophesize(WorkflowExecutionServiceInterface::class)->reveal(),
      $this->prophesize(WorkflowSchemaResolverInterface::class)->reveal(),
    );
    $this->assertInstanceOf(CancellableNodeProcessorInterface::class, $node);
  }

  /**
   * Cascade prefers sub_pipeline_id over sub_session_id when both stamped.
   *
   * CancelPipeline targets the specific sub-pipeline directly — required
   * for shared-session sub-workflows where multiple sub-pipelines may
   * coexist in one session.
   */
  public function testCancelPrefersSubPipelineId(): void {
    $executionService = $this->prophesize(WorkflowExecutionServiceInterface::class);
    $executionService->cancelPipeline('pipe_456', 'Parent cancelled', 42)->shouldBeCalled();
    $executionService->cancelExecution(Argument::cetera())->shouldNotBeCalled();

    $node = new WorkflowNode(
      [],
      'flowdrop_workflow:foo',
      [],
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $executionService->reveal(),
      $this->prophesize(WorkflowSchemaResolverInterface::class)->reveal(),
      $this->prophesize(InterruptManagerInterface::class)->reveal(),
    );

    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->getContext()->willReturn([
      'sub_pipeline_id' => 'pipe_456',
      'sub_session_id' => 'sess_123',
    ]);
    $interrupt->getReason()->willReturn('Parent cancelled');
    $interrupt->getInitiatorUid()->willReturn(42);

    $node->cancel(
      $interrupt->reveal(),
      $this->prophesize(ParameterBagInterface::class)->reveal(),
    );
  }

  /**
   * Legacy fallback: only sub_session_id present → cancelExecution.
   *
   * Pre-Phase-B interrupts stamped only the session id; the cascade
   * still resolves an active sub-pipeline via reverse-lookup.
   */
  public function testCancelDelegatesToExecutionService(): void {
    $executionService = $this->prophesize(WorkflowExecutionServiceInterface::class);
    $executionService->cancelExecution('sess_123', 'Parent cancelled', 42)->shouldBeCalled();

    $node = new WorkflowNode(
      [],
      'flowdrop_workflow:foo',
      [],
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $executionService->reveal(),
      $this->prophesize(WorkflowSchemaResolverInterface::class)->reveal(),
      $this->prophesize(InterruptManagerInterface::class)->reveal(),
    );

    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->getContext()->willReturn(['sub_session_id' => 'sess_123']);
    $interrupt->getReason()->willReturn('Parent cancelled');
    $interrupt->getInitiatorUid()->willReturn(42);

    $params = $this->prophesize(ParameterBagInterface::class);

    $node->cancel($interrupt->reveal(), $params->reveal());
  }

  /**
   * Missing sub_session_id in context: no-op.
   *
   * The interrupt is from a different code path or the context was never
   * populated — cascade simply doesn't fire.
   */
  public function testCancelWithoutSubSessionIdIsNoOp(): void {
    $executionService = $this->prophesize(WorkflowExecutionServiceInterface::class);
    $executionService->cancelExecution(Argument::cetera())->shouldNotBeCalled();

    $node = new WorkflowNode(
      [],
      'flowdrop_workflow:foo',
      [],
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $executionService->reveal(),
      $this->prophesize(WorkflowSchemaResolverInterface::class)->reveal(),
    );

    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->getContext()->willReturn([]);

    $node->cancel($interrupt->reveal(), $this->prophesize(ParameterBagInterface::class)->reveal());
  }

  /**
   * Non-FlowDropInterruptInterface argument: no-op.
   *
   * Interface-only resolution interfaces don't carry context, so the
   * cascade can't extract the sub-session ID and exits silently.
   */
  public function testCancelWithBareResolvedInterfaceIsNoOp(): void {
    $executionService = $this->prophesize(WorkflowExecutionServiceInterface::class);
    $executionService->cancelExecution(Argument::cetera())->shouldNotBeCalled();

    $node = new WorkflowNode(
      [],
      'flowdrop_workflow:foo',
      [],
      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
      $executionService->reveal(),
      $this->prophesize(WorkflowSchemaResolverInterface::class)->reveal(),
    );

    // Bare ResolvedInterruptInterface — no getContext() method.
    $bare = $this->prophesize(ResolvedInterruptInterface::class);

    $node->cancel($bare->reveal(), $this->prophesize(ParameterBagInterface::class)->reveal());
  }

}
