<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_memory\Unit\Plugin\MemoryBackend;

use Drupal\flowdrop_memory\Plugin\MemoryBackend\StaticMemoryBackend;
use Drupal\Tests\UnitTestCase;

/**
 * Tests the StaticMemoryBackend plugin.
 *
 * @coversDefaultClass \Drupal\flowdrop_memory\Plugin\MemoryBackend\StaticMemoryBackend
 * @group flowdrop_memory
 */
class StaticMemoryBackendTest extends UnitTestCase {

  /**
   * The backend under test.
   *
   * @var \Drupal\flowdrop_memory\Plugin\MemoryBackend\StaticMemoryBackend
   */
  private StaticMemoryBackend $backend;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->backend = new StaticMemoryBackend([], 'static', ['id' => 'static']);
  }

  /**
   * {@inheritdoc}
   */
  protected function tearDown(): void {
    parent::tearDown();
    // Reset the static store between tests to prevent interference.
    $prop = new \ReflectionProperty(StaticMemoryBackend::class, 'store');
    $prop->setAccessible(TRUE);
    $prop->setValue(NULL, []);
  }

  /**
   * @covers ::get
   */
  public function testGetReturnsNullDefaultWhenKeyNotSet(): void {
    $this->assertNull($this->backend->get('global', '', 'missing'));
  }

  /**
   * @covers ::get
   */
  public function testGetReturnsProvidedDefaultWhenKeyNotSet(): void {
    $result = $this->backend->get('global', '', 'missing', 'fallback');
    $this->assertSame('fallback', $result);
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testSetAndGetRoundtrip(): void {
    $this->backend->set('global', '', 'mykey', 'myvalue');
    $this->assertSame('myvalue', $this->backend->get('global', '', 'mykey'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testSetAndGetWithExplicitScopeId(): void {
    $this->backend->set('workflow', 'wf-123', 'counter', 42);
    $this->assertSame(42, $this->backend->get('workflow', 'wf-123', 'counter'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testSetStoresComplexValue(): void {
    $value = ['nested' => ['a' => 1, 'b' => [TRUE, FALSE, NULL]]];
    $this->backend->set('pipeline', 'p-1', 'data', $value);
    $this->assertSame($value, $this->backend->get('pipeline', 'p-1', 'data'));
  }

  /**
   * @covers ::set
   * @covers ::has
   */
  public function testSetNullValueIsStoredAndHasReturnsTrue(): void {
    $this->backend->set('global', '', 'nullval', NULL);
    $this->assertTrue($this->backend->has('global', '', 'nullval'));
    $this->assertNull($this->backend->get('global', '', 'nullval'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testOverwriteExistingKey(): void {
    $this->backend->set('global', '', 'counter', 1);
    $this->backend->set('global', '', 'counter', 2);
    $this->assertSame(2, $this->backend->get('global', '', 'counter'));
  }

  /**
   * @covers ::has
   */
  public function testHasReturnsFalseWhenKeyNotSet(): void {
    $this->assertFalse($this->backend->has('global', '', 'missing'));
  }

  /**
   * @covers ::set
   * @covers ::has
   */
  public function testHasReturnsTrueWhenKeySet(): void {
    $this->backend->set('global', '', 'present', 'yes');
    $this->assertTrue($this->backend->has('global', '', 'present'));
  }

  /**
   * @covers ::delete
   * @covers ::has
   */
  public function testDeleteRemovesKey(): void {
    $this->backend->set('global', '', 'toDelete', 'gone');
    $this->backend->delete('global', '', 'toDelete');
    $this->assertFalse($this->backend->has('global', '', 'toDelete'));
  }

  /**
   * @covers ::delete
   */
  public function testDeleteNonExistentKeyIsNoOp(): void {
    // Must not throw any exception.
    $this->backend->delete('global', '', 'nonexistent');
    $this->assertFalse($this->backend->has('global', '', 'nonexistent'));
  }

  /**
   * @covers ::keys
   */
  public function testKeysReturnsEmptyArrayForUnpopulatedScope(): void {
    $this->assertSame([], $this->backend->keys('global', ''));
  }

  /**
   * @covers ::set
   * @covers ::keys
   */
  public function testKeysReturnsAllKeysInScope(): void {
    $this->backend->set('global', '', 'alpha', 1);
    $this->backend->set('global', '', 'beta', 2);
    $keys = $this->backend->keys('global', '');
    sort($keys);
    $this->assertSame(['alpha', 'beta'], $keys);
  }

  /**
   * @covers ::set
   * @covers ::keys
   */
  public function testKeysDoesNotReturnKeysFromOtherScopes(): void {
    $this->backend->set('global', '', 'a', 1);
    $this->backend->set('pipeline', 'p-1', 'b', 2);
    $keys = $this->backend->keys('global', '');
    $this->assertSame(['a'], $keys);
  }

  /**
   * @covers ::set
   * @covers ::keys
   */
  public function testKeysDoesNotReturnKeysFromOtherScopeIds(): void {
    $this->backend->set('workflow', 'wf-aaa', 'k', 1);
    $this->backend->set('workflow', 'wf-bbb', 'k', 2);
    $keys = $this->backend->keys('workflow', 'wf-aaa');
    $this->assertSame(['k'], $keys);
  }

  /**
   * @covers ::set
   * @covers ::clearScope
   * @covers ::keys
   */
  public function testClearScopeRemovesAllKeysInScope(): void {
    $this->backend->set('execution', 'e-1', 'x', 1);
    $this->backend->set('execution', 'e-1', 'y', 2);
    $this->backend->clearScope('execution', 'e-1');
    $this->assertSame([], $this->backend->keys('execution', 'e-1'));
  }

  /**
   * @covers ::set
   * @covers ::clearScope
   * @covers ::has
   */
  public function testClearScopeDoesNotAffectOtherScopes(): void {
    $this->backend->set('execution', 'e-1', 'x', 1);
    $this->backend->set('global', '', 'z', 3);
    $this->backend->clearScope('execution', 'e-1');
    $this->assertTrue($this->backend->has('global', '', 'z'));
  }

  /**
   * @covers ::set
   * @covers ::clearScope
   * @covers ::has
   */
  public function testClearScopeDoesNotAffectSiblingScopes(): void {
    $this->backend->set('workflow', 'wf-1', 'key', 'val1');
    $this->backend->set('workflow', 'wf-2', 'key', 'val2');
    $this->backend->clearScope('workflow', 'wf-1');
    $this->assertTrue($this->backend->has('workflow', 'wf-2', 'key'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testTtlIsIgnoredDataStillRetrievable(): void {
    // StaticMemoryBackend silently ignores TTL.
    $this->backend->set('global', '', 'withTtl', 'value', 1);
    $this->assertSame('value', $this->backend->get('global', '', 'withTtl'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testEmptyScopeIdDoesNotClashWithNonEmptyScopeId(): void {
    // Empty scope_id uses "__global__" placeholder; must not clash with
    // an actual scope_id string "__global__" or any other value.
    $this->backend->set('global', '', 'key', 'global-val');
    $this->backend->set('global', 'other', 'key', 'scoped-val');
    $this->assertSame('global-val', $this->backend->get('global', '', 'key'));
    $this->assertSame('scoped-val', $this->backend->get('global', 'other', 'key'));
  }

  /**
   * @covers ::set
   * @covers ::get
   */
  public function testDifferentScopeIdsAreIsolated(): void {
    $this->backend->set('workflow', 'wf-aaa', 'key', 'value-aaa');
    $this->backend->set('workflow', 'wf-bbb', 'key', 'value-bbb');
    $this->assertSame('value-aaa', $this->backend->get('workflow', 'wf-aaa', 'key'));
    $this->assertSame('value-bbb', $this->backend->get('workflow', 'wf-bbb', 'key'));
  }

}
