<?php

declare(strict_types=1);

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

use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop_stategraph\Plugin\FlowDropNodeProcessor\ForEachNode;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the ForEachNode processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\Plugin\FlowDropNodeProcessor\ForEachNode
 * @group flowdrop_stategraph
 */
class ForEachNodeTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The ForEachNode processor under test.
   *
   * @var \Drupal\flowdrop_stategraph\Plugin\FlowDropNodeProcessor\ForEachNode
   */
  private ForEachNode $node;

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

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

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsWithItemsPasses(): void {
    $result = $this->node->validateParams(['items' => [1, 2, 3]]);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsWithIteratorPasses(): void {
    $result = $this->node->validateParams(['__iterator__' => ['items' => [1]]]);
    $this->assertTrue($result->isValid());
  }

  /**
   * @covers ::process
   */
  public function testInitializeEmptyArray(): void {
    $params = $this->createParams(['items' => []]);

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

    $this->assertFalse($output['loop_active']);
    $this->assertNull($output['current_item']);
    $this->assertEquals(0, $output['total_count']);
    $this->assertEquals([], $output['collected_results']);
    $this->assertNull($output['state_update']['iterator']);
  }

  /**
   * @covers ::process
   */
  public function testInitializeSingleItem(): void {
    $params = $this->createParams(['items' => ['only']]);

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

    $this->assertTrue($output['loop_active']);
    $this->assertEquals('only', $output['current_item']);
    $this->assertEquals(0, $output['current_index']);
    $this->assertTrue($output['is_first']);
    $this->assertTrue($output['is_last']);
    $this->assertFalse($output['has_more']);
    $this->assertEquals(1, $output['total_count']);
  }

  /**
   * @covers ::process
   */
  public function testInitializeMultipleItems(): void {
    $params = $this->createParams(['items' => ['a', 'b', 'c']]);

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

    $this->assertTrue($output['loop_active']);
    $this->assertEquals('a', $output['current_item']);
    $this->assertEquals(0, $output['current_index']);
    $this->assertTrue($output['is_first']);
    $this->assertFalse($output['is_last']);
    $this->assertTrue($output['has_more']);
    $this->assertEquals(3, $output['total_count']);
    $this->assertNotNull($output['state_update']['iterator']);
  }

  /**
   * @covers ::process
   */
  public function testInitializeNonArrayWrapped(): void {
    $params = $this->createParams(['items' => 'scalar_value']);

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

    $this->assertTrue($output['loop_active']);
    $this->assertEquals('scalar_value', $output['current_item']);
    $this->assertEquals(1, $output['total_count']);
    $this->assertTrue($output['is_first']);
    $this->assertTrue($output['is_last']);
  }

  /**
   * @covers ::process
   */
  public function testContinueAdvanceToNextItem(): void {
    // First initialize to get iterator state.
    $initParams = $this->createParams(['items' => ['a', 'b', 'c']]);
    $initOutput = $this->node->process($initParams->reveal());

    // Continue with iterator state.
    $continueParams = $this->createParams([
      '__iterator__' => $initOutput['state_update']['iterator'],
      'item_result' => 'result_a',
    ]);

    $output = $this->node->process($continueParams->reveal());

    $this->assertTrue($output['loop_active']);
    $this->assertEquals('b', $output['current_item']);
    $this->assertEquals(1, $output['current_index']);
    $this->assertFalse($output['is_first']);
    $this->assertFalse($output['is_last']);
    $this->assertTrue($output['has_more']);
  }

  /**
   * @covers ::process
   */
  public function testContinueLastItemCompletesLoop(): void {
    // Initialize with 2 items.
    $initParams = $this->createParams(['items' => ['a', 'b']]);
    $initOutput = $this->node->process($initParams->reveal());

    // Advance to b (second item).
    $step1Params = $this->createParams([
      '__iterator__' => $initOutput['state_update']['iterator'],
      'item_result' => 'result_a',
    ]);
    $step1Output = $this->node->process($step1Params->reveal());

    // Now advance past b - this should complete the loop.
    $step2Params = $this->createParams([
      '__iterator__' => $step1Output['state_update']['iterator'],
      'item_result' => 'result_b',
    ]);
    $output = $this->node->process($step2Params->reveal());

    $this->assertFalse($output['loop_active']);
    $this->assertFalse($output['has_more']);
    $this->assertNull($output['current_item']);
    $this->assertEquals(['result_a', 'result_b'], $output['collected_results']);
    $this->assertNull($output['state_update']['iterator']);
  }

  /**
   * @covers ::process
   */
  public function testLoopBackPortUsedAsItemResult(): void {
    $initParams = $this->createParams(['items' => ['a', 'b']]);
    $initOutput = $this->node->process($initParams->reveal());

    // Use loop_back instead of item_result.
    $params = $this->createParams([
      '__iterator__' => $initOutput['state_update']['iterator'],
      'loop_back' => 'loopback_result',
    ]);

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

    // Should advance and collect the loopback result.
    $this->assertEquals('b', $output['current_item']);
  }

  /**
   * @covers ::process
   */
  public function testStateUpdateContainsIteratorArray(): void {
    $params = $this->createParams(['items' => ['x', 'y']]);

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

    $this->assertIsArray($output['state_update']['iterator']);
    $this->assertArrayHasKey('items', $output['state_update']['iterator']);
    $this->assertArrayHasKey('currentIndex', $output['state_update']['iterator']);
  }

  /**
   * @covers ::process
   */
  public function testInitializeNullItems(): void {
    $params = $this->createParams(['items' => NULL]);

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

    $this->assertFalse($output['loop_active']);
    $this->assertEquals(0, $output['total_count']);
  }

  /**
   * Creates a mock ParameterBagInterface with the given data.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\flowdrop\DTO\ParameterBagInterface>
   *   The mock parameter bag.
   */
  private function createParams(array $data) {
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->get('items')->willReturn($data['items'] ?? NULL);
    $params->get('__iterator__')->willReturn($data['__iterator__'] ?? NULL);
    $params->get('item_result')->willReturn($data['item_result'] ?? NULL);
    $params->get('loop_back')->willReturn($data['loop_back'] ?? NULL);
    return $params;
  }

}
