<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop\Unit\Utility;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\ExecutionEdge;
use Drupal\flowdrop\DTO\JsApi\Edge as TransportEdge;
use Drupal\flowdrop\Utility\EdgePortClassifier;

/**
 * Tests the shared edge/port classification rules.
 *
 * @coversDefaultClass \Drupal\flowdrop\Utility\EdgePortClassifier
 * @group flowdrop
 */
class EdgePortClassifierTest extends UnitTestCase {

  /**
   * Reserved ports match on exact suffix, not substring.
   *
   * @covers ::isTriggerTarget
   * @covers ::isLoopbackTarget
   * @covers ::isToolTarget
   */
  public function testExactSuffixMatching(): void {
    $this->assertTrue(EdgePortClassifier::isTriggerTarget('node1-input-trigger'));
    $this->assertTrue(EdgePortClassifier::isLoopbackTarget('fe-input-loop_back'));
    $this->assertTrue(EdgePortClassifier::isToolTarget('agent-input-tool'));

    // Substring collisions must NOT match.
    $this->assertFalse(EdgePortClassifier::isToolTarget('agent-input-tool_calls'));
    $this->assertFalse(EdgePortClassifier::isLoopbackTarget('fe-input-loop_back_condition'));
    $this->assertFalse(EdgePortClassifier::isTriggerTarget('node1-input-trigger_count'));

    // Output handles never match input ports.
    $this->assertFalse(EdgePortClassifier::isToolTarget('agent-output-tool'));
  }

  /**
   * Port names extract from either direction.
   *
   * @covers ::portName
   */
  public function testPortNameExtraction(): void {
    $this->assertSame('text', EdgePortClassifier::portName('text_input.1-output-text', 'output'));
    $this->assertSame('True', EdgePortClassifier::portName('gateway-output-True', 'output'));
    $this->assertSame('message', EdgePortClassifier::portName('logger-input-message', 'input'));
    $this->assertSame('', EdgePortClassifier::portName('', 'output'));
    $this->assertSame('', EdgePortClassifier::portName('no-direction-marker-here', 'xinput'));
  }

  /**
   * Explicit data.edgeType wins over handle classification.
   *
   * @covers ::isToolEdge
   */
  public function testExplicitEdgeTypePrecedence(): void {
    $this->assertTrue(EdgePortClassifier::isToolEdge(TransportEdge::fromArray([
      'targetHandle' => 'consumer-input-something',
      'data' => ['edgeType' => 'tool_availability'],
    ])));
    // An explicit non-tool declaration overrides a tool-looking handle.
    $this->assertFalse(EdgePortClassifier::isToolEdge(TransportEdge::fromArray([
      'targetHandle' => 'consumer-input-tool',
      'data' => ['edgeType' => 'data'],
    ])));
    // No declaration: the handle decides.
    $this->assertTrue(EdgePortClassifier::isToolEdge(TransportEdge::fromArray([
      'targetHandle' => 'consumer-input-tool',
    ])));
  }

  /**
   * Error edges are recognised on the execution record's source handle.
   *
   * @covers ::isReservedOutputPort
   * @covers ::isErrorEdge
   */
  public function testErrorEdgeClassification(): void {
    // Job-metadata execution record (snake_case source_handle).
    $this->assertTrue(EdgePortClassifier::isErrorEdge(ExecutionEdge::fromMetadata([
      'source' => 'http',
      'source_handle' => 'http-output-error',
    ])));
    // A normal output edge is not an error edge.
    $this->assertFalse(EdgePortClassifier::isErrorEdge(ExecutionEdge::fromMetadata([
      'source' => 'http',
      'source_handle' => 'http-output-result',
    ])));
    // Substring collisions must NOT match.
    $this->assertFalse(EdgePortClassifier::isErrorEdge(ExecutionEdge::fromMetadata([
      'source_handle' => 'http-output-error_code',
    ])));
    // Missing handle: not an error edge.
    $this->assertFalse(EdgePortClassifier::isErrorEdge(ExecutionEdge::fromMetadata(['source' => 'http'])));

    // The reserved-output check is symmetric with the input variant.
    $this->assertTrue(EdgePortClassifier::isReservedOutputPort('n-output-error', 'error'));
    $this->assertFalse(EdgePortClassifier::isReservedOutputPort('n-input-error', 'error'));
  }

  /**
   * Tool-only detection mirrors the compiler rule.
   *
   * @covers ::isToolOnlyNode
   */
  public function testToolOnlyNode(): void {
    $edges = array_map(TransportEdge::fromArray(...), [
      ['source' => 'tool_a', 'target' => 'agent', 'targetHandle' => 'agent-input-tool'],
      ['source' => 'mixed', 'target' => 'agent', 'targetHandle' => 'agent-input-tool'],
      ['source' => 'mixed', 'target' => 'sink', 'targetHandle' => 'sink-input-message'],
      ['source' => 'plain', 'target' => 'sink', 'targetHandle' => 'sink-input-message'],
    ]);

    $this->assertTrue(EdgePortClassifier::isToolOnlyNode('tool_a', $edges));
    // Mixed outputs keep the node executable.
    $this->assertFalse(EdgePortClassifier::isToolOnlyNode('mixed', $edges));
    $this->assertFalse(EdgePortClassifier::isToolOnlyNode('plain', $edges));
    // No outgoing edges at all: not tool-only (it is a sink/terminal node).
    $this->assertFalse(EdgePortClassifier::isToolOnlyNode('sink', $edges));
  }

}
