<?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 injected `ports` reserved config property.
 *
 * Every executable node type gets a `ports` object config property injected
 * into its config schema, carrying the `ports` widget format and a Ports group
 * hint, so the editor renders the unified port order + exposure widget and the
 * validator recognises the key. Non-executable node types (Note) never run, so
 * they must not get the field.
 *
 * @group flowdrop_node_type
 * @coversDefaultClass \Drupal\flowdrop_node_type\Service\NodeMetadataResolver
 */
class PortsConfigInjectionTest 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');
  }

  /**
   * Returns the full API metadata block for a freshly created node type.
   *
   * @param string $id
   *   The node type ID.
   * @param string $executorPlugin
   *   The executor plugin ID.
   *
   * @return array<string, mixed>
   *   The `data.metadata` block from the metadata response.
   */
  private function metadataFor(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'] ?? [];
  }

  /**
   * Returns the config-schema properties for a node type's API metadata.
   *
   * @param string $id
   *   The node type ID.
   * @param string $executorPlugin
   *   The executor plugin ID.
   *
   * @return array<string, mixed>
   *   The configSchema "properties" map from the metadata response.
   */
  private function configSchemaPropertiesFor(string $id, string $executorPlugin): array {
    return $this->metadataFor($id, $executorPlugin)['configSchema']['properties'] ?? [];
  }

  /**
   * Finds a uiSchema Group element by its label.
   *
   * @param array<string, mixed> $uiSchema
   *   The uiSchema to search.
   * @param string $label
   *   The group label.
   *
   * @return array<string, mixed>|null
   *   The matching Group element, or NULL if absent.
   */
  private function uiSchemaGroup(array $uiSchema, string $label): ?array {
    foreach ($uiSchema['elements'] ?? [] as $element) {
      if (($element['type'] ?? NULL) === 'Group' && (string) ($element['label'] ?? '') === $label) {
        return $element;
      }
    }
    return NULL;
  }

  /**
   * An executable node type gets the ports field injected.
   */
  public function testPortsInjectedForExecutableNode(): void {
    $properties = $this->configSchemaPropertiesFor('proc', 'flowdrop_node_processor:nop');

    $this->assertArrayHasKey(ReservedName::PORTS, $properties);
    $field = $properties[ReservedName::PORTS];
    $this->assertSame('object', $field['type']);
    $this->assertSame('ports', $field['format']);
  }

  /**
   * A non-executable node type (Note) does not get the ports field.
   */
  public function testPortsNotInjectedForNonExecutableNode(): void {
    $properties = $this->configSchemaPropertiesFor('noteType', 'flowdrop_node_processor:note');

    $this->assertArrayNotHasKey(ReservedName::PORTS, $properties);
  }

  /**
   * Reserved injected ports default to the bottom via a high displayOrder.
   */
  public function testReservedPortsCarryBottomDisplayOrder(): void {
    $metadata = $this->metadataFor('proc', 'flowdrop_node_processor:nop');

    $outputsByid = array_column($metadata['outputs'] ?? [], NULL, 'id');
    $this->assertArrayHasKey(ReservedName::PORT_ERROR, $outputsByid);
    $this->assertSame(120, $outputsByid[ReservedName::PORT_ERROR]['displayOrder']);
    $this->assertArrayHasKey(ReservedName::PORT_TRIGGER, $outputsByid);
    $this->assertSame(100, $outputsByid[ReservedName::PORT_TRIGGER]['displayOrder']);
  }

  /**
   * The ports field is grouped into a collapsible Ports fieldset.
   */
  public function testPortsTaggedWithPortsGroup(): void {
    $metadata = $this->metadataFor('proc', 'flowdrop_node_processor:nop');
    $properties = $metadata['configSchema']['properties'] ?? [];

    $this->assertArrayHasKey(ReservedName::PORTS, $properties);
    $this->assertSame('ports', $properties[ReservedName::PORTS]['x-group']);

    $uiSchema = $metadata['uiSchema'];
    $ports = $this->uiSchemaGroup($uiSchema, 'Ports');
    $this->assertNotNull($ports, 'Ports group present');
    $this->assertFalse($ports['defaultOpen'], 'baseline collapsed');
    $scopes = array_column($ports['elements'], 'scope');
    $this->assertContains('#/properties/' . ReservedName::PORTS, $scopes);
  }

  /**
   * Enrich-on-read opens the Ports group when this instance set port config.
   */
  public function testEnrichOpensPortsGroupWhenPortsSet(): void {
    $this->metadataFor('proc', 'flowdrop_node_processor:nop');

    $node = [
      'id' => 'node-1',
      'data' => [
        'metadata' => ['node_type_id' => 'proc'],
        'config' => [ReservedName::PORTS => ['outputs' => [['id' => 'error', 'exposed' => TRUE]]]],
      ],
    ];

    $enriched = $this->container->get('flowdrop_node_type.metadata_resolver')->enrichNode($node);
    $uiSchema = $enriched['data']['metadata']['uiSchema'];

    $this->assertTrue($this->uiSchemaGroup($uiSchema, 'Ports')['defaultOpen']);
  }

}
