<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Kernel;

use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\flowdrop\Entity\FlowDropTrustedPublisher;
use Drupal\flowdrop\Service\Bundle\BundleCanonicalizer;
use Drupal\flowdrop\Service\Bundle\BundleSigner;
use Drupal\flowdrop\Service\Bundle\BundleVerifier;
use Drupal\flowdrop_workflow\DTO\BundleImportResult;
use Drupal\flowdrop_workflow\Service\BundleImporter;

/**
 * Tests the end-to-end bundle import orchestration.
 *
 * @group flowdrop_workflow
 * @covers \Drupal\flowdrop_workflow\Service\BundleImporter
 */
class BundleImporterTest extends EntityKernelTestBase {

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

  /**
   * The raw Ed25519 secret key used to sign test bundles.
   */
  private string $secretKey;

  /**
   * The raw Ed25519 public key registered as a trusted publisher.
   */
  private string $publicKey;

  /**
   * {@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();

    $keypair = sodium_crypto_sign_keypair();
    $this->publicKey = sodium_crypto_sign_publickey($keypair);
    $this->secretKey = sodium_crypto_sign_secretkey($keypair);
  }

  /**
   * Returns the importer service.
   */
  private function importer(): BundleImporter {
    return $this->container->get('flowdrop_workflow.bundle_importer');
  }

  /**
   * Registers the test key as an enabled trusted publisher.
   */
  private function trustPublisher(string $keyId): void {
    FlowDropTrustedPublisher::create([
      'id' => $keyId,
      'label' => ucfirst($keyId),
      'public_key' => base64_encode($this->publicKey),
      'enabled' => TRUE,
    ])->save();
  }

  /**
   * Builds a flow payload around the given nodes.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The workflow nodes.
   * @param string $publisher
   *   The publisher key id to stamp into the payload.
   *
   * @return array<string, mixed>
   *   The bundle payload.
   */
  private function payload(array $nodes, string $publisher = ''): array {
    return [
      'publisher' => $publisher,
      'flow' => [
        'id' => 'imported_flow',
        'label' => 'Imported Flow',
        'nodes' => $nodes,
        'edges' => [],
      ],
    ];
  }

  /**
   * Signs a payload into a trusted bundle.
   *
   * @param array<string, mixed> $payload
   *   The payload.
   *
   * @return array<string, mixed>
   *   The signed bundle.
   */
  private function sign(array $payload): array {
    $signer = new BundleSigner(new BundleCanonicalizer());
    return $signer->sign($payload, (string) $payload['publisher'], $this->secretKey);
  }

  /**
   * Wraps a payload into an unsigned bundle envelope with a valid format.
   *
   * A real exported bundle always carries the format discriminator; the
   * importer now rejects envelopes that omit it, so even the unsigned-path
   * fixtures must include it.
   *
   * @param array<string, mixed> $payload
   *   The payload.
   *
   * @return array<string, mixed>
   *   The unsigned bundle.
   */
  private function unsigned(array $payload): array {
    return ['format' => BundleVerifier::FORMAT, 'payload' => $payload];
  }

  /**
   * A calculator node with full metadata (lets node-type generation succeed).
   *
   * @return array<string, mixed>
   *   The node.
   */
  private function calculatorNode(): array {
    return [
      'id' => 'calc.1',
      'data' => [
        'metadata' => [
          'node_type_id' => 'calculator',
          'executor_plugin' => 'flowdrop_node_processor:calculator',
          'name' => 'Calculator',
          'inputs' => [['id' => 'values', 'name' => 'Values', 'dataType' => 'array']],
          'configSchema' => [
            'type' => 'object',
            'properties' => [
              'operation' => ['type' => 'string', 'default' => 'add'],
              'precision' => ['type' => 'integer', 'default' => 2],
            ],
            'required' => [],
          ],
          'config' => ['operation' => 'add', 'precision' => 2],
          'outputs' => [['id' => 'result', 'name' => 'Result', 'dataType' => 'number']],
        ],
      ],
    ];
  }

  /**
   * A trusted bundle is imported: workflow saved, node type generated.
   */
  public function testTrustedBundleIsApplied(): void {
    $this->trustPublisher('acme');
    $bundle = $this->sign($this->payload([$this->calculatorNode()], 'acme'));

    $result = $this->importer()->import($bundle, FALSE, FALSE);

    $this->assertSame(BundleImportResult::APPLIED, $result->getStatus());
    $this->assertSame('imported_flow', $result->getWorkflowId());
    $this->assertSame(['calculator'], $result->getGeneratedNodeTypes());
    $this->assertNotNull($this->container->get('entity_type.manager')->getStorage('flowdrop_workflow')->load('imported_flow'));
    $this->assertNotNull($this->container->get('entity_type.manager')->getStorage('flowdrop_node_type')->load('calculator'));
  }

  /**
   * An untrusted bundle is blocked when the override is not granted.
   */
  public function testUntrustedBundleBlocked(): void {
    // Unsigned bundle (no publisher trusted).
    $bundle = $this->unsigned($this->payload([$this->calculatorNode()]));

    $result = $this->importer()->import($bundle, FALSE, FALSE);

    $this->assertSame(BundleImportResult::UNTRUSTED_BLOCKED, $result->getStatus());
    $this->assertNull($this->container->get('entity_type.manager')->getStorage('flowdrop_workflow')->load('imported_flow'));
  }

  /**
   * An untrusted bundle is applied when the override is granted.
   */
  public function testUntrustedBundleAppliedWithOverride(): void {
    $bundle = $this->unsigned($this->payload([$this->calculatorNode()]));

    $result = $this->importer()->import($bundle, FALSE, TRUE);

    $this->assertSame(BundleImportResult::APPLIED, $result->getStatus());
  }

  /**
   * A dry run previews without writing anything.
   */
  public function testDryRunWritesNothing(): void {
    $this->trustPublisher('acme');
    $bundle = $this->sign($this->payload([$this->calculatorNode()], 'acme'));

    $result = $this->importer()->import($bundle, TRUE, FALSE);

    $this->assertSame(BundleImportResult::PREFLIGHT_OK, $result->getStatus());
    $this->assertNull($this->container->get('entity_type.manager')->getStorage('flowdrop_workflow')->load('imported_flow'));
    $this->assertNull($this->container->get('entity_type.manager')->getStorage('flowdrop_node_type')->load('calculator'));
  }

  /**
   * A bundle needing an uninstalled processor is blocked on capability.
   */
  public function testMissingCapabilityBlocked(): void {
    $ghost = [
      'id' => 'g.1',
      'data' => ['metadata' => ['node_type_id' => 'ghost', 'executor_plugin' => 'ghost_module:ghost']],
    ];
    $result = $this->importer()->import($this->unsigned($this->payload([$ghost])), FALSE, TRUE);

    $this->assertSame(BundleImportResult::MISSING_CAPABILITY, $result->getStatus());
    $this->assertSame(['ghost_module'], $result->getManifest()?->missingModules());
  }

  /**
   * Importing onto an existing id never overwrites.
   */
  public function testConflictDoesNotOverwrite(): void {
    $this->container->get('entity_type.manager')->getStorage('flowdrop_workflow')->create([
      'id' => 'imported_flow',
      'label' => 'Pre-existing',
    ])->save();

    $result = $this->importer()->import($this->unsigned($this->payload([$this->calculatorNode()])), FALSE, TRUE);

    $this->assertSame(BundleImportResult::CONFLICT, $result->getStatus());
  }

  /**
   * A bundle whose envelope omits or misdeclares the format is rejected.
   */
  public function testUnsupportedFormatRejected(): void {
    // No format key at all.
    $missing = ['payload' => $this->payload([$this->calculatorNode()])];
    $this->assertSame(
      BundleImportResult::UNSUPPORTED_FORMAT,
      $this->importer()->import($missing, FALSE, TRUE)->getStatus(),
    );

    // A future/garbage format string.
    $wrong = ['format' => 'flowdrop.bundle/v2', 'payload' => $this->payload([$this->calculatorNode()])];
    $this->assertSame(
      BundleImportResult::UNSUPPORTED_FORMAT,
      $this->importer()->import($wrong, FALSE, TRUE)->getStatus(),
    );

    $this->assertNull($this->container->get('entity_type.manager')->getStorage('flowdrop_workflow')->load('imported_flow'));
  }

  /**
   * An untrusted flow id that isn't a valid machine name is rejected.
   */
  public function testInvalidFlowIdRejected(): void {
    $payload = $this->payload([$this->calculatorNode()]);
    $payload['flow']['id'] = 'evil.config.name';

    $result = $this->importer()->import($this->unsigned($payload), FALSE, TRUE);

    $this->assertSame(BundleImportResult::INVALID, $result->getStatus());
  }

}
