<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Unit\DTO;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState;

/**
 * Tests the IteratorState DTO.
 *
 * @coversDefaultClass \Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState
 * @group flowdrop_stategraph
 */
class IteratorStateTest extends UnitTestCase {

  /**
   * Tests constructor with default values.
   *
   * @covers ::__construct
   */
  public function testConstructorDefaults(): void {
    $state = new IteratorState();

    $this->assertEquals([], $state->items);
    $this->assertEquals(0, $state->currentIndex);
    $this->assertNull($state->currentItem);
    $this->assertTrue($state->hasMore);
    $this->assertEquals([], $state->results);
  }

  /**
   * Tests constructor with custom values.
   *
   * @covers ::__construct
   */
  public function testConstructorWithValues(): void {
    $items = ["a", "b", "c"];
    $results = ["result1", "result2"];

    $state = new IteratorState(
      items: $items,
      currentIndex: 1,
      currentItem: "b",
      hasMore: TRUE,
      results: $results,
    );

    $this->assertEquals($items, $state->items);
    $this->assertEquals(1, $state->currentIndex);
    $this->assertEquals("b", $state->currentItem);
    $this->assertTrue($state->hasMore);
    $this->assertEquals($results, $state->results);
  }

  /**
   * Tests fromItems factory with non-empty array.
   *
   * @covers ::fromItems
   */
  public function testFromItemsNonEmpty(): void {
    $items = ["first", "second", "third"];

    $state = IteratorState::fromItems($items);

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

  /**
   * Tests fromItems with single item.
   *
   * @covers ::fromItems
   */
  public function testFromItemsSingleItem(): void {
    $state = IteratorState::fromItems(["only"]);

    $this->assertEquals(["only"], $state->items);
    $this->assertEquals(0, $state->currentIndex);
    $this->assertEquals("only", $state->currentItem);
    $this->assertFalse($state->hasMore);
    $this->assertEquals([], $state->results);
  }

  /**
   * Tests fromItems with empty array.
   *
   * @covers ::fromItems
   */
  public function testFromItemsEmpty(): void {
    $state = IteratorState::fromItems([]);

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

  /**
   * Tests fromItems reindexes associative arrays.
   *
   * @covers ::fromItems
   */
  public function testFromItemsReindexes(): void {
    $items = ["a" => "first", "b" => "second"];

    $state = IteratorState::fromItems($items);

    // Array values should be reindexed.
    $this->assertEquals(["first", "second"], $state->items);
    $this->assertEquals("first", $state->currentItem);
  }

  /**
   * Tests advance moves to next item.
   *
   * @covers ::advance
   */
  public function testAdvance(): void {
    $state = IteratorState::fromItems(["a", "b", "c"]);

    $advanced = $state->advance();

    // Original unchanged.
    $this->assertEquals(0, $state->currentIndex);
    $this->assertEquals("a", $state->currentItem);

    // Advanced state.
    $this->assertEquals(1, $advanced->currentIndex);
    $this->assertEquals("b", $advanced->currentItem);
    $this->assertTrue($advanced->hasMore);
  }

  /**
   * Tests advance to last item sets hasMore false.
   *
   * @covers ::advance
   */
  public function testAdvanceToLastItem(): void {
    $state = IteratorState::fromItems(["a", "b", "c"]);

    // Advance twice to reach last item.
    $advanced = $state->advance()->advance();

    $this->assertEquals(2, $advanced->currentIndex);
    $this->assertEquals("c", $advanced->currentItem);
    $this->assertFalse($advanced->hasMore);
  }

  /**
   * Tests advance past end marks exhausted.
   *
   * @covers ::advance
   */
  public function testAdvancePastEnd(): void {
    $state = IteratorState::fromItems(["a", "b"]);

    // Advance past end.
    $exhausted = $state->advance()->advance()->advance();

    $this->assertEquals(3, $exhausted->currentIndex);
    $this->assertNull($exhausted->currentItem);
    $this->assertFalse($exhausted->hasMore);
  }

  /**
   * Tests addResult accumulates results.
   *
   * @covers ::addResult
   */
  public function testAddResult(): void {
    $state = IteratorState::fromItems(["a", "b"]);

    $withResult = $state->addResult(["processed" => "a"]);

    // Original unchanged.
    $this->assertEquals([], $state->results);

    // New state has result.
    $this->assertCount(1, $withResult->results);
    $this->assertEquals(["processed" => "a"], $withResult->results[0]);

    // Add another result.
    $withTwoResults = $withResult->addResult(["processed" => "b"]);
    $this->assertCount(2, $withTwoResults->results);
  }

  /**
   * Tests addResult preserves other state.
   *
   * @covers ::addResult
   */
  public function testAddResultPreservesState(): void {
    $state = IteratorState::fromItems(["a", "b", "c"]);
    $advanced = $state->advance();
    $withResult = $advanced->addResult("result");

    $this->assertEquals(["a", "b", "c"], $withResult->items);
    $this->assertEquals(1, $withResult->currentIndex);
    $this->assertEquals("b", $withResult->currentItem);
    $this->assertTrue($withResult->hasMore);
  }

  /**
   * Tests isExhausted when not exhausted.
   *
   * @covers ::isExhausted
   */
  public function testIsExhaustedFalse(): void {
    $state = IteratorState::fromItems(["a", "b", "c"]);

    $this->assertFalse($state->isExhausted());
    $this->assertFalse($state->advance()->isExhausted());
    $this->assertFalse($state->advance()->advance()->isExhausted());
  }

  /**
   * Tests isExhausted when exhausted.
   *
   * @covers ::isExhausted
   */
  public function testIsExhaustedTrue(): void {
    $state = IteratorState::fromItems(["a", "b"]);

    $exhausted = $state->advance()->advance();

    $this->assertTrue($exhausted->isExhausted());
  }

  /**
   * Tests isExhausted with empty array.
   *
   * @covers ::isExhausted
   */
  public function testIsExhaustedEmptyArray(): void {
    $state = IteratorState::fromItems([]);

    $this->assertTrue($state->isExhausted());
  }

  /**
   * Tests getTotalCount returns item count.
   *
   * @covers ::getTotalCount
   */
  public function testGetTotalCount(): void {
    $this->assertEquals(0, IteratorState::fromItems([])->getTotalCount());
    $this->assertEquals(1, IteratorState::fromItems(["a"])->getTotalCount());
    $this->assertEquals(5, IteratorState::fromItems([1, 2, 3, 4, 5])->getTotalCount());
  }

  /**
   * Tests getRemainingCount at various positions.
   *
   * @covers ::getRemainingCount
   */
  public function testGetRemainingCount(): void {
    $state = IteratorState::fromItems(["a", "b", "c", "d"]);

    // At start, 3 remaining (after current).
    $this->assertEquals(3, $state->getRemainingCount());

    // After one advance, 2 remaining.
    $this->assertEquals(2, $state->advance()->getRemainingCount());

    // At last item, 0 remaining.
    $this->assertEquals(0, $state->advance()->advance()->advance()->getRemainingCount());
  }

  /**
   * Tests getRemainingCount never goes negative.
   *
   * @covers ::getRemainingCount
   */
  public function testGetRemainingCountNotNegative(): void {
    $state = IteratorState::fromItems(["a"]);
    $exhausted = $state->advance()->advance()->advance();

    $this->assertEquals(0, $exhausted->getRemainingCount());
  }

  /**
   * Tests fromArray creates state correctly.
   *
   * @covers ::fromArray
   */
  public function testFromArray(): void {
    $data = [
      "items" => ["x", "y", "z"],
      "currentIndex" => 1,
      "currentItem" => "y",
      "hasMore" => TRUE,
      "results" => ["res1"],
    ];

    $state = IteratorState::fromArray($data);

    $this->assertEquals(["x", "y", "z"], $state->items);
    $this->assertEquals(1, $state->currentIndex);
    $this->assertEquals("y", $state->currentItem);
    $this->assertTrue($state->hasMore);
    $this->assertEquals(["res1"], $state->results);
  }

  /**
   * Tests fromArray with minimal data.
   *
   * @covers ::fromArray
   */
  public function testFromArrayMinimal(): void {
    $state = IteratorState::fromArray([]);

    $this->assertEquals([], $state->items);
    $this->assertEquals(0, $state->currentIndex);
    $this->assertNull($state->currentItem);
    $this->assertTrue($state->hasMore);
    $this->assertEquals([], $state->results);
  }

  /**
   * Tests toArray serialization.
   *
   * @covers ::toArray
   */
  public function testToArray(): void {
    $state = new IteratorState(
      items: [1, 2, 3],
      currentIndex: 1,
      currentItem: 2,
      hasMore: TRUE,
      results: ["a", "b"],
    );

    $array = $state->toArray();

    $this->assertEquals([1, 2, 3], $array["items"]);
    $this->assertEquals(1, $array["currentIndex"]);
    $this->assertEquals(2, $array["currentItem"]);
    $this->assertTrue($array["hasMore"]);
    $this->assertEquals(["a", "b"], $array["results"]);
  }

  /**
   * Tests JSON serialization.
   *
   * @covers ::jsonSerialize
   */
  public function testJsonSerialize(): void {
    $state = IteratorState::fromItems(["a", "b"]);

    $json = json_encode($state);
    $this->assertIsString($json);

    $decoded = json_decode($json, TRUE);
    $this->assertEquals(["a", "b"], $decoded["items"]);
    $this->assertEquals(0, $decoded["currentIndex"]);
    $this->assertEquals("a", $decoded["currentItem"]);
  }

  /**
   * Tests round-trip serialization.
   *
   * @covers ::toArray
   * @covers ::fromArray
   */
  public function testRoundTripSerialization(): void {
    $original = IteratorState::fromItems(["first", "second", "third"])
      ->advance()
      ->addResult("processed_first");

    $restored = IteratorState::fromArray($original->toArray());

    $this->assertEquals($original->items, $restored->items);
    $this->assertEquals($original->currentIndex, $restored->currentIndex);
    $this->assertEquals($original->currentItem, $restored->currentItem);
    $this->assertEquals($original->hasMore, $restored->hasMore);
    $this->assertEquals($original->results, $restored->results);
  }

  /**
   * Tests iteration workflow.
   *
   * @covers ::fromItems
   * @covers ::advance
   * @covers ::addResult
   * @covers ::isExhausted
   */
  public function testIterationWorkflow(): void {
    $items = ["a", "b", "c"];
    $state = IteratorState::fromItems($items);
    $results = [];

    // Simulate iteration loop.
    while (!$state->isExhausted()) {
      $item = $state->currentItem;
      $results[] = "processed_{$item}";
      $state = $state->addResult("processed_{$item}");
      $state = $state->advance();
    }

    $this->assertCount(3, $results);
    $this->assertEquals(["processed_a", "processed_b", "processed_c"], $results);
    $this->assertEquals($results, $state->results);
    $this->assertTrue($state->isExhausted());
  }

  /**
   * Tests complex items (arrays/objects).
   *
   * @covers ::fromItems
   * @covers ::advance
   */
  public function testComplexItems(): void {
    $items = [
      ["id" => 1, "name" => "first"],
      ["id" => 2, "name" => "second"],
    ];

    $state = IteratorState::fromItems($items);

    $this->assertEquals(["id" => 1, "name" => "first"], $state->currentItem);

    $advanced = $state->advance();
    $this->assertEquals(["id" => 2, "name" => "second"], $advanced->currentItem);
  }

}
