<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_type\Unit\Service;

use Drupal\flowdrop_node_type\Service\UiSchemaGenerator;
use Drupal\Tests\UnitTestCase;

/**
 * Tests the UiSchemaGenerator.
 *
 * Two responsibilities: build the static node-type uiSchema from configSchema
 * `x-group` hints, and patch a stored instance's group open-state from its
 * saved config. Both are pure array transforms.
 *
 * @coversDefaultClass \Drupal\flowdrop_node_type\Service\UiSchemaGenerator
 * @group flowdrop_node_type
 */
class UiSchemaGeneratorTest extends UnitTestCase {

  /**
   * The generator under test.
   *
   * @var \Drupal\flowdrop_node_type\Service\UiSchemaGenerator
   */
  private UiSchemaGenerator $generator;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->generator = new UiSchemaGenerator();
    $this->generator->setStringTranslation($this->getStringTranslationStub());
  }

  /**
   * A configSchema mirroring what the resolver produces for a typical node.
   *
   * @return array<string, mixed>
   *   Two ungrouped params plus the four injected reserved props.
   */
  private function sampleConfigSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'model' => ['type' => 'string', 'title' => 'Model', 'x-display-order' => 10],
        'temperature' => ['type' => 'number', 'title' => 'Temperature', 'x-display-order' => 20],
        'instanceTitle' => ['type' => 'string', 'x-display-order' => -2, 'x-group' => 'general'],
        'instanceDescription' => ['type' => 'string', 'x-display-order' => -1, 'x-group' => 'general'],
        'nodeType' => ['type' => 'string', 'default' => 'basic', 'x-group' => 'general'],
        'maxRetries' => ['type' => 'integer', 'default' => 0, 'x-display-order' => 50, 'x-group' => 'execution'],
      ],
    ];
  }

  /**
   * @covers ::generate
   */
  public function testReturnsNullWhenEverythingUngrouped(): void {
    $configSchema = [
      'type' => 'object',
      'properties' => [
        'model' => ['type' => 'string'],
        'temperature' => ['type' => 'number'],
      ],
    ];
    $this->assertNull($this->generator->generate($configSchema));
  }

  /**
   * @covers ::generate
   */
  public function testReturnsNullForEmptySchema(): void {
    $this->assertNull($this->generator->generate([]));
    $this->assertNull($this->generator->generate(['type' => 'object', 'properties' => []]));
  }

  /**
   * @covers ::generate
   */
  public function testUnknownGroupFallsBackToDefault(): void {
    $configSchema = [
      'type' => 'object',
      'properties' => [
        'a' => ['type' => 'string', 'x-group' => 'not-a-real-group'],
      ],
    ];
    // The only tagged prop falls back to default, so nothing is grouped.
    $this->assertNull($this->generator->generate($configSchema));
  }

  /**
   * @covers ::generate
   */
  public function testGroupsTaggedPropsAndKeepsDefaultsInline(): void {
    $uiSchema = $this->generator->generate($this->sampleConfigSchema());

    $this->assertIsArray($uiSchema);
    $this->assertSame('VerticalLayout', $uiSchema['type']);

    $elements = $uiSchema['elements'];
    // Inline defaults first (model, temperature by order), then the
    // General group (order 80), then Execution (order 100).
    $this->assertCount(4, $elements);

    $this->assertSame(['type' => 'Control', 'scope' => '#/properties/model'], $elements[0]);
    $this->assertSame(['type' => 'Control', 'scope' => '#/properties/temperature'], $elements[1]);

    $general = $elements[2];
    $this->assertSame('Group', $general['type']);
    $this->assertSame('General', (string) $general['label']);
    $this->assertTrue($general['collapsible']);
    $this->assertFalse($general['defaultOpen']);
    // Members sorted by x-display-order: title(-2), desc(-1), nodeType(0).
    $this->assertSame([
      ['type' => 'Control', 'scope' => '#/properties/instanceTitle'],
      ['type' => 'Control', 'scope' => '#/properties/instanceDescription'],
      ['type' => 'Control', 'scope' => '#/properties/nodeType'],
    ], $general['elements']);

    $execution = $elements[3];
    $this->assertSame('Group', $execution['type']);
    $this->assertSame('Execution', (string) $execution['label']);
    $this->assertFalse($execution['defaultOpen']);
    $this->assertSame([
      ['type' => 'Control', 'scope' => '#/properties/maxRetries'],
    ], $execution['elements']);
  }

  /**
   * @covers ::applyInstanceOpenState
   */
  public function testInstanceOpenStateOpensCustomisedExecutionGroup(): void {
    $configSchema = $this->sampleConfigSchema();
    $uiSchema = $this->generator->generate($configSchema);

    // maxRetries set to a non-default value opens the Execution group.
    $patched = $this->generator->applyInstanceOpenState($uiSchema, ['maxRetries' => 3], $configSchema);
    $this->assertTrue($this->group($patched, 'Execution')['defaultOpen']);
    // General has no customised member, so it stays collapsed.
    $this->assertFalse($this->group($patched, 'General')['defaultOpen']);
  }

  /**
   * @covers ::applyInstanceOpenState
   */
  public function testInstanceOpenStateKeepsDefaultValueCollapsed(): void {
    $configSchema = $this->sampleConfigSchema();
    $uiSchema = $this->generator->generate($configSchema);

    // maxRetries explicitly at its default (0) is not a customisation.
    $patched = $this->generator->applyInstanceOpenState($uiSchema, ['maxRetries' => 0], $configSchema);
    $this->assertFalse($this->group($patched, 'Execution')['defaultOpen']);

    // Empty string / missing are likewise not customisations.
    $patched = $this->generator->applyInstanceOpenState($uiSchema, ['instanceTitle' => ''], $configSchema);
    $this->assertFalse($this->group($patched, 'General')['defaultOpen']);

    $patched = $this->generator->applyInstanceOpenState($uiSchema, [], $configSchema);
    $this->assertFalse($this->group($patched, 'General')['defaultOpen']);
  }

  /**
   * @covers ::applyInstanceOpenState
   */
  public function testInstanceOpenStateOpensGeneralOnTitle(): void {
    $configSchema = $this->sampleConfigSchema();
    $uiSchema = $this->generator->generate($configSchema);

    $patched = $this->generator->applyInstanceOpenState(
      $uiSchema,
      ['instanceTitle' => 'My renamed node'],
      $configSchema
    );
    $this->assertTrue($this->group($patched, 'General')['defaultOpen']);
    $this->assertFalse($this->group($patched, 'Execution')['defaultOpen']);
  }

  /**
   * Find a Group element by its (stringified) label.
   *
   * @param array<string, mixed> $uiSchema
   *   The uiSchema to search.
   * @param string $label
   *   The group label.
   *
   * @return array<string, mixed>
   *   The matching Group element.
   */
  private function group(array $uiSchema, string $label): array {
    foreach ($uiSchema['elements'] as $element) {
      if (($element['type'] ?? NULL) === 'Group' && (string) $element['label'] === $label) {
        return $element;
      }
    }
    $this->fail("No group labelled {$label}");
  }

}
