<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service\Runtime;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ResumableNodeProcessorInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface;
use Drupal\flowdrop_runtime\Service\ParameterResolverInterface;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface;
use Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeService;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Pins the ownership gate on resume-interrupt resolution.
 *
 * An interrupt resolution is the answer to one specific question asked by
 * one specific job. Job metadata can carry a foreign interrupt UUID
 * (loop-iteration clones, retries, copies) — honoring it would replay
 * another execution's answer, which for HITL gates is a silent
 * auto-approve. resolveResumeInterrupt() must only return an interrupt
 * whose job_id matches the executing job, and must be strict on missing
 * data: falling through to process() re-asks the question, which is always
 * the safe direction for a consent gate.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeService
 * @group flowdrop_runtime
 */
class NodeRuntimeServiceResumeOwnershipTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * Interrupt UUID used across cases.
   */
  private const INTERRUPT_UUID = '4f9c2a52-0000-4000-8000-000000000001';

  /**
   * Calls the protected resolveResumeInterrupt() with a prepared fixture.
   *
   * @param int|null $interruptJobId
   *   The job id stored on the interrupt entity (its owner).
   * @param int|null $executingJobId
   *   The id of the job currently executing.
   * @param bool $resolved
   *   Whether the interrupt has been resolved.
   *
   * @return \Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface|null
   *   What resolveResumeInterrupt() returned.
   */
  private function resolveWith(
    ?int $interruptJobId,
    ?int $executingJobId,
    bool $resolved = TRUE,
  ): ?ResolvedInterruptInterface {
    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResolvedAt()->willReturn($resolved ? 1700000000 : NULL);
    $interrupt->getJobId()->willReturn($interruptJobId);

    $storage = $this->prophesize(EntityStorageInterface::class);
    $storage->loadByProperties(['uuid' => self::INTERRUPT_UUID])
      ->willReturn([$interrupt->reveal()]);

    $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $entityTypeManager->hasDefinition('flowdrop_interrupt')->willReturn(TRUE);
    $entityTypeManager->getStorage('flowdrop_interrupt')
      ->willReturn($storage->reveal());

    $service = new NodeRuntimeService(
      $this->prophesize(ExecutionLoggerInterface::class)->reveal(),
      $this->prophesize(EventDispatcherInterface::class)->reveal(),
      $this->prophesize(FlowDropNodeProcessorPluginManager::class)->reveal(),
      $this->prophesize(RealTimeManagerInterface::class)->reveal(),
      $this->prophesize(ParameterResolverInterface::class)->reveal(),
      $entityTypeManager->reveal(),
    );

    $parameterBag = $this->prophesize(ParameterBagInterface::class);
    $parameterBag->get('__interrupt_id__')->willReturn(self::INTERRUPT_UUID);

    $method = new \ReflectionMethod(NodeRuntimeService::class, 'resolveResumeInterrupt');
    return $method->invoke(
      $service,
      $this->prophesize(ResumableNodeProcessorInterface::class)->reveal(),
      $parameterBag->reveal(),
      $executingJobId,
    );
  }

  /**
   * The owning job consumes its own resolved interrupt.
   *
   * @covers ::resolveResumeInterrupt
   */
  public function testOwnedResolvedInterruptIsHonored(): void {
    $this->assertNotNull($this->resolveWith(42, 42));
  }

  /**
   * A foreign interrupt (e.g. inherited by a loop-iteration clone) is not.
   *
   * @covers ::resolveResumeInterrupt
   */
  public function testForeignInterruptFallsThroughToProcess(): void {
    $this->assertNull($this->resolveWith(42, 43));
  }

  /**
   * An interrupt with no recorded owner cannot be verified — not honored.
   *
   * @covers ::resolveResumeInterrupt
   */
  public function testOwnerlessInterruptFallsThroughToProcess(): void {
    $this->assertNull($this->resolveWith(NULL, 42));
  }

  /**
   * An execution with no job identity cannot claim any interrupt.
   *
   * @covers ::resolveResumeInterrupt
   */
  public function testUnknownExecutingJobFallsThroughToProcess(): void {
    $this->assertNull($this->resolveWith(42, NULL));
  }

  /**
   * Ownership is checked on top of resolution, not instead of it.
   *
   * @covers ::resolveResumeInterrupt
   */
  public function testPendingInterruptStillFallsThroughEvenWhenOwned(): void {
    $this->assertNull($this->resolveWith(42, 42, resolved: FALSE));
  }

}
