<?php

declare(strict_types=1);

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

use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop_memory\Plugin\FlowDropNodeProcessor\MemoryWriteRaw;
use Drupal\flowdrop_memory\Service\MemoryManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the MemoryWriteRaw node processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_memory\Plugin\FlowDropNodeProcessor\MemoryWriteRaw
 * @group flowdrop_memory
 */
class MemoryWriteRawTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The memory manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_memory\Service\MemoryManager
   */
  private $memoryManager;

  /**
   * The processor under test.
   *
   * @var \Drupal\flowdrop_memory\Plugin\FlowDropNodeProcessor\MemoryWriteRaw
   */
  private MemoryWriteRaw $processor;

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

    $this->memoryManager = $this->prophesize(MemoryManager::class);
    $this->processor = new MemoryWriteRaw(
      [],
      'memory_write_raw',
      ['id' => 'memory_write_raw'],
      $this->memoryManager->reveal(),
    );
  }

  /**
   * Stubs a ParameterBagInterface prophecy.
   */
  private function stubParams(string $scope, string $scopeId, string $key, mixed $value, int $ttl = 0) {
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getString('scope')->willReturn($scope);
    $params->getString('scope_id', '')->willReturn($scopeId);
    $params->getString('key')->willReturn($key);
    $params->get('value')->willReturn($value);
    $params->getInt('ttl', 0)->willReturn($ttl);
    $params->getString('backend', 'static')->willReturn('static');
    return $params;
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenAllParamsMissing(): void {
    $result = $this->processor->validateParams([]);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenScopeMissing(): void {
    $result = $this->processor->validateParams(['key' => 'k', 'value' => 'v']);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenScopeIsEmpty(): void {
    $result = $this->processor->validateParams(['scope' => '', 'key' => 'k', 'value' => 'v']);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenKeyMissing(): void {
    $result = $this->processor->validateParams(['scope' => 'global', 'value' => 'v']);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenKeyIsEmpty(): void {
    $result = $this->processor->validateParams(['scope' => 'global', 'key' => '', 'value' => 'v']);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenValueKeyAbsent(): void {
    $result = $this->processor->validateParams(['scope' => 'global', 'key' => 'k']);
    $this->assertFalse($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsSucceedsWithNullValue(): void {
    // NULL value is valid as long as the 'value' key is present.
    $result = $this->processor->validateParams(['scope' => 'global', 'key' => 'k', 'value' => NULL]);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsSucceedsWithAllRequired(): void {
    $result = $this->processor->validateParams(['scope' => 'global', 'key' => 'k', 'value' => 'v']);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::process
   */
  public function testProcessWritesWithExplicitScopeId(): void {
    $params = $this->stubParams('workflow', 'wf-explicit', 'key', 'value');
    $this->memoryManager->set('workflow', 'wf-explicit', 'key', 'value', NULL, 'static')->shouldBeCalled();

    $this->processor->process($params->reveal());
  }

  /**
   * @covers ::process
   */
  public function testProcessWritesWithEmptyScopeId(): void {
    $params = $this->stubParams('global', '', 'key', 'value');
    $this->memoryManager->set('global', '', 'key', 'value', NULL, 'static')->shouldBeCalled();

    $this->processor->process($params->reveal());
  }

  /**
   * @covers ::process
   */
  public function testProcessPassesTtlToBackendWhenPositive(): void {
    $params = $this->stubParams('global', '', 'key', 'val', 600);
    $this->memoryManager->set('global', '', 'key', 'val', 600, 'static')->shouldBeCalled();

    $this->processor->process($params->reveal());
  }

  /**
   * @covers ::process
   */
  public function testProcessPassesNullTtlWhenZero(): void {
    $params = $this->stubParams('global', '', 'key', 'val', 0);
    $this->memoryManager->set('global', '', 'key', 'val', NULL, 'static')->shouldBeCalled();

    $this->processor->process($params->reveal());
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsTrueForSuccess(): void {
    $params = $this->stubParams('global', '', 'key', 'value');
    $this->memoryManager->set('global', '', 'key', 'value', NULL, 'static')->shouldBeCalled();

    $output = $this->processor->process($params->reveal());

    $this->assertTrue($output['success']);
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsScopeAndKeyInOutput(): void {
    $params = $this->stubParams('custom', 'id-1', 'my_key', 'my_value');
    $this->memoryManager->set('custom', 'id-1', 'my_key', 'my_value', NULL, 'static')->shouldBeCalled();

    $output = $this->processor->process($params->reveal());

    $this->assertSame('custom', $output['scope']);
    $this->assertSame('my_key', $output['key']);
  }

  /**
   * @covers ::process
   */
  public function testProcessDoesNotExposeResolvedScopeId(): void {
    // MemoryWriteRaw does not auto-resolve scope_id from context.
    $params = $this->stubParams('global', '', 'key', 'val');
    $this->memoryManager->set('global', '', 'key', 'val', NULL, 'static')->shouldBeCalled();

    $output = $this->processor->process($params->reveal());

    $this->assertArrayNotHasKey('resolved_scope_id', $output);
  }

  /**
   * @covers ::process
   */
  public function testProcessExposesAllRequiredOutputKeys(): void {
    $params = $this->stubParams('global', '', 'k', 'v');
    $this->memoryManager->set('global', '', 'k', 'v', NULL, 'static')->shouldBeCalled();

    $output = $this->processor->process($params->reveal());

    $this->assertArrayHasKey('success', $output);
    $this->assertArrayHasKey('key', $output);
    $this->assertArrayHasKey('scope', $output);
  }

}
