<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_memory\Unit\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\flowdrop_memory\Exception\MemoryBackendException;
use Drupal\flowdrop_memory\Plugin\MemoryBackend\MemoryBackendInterface;
use Drupal\flowdrop_memory\Service\MemoryBackendPluginManager;
use Drupal\flowdrop_memory\Service\MemoryManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the MemoryManager service.
 *
 * @coversDefaultClass \Drupal\flowdrop_memory\Service\MemoryManager
 * @group flowdrop_memory
 */
class MemoryManagerTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The plugin manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_memory\Service\MemoryBackendPluginManager
   */
  private $pluginManager;

  /**
   * The config factory mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ConfigFactoryInterface
   */
  private $configFactory;

  /**
   * The settings config mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ImmutableConfig
   */
  private $config;

  /**
   * A backend mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_memory\Plugin\MemoryBackend\MemoryBackendInterface
   */
  private $backend;

  /**
   * The manager under test.
   *
   * @var \Drupal\flowdrop_memory\Service\MemoryManager
   */
  private MemoryManager $manager;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->pluginManager = $this->prophesize(MemoryBackendPluginManager::class);
    $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
    $this->config = $this->prophesize(ImmutableConfig::class);
    $this->backend = $this->prophesize(MemoryBackendInterface::class);

    $this->configFactory->get('flowdrop_memory.settings')
      ->willReturn($this->config->reveal());

    $this->manager = new MemoryManager(
      $this->pluginManager->reveal(),
      $this->configFactory->reveal(),
    );
  }

  /**
   * @covers ::getBackend
   */
  public function testGetBackendUsesProvidedBackendId(): void {
    $this->config->get('backend_config.entity')->willReturn(NULL);
    $this->pluginManager->createInstance('entity', [])->willReturn($this->backend->reveal());

    $backend = $this->manager->getBackend('entity');

    $this->assertSame($this->backend->reveal(), $backend);
  }

  /**
   * @covers ::getBackend
   */
  public function testGetBackendDefaultsToStatic(): void {
    $this->config->get('backend_config.static')->willReturn(NULL);
    $this->pluginManager->createInstance('static', [])->willReturn($this->backend->reveal());

    $backend = $this->manager->getBackend();

    $this->assertSame($this->backend->reveal(), $backend);
  }

  /**
   * @covers ::getBackend
   */
  public function testGetBackendPassesBackendConfigToPlugin(): void {
    $this->config->get('backend_config.cached')->willReturn(['cache_bin' => 'render']);
    $this->pluginManager->createInstance('cached', ['cache_bin' => 'render'])
      ->willReturn($this->backend->reveal());

    $backend = $this->manager->getBackend('cached');

    $this->assertSame($this->backend->reveal(), $backend);
  }

  /**
   * @covers ::getBackend
   */
  public function testGetBackendCachesInstancePerBackendId(): void {
    $this->config->get('backend_config.static')->willReturn(NULL);
    $this->pluginManager->createInstance('static', [])
      ->willReturn($this->backend->reveal())
      ->shouldBeCalledOnce();

    $first = $this->manager->getBackend('static');
    $second = $this->manager->getBackend('static');

    $this->assertSame($first, $second);
  }

  /**
   * @covers ::getBackend
   */
  public function testGetBackendCreatesNewInstanceForDifferentBackendIds(): void {
    $backendA = $this->prophesize(MemoryBackendInterface::class);
    $backendB = $this->prophesize(MemoryBackendInterface::class);

    $this->config->get('backend_config.static')->willReturn(NULL);
    $this->config->get('backend_config.entity')->willReturn(NULL);

    $this->pluginManager->createInstance('static', [])->willReturn($backendA->reveal());
    $this->pluginManager->createInstance('entity', [])->willReturn($backendB->reveal());

    $static = $this->manager->getBackend('static');
    $entity = $this->manager->getBackend('entity');

    $this->assertNotSame($static, $entity);
    $this->assertInstanceOf(MemoryBackendInterface::class, $static);
    $this->assertInstanceOf(MemoryBackendInterface::class, $entity);
  }

  /**
   * @covers ::get
   */
  public function testGetProxiesToBackend(): void {
    $this->setupBackend('static');
    $this->backend->get('global', 'scope-1', 'key', NULL)->willReturn('value');

    $result = $this->manager->get('global', 'scope-1', 'key', NULL, 'static');

    $this->assertSame('value', $result);
  }

  /**
   * @covers ::get
   */
  public function testGetPassesDefaultToBackend(): void {
    $this->setupBackend('static');
    $this->backend->get('global', '', 'missing', 'fallback')->willReturn('fallback');

    $result = $this->manager->get('global', '', 'missing', 'fallback', 'static');

    $this->assertSame('fallback', $result);
  }

  /**
   * @covers ::set
   */
  public function testSetProxiesToBackendWithoutTtl(): void {
    $this->setupBackend('static');
    $this->backend->set('global', '', 'key', 'val', NULL)->shouldBeCalled();

    $this->manager->set('global', '', 'key', 'val', NULL, 'static');
  }

  /**
   * @covers ::set
   */
  public function testSetProxiesToBackendWithTtl(): void {
    $this->setupBackend('static');
    $this->backend->set('global', '', 'key', 'val', 300)->shouldBeCalled();

    $this->manager->set('global', '', 'key', 'val', 300, 'static');
  }

  /**
   * @covers ::set
   */
  public function testSetThrowsWhenValueExceedsMaxBytes(): void {
    $this->expectException(MemoryBackendException::class);
    $this->expectExceptionMessageMatches('/exceeds the maximum allowed size/');

    $this->backend->set(Argument::cetera())->shouldNotBeCalled();

    $oversized = str_repeat('x', MemoryManager::MAX_VALUE_BYTES + 1);

    $this->setupBackend('static');
    $this->manager->set('global', '', 'key', $oversized, NULL, 'static');
  }

  /**
   * @covers ::set
   */
  public function testSetAcceptsValueAtExactMaxBytes(): void {
    $value = str_repeat('x', MemoryManager::MAX_VALUE_BYTES - 2);

    $this->setupBackend('static');
    $this->backend->set('global', '', 'key', $value, NULL)->shouldBeCalled();

    $this->manager->set('global', '', 'key', $value, NULL, 'static');
  }

  /**
   * @covers ::delete
   */
  public function testDeleteProxiesToBackend(): void {
    $this->setupBackend('static');
    $this->backend->delete('global', '', 'key')->shouldBeCalled();

    $this->manager->delete('global', '', 'key', 'static');
  }

  /**
   * @covers ::has
   */
  public function testHasProxiesToBackendReturningTrue(): void {
    $this->setupBackend('static');
    $this->backend->has('global', '', 'key')->willReturn(TRUE);

    $this->assertTrue($this->manager->has('global', '', 'key', 'static'));
  }

  /**
   * @covers ::has
   */
  public function testHasProxiesToBackendReturningFalse(): void {
    $this->setupBackend('static');
    $this->backend->has('global', '', 'key')->willReturn(FALSE);

    $this->assertFalse($this->manager->has('global', '', 'key', 'static'));
  }

  /**
   * @covers ::keys
   */
  public function testKeysProxiesToBackend(): void {
    $this->setupBackend('static');
    $this->backend->keys('global', '')->willReturn(['alpha', 'beta']);

    $this->assertSame(['alpha', 'beta'], $this->manager->keys('global', '', 'static'));
  }

  /**
   * @covers ::clearScope
   */
  public function testClearScopeProxiesToBackend(): void {
    $this->setupBackend('static');
    $this->backend->clearScope('global', '')->shouldBeCalled();

    $this->manager->clearScope('global', '', 'static');
  }

  /**
   * Sets up the backend mock for a given backend plugin ID.
   *
   * @param string $backendId
   *   The backend plugin ID.
   */
  private function setupBackend(string $backendId): void {
    $this->config->get('backend_config.' . $backendId)->willReturn(NULL);
    $this->pluginManager->createInstance($backendId, [])
      ->willReturn($this->backend->reveal());
  }

}
