<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\DependencyInjection\Compiler;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_runtime\DependencyInjection\Compiler\RegisterPipelineStoragesPass;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistry;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Tests the RegisterPipelineStoragesPass compiler pass.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\DependencyInjection\Compiler\RegisterPipelineStoragesPass
 * @group flowdrop_runtime
 */
class RegisterPipelineStoragesPassTest extends UnitTestCase {

  // ---------------------------------------------------------------------------
  // No-op when registry definition is absent
  // ---------------------------------------------------------------------------

  /**
   * @covers ::process
   */
  public function testProcessIsNoOpWhenRegistryDefinitionAbsent(): void {
    $container = new ContainerBuilder();
    // No registry definition — the pass must simply return without error.
    $pass = new RegisterPipelineStoragesPass();
    // If this throws, the test will fail.
    $pass->process($container);

    $this->assertTrue(TRUE, 'process() returned without throwing when registry is absent.');
  }

  /**
   * @covers ::process
   */
  public function testProcessDoesNotRegisterStoragesWhenRegistryAbsent(): void {
    $container = new ContainerBuilder();
    // Register a tagged storage without the registry present.
    $container->register('storage.default', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'default']);

    (new RegisterPipelineStoragesPass())->process($container);

    // The container must not have invented a registry definition.
    $this->assertFalse($container->hasDefinition('flowdrop_runtime.pipeline_storage_registry'));
  }

  // ---------------------------------------------------------------------------
  // Tags WITH a bundle attribute are wired up
  // ---------------------------------------------------------------------------

  /**
   * @covers ::process
   */
  public function testTaggedStorageWithBundleIsAddedToRegistry(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    $container->register('storage.default', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'default']);

    (new RegisterPipelineStoragesPass())->process($container);

    $methodCalls = $registryDef->getMethodCalls();
    $this->assertCount(1, $methodCalls);
    [$method, $args] = $methodCalls[0];
    $this->assertSame('addStorage', $method);
    $this->assertSame('default', $args[0]);
    $this->assertEquals(new Reference('storage.default'), $args[1]);
  }

  // ---------------------------------------------------------------------------
  // Tags WITHOUT a bundle attribute are silently skipped
  // ---------------------------------------------------------------------------

  /**
   * @covers ::process
   */
  public function testTaggedStorageWithoutBundleIsSkipped(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    // Tag with no 'bundle' key.
    $container->register('storage.unknown', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', []);

    (new RegisterPipelineStoragesPass())->process($container);

    $this->assertSame([], $registryDef->getMethodCalls());
  }

  /**
   * @covers ::process
   */
  public function testMixedTagsOnlyWiresUpThoseWithBundle(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    // One tag with bundle, one without.
    $container->register('storage.mixed', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'in_memory'])
      ->addTag('flowdrop_runtime.pipeline_storage', []);

    (new RegisterPipelineStoragesPass())->process($container);

    $methodCalls = $registryDef->getMethodCalls();
    $this->assertCount(1, $methodCalls);
    $this->assertSame('addStorage', $methodCalls[0][0]);
    $this->assertSame('in_memory', $methodCalls[0][1][0]);
  }

  // ---------------------------------------------------------------------------
  // Multiple tagged services each get their own addMethodCall
  // ---------------------------------------------------------------------------

  /**
   * @covers ::process
   */
  public function testMultipleTaggedServicesEachGetAddMethodCall(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    $container->register('storage.default', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'default']);
    $container->register('storage.in_memory', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'in_memory']);
    $container->register('storage.redis', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'redis']);

    (new RegisterPipelineStoragesPass())->process($container);

    $methodCalls = $registryDef->getMethodCalls();
    $this->assertCount(3, $methodCalls);

    // Collect all registered bundles.
    $registeredBundles = array_map(static fn($call) => $call[1][0], $methodCalls);
    $this->assertContains('default', $registeredBundles);
    $this->assertContains('in_memory', $registeredBundles);
    $this->assertContains('redis', $registeredBundles);

    // Every call must use 'addStorage'.
    foreach ($methodCalls as [$method]) {
      $this->assertSame('addStorage', $method);
    }
  }

  /**
   * @covers ::process
   */
  public function testEachMethodCallPassesCorrectServiceReference(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    $container->register('storage.default', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'default']);
    $container->register('storage.in_memory', \stdClass::class)
      ->addTag('flowdrop_runtime.pipeline_storage', ['bundle' => 'in_memory']);

    (new RegisterPipelineStoragesPass())->process($container);

    $methodCalls = $registryDef->getMethodCalls();
    // Build a bundle => Reference map for easier assertion.
    $callsByBundle = [];
    foreach ($methodCalls as [, $args]) {
      $callsByBundle[$args[0]] = $args[1];
    }

    $this->assertEquals(new Reference('storage.default'), $callsByBundle['default']);
    $this->assertEquals(new Reference('storage.in_memory'), $callsByBundle['in_memory']);
  }

  // ---------------------------------------------------------------------------
  // No tagged services at all
  // ---------------------------------------------------------------------------

  /**
   * @covers ::process
   */
  public function testNoTaggedServicesLeavesRegistryWithNoMethodCalls(): void {
    $container = new ContainerBuilder();
    $registryDef = $container->register('flowdrop_runtime.pipeline_storage_registry', PipelineStorageRegistry::class);
    // No storage services tagged at all.
    (new RegisterPipelineStoragesPass())->process($container);

    $this->assertSame([], $registryDef->getMethodCalls());
  }

}
