<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow_executor\Unit\Service;

use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;
use Drupal\flowdrop_pipeline\Service\JobGenerationService;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistryInterface;
use Drupal\flowdrop\DTO\ValidationError;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Drupal\flowdrop_workflow\Service\WorkflowSchemaResolverInterface;
use Drupal\flowdrop_workflow\Service\WorkflowValidatorInterface;
use Drupal\flowdrop_workflow_executor\DTO\LaunchOptions;
use Drupal\flowdrop_workflow_executor\Exception\WorkflowLaunchValidationException;
use Drupal\flowdrop_workflow_executor\Service\DeferredPipelineProcessor;
use Drupal\flowdrop_workflow_executor\Service\WorkflowLauncher;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;

/**
 * Tests the launch dispatch paths, in particular LaunchOptions::wait.
 *
 * @coversDefaultClass \Drupal\flowdrop_workflow_executor\Service\WorkflowLauncher
 * @group flowdrop_workflow_executor
 */
class WorkflowLauncherTest extends UnitTestCase {

  /**
   * The schema resolver mock.
   */
  protected WorkflowSchemaResolverInterface&MockObject $schemaResolver;

  /**
   * The job generation service mock.
   */
  protected JobGenerationService&MockObject $jobGenerationService;

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

  /**
   * The pipeline storage registry mock.
   */
  protected PipelineStorageRegistryInterface&MockObject $pipelineStorageRegistry;

  /**
   * The deferred pipeline processor mock.
   */
  protected DeferredPipelineProcessor&MockObject $deferredPipelineProcessor;

  /**
   * The queue factory mock.
   */
  protected QueueFactory&MockObject $queueFactory;

  /**
   * The queue mock.
   */
  protected QueueInterface&MockObject $queue;

  /**
   * The pipeline storage mock.
   */
  protected PipelineStorageInterface&MockObject $pipelineStorage;

  /**
   * The workflow validator mock.
   */
  protected WorkflowValidatorInterface&MockObject $validator;

  /**
   * The launcher under test.
   */
  protected WorkflowLauncher $launcher;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->schemaResolver = $this->createMock(WorkflowSchemaResolverInterface::class);
    $this->jobGenerationService = $this->createMock(JobGenerationService::class);
    $this->orchestratorHelper = $this->createMock(OrchestratorHelperInterface::class);
    $this->pipelineStorageRegistry = $this->createMock(PipelineStorageRegistryInterface::class);
    $this->deferredPipelineProcessor = $this->createMock(DeferredPipelineProcessor::class);
    $this->queueFactory = $this->createMock(QueueFactory::class);
    $this->queue = $this->createMock(QueueInterface::class);
    $this->pipelineStorage = $this->createMock(PipelineStorageInterface::class);
    $this->validator = $this->createMock(WorkflowValidatorInterface::class);

    $this->queueFactory->method('get')->willReturn($this->queue);
    $this->pipelineStorageRegistry->method('getStorage')->willReturn($this->pipelineStorage);
    $this->pipelineStorageRegistry->method('getBundleForOrchestrator')->willReturn('default');
    $this->schemaResolver->method('resolveInputs')->willReturn([]);
    // Definitions validate cleanly unless a test overrides this.
    $this->validator->method('validate')->willReturn(ValidationResult::success());

    $this->launcher = new WorkflowLauncher(
      $this->schemaResolver,
      $this->jobGenerationService,
      $this->orchestratorHelper,
      $this->pipelineStorageRegistry,
      $this->deferredPipelineProcessor,
      $this->queueFactory,
      $this->validator,
      $this->createMock(LoggerInterface::class),
    );
  }

  /**
   * Builds a workflow mock declaring the given orchestrator type.
   */
  protected function mockWorkflow(string $orchestratorType): WorkflowDefinitionInterface {
    $workflow = $this->createMock(WorkflowDefinitionInterface::class);
    $workflow->method('id')->willReturn('wf_test');
    $workflow->method('getInputPorts')->willReturn([]);
    $workflow->method('getMetadata')->willReturn([
      'orchestrator_settings' => ['type' => $orchestratorType],
    ]);
    return $workflow;
  }

  /**
   * Wires the orchestrator helper to return a plugin for the given type.
   *
   * Mirrors OrchestratorPluginManager, which namespaces plugin ids as
   * `<provider>:<id>` during discovery — getPluginId() returns the
   * namespaced form, and the launcher must use it verbatim (re-prepending
   * the provider once doubled the namespace and broke dispatch routing).
   */
  protected function mockOrchestratorPlugin(string $namespacedType): void {
    $plugin = $this->createMock(OrchestratorPluginInterface::class);
    $plugin->method('getPluginId')->willReturn($namespacedType);
    $this->orchestratorHelper->method('getInstanceFromSettings')->willReturn($plugin);
  }

  /**
   * Builds the pipeline mock the storage create() returns.
   */
  protected function mockCreatedPipeline(string $status = PipelineStatus::PENDING): FlowDropPipelineRuntimeInterface&MockObject {
    $pipeline = $this->createMock(FlowDropPipelineRuntimeInterface::class);
    $pipeline->method('id')->willReturn(42);
    $pipeline->method('getStatus')->willReturn($status);
    $this->pipelineStorage->method('create')->willReturn($pipeline);
    return $pipeline;
  }

  /**
   * Default (no wait): non-async launches defer and return PENDING.
   *
   * @covers ::launch
   */
  public function testNonAsyncLaunchDefersByDefault(): void {
    $this->mockOrchestratorPlugin(OrchestratorHelperInterface::STATEGRAPH);
    $pipeline = $this->mockCreatedPipeline();

    $this->deferredPipelineProcessor->expects($this->once())
      ->method('schedule')
      ->with($pipeline);
    $this->deferredPipelineProcessor->expects($this->never())
      ->method('executeNow');
    $this->queue->expects($this->never())->method('createItem');

    $result = $this->launcher->launch($this->mockWorkflow(OrchestratorHelperInterface::STATEGRAPH));

    $this->assertFalse($result->queued);
    $this->assertSame('42', $result->pipelineId);
    $this->assertSame(PipelineStatus::PENDING, $result->status);
  }

  /**
   * With wait, non-async launches execute inline and return final status.
   *
   * @covers ::launch
   */
  public function testWaitExecutesInlineAndReturnsFinalStatus(): void {
    $this->mockOrchestratorPlugin(OrchestratorHelperInterface::STATEGRAPH);
    $pipeline = $this->mockCreatedPipeline();

    $this->deferredPipelineProcessor->expects($this->once())
      ->method('executeNow')
      ->with($pipeline);
    $this->deferredPipelineProcessor->expects($this->never())
      ->method('schedule');

    // After inline execution the launcher reloads for the final status.
    $completed = $this->createMock(FlowDropPipelineRuntimeInterface::class);
    $completed->method('id')->willReturn(42);
    $completed->method('getStatus')->willReturn(PipelineStatus::COMPLETED);
    $this->pipelineStorage->expects($this->once())
      ->method('load')
      ->with(42)
      ->willReturn($completed);

    $result = $this->launcher->launch(
      $this->mockWorkflow(OrchestratorHelperInterface::STATEGRAPH),
      [],
      new LaunchOptions(wait: TRUE),
    );

    $this->assertFalse($result->queued);
    $this->assertSame(PipelineStatus::COMPLETED, $result->status);
  }

  /**
   * With wait + async orchestrator: ignored; the run queues as declared.
   *
   * @covers ::launch
   */
  public function testWaitIsIgnoredForAsyncOrchestrator(): void {
    $this->mockOrchestratorPlugin(OrchestratorHelperInterface::ASYNCHRONOUS);
    $pipeline = $this->mockCreatedPipeline();
    $pipeline->expects($this->once())->method('markAsStarted');

    $this->queue->expects($this->once())
      ->method('createItem')
      ->with(['pipeline_id' => 42, 'action' => 'execute']);
    $this->deferredPipelineProcessor->expects($this->never())->method('executeNow');
    $this->deferredPipelineProcessor->expects($this->never())->method('schedule');
    $this->pipelineStorage->expects($this->never())->method('load');

    $result = $this->launcher->launch(
      $this->mockWorkflow(OrchestratorHelperInterface::ASYNCHRONOUS),
      [],
      new LaunchOptions(wait: TRUE),
    );

    $this->assertTrue($result->queued);
  }

  /**
   * An invalid definition is rejected before any pipeline work happens.
   *
   * @covers ::launch
   */
  public function testInvalidDefinitionIsRejectedBeforeLaunch(): void {
    $failure = ValidationResult::failure([
      new ValidationError(
        'node.n1',
        'Edge targets a non-exposed input port.',
        WorkflowValidatorInterface::CODE_EDGE_TARGET_NOT_EXPOSED,
      ),
    ]);
    $validator = $this->createMock(WorkflowValidatorInterface::class);
    $validator->expects($this->once())
      ->method('validate')
      ->willReturn($failure);

    $launcher = new WorkflowLauncher(
      $this->schemaResolver,
      $this->jobGenerationService,
      $this->orchestratorHelper,
      $this->pipelineStorageRegistry,
      $this->deferredPipelineProcessor,
      $this->queueFactory,
      $validator,
      $this->createMock(LoggerInterface::class),
    );

    // Validation fails fast: no jobs, no dispatch, no pipeline creation.
    $this->jobGenerationService->expects($this->never())->method('generateJobs');
    $this->pipelineStorage->expects($this->never())->method('create');
    $this->deferredPipelineProcessor->expects($this->never())->method('schedule');
    $this->queue->expects($this->never())->method('createItem');

    try {
      $launcher->launch($this->mockWorkflow(OrchestratorHelperInterface::STATEGRAPH));
      $this->fail('Expected WorkflowLaunchValidationException.');
    }
    catch (WorkflowLaunchValidationException $e) {
      // The result rides along so the HTTP boundary can render per-error
      // detail (codes + locators) without re-running the validator.
      $this->assertSame($failure, $e->getResult());
      $this->assertStringContainsString(
        WorkflowValidatorInterface::CODE_EDGE_TARGET_NOT_EXPOSED,
        $e->getMessage(),
      );
    }
  }

}
