<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\DTO\Compiler;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge;
use Drupal\flowdrop_workflow\DTO\WorkflowEdgeDTO;

/**
 * Tests edge-type classification from real, editor-shaped workflow edges.
 *
 * These cases build the edge the way it is actually persisted by the editor
 * (via WorkflowEdgeDTO::fromArray) and assert how DependencyEdge classifies it.
 * This is the round-trip that was previously untested — every existing tool
 * test constructed a DependencyEdge directly with the type constant, bypassing
 * determineEdgeType() entirely, which is how the tool-edge classification gap
 * shipped unnoticed.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge
 * @group flowdrop_runtime
 */
class DependencyEdgeTest extends UnitTestCase {

  /**
   * Builds a DependencyEdge from raw editor edge data.
   *
   * @param array<string, mixed> $raw
   *   The persisted edge array (id, source, target, handles, data).
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge
   *   The classified dependency edge.
   */
  private function fromRaw(array $raw): DependencyEdge {
    return DependencyEdge::fromWorkflowEdge(WorkflowEdgeDTO::fromArray($raw));
  }

  /**
   * A tool connection (tool.output -> consumer.input) is a tool edge.
   *
   * The editor draws tool edges into the consumer's tool input handle
   * ("{nodeId}-input-tool"); the runtime must classify it the same way it
   * classifies trigger/loopback handles.
   *
   * @covers ::determineEdgeType
   */
  public function testToolInputHandleClassifiesAsToolAvailability(): void {
    $edge = $this->fromRaw([
      "id" => "e1",
      "source" => "tool_1",
      "target" => "agent_1",
      "sourceHandle" => "tool_1-output-tool",
      "targetHandle" => "agent_1-input-tool",
    ]);

    $this->assertSame(
      DependencyEdge::TYPE_TOOL_AVAILABILITY,
      $edge->getEdgeType(),
    );
    $this->assertTrue($edge->isToolAvailability());
    // The consumer is the target, the tool node is the source.
    $this->assertSame("tool_1", $edge->getSource());
    $this->assertSame("agent_1", $edge->getTarget());
  }

  /**
   * Trigger and loopback handles still classify correctly.
   *
   * @covers ::determineEdgeType
   */
  public function testTriggerAndLoopbackHandles(): void {
    $trigger = $this->fromRaw([
      "id" => "e1",
      "source" => "a",
      "target" => "b",
      "targetHandle" => "b-input-trigger",
    ]);
    $this->assertSame(DependencyEdge::TYPE_TRIGGER, $trigger->getEdgeType());

    // The loopback port is named "loop_back" everywhere in the runtime
    // (ForEach, StateGraphOrchestrator, editor); the classifier must agree.
    $loopback = $this->fromRaw([
      "id" => "e2",
      "source" => "b",
      "target" => "a",
      "targetHandle" => "a-input-loop_back",
    ]);
    $this->assertSame(DependencyEdge::TYPE_LOOPBACK, $loopback->getEdgeType());
  }

  /**
   * A data edge whose port name merely starts with a reserved name is data.
   *
   * Regression: a substring match misread "{nodeId}-input-tool_calls" (a normal
   * data edge) as a tool edge, and likewise any "tool_*" / "trigger_*" /
   * "loop_back_*" port. Classification must match the full "-input-{port}"
   * suffix.
   *
   * @covers ::determineEdgeType
   */
  public function testReservedPrefixedPortNamesAreNotSpecialEdges(): void {
    foreach (["tool_calls", "trigger_count", "loop_back_index"] as $port) {
      $edge = $this->fromRaw([
        "id" => "e_{$port}",
        "source" => "reason",
        "target" => "invoke",
        "targetHandle" => "invoke-input-{$port}",
      ]);
      $this->assertSame(
        DependencyEdge::TYPE_DATA,
        $edge->getEdgeType(),
        "Port '{$port}' must classify as a data edge, not a special edge.",
      );
    }
  }

  /**
   * A plain connection with no special handle is a data edge.
   *
   * @covers ::determineEdgeType
   */
  public function testPlainHandleIsDataEdge(): void {
    $edge = $this->fromRaw([
      "id" => "e1",
      "source" => "a",
      "target" => "b",
      "sourceHandle" => "a-output-value",
      "targetHandle" => "b-input-value",
    ]);
    $this->assertSame(DependencyEdge::TYPE_DATA, $edge->getEdgeType());
    $this->assertFalse($edge->isToolAvailability());
  }

  /**
   * An explicit edgeType in edge data overrides handle-based detection.
   *
   * @covers ::determineEdgeType
   */
  public function testExplicitEdgeTypeWins(): void {
    $edge = $this->fromRaw([
      "id" => "e1",
      "source" => "tool_1",
      "target" => "agent_1",
      "targetHandle" => "agent_1-input-value",
      "data" => ["edgeType" => DependencyEdge::TYPE_TOOL_AVAILABILITY],
    ]);
    $this->assertSame(
      DependencyEdge::TYPE_TOOL_AVAILABILITY,
      $edge->getEdgeType(),
    );
  }

}
