<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Kernel;

use Drupal\flowdrop_workflow\Controller\Api\WorkflowsController;
use Drupal\flowdrop_workflow\Entity\FlowDropWorkflow;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\flowdrop\Kernel\FlowDropKernelTestTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Tests structural validation wiring with the real plugin manager.
 *
 * Two things only the integration level can prove:
 * - calculateDependencies() records a config dependency on every referenced
 *   node type, so uninstalling a node type's provider module cascades through
 *   the node type to the workflow rather than vanishing out from under it.
 * - The REST API runs the shared validator and rejects a structurally invalid
 *   workflow before persisting it (the API path previously validated nothing
 *   beyond name/length).
 *
 * @group flowdrop_workflow
 */
class WorkflowStructuralValidationTest extends EntityKernelTestBase {

  use FlowDropKernelTestTrait;

  /**
   * A processor plugin id known to be provided by flowdrop_node_processor.
   */
  private const REAL_PLUGIN = 'flowdrop_node_processor:calculator';

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

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installConfig(['system']);
    $this->installEntitySchema('flowdrop_node_category');
    $this->installEntitySchema('flowdrop_node_type');
    $this->installEntitySchema('flowdrop_workflow');
    $this->config('system.logging')->set('error_level', 'hide')->save();
  }

  /**
   * Referenced node types are config deps; the module dep is transitive.
   */
  public function testReferencedNodeTypeContributesConfigDependency(): void {
    $this->createCalculatorNodeType();
    $storage = $this->entityTypeManager->getStorage('flowdrop_workflow');
    $entity = $storage->create(['id' => 'dep_wf', 'label' => 'Dep WF']);
    $this->assertInstanceOf(FlowDropWorkflowInterface::class, $entity);

    // A single internal node, nothing exposed on the input/output maps. The
    // node type is the anchor, resolved from data.metadata.id.
    $entity->setNodes([
      [
        'id' => 'calc1',
        'data' => ['metadata' => ['node_type_id' => 'calculator']],
      ],
    ]);
    $entity->save();

    // The workflow declares a config dependency on the node type it references.
    // The provider-module dependency is not recorded directly — it is carried
    // transitively by the node type's own calculateDependencies().
    $dependencies = $entity->getDependencies();
    $this->assertContains(
      'flowdrop_node_type.flowdrop_node_type.calculator',
      $dependencies['config'] ?? [],
      'A referenced node type must be recorded as a config dependency.',
    );
    $this->assertNotContains(
      'flowdrop_node_processor',
      $dependencies['module'] ?? [],
      'The provider module is transitive via the node type, not a direct dep.',
    );

    // Prove the chain is real: uninstalling the processor module would
    // cascade-delete the node type and, through it, this workflow — so the
    // module cannot be removed silently while a workflow depends on it.
    $affected = \Drupal::service('config.manager')
      ->getConfigEntitiesToChangeOnDependencyRemoval('module', ['flowdrop_node_processor']);
    $deleted = array_map(
      static fn ($config_entity) => $config_entity->getConfigDependencyName(),
      $affected['delete'],
    );
    $this->assertContains('flowdrop_node_type.flowdrop_node_type.calculator', $deleted);
    $this->assertContains('flowdrop_workflow.flowdrop_workflow.dep_wf', $deleted);
  }

  /**
   * The API rejects a node referencing an unavailable processor (422).
   */
  public function testApiRejectsMissingPlugin(): void {
    // A node type whose declared processor plugin is not installed. The
    // executor plugin is re-resolved from this anchor, then found missing.
    $this->entityTypeManager->getStorage('flowdrop_node_type')->create([
      'id' => 'ghost_type',
      'label' => 'Ghost',
      'enabled' => TRUE,
      'executor_plugin' => 'flowdrop_node_processor:does_not_exist',
    ])->save();

    $controller = WorkflowsController::create($this->container);
    $request = $this->jsonPost([
      'id' => 'ghost_wf',
      'name' => 'Ghost WF',
      'nodes' => [
        [
          'id' => 'n1',
          'data' => ['metadata' => ['node_type_id' => 'ghost_type']],
        ],
      ],
    ]);

    $response = $controller->createWorkflow($request);

    $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
    $body = json_decode((string) $response->getContent(), TRUE);
    $this->assertFalse($body['success']);
    $this->assertSame('R1_PLUGIN_MISSING', $body['details'][0]['code']);
    // The invalid workflow was not persisted.
    $this->assertNull(FlowDropWorkflow::load('ghost_wf'));
  }

  /**
   * The API persists a structurally valid workflow (201).
   */
  public function testApiAcceptsValidWorkflow(): void {
    $this->createCalculatorNodeType();
    $controller = WorkflowsController::create($this->container);
    $request = $this->jsonPost([
      'id' => 'good_wf',
      'name' => 'Good WF',
      'nodes' => [
        [
          'id' => 'calc1',
          'data' => ['metadata' => ['node_type_id' => 'calculator', 'executor_plugin' => self::REAL_PLUGIN]],
        ],
      ],
    ]);

    $response = $controller->createWorkflow($request);

    $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode());
    $this->assertInstanceOf(FlowDropWorkflow::class, FlowDropWorkflow::load('good_wf'));
  }

  /**
   * R6: the API rejects a config value that violates its schema (422).
   */
  public function testApiRejectsInvalidConfigValue(): void {
    $this->createCalculatorNodeType();
    $controller = WorkflowsController::create($this->container);
    $request = $this->jsonPost([
      'id' => 'badcfg_wf',
      'name' => 'Bad Config WF',
      'nodes' => [
        [
          'id' => 'calc1',
          'data' => [
            'metadata' => ['node_type_id' => 'calculator', 'executor_plugin' => self::REAL_PLUGIN],
            'config' => ['operation' => 'not_an_operation'],
          ],
        ],
      ],
    ]);

    $response = $controller->createWorkflow($request);

    $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode(), (string) $response->getContent());
    $body = json_decode((string) $response->getContent(), TRUE);
    $this->assertFalse($body['success']);
    $this->assertContains('R6_CONFIG_INVALID', array_column($body['details'], 'code'));
    // The invalid workflow was not persisted.
    $this->assertNull(FlowDropWorkflow::load('badcfg_wf'));
  }

  /**
   * R6: an unknown config key is a non-blocking warning, not an error.
   */
  public function testValidatorWarnsButAcceptsUnknownConfigKey(): void {
    $this->createCalculatorNodeType();
    $result = $this->container->get('flowdrop_workflow.validator')->validate(
      [
        [
          'id' => 'calc1',
          'data' => [
            'metadata' => ['node_type_id' => 'calculator', 'executor_plugin' => self::REAL_PLUGIN],
            'config' => ['operation' => 'add', 'mystery' => 'x'],
          ],
        ],
      ],
      [],
      [],
      [],
    );

    $this->assertTrue($result->isValid(), 'Unknown config keys must not block a save.');
    $warnings = $result->toArray()['warnings'];
    $this->assertNotEmpty($warnings);
    $this->assertStringContainsString('mystery', $warnings[0]['message']);
  }

  /**
   * R6: a config value that satisfies the schema yields no config error.
   */
  public function testValidatorAcceptsValidConfig(): void {
    $this->createCalculatorNodeType();
    $result = $this->container->get('flowdrop_workflow.validator')->validate(
      [
        [
          'id' => 'calc1',
          'data' => [
            'metadata' => ['node_type_id' => 'calculator', 'executor_plugin' => self::REAL_PLUGIN],
            'config' => ['operation' => 'multiply', 'precision' => 3],
          ],
        ],
      ],
      [],
      [],
      [],
    );

    $this->assertTrue($result->isValid(), (string) json_encode($result->toArray()));
  }

  /**
   * Creates a 'calculator' node type with configurable operation + precision.
   */
  private function createCalculatorNodeType(): void {
    $this->entityTypeManager->getStorage('flowdrop_node_type')->create([
      'id' => 'calculator',
      'label' => 'Calculator',
      'enabled' => TRUE,
      'executor_plugin' => self::REAL_PLUGIN,
      'parameters' => [
        'operation' => ['configurable' => TRUE],
        'precision' => ['configurable' => TRUE],
      ],
    ])->save();
  }

  /**
   * Builds a JSON POST request with the given payload.
   *
   * @param array<string, mixed> $payload
   *   The request body.
   */
  private function jsonPost(array $payload): Request {
    return Request::create(
      '/api/flowdrop/workflows',
      'POST',
      [],
      [],
      [],
      ['CONTENT_TYPE' => 'application/json'],
      (string) json_encode($payload),
    );
  }

}
