<?php

declare(strict_types=1);

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

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\flowdrop_runtime\DTO\Compiler\ExecutionNode;
use Drupal\flowdrop_runtime\DTO\Compiler\NodeMapping;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_runtime\Service\Compiler\WorkflowCompiler;
use Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\SynchronousOrchestrator;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManager;
use Drupal\flowdrop_runtime\Service\Runtime\ExecutionContext;
use Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeService;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistryInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Tests for SynchronousOrchestrator iterator handling.
 *
 * Tests that the orchestrator properly detects and delegates
 * iterator node execution via ExecutionNode special types.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\Orchestrator\SynchronousOrchestrator
 * @group flowdrop_runtime
 */
class SynchronousOrchestratorIteratorTest extends TestCase {

  /**
   * The orchestrator under test.
   *
   * @var \Drupal\flowdrop_runtime\Service\Orchestrator\SynchronousOrchestrator
   */
  protected SynchronousOrchestrator $orchestrator;

  /**
   * Mock node runtime.
   *
   * @var \Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeService|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $nodeRuntime;

  /**
   * Mock execution context.
   *
   * @var \Drupal\flowdrop_runtime\Service\Runtime\ExecutionContext|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $executionContext;

  /**
   * Mock real-time manager.
   *
   * @var \Drupal\flowdrop_runtime\Service\RealTime\RealTimeManager|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $realTimeManager;

  /**
   * Mock workflow compiler.
   *
   * @var \Drupal\flowdrop_runtime\Service\Compiler\WorkflowCompiler|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $workflowCompiler;

  /**
   * Mock logger.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $logger;

  /**
   * Mock event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $eventDispatcher;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->nodeRuntime = $this->createMock(NodeRuntimeService::class);
    $this->executionContext = $this->createMock(ExecutionContext::class);
    $this->realTimeManager = $this->createMock(RealTimeManager::class);
    $this->workflowCompiler = $this->createMock(WorkflowCompiler::class);
    $executionLogger = $this->createMock(ExecutionLoggerInterface::class);
    $this->logger = $this->createMock(LoggerChannelInterface::class);
    $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
    $nodeProcessorManager = $this->createMock(FlowDropNodeProcessorPluginManager::class);

    $loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);
    $loggerFactory->method("get")->willReturn($this->logger);

    $pipelineStorageRegistry = $this->createMock(PipelineStorageRegistryInterface::class);

    $this->orchestrator = new SynchronousOrchestrator(
      $this->nodeRuntime,
      $this->executionContext,
      $this->realTimeManager,
      $this->workflowCompiler,
      $executionLogger,
      $this->eventDispatcher,
      $nodeProcessorManager,
      $loggerFactory,
      $pipelineStorageRegistry,
    );
  }

  /**
   * Test ExecutionNode correctly identifies iterator nodes via special type.
   *
   * Iterator detection is done on ExecutionNode with SPECIAL_TYPE_ITERATOR.
   *
   * @covers \Drupal\flowdrop_runtime\DTO\Compiler\ExecutionNode::isIterator
   */
  public function testExecutionNodeIteratorDetection(): void {
    // Create an iterator execution node.
    $iteratorNode = new ExecutionNode(
      id: "iterator_1",
      typeId: "iterator",
      config: ["maxIterations" => 100],
      triggerDependencies: [],
      dataDependencies: ["start_node"],
      isRoot: FALSE,
      isLeaf: FALSE,
      specialType: ExecutionNode::SPECIAL_TYPE_ITERATOR,
      metadata: [],
    );

    $this->assertTrue($iteratorNode->isIterator());
    $this->assertFalse($iteratorNode->isAgent());
    $this->assertTrue($iteratorNode->requiresSpecialHandling());
    $this->assertEquals(ExecutionNode::SPECIAL_TYPE_ITERATOR, $iteratorNode->getSpecialType());
  }

  /**
   * Test that regular nodes are not identified as iterators.
   *
   * @covers \Drupal\flowdrop_runtime\DTO\Compiler\ExecutionNode::isIterator
   */
  public function testRegularNodeNotIterator(): void {
    // Create various regular execution nodes.
    $textInputNode = new ExecutionNode(
      id: "text_input_1",
      typeId: "text_input",
      config: [],
      triggerDependencies: [],
      dataDependencies: [],
      isRoot: TRUE,
      isLeaf: FALSE,
      specialType: NULL,
      metadata: [],
    );

    $processorNode = new ExecutionNode(
      id: "processor_1",
      typeId: "processor",
      config: [],
      triggerDependencies: [],
      dataDependencies: [],
      isRoot: FALSE,
      isLeaf: FALSE,
      specialType: NULL,
      metadata: [],
    );

    $gatewayNode = new ExecutionNode(
      id: "gateway_1",
      typeId: "gateway",
      config: [],
      triggerDependencies: [],
      dataDependencies: [],
      isRoot: FALSE,
      isLeaf: FALSE,
      specialType: NULL,
      metadata: [],
    );

    $this->assertFalse($textInputNode->isIterator());
    $this->assertFalse($processorNode->isIterator());
    $this->assertFalse($gatewayNode->isIterator());

    $this->assertFalse($textInputNode->requiresSpecialHandling());
    $this->assertFalse($processorNode->requiresSpecialHandling());
    $this->assertFalse($gatewayNode->requiresSpecialHandling());
  }

  /**
   * Test orchestrator logs when iterator node is detected.
   *
   * @covers ::orchestrate
   */
  public function testOrchestratorLogsIteratorDetection(): void {
    // Skip this test if flowdrop_iterator module is not available.
    // In unit tests, we can't actually load the module.
    $this->markTestSkipped("Requires flowdrop_iterator module to be enabled");
  }

  /**
   * Test getType returns synchronous.
   *
   * @covers ::getType
   */
  public function testGetType(): void {
    $this->assertSame("flowdrop_runtime:synchronous", $this->orchestrator->getType());
  }

  /**
   * Test getCapabilities includes workflow_compilation.
   *
   * @covers ::getCapabilities
   */
  public function testGetCapabilities(): void {
    $capabilities = $this->orchestrator->getCapabilities();

    $this->assertTrue($capabilities["synchronous_execution"]);
    $this->assertTrue($capabilities["workflow_compilation"]);
    $this->assertTrue($capabilities["retry_support"]);
  }

  /**
   * Test that NodeMapping correctly stores iterator processor ID.
   *
   * Tests the integration between node mapping processor ID
   * and the ExecutionNode isIterator check.
   *
   * @covers \Drupal\flowdrop_runtime\DTO\Compiler\NodeMapping::getProcessorId
   */
  public function testNodeMappingIteratorProcessorId(): void {
    // Verify that a node with processorId "iterator" is correctly stored.
    $nodeMapping = new NodeMapping(
      nodeId: "iterator_1",
      processorId: "iterator",
      config: ["maxIterations" => 100],
      metadata: [],
    );

    $this->assertSame("iterator", $nodeMapping->getProcessorId());
    $this->assertEquals(["maxIterations" => 100], $nodeMapping->getConfig());
  }

  /**
   * Test that NodeMapping exposes a node-type ID distinct from the plugin ID.
   *
   * The synchronous orchestrator must hand the runtime the node-type
   * config-entity ID (used to load the node type from storage), NOT the
   * executor plugin ID returned by getProcessorId().
   *
   * @covers \Drupal\flowdrop_runtime\DTO\Compiler\NodeMapping::getNodeTypeId
   */
  public function testNodeMappingNodeTypeIdDistinctFromProcessorId(): void {
    $nodeMapping = new NodeMapping(
      nodeId: "chat_input.1",
      processorId: "flowdrop_node_processor:chat_input",
      config: [],
      metadata: [],
      nodeTypeId: "chat_input",
    );

    $this->assertSame("flowdrop_node_processor:chat_input", $nodeMapping->getProcessorId());
    $this->assertSame("chat_input", $nodeMapping->getNodeTypeId());
  }

  /**
   * Test that getNodeTypeId() returns '' when the node carries no anchor.
   *
   * It must NOT fall back to the processor ID: plugin IDs are always
   * colon-namespaced and entity IDs never are, so the plugin ID could only
   * ever be an invalid node-type entity ID. A missing anchor must surface as
   * an empty ID (failing the entity load loudly), not be masked.
   *
   * @covers \Drupal\flowdrop_runtime\DTO\Compiler\NodeMapping::getNodeTypeId
   */
  public function testNodeMappingNodeTypeIdEmptyWhenNoAnchor(): void {
    $nodeMapping = new NodeMapping(
      nodeId: "iterator_1",
      processorId: "flowdrop_iterator:iterator",
      config: [],
      metadata: [],
    );

    $this->assertSame("", $nodeMapping->getNodeTypeId());
  }

  /**
   * Test edge metadata preservation in compiled workflow.
   *
   * Tests that special edge metadata is preserved through compilation
   * so the orchestrator can access it.
   */
  public function testEdgeMetadataPreservation(): void {
    // This is a structural test to ensure edge metadata flows through.
    $workflowData = [
      "id" => "test_workflow",
      "edges" => [
        [
          "id" => "e1",
          "source" => "node_a",
          "target" => "iterator_1",
          "data" => [
            "metadata" => [
              "edgeType" => "loopback",
            ],
          ],
        ],
      ],
    ];

    // Verify edge data structure.
    $edgeData = $workflowData["edges"][0]["data"]["metadata"];
    $this->assertSame("loopback", $edgeData["edgeType"]);
  }

}
