<?php

declare(strict_types=1);

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

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

/**
 * Tests the MemoryRead node processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_memory\Plugin\FlowDropNodeProcessor\MemoryRead
 * @group flowdrop_memory
 */
class MemoryReadTest 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\MemoryRead
   */
  private MemoryRead $processor;

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

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

  /**
   * Builds an ExecutionContextDTO for testing.
   */
  private function makeContext(
    string $workflowId = 'wf-id',
    string $pipelineId = 'pl-id',
    string $executionId = 'ex-id',
    array $metadata = [],
  ): ExecutionContextDTO {
    return new ExecutionContextDTO([], $workflowId, $pipelineId, $executionId, 'node-id', $metadata);
  }

  /**
   * Stubs a ParameterBagInterface prophecy.
   *
   * @param string $scope
   *   The scope string.
   * @param string $key
   *   The key string.
   * @param mixed $default
   *   The default value.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy
   *   The params prophecy.
   */
  private function stubParams(string $scope, string $key, mixed $default = NULL) {
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getString('scope', 'global')->willReturn($scope);
    $params->getString('key')->willReturn($key);
    $params->get('default_value')->willReturn($default);
    $params->getString('backend', 'static')->willReturn('static');
    return $params;
  }

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

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

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsSucceedsWithKey(): void {
    $result = $this->processor->validateParams(['key' => 'my_key']);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsSucceedsWithKeyAndOptionalScope(): void {
    $result = $this->processor->validateParams(['key' => 'my_key', 'scope' => 'workflow']);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsValueFromMemory(): void {
    $params = $this->stubParams('global', 'counter');
    $this->memoryManager->get('global', '', 'counter', NULL, 'static')->willReturn(42);
    $this->memoryManager->has('global', '', 'counter', 'static')->willReturn(TRUE);

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

    $this->assertSame(42, $output['value']);
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsDefaultWhenKeyNotFound(): void {
    $params = $this->stubParams('global', 'missing', 'default_val');
    $this->memoryManager->get('global', '', 'missing', 'default_val', 'static')->willReturn('default_val');
    $this->memoryManager->has('global', '', 'missing', 'static')->willReturn(FALSE);

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

    $this->assertSame('default_val', $output['value']);
    $this->assertFalse($output['found']);
  }

  /**
   * @covers ::process
   */
  public function testProcessSetFoundTrueWhenKeyExists(): void {
    $params = $this->stubParams('global', 'present');
    $this->memoryManager->get('global', '', 'present', NULL, 'static')->willReturn('yes');
    $this->memoryManager->has('global', '', 'present', 'static')->willReturn(TRUE);

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

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

  /**
   * @covers ::process
   */
  public function testProcessSetFoundFalseWhenKeyAbsent(): void {
    $params = $this->stubParams('global', 'absent');
    $this->memoryManager->get('global', '', 'absent', NULL, 'static')->willReturn(NULL);
    $this->memoryManager->has('global', '', 'absent', 'static')->willReturn(FALSE);

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

    $this->assertFalse($output['found']);
  }

  /**
   * @covers ::process
   */
  public function testProcessDefaultsToGlobalScope(): void {
    $params = $this->stubParams('global', 'key');
    $this->memoryManager->get('global', '', 'key', NULL, 'static')->willReturn('val');
    $this->memoryManager->has('global', '', 'key', 'static')->willReturn(TRUE);

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

    $this->assertSame('global', $output['scope']);
    $this->assertSame('', $output['resolved_scope_id']);
  }

  /**
   * @covers ::process
   */
  public function testProcessResolvesWorkflowScopeFromContext(): void {
    $this->processor->setExecutionContext($this->makeContext(workflowId: 'wf-42'));

    $params = $this->stubParams('workflow', 'key');
    $this->memoryManager->get('workflow', 'wf-42', 'key', NULL, 'static')->willReturn('wf-val');
    $this->memoryManager->has('workflow', 'wf-42', 'key', 'static')->willReturn(TRUE);

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

    $this->assertSame('workflow', $output['scope']);
    $this->assertSame('wf-42', $output['resolved_scope_id']);
  }

  /**
   * @covers ::process
   */
  public function testProcessResolvesPipelineScopeFromContext(): void {
    $this->processor->setExecutionContext($this->makeContext(pipelineId: 'pl-99'));

    $params = $this->stubParams('pipeline', 'key');
    $this->memoryManager->get('pipeline', 'pl-99', 'key', NULL, 'static')->willReturn('pl-val');
    $this->memoryManager->has('pipeline', 'pl-99', 'key', 'static')->willReturn(FALSE);

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

    $this->assertSame('pl-99', $output['resolved_scope_id']);
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsKeyInOutput(): void {
    $params = $this->stubParams('global', 'my_key');
    $this->memoryManager->get('global', '', 'my_key', NULL, 'static')->willReturn(NULL);
    $this->memoryManager->has('global', '', 'my_key', 'static')->willReturn(FALSE);

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

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

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

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

    $this->assertArrayHasKey('value', $output);
    $this->assertArrayHasKey('found', $output);
    $this->assertArrayHasKey('key', $output);
    $this->assertArrayHasKey('scope', $output);
    $this->assertArrayHasKey('resolved_scope_id', $output);
  }

}
