<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Integration;

use Prophecy\Argument;
use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_stategraph\DTO\GraphState;
use Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState;
use Drupal\flowdrop_stategraph\DTO\StateUpdate;
use Drupal\flowdrop_stategraph\Exception\IteratorExhaustedException;
use Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper;
use Drupal\flowdrop_stategraph\Reducer\AppendReducer;
use Drupal\flowdrop_stategraph\Reducer\MergeReducer;
use Drupal\flowdrop_stategraph\Reducer\ReplaceReducer;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Integration tests for iterator workflow functionality.
 *
 * @group flowdrop_stategraph
 */
class StateGraphIteratorTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The iterator helper under test.
   *
   * @var \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper
   */
  private ArrayIteratorHelper $iteratorHelper;

  /**
   * Map of reducer ID to reducer instance.
   *
   * @var array<string, \Drupal\flowdrop_stategraph\Reducer\ReducerInterface>
   */
  private array $reducers;

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

    // Note: For void methods, just stub without willReturn().
    $logger = $this->prophesize(LoggerInterface::class);
    $logger->debug(Argument::cetera());

    $this->iteratorHelper = new ArrayIteratorHelper($logger->reveal());

    // Initialize reducers.
    $appendReducer = new AppendReducer();
    $replaceReducer = new ReplaceReducer();
    $mergeReducer = new MergeReducer();

    $this->reducers = [
      $appendReducer->getId() => $appendReducer,
      $replaceReducer->getId() => $replaceReducer,
      $mergeReducer->getId() => $mergeReducer,
    ];
  }

  /**
   * Tests iterator initialization with items.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::initialize
   */
  public function testIteratorStartInitializesState(): void {
    $items = ["item1", "item2", "item3"];

    $iterator = $this->iteratorHelper->initialize($items);

    $this->assertEquals($items, $iterator->items);
    $this->assertEquals(0, $iterator->currentIndex);
    $this->assertEquals("item1", $iterator->currentItem);
    $this->assertTrue($iterator->hasMore);
    $this->assertEquals([], $iterator->results);
  }

  /**
   * Tests iterator advances index correctly.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::advance
   */
  public function testIteratorNextAdvancesIndex(): void {
    $iterator = $this->iteratorHelper->initialize(["a", "b", "c"]);

    $advanced = $this->iteratorHelper->advance($iterator);

    $this->assertEquals(1, $advanced->currentIndex);
    // Original unchanged.
    $this->assertEquals(0, $iterator->currentIndex);
  }

  /**
   * Tests iterator sets current item on advance.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::advance
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::getCurrentItem
   */
  public function testIteratorNextSetsCurrentItem(): void {
    $iterator = $this->iteratorHelper->initialize(["first", "second", "third"]);

    $this->assertEquals("first", $this->iteratorHelper->getCurrentItem($iterator));

    $iterator = $this->iteratorHelper->advance($iterator);
    $this->assertEquals("second", $this->iteratorHelper->getCurrentItem($iterator));

    $iterator = $this->iteratorHelper->advance($iterator);
    $this->assertEquals("third", $this->iteratorHelper->getCurrentItem($iterator));
  }

  /**
   * Tests iterator sets hasMore false at end.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::hasMore
   */
  public function testIteratorNextSetsHasMoreFalseAtEnd(): void {
    $iterator = $this->iteratorHelper->initialize(["a", "b"]);

    // At first item, hasMore is true (more items after current).
    $this->assertTrue($this->iteratorHelper->hasMore($iterator));

    // Advance to second (last) item.
    $iterator = $this->iteratorHelper->advance($iterator);

    // hasMore should now be false (no more items after current).
    $this->assertFalse($this->iteratorHelper->hasMore($iterator));
  }

  /**
   * Tests iterator collects results.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::addResult
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::getResults
   */
  public function testIteratorCollectAggregatesResults(): void {
    $iterator = $this->iteratorHelper->initialize(["a", "b", "c"]);
    $allResults = [];

    // Process each item.
    while (!$iterator->isExhausted()) {
      $item = $this->iteratorHelper->getCurrentItem($iterator);
      $result = "processed_{$item}";
      $allResults[] = $result;
      $iterator = $this->iteratorHelper->addResult($iterator, $result);
      $iterator = $this->iteratorHelper->advance($iterator);
    }

    $this->assertEquals(
      ["processed_a", "processed_b", "processed_c"],
      $this->iteratorHelper->getResults($iterator)
    );
  }

  /**
   * Tests iterator loop with conditional edge evaluation.
   *
   * Simulates how the orchestrator would use iterator state with
   * conditional edges for loop control.
   */
  public function testIteratorLoopWithConditionalEdge(): void {
    $items = [1, 2, 3, 4, 5];
    $processedResults = [];

    // Create initial state with iterator.
    $iterator = $this->iteratorHelper->initialize($items);
    $state = new GraphState(
      threadId: "thread_loop",
      iterator: $iterator,
    );

    // Simulate loop: process while hasMore is true.
    $loopCount = 0;
    $maxLoops = 10;

    while ($this->iteratorHelper->hasMore($state->iterator) && $loopCount < $maxLoops) {
      // Process current item.
      $currentItem = $this->iteratorHelper->getCurrentItem($state->iterator);
      $result = $currentItem * 2;
      $processedResults[] = $result;

      // Update iterator state.
      $newIterator = $this->iteratorHelper->addResult($state->iterator, $result);
      $newIterator = $this->iteratorHelper->advance($newIterator);

      // Update state with new iterator.
      $state = $state->withUpdate(
        StateUpdate::create(["iterator" => $newIterator]),
        $this->reducers
      );

      $loopCount++;
    }

    // Process the last item (when hasMore becomes false).
    if (!$state->iterator->isExhausted()) {
      $currentItem = $this->iteratorHelper->getCurrentItem($state->iterator);
      $result = $currentItem * 2;
      $processedResults[] = $result;

      $newIterator = $this->iteratorHelper->addResult($state->iterator, $result);
      $state = $state->withUpdate(
        StateUpdate::create(["iterator" => $newIterator]),
        $this->reducers
      );
    }

    $this->assertEquals([2, 4, 6, 8, 10], $processedResults);
    $this->assertEquals(4, $loopCount);
  }

  /**
   * Tests iterator handles empty array.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::initialize
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::hasMore
   */
  public function testIteratorWithEmptyArray(): void {
    $iterator = $this->iteratorHelper->initialize([]);

    $this->assertEquals([], $iterator->items);
    $this->assertEquals(0, $iterator->currentIndex);
    $this->assertNull($iterator->currentItem);
    $this->assertFalse($iterator->hasMore);
    $this->assertFalse($this->iteratorHelper->hasMore($iterator));
  }

  /**
   * Tests iterator throws when advancing past end.
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::advance
   */
  public function testIteratorThrowsWhenExhausted(): void {
    $iterator = $this->iteratorHelper->initialize(["single"]);

    // Advance past the only item.
    $iterator = $this->iteratorHelper->advance($iterator);

    // Further advance should throw.
    $this->expectException(IteratorExhaustedException::class);
    $this->iteratorHelper->advance($iterator);
  }

  /**
   * Tests iterator with complex items (arrays/objects).
   *
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::initialize
   * @covers \Drupal\flowdrop_stategraph\Iterator\ArrayIteratorHelper::getCurrentItem
   */
  public function testIteratorWithComplexItems(): void {
    $items = [
      ["id" => 1, "name" => "First"],
      ["id" => 2, "name" => "Second"],
      ["id" => 3, "name" => "Third"],
    ];

    $iterator = $this->iteratorHelper->initialize($items);

    $current = $this->iteratorHelper->getCurrentItem($iterator);
    $this->assertEquals(["id" => 1, "name" => "First"], $current);

    $iterator = $this->iteratorHelper->advance($iterator);
    $current = $this->iteratorHelper->getCurrentItem($iterator);
    $this->assertEquals(["id" => 2, "name" => "Second"], $current);
  }

  /**
   * Tests complete iteration workflow integration.
   *
   * This test simulates a complete iteration workflow as it would
   * work within the StateGraph orchestrator.
   */
  public function testCompleteIterationWorkflow(): void {
    // Initial input data containing items to process.
    $inputData = [
      "users" => [
        ["id" => 1, "email" => "user1@example.com"],
        ["id" => 2, "email" => "user2@example.com"],
        ["id" => 3, "email" => "user3@example.com"],
      ],
    ];

    // Step 1: Initialize iterator from input data.
    $iterator = $this->iteratorHelper->initialize($inputData["users"]);

    $state = new GraphState(
      threadId: "iteration_workflow",
      data: $inputData,
      iterator: $iterator,
    );

    // Step 2: Process each item in a loop.
    $processedEmails = [];
    $iterations = 0;

    while (!$state->iterator->isExhausted()) {
      $user = $this->iteratorHelper->getCurrentItem($state->iterator);

      // Simulate processing (e.g., sending email).
      $result = [
        "user_id" => $user["id"],
        "email" => $user["email"],
        "status" => "sent",
      ];
      $processedEmails[] = $result;

      // Update iterator with result and advance.
      $newIterator = $this->iteratorHelper->addResult($state->iterator, $result);
      $newIterator = $this->iteratorHelper->advance($newIterator);

      // Update state.
      $state = $state->withUpdate(
        StateUpdate::create([
          "iterator" => $newIterator,
          "data" => [
            "last_processed" => $user["id"],
          ],
        ]),
        $this->reducers
      );

      $iterations++;
    }

    // Verify all items processed.
    $this->assertEquals(3, $iterations);
    $this->assertCount(3, $processedEmails);

    // Verify results collected in iterator.
    $this->assertCount(3, $state->iterator->results);

    // Verify state data was updated.
    $this->assertEquals(3, $state->data["last_processed"]);

    // Verify iterator is exhausted.
    $this->assertTrue($state->iterator->isExhausted());
  }

  /**
   * Tests iterator preserves state through serialization.
   */
  public function testIteratorStateSerializationRoundTrip(): void {
    $iterator = $this->iteratorHelper->initialize(["a", "b", "c"]);
    $iterator = $this->iteratorHelper->advance($iterator);
    $iterator = $this->iteratorHelper->addResult($iterator, "result_a");

    // Serialize and deserialize (as would happen with checkpointing).
    $array = $iterator->toArray();
    $restored = IteratorState::fromArray($array);

    // Verify restored state matches.
    $this->assertEquals($iterator->items, $restored->items);
    $this->assertEquals($iterator->currentIndex, $restored->currentIndex);
    $this->assertEquals($iterator->currentItem, $restored->currentItem);
    $this->assertEquals($iterator->hasMore, $restored->hasMore);
    $this->assertEquals($iterator->results, $restored->results);

    // Can continue iteration after restore.
    $advanced = $this->iteratorHelper->advance($restored);
    $this->assertEquals(2, $advanced->currentIndex);
    $this->assertEquals("c", $advanced->currentItem);
  }

  /**
   * Tests iterator within GraphState serialization.
   */
  public function testIteratorInGraphStateSerialization(): void {
    $iterator = $this->iteratorHelper->initialize([1, 2, 3]);
    $iterator = $this->iteratorHelper->advance($iterator);

    $state = new GraphState(
      threadId: "thread_serialize",
      data: ["key" => "value"],
      iterator: $iterator,
      iterationCount: 5,
    );

    // Full round-trip through GraphState serialization.
    $array = $state->toArray();
    $restored = GraphState::fromArray($array);

    // Verify iterator was preserved.
    $this->assertInstanceOf(IteratorState::class, $restored->iterator);
    $this->assertEquals(1, $restored->iterator->currentIndex);
    $this->assertEquals(2, $restored->iterator->currentItem);
    $this->assertEquals([1, 2, 3], $restored->iterator->items);
  }

}
