<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service\Storage;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\PipelineAwareOrchestratorInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistry;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the PipelineStorageRegistry service.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\Storage\PipelineStorageRegistry
 * @group flowdrop_runtime
 */
class PipelineStorageRegistryTest extends UnitTestCase {

  use ProphecyTrait;

  // ---------------------------------------------------------------------------
  // getStorage()
  // ---------------------------------------------------------------------------

  /**
   * @covers ::getStorage
   */
  public function testGetStorageReturnsRegisteredStorage(): void {
    $storage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $registry = new PipelineStorageRegistry(['default' => $storage]);

    $this->assertSame($storage, $registry->getStorage('default'));
  }

  /**
   * @covers ::getStorage
   */
  public function testGetStorageThrowsForUnknownBundle(): void {
    $registry = new PipelineStorageRegistry();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage("No pipeline storage registered for bundle 'unknown'");

    $registry->getStorage('unknown');
  }

  /**
   * @covers ::getStorage
   */
  public function testGetStorageThrowsWhenRegistryIsEmpty(): void {
    $registry = new PipelineStorageRegistry();

    $this->expectException(\InvalidArgumentException::class);

    $registry->getStorage('default');
  }

  // ---------------------------------------------------------------------------
  // addStorage()
  // ---------------------------------------------------------------------------

  /**
   * @covers ::addStorage
   * @covers ::getStorage
   */
  public function testAddStorageRoundTrip(): void {
    $registry = new PipelineStorageRegistry();
    $storage = $this->prophesize(PipelineStorageInterface::class)->reveal();

    $registry->addStorage('in_memory', $storage);

    $this->assertSame($storage, $registry->getStorage('in_memory'));
  }

  /**
   * @covers ::addStorage
   * @covers ::getStorage
   */
  public function testAddStorageOverwritesExistingBundle(): void {
    $first = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $second = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $registry = new PipelineStorageRegistry(['default' => $first]);

    $registry->addStorage('default', $second);

    $this->assertSame($second, $registry->getStorage('default'));
  }

  /**
   * @covers ::addStorage
   * @covers ::getStorage
   */
  public function testMultipleBundlesStoredIndependently(): void {
    $defaultStorage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $inMemoryStorage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $registry = new PipelineStorageRegistry();

    $registry->addStorage('default', $defaultStorage);
    $registry->addStorage('in_memory', $inMemoryStorage);

    $this->assertSame($defaultStorage, $registry->getStorage('default'));
    $this->assertSame($inMemoryStorage, $registry->getStorage('in_memory'));
  }

  // ---------------------------------------------------------------------------
  // getBundleForOrchestrator()
  // ---------------------------------------------------------------------------

  /**
   * @covers ::getBundleForOrchestrator
   */
  public function testGetBundleForOrchestratorFallsBackToDefault(): void {
    $orchestrator = $this->prophesize(OrchestratorInterface::class)->reveal();
    $registry = new PipelineStorageRegistry();

    $this->assertSame('default', $registry->getBundleForOrchestrator($orchestrator));
  }

  /**
   * @covers ::getBundleForOrchestrator
   */
  public function testGetBundleForOrchestratorReturnsDeclaredBundle(): void {
    // Create an orchestrator that implements both interfaces.
    $orchestrator = $this->prophesize(OrchestratorInterface::class)
      ->willImplement(PipelineAwareOrchestratorInterface::class);
    $orchestrator->getPreferredPipelineBundle()->willReturn('in_memory');
    $registry = new PipelineStorageRegistry();

    $this->assertSame('in_memory', $registry->getBundleForOrchestrator($orchestrator->reveal()));
  }

  /**
   * @covers ::getBundleForOrchestrator
   */
  public function testGetBundleForOrchestratorIgnoresReturnValueWhenNotPipelineAware(): void {
    // Even if the orchestrator has no getPreferredPipelineBundle, 'default' is
    // returned and no method is called on the orchestrator.
    $orchestrator = $this->prophesize(OrchestratorInterface::class);
    $registry = new PipelineStorageRegistry();

    $bundle = $registry->getBundleForOrchestrator($orchestrator->reveal());

    $this->assertSame('default', $bundle);
  }

  // ---------------------------------------------------------------------------
  // getStorageForOrchestrator()
  // ---------------------------------------------------------------------------

  /**
   * @covers ::getStorageForOrchestrator
   */
  public function testGetStorageForOrchestratorReturnsDefaultStorageForPlainOrchestrator(): void {
    $defaultStorage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $registry = new PipelineStorageRegistry(['default' => $defaultStorage]);

    $orchestrator = $this->prophesize(OrchestratorInterface::class)->reveal();

    $this->assertSame($defaultStorage, $registry->getStorageForOrchestrator($orchestrator));
  }

  /**
   * @covers ::getStorageForOrchestrator
   */
  public function testGetStorageForOrchestratorReturnsBundleStorageForPipelineAwareOrchestrator(): void {
    $defaultStorage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $inMemoryStorage = $this->prophesize(PipelineStorageInterface::class)->reveal();
    $registry = new PipelineStorageRegistry([
      'default' => $defaultStorage,
      'in_memory' => $inMemoryStorage,
    ]);

    $orchestrator = $this->prophesize(OrchestratorInterface::class)
      ->willImplement(PipelineAwareOrchestratorInterface::class);
    $orchestrator->getPreferredPipelineBundle()->willReturn('in_memory');

    $this->assertSame($inMemoryStorage, $registry->getStorageForOrchestrator($orchestrator->reveal()));
  }

  /**
   * @covers ::getStorageForOrchestrator
   */
  public function testGetStorageForOrchestratorThrowsWhenPreferredBundleNotRegistered(): void {
    $registry = new PipelineStorageRegistry(['default' => $this->prophesize(PipelineStorageInterface::class)->reveal()]);

    $orchestrator = $this->prophesize(OrchestratorInterface::class)
      ->willImplement(PipelineAwareOrchestratorInterface::class);
    $orchestrator->getPreferredPipelineBundle()->willReturn('redis');

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage("No pipeline storage registered for bundle 'redis'");

    $registry->getStorageForOrchestrator($orchestrator->reveal());
  }

}
