<?php

declare(strict_types=1);

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

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorAwareInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorResolver;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;

/**
 * Tests the pipeline → executor resolution chain.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorResolver
 * @group flowdrop_runtime
 */
class PipelineExecutorResolverTest extends UnitTestCase {

  /**
   * The synchronous fallback executor.
   */
  protected PipelineExecutorInterface&MockObject $syncExecutor;

  /**
   * The orchestrator helper mock.
   */
  protected OrchestratorHelperInterface&MockObject $orchestratorHelper;

  /**
   * The logger mock.
   */
  protected LoggerInterface&MockObject $logger;

  /**
   * The resolver under test.
   */
  protected PipelineExecutorResolver $resolver;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->syncExecutor = $this->createMock(PipelineExecutorInterface::class);
    $this->orchestratorHelper = $this->createMock(OrchestratorHelperInterface::class);
    $this->logger = $this->createMock(LoggerInterface::class);
    $this->resolver = new PipelineExecutorResolver(
      $this->orchestratorHelper,
      $this->syncExecutor,
      $this->logger,
    );
  }

  /**
   * Builds a pipeline mock with the given context and workflow.
   *
   * @param array<string, mixed> $context
   *   The execution context.
   * @param \Drupal\flowdrop_workflow\WorkflowDefinitionInterface|null $workflow
   *   The workflow, or NULL.
   *
   * @return \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface
   *   The pipeline mock.
   */
  protected function mockPipeline(array $context, ?WorkflowDefinitionInterface $workflow = NULL): FlowDropPipelineRuntimeInterface {
    $pipeline = $this->createMock(FlowDropPipelineRuntimeInterface::class);
    $pipeline->method('getExecutionContext')->willReturn($context);
    $pipeline->method('getWorkflow')->willReturn($workflow);
    $pipeline->method('id')->willReturn(1);
    return $pipeline;
  }

  /**
   * A stamped type whose plugin exposes an executor wins.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testStampedTypeResolvesToAwarePluginExecutor(): void {
    $stategraphExecutor = $this->createMock(PipelineExecutorInterface::class);
    $plugin = $this->createMockForIntersectionOfInterfaces([
      OrchestratorPluginInterface::class,
      PipelineExecutorAwareInterface::class,
    ]);
    $plugin->method('getPipelineExecutor')->willReturn($stategraphExecutor);

    $this->orchestratorHelper->expects($this->once())
      ->method('getInstance')
      ->with(OrchestratorHelperInterface::STATEGRAPH, OrchestratorHelperInterface::SYNCHRONOUS_PIPELINE)
      ->willReturn($plugin);

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => OrchestratorHelperInterface::STATEGRAPH,
    ]);

    $this->assertSame($stategraphExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * A stamped sync-pipeline type short-circuits to the sync executor.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testStampedSyncPipelineTypeShortCircuits(): void {
    $this->orchestratorHelper->expects($this->never())->method('getInstance');

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => OrchestratorHelperInterface::SYNCHRONOUS_PIPELINE,
    ]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * Without a stamp, the workflow's declared settings are consulted.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testFallsBackToWorkflowMetadata(): void {
    $stategraphExecutor = $this->createMock(PipelineExecutorInterface::class);
    $plugin = $this->createMockForIntersectionOfInterfaces([
      OrchestratorPluginInterface::class,
      PipelineExecutorAwareInterface::class,
    ]);
    $plugin->method('getPipelineExecutor')->willReturn($stategraphExecutor);

    $this->orchestratorHelper->expects($this->once())
      ->method('getInstance')
      ->with(OrchestratorHelperInterface::STATEGRAPH, OrchestratorHelperInterface::SYNCHRONOUS_PIPELINE)
      ->willReturn($plugin);

    $workflow = $this->createMock(WorkflowDefinitionInterface::class);
    $workflow->method('getMetadata')->willReturn([
      'orchestrator_settings' => ['type' => OrchestratorHelperInterface::STATEGRAPH],
    ]);

    $pipeline = $this->mockPipeline([], $workflow);

    $this->assertSame($stategraphExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * No stamp and no workflow → sync executor.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testNoDeclarationFallsBackToSync(): void {
    $this->orchestratorHelper->expects($this->never())->method('getInstance');

    $pipeline = $this->mockPipeline([]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * The legacy synchronous orchestrator resolves to sync quietly.
   *
   * It shares the sync pipeline loop's semantics, so executing its
   * pre-built pipelines through the sync pipeline executor is faithful —
   * no log line, unlike a real substitution.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testLegacySynchronousTypeShortCircuitsQuietly(): void {
    $this->orchestratorHelper->expects($this->never())->method('getInstance');
    $this->logger->expects($this->never())->method('warning');
    $this->logger->expects($this->never())->method('info');

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => OrchestratorHelperInterface::SYNCHRONOUS,
    ]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * A literal "default" type is resolved via the helper before classifying.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testDefaultTypeResolvesViaHelper(): void {
    $this->orchestratorHelper->expects($this->once())
      ->method('resolveDefault')
      ->willReturn(OrchestratorHelperInterface::SYNCHRONOUS_PIPELINE);
    $this->orchestratorHelper->expects($this->never())->method('getInstance');
    $this->logger->expects($this->never())->method('warning');

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => 'default',
    ]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * A plugin that cannot execute pipelines falls back to sync, with a warning.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testNonAwarePluginFallsBackToSyncWithWarning(): void {
    $plugin = $this->createMock(OrchestratorPluginInterface::class);

    $this->orchestratorHelper->expects($this->once())
      ->method('getInstance')
      ->willReturn($plugin);

    // The declared orchestrator is silently substituted — that must be
    // visible in production logs, so it is a warning, not info.
    $this->logger->expects($this->once())->method('warning');
    $this->logger->expects($this->never())->method('info');

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => 'some_module:custom',
    ]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

  /**
   * An async declaration falls back to sync quietly (info, not warning).
   *
   * The async orchestrator has never had a pre-built-pipeline executor;
   * resuming its pipelines through the sync loop is long-standing behavior,
   * not a substitution worth alarming operators about on every resume.
   *
   * @covers ::getExecutorForPipeline
   */
  public function testAsyncDeclarationFallsBackQuietly(): void {
    $plugin = $this->createMock(OrchestratorPluginInterface::class);

    $this->orchestratorHelper->expects($this->once())
      ->method('getInstance')
      ->willReturn($plugin);

    $this->logger->expects($this->once())->method('info');
    $this->logger->expects($this->never())->method('warning');

    $pipeline = $this->mockPipeline([
      'orchestrator_type' => OrchestratorHelperInterface::ASYNCHRONOUS,
    ]);

    $this->assertSame($this->syncExecutor, $this->resolver->getExecutorForPipeline($pipeline));
  }

}
