<?php

declare(strict_types=1);

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

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\OutputInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph;
use Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionContext;
use Drupal\flowdrop_runtime\Service\Compiler\WorkflowCompilerInterface;
use Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\SynchronousOrchestrator;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface;
use Drupal\flowdrop_runtime\Service\Runtime\ExecutionContext;
use Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeServiceInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Pins last-executor-wins for SynchronousOrchestrator::resolveNodeInputs().
 *
 * This orchestrator resolves a node's inputs from its DependencyGraph parents,
 * a separate code path from the pipeline/job based buildRuntimeInputData() that
 * {@see MultiSourcePortResolutionTest} pins. When several parents target one
 * input port it must keep a SINGLE value from the LATEST executor (highest
 * node_execution_order), never an aggregated array. Any other winner is a bug.
 *
 * @group flowdrop_runtime
 */
class SynchronousOrchestratorMultiSourceInputTest extends UnitTestCase {

  /**
   * Two parents → one port resolves to the latest executor, as one value.
   */
  public function testLatestExecutorWinsForOnePort(): void {
    $context = $this->createMock(NodeExecutionContext::class);
    // node_late executed after node_early (higher counter).
    $context->method('getMetadata')->willReturn([
      'node_execution_order' => ['node_early' => 1, 'node_late' => 2],
    ]);
    $context->method('getNodeOutput')->willReturnMap([
      ['node_early', $this->makeOutput(['out' => 'first'])],
      ['node_late', $this->makeOutput(['out' => 'second'])],
    ]);

    $inputs = $this->resolve($this->makeOrchestrator(), $this->makeGraph(), $context);

    $this->assertArrayHasKey('data', $inputs);
    $this->assertIsNotArray($inputs['data']);
    $this->assertSame('second', $inputs['data']);
  }

  /**
   * A collision emits a warning and keeps the latest executor's value.
   */
  public function testCollisionWarnsAndKeepsLatestExecutor(): void {
    $logger = $this->createMock(LoggerInterface::class);
    $logger->expects($this->once())
      ->method('warning')
      ->with($this->stringContains('Multiple sources target'));

    $context = $this->createMock(NodeExecutionContext::class);
    $context->method('getMetadata')->willReturn([
      'node_execution_order' => ['node_early' => 1, 'node_late' => 2],
    ]);
    $context->method('getNodeOutput')->willReturnMap([
      ['node_early', $this->makeOutput(['out' => 'first'])],
      ['node_late', $this->makeOutput(['out' => 'second'])],
    ]);

    $inputs = $this->resolve($this->makeOrchestrator($logger), $this->makeGraph(), $context);

    $this->assertSame('second', $inputs['data']);
  }

  // --- helpers --------------------------------------------------------------

  /**
   * Invokes the private resolveNodeInputs() via reflection.
   *
   * @return array<string, mixed>
   *   The resolved node inputs.
   */
  private function resolve(SynchronousOrchestrator $orchestrator, DependencyGraph $graph, NodeExecutionContext $context): array {
    $method = (new \ReflectionClass(SynchronousOrchestrator::class))->getMethod('resolveNodeInputs');
    $method->setAccessible(TRUE);

    return $method->invoke($orchestrator, 'node_z', $graph, $context, 'exec', 'wf', 'pipe');
  }

  /**
   * Builds the orchestrator with mocked dependencies and the given logger.
   *
   * @param \Psr\Log\LoggerInterface|null $logger
   *   The logger the orchestrator should use; a no-op mock when omitted.
   */
  private function makeOrchestrator(?LoggerInterface $logger = NULL): SynchronousOrchestrator {
    $logger ??= $this->createMock(LoggerInterface::class);
    $loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);
    $loggerFactory->method('get')->willReturn($logger);

    return new SynchronousOrchestrator(
      $this->createMock(NodeRuntimeServiceInterface::class),
      $this->createMock(ExecutionContext::class),
      $this->createMock(RealTimeManagerInterface::class),
      $this->createMock(WorkflowCompilerInterface::class),
      $this->createMock(ExecutionLoggerInterface::class),
      $this->createMock(EventDispatcherInterface::class),
      $this->createMock(FlowDropNodeProcessorPluginManager::class),
      $loggerFactory,
      $this->createMock(PipelineStorageRegistryInterface::class),
    );
  }

  /**
   * Builds a real DependencyGraph with two data parents into node_z's 'data'.
   */
  private function makeGraph(): DependencyGraph {
    $edges = [
      'e1' => $this->dataEdge('e1', 'node_early'),
      'e2' => $this->dataEdge('e2', 'node_late'),
    ];

    return new DependencyGraph(
      nodes: [],
      edges: $edges,
      incomingEdgeIndex: ['node_z' => array_values($edges)],
      outgoingEdgeIndex: [],
    );
  }

  /**
   * Builds a data DependencyEdge from $source's 'out' port into node_z 'data'.
   */
  private function dataEdge(string $id, string $source): DependencyEdge {
    return new DependencyEdge(
      id: $id,
      source: $source,
      target: 'node_z',
      sourceHandle: $source . '-output-out',
      targetHandle: 'node_z-input-data',
      edgeType: DependencyEdge::TYPE_DATA,
      sourcePortName: 'out',
      targetPortName: 'data',
    );
  }

  /**
   * Builds an Output mock returning the given port => value map.
   *
   * @param array<string, mixed> $data
   *   The output port values.
   */
  private function makeOutput(array $data): OutputInterface {
    $output = $this->createMock(OutputInterface::class);
    $output->method('toArray')->willReturn($data);

    return $output;
  }

}
