<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Unit\DTO;

use Drupal\flowdrop_workflow\DTO\WorkflowDTO;
use Drupal\Tests\UnitTestCase;

/**
 * Tests WorkflowDTO's repair-on-read pruning of dangling edges.
 *
 * The editor cannot persist an edge that references a missing node, so an
 * orphan only arises from hand-edited JSON, an import, or a migration. The DTO
 * is the normalization point the orchestrator and compiler read through, so it
 * prunes such edges to keep that graph consistent (the save path rejects the
 * same condition via WorkflowValidator R8). Pruned edges remain observable via
 * getDroppedEdges() so a caller can surface them.
 *
 * @coversDefaultClass \Drupal\flowdrop_workflow\DTO\WorkflowDTO
 * @group flowdrop_workflow
 */
class WorkflowDTOTest extends UnitTestCase {

  /**
   * Builds raw workflow data with the given nodes and edges.
   *
   * @param list<string> $nodeIds
   *   The node ids to include.
   * @param list<array<string, mixed>> $edges
   *   The raw edges.
   *
   * @return array<string, mixed>
   *   The raw workflow data.
   */
  private function workflowData(array $nodeIds, array $edges): array {
    return [
      'id' => 'wf',
      'nodes' => array_map(static fn (string $id): array => ['id' => $id], $nodeIds),
      'edges' => $edges,
    ];
  }

  /**
   * An edge whose target node is absent is pruned and recorded as dropped.
   */
  public function testPrunesEdgeWithMissingTarget(): void {
    $dto = WorkflowDTO::fromArray($this->workflowData(['a', 'b'], [
      ['id' => 'e1', 'source' => 'a', 'target' => 'b'],
      ['id' => 'e2', 'source' => 'a', 'target' => 'gone'],
    ]));

    $this->assertSame(['e1'], array_keys($dto->getEdges()));
    $this->assertSame(['e2'], array_keys($dto->getDroppedEdges()));
  }

  /**
   * An edge whose source node is absent is pruned.
   */
  public function testPrunesEdgeWithMissingSource(): void {
    $dto = WorkflowDTO::fromArray($this->workflowData(['b'], [
      ['id' => 'e1', 'source' => 'gone', 'target' => 'b'],
    ]));

    $this->assertSame([], array_keys($dto->getEdges()));
    $this->assertSame(['e1'], array_keys($dto->getDroppedEdges()));
  }

  /**
   * A clean graph prunes nothing.
   */
  public function testCleanGraphDropsNothing(): void {
    $dto = WorkflowDTO::fromArray($this->workflowData(['a', 'b'], [
      ['id' => 'e1', 'source' => 'a', 'target' => 'b'],
    ]));

    $this->assertCount(1, $dto->getEdges());
    $this->assertSame([], $dto->getDroppedEdges());
  }

  /**
   * A pruned edge is absent from the edge index, not just the edge list.
   *
   * Guards the previously latent inconsistency where a dangling edge stayed in
   * the edge list while being silently excluded from the index.
   */
  public function testPrunedEdgeIsAbsentFromIndex(): void {
    $dto = WorkflowDTO::fromArray($this->workflowData(['a'], [
      ['id' => 'e1', 'source' => 'a', 'target' => 'gone'],
    ]));

    $this->assertSame([], $dto->getOutgoingEdges('a'));
    $this->assertSame([], $dto->getDependents('a'));
  }

}
