<?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 `maxRetries` reserved config property.
 *
 * Every executable node type gets a `maxRetries` integer config property
 * injected into its config schema (so the schema-driven editor renders a
 * retry-budget field and the validator recognises the key), mirroring the
 * instanceTitle/instanceDescription injection. Non-executable node types
 * (NonExecutableNodeProcessorInterface, e.g. Note) never run, so they must
 * not get the field.
 *
 * @group flowdrop_node_type
 * @coversDefaultClass \Drupal\flowdrop_node_type\Service\NodeMetadataResolver
 */
class MaxRetriesConfigInjectionTest 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) {
      // The label may be a TranslatableMarkup (pre-JSON enrich path) or a
      // plain string (post-JSON catalog path); compare stringified.
      if (($element['type'] ?? NULL) === 'Group' && (string) ($element['label'] ?? '') === $label) {
        return $element;
      }
    }
    return NULL;
  }

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

    $this->assertArrayHasKey(ReservedName::MAX_RETRIES, $properties);
    $field = $properties[ReservedName::MAX_RETRIES];
    $this->assertSame('integer', $field['type']);
    $this->assertSame(0, $field['minimum']);
    $this->assertSame(0, $field['default']);
  }

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

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

  /**
   * The maxRetries field carries its execution group hint.
   */
  public function testMaxRetriesTaggedWithExecutionGroup(): void {
    $properties = $this->configSchemaPropertiesFor('proc', 'flowdrop_node_processor:nop');

    $this->assertArrayHasKey(ReservedName::MAX_RETRIES, $properties);
    $this->assertSame('execution', $properties[ReservedName::MAX_RETRIES]['x-group']);
  }

  /**
   * An executable node type emits a uiSchema grouping its reserved props.
   *
   * The catalog response ships the Execution group collapsed (the per-instance
   * open-state is applied later, at enrich-on-read).
   */
  public function testUiSchemaGroupsReservedProps(): void {
    $metadata = $this->metadataFor('proc', 'flowdrop_node_processor:nop');

    $this->assertArrayHasKey('uiSchema', $metadata);
    $uiSchema = $metadata['uiSchema'];
    $this->assertSame('VerticalLayout', $uiSchema['type']);

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

  /**
   * Enrich-on-read opens a group whose field this instance has customised.
   */
  public function testEnrichOpensCustomisedGroup(): void {
    $this->metadataFor('proc', 'flowdrop_node_processor:nop');

    $node = [
      'id' => 'node-1',
      'data' => [
        'metadata' => ['node_type_id' => 'proc'],
        'config' => [ReservedName::MAX_RETRIES => 3],
      ],
    ];

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

    $this->assertTrue($this->uiSchemaGroup($uiSchema, 'Execution')['defaultOpen']);
    // Untouched General group stays collapsed.
    $this->assertFalse($this->uiSchemaGroup($uiSchema, 'General')['defaultOpen']);
  }

}
