<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_type\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\flowdrop\Constants\ReservedName;
use Drupal\flowdrop_node_type\Controller\Api\NodesController;

/**
 * Pins the reserved `error` output port injected on executable node types.
 *
 * Every executable node type gets an `error` output port injected into its API
 * metadata, so an author can reveal it and wire an error path. It ships NOT
 * exposed (`exposedByDefault: false`) — hidden on the canvas until opted into.
 * Non-executable node types (NonExecutableNodeProcessorInterface, e.g. Note)
 * never fail, so they must not get it. See `.claude/plans/exposed-ports.md`.
 *
 * @group flowdrop_node_type
 * @coversDefaultClass \Drupal\flowdrop_node_type\Service\NodeMetadataResolver
 */
class ErrorPortInjectionTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'flowdrop',
    'flowdrop_node_category',
    'flowdrop_node_type',
    'flowdrop_node_processor',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('flowdrop_node_category');
    $this->installEntitySchema('flowdrop_node_type');
  }

  /**
   * Creates a node type and returns its API metadata output ports.
   *
   * @param string $id
   *   The node type ID.
   * @param string $executorPlugin
   *   The executor plugin ID.
   *
   * @return array<int, array<string, mixed>>
   *   The output port arrays from the metadata response.
   */
  private function outputPortsFor(string $id, string $executorPlugin): array {
    $this->container->get('entity_type.manager')
      ->getStorage('flowdrop_node_type')
      ->create([
        'id' => $id,
        'label' => ucfirst($id),
        'enabled' => TRUE,
        'executor_plugin' => $executorPlugin,
      ])->save();

    $controller = NodesController::create($this->container);
    $response = $controller->getNodeMetadata($id);
    $this->assertSame(200, $response->getStatusCode(), (string) $response->getContent());

    $payload = json_decode((string) $response->getContent(), TRUE);
    return $payload['data']['metadata']['outputs'] ?? [];
  }

  /**
   * Filters ports down to those with the reserved error id.
   *
   * @param array<int, array<string, mixed>> $ports
   *   The output port arrays.
   *
   * @return array<int, array<string, mixed>>
   *   The error ports.
   */
  private function errorPorts(array $ports): array {
    return array_values(array_filter(
      $ports,
      static fn (array $port): bool => ($port['id'] ?? '') === ReservedName::PORT_ERROR,
    ));
  }

  /**
   * An executable node type gets the error output port, shipped not-exposed.
   *
   * @covers ::buildForNodeType
   */
  public function testErrorPortInjectedForExecutableNode(): void {
    $ports = $this->outputPortsFor('proc', 'flowdrop_node_processor:nop');
    $errorPorts = $this->errorPorts($ports);

    $this->assertCount(1, $errorPorts, json_encode($ports));
    $this->assertSame('output', $errorPorts[0]['type']);
    $this->assertSame('json', $errorPorts[0]['dataType'],
      'The error payload is a JSON object so it wires to a handler json input.');
    $this->assertFalse($errorPorts[0]['exposedByDefault'],
      'The error port must ship hidden until the author opts in.');
  }

  /**
   * A non-executable node type (Note) does not get the error port.
   *
   * @covers ::buildForNodeType
   */
  public function testErrorPortNotInjectedForNonExecutableNode(): void {
    $ports = $this->outputPortsFor('noteType', 'flowdrop_node_processor:note');

    $this->assertSame([], $this->errorPorts($ports), json_encode($ports));
  }

  /**
   * Ordinary exposed ports omit exposedByDefault (lean payload, default TRUE).
   *
   * @covers ::buildForNodeType
   */
  public function testExposedPortsOmitTheExposedByDefaultKey(): void {
    $ports = $this->outputPortsFor('proc2', 'flowdrop_node_processor:nop');

    foreach ($ports as $port) {
      if (($port['id'] ?? '') === ReservedName::PORT_ERROR) {
        continue;
      }
      $this->assertArrayNotHasKey('exposedByDefault', $port,
        'A default-exposed port must not serialize the key: ' . json_encode($port));
    }
  }

}
