<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Unit\DTO;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\DTO\GraphState;

/**
 * Tests the Checkpoint DTO.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\DTO\Checkpoint
 * @group flowdrop_stategraph
 */
class CheckpointTest extends UnitTestCase {

  /**
   * Tests constructor sets all values.
   *
   * @covers ::__construct
   */
  public function testConstructor(): void {
    $state = new GraphState(threadId: "thread_1");

    $checkpoint = new Checkpoint(
      id: "cp_test123",
      threadId: "thread_1",
      state: $state,
      nodeId: "process_node",
      createdAt: 1234567890,
      parentCheckpointId: "cp_parent",
      metadata: ["iteration_count" => 5],
    );

    $this->assertEquals("cp_test123", $checkpoint->id);
    $this->assertEquals("thread_1", $checkpoint->threadId);
    $this->assertInstanceOf(GraphState::class, $checkpoint->state);
    $this->assertEquals("process_node", $checkpoint->nodeId);
    $this->assertEquals(1234567890, $checkpoint->createdAt);
    $this->assertEquals("cp_parent", $checkpoint->parentCheckpointId);
    $this->assertEquals(["iteration_count" => 5], $checkpoint->metadata);
  }

  /**
   * Tests constructor with optional parameters.
   *
   * @covers ::__construct
   */
  public function testConstructorDefaults(): void {
    $state = new GraphState(threadId: "thread_1");

    $checkpoint = new Checkpoint(
      id: "cp_test",
      threadId: "thread_1",
      state: $state,
      nodeId: "node_1",
      createdAt: 1234567890,
    );

    $this->assertNull($checkpoint->parentCheckpointId);
    $this->assertEquals([], $checkpoint->metadata);
  }

  /**
   * Tests create factory method.
   *
   * @covers ::create
   */
  public function testCreate(): void {
    $state = new GraphState(
      threadId: "thread_abc",
      iterationCount: 3,
      isComplete: FALSE,
    );

    $checkpoint = Checkpoint::create($state, "current_node");

    // ID should be generated with "cp_" prefix.
    $this->assertStringStartsWith("cp_", $checkpoint->id);
    $this->assertEquals(24 + 3, strlen($checkpoint->id));

    // ThreadId should come from state.
    $this->assertEquals("thread_abc", $checkpoint->threadId);

    // State should be preserved.
    $this->assertSame($state, $checkpoint->state);

    // NodeId should be set.
    $this->assertEquals("current_node", $checkpoint->nodeId);

    // CreatedAt should be recent.
    $this->assertGreaterThan(time() - 5, $checkpoint->createdAt);
    $this->assertLessThanOrEqual(time(), $checkpoint->createdAt);

    // No parent by default.
    $this->assertNull($checkpoint->parentCheckpointId);

    // Metadata should include state info.
    $this->assertEquals(3, $checkpoint->metadata["iteration_count"]);
    $this->assertFalse($checkpoint->metadata["is_complete"]);
  }

  /**
   * Tests create with parent checkpoint.
   *
   * @covers ::create
   */
  public function testCreateWithParent(): void {
    $state = new GraphState(threadId: "thread_1");

    $checkpoint = Checkpoint::create($state, "node_2", "cp_parent_123");

    $this->assertEquals("cp_parent_123", $checkpoint->parentCheckpointId);
  }

  /**
   * Tests create generates thread ID if empty.
   *
   * @covers ::create
   */
  public function testCreateGeneratesThreadIdIfEmpty(): void {
    $state = new GraphState(threadId: "");

    $checkpoint = Checkpoint::create($state, "node_1");

    // Should generate a thread ID starting with "thread_".
    $this->assertStringStartsWith("thread_", $checkpoint->threadId);
    $this->assertGreaterThan(7, strlen($checkpoint->threadId));
  }

  /**
   * Tests fromArray creates checkpoint correctly.
   *
   * @covers ::fromArray
   */
  public function testFromArray(): void {
    $data = [
      "id" => "cp_test456",
      "threadId" => "thread_xyz",
      "state" => [
        "messages" => [],
        "data" => ["key" => "value"],
        "metadata" => [],
        "iterator" => NULL,
        "currentNodeId" => "node_a",
        "iterationCount" => 2,
        "threadId" => "thread_xyz",
        "isComplete" => FALSE,
        "error" => NULL,
      ],
      "nodeId" => "node_a",
      "createdAt" => 1234567890,
      "parentCheckpointId" => "cp_parent",
      "metadata" => ["custom" => "data"],
    ];

    $checkpoint = Checkpoint::fromArray($data);

    $this->assertEquals("cp_test456", $checkpoint->id);
    $this->assertEquals("thread_xyz", $checkpoint->threadId);
    $this->assertInstanceOf(GraphState::class, $checkpoint->state);
    $this->assertEquals(["key" => "value"], $checkpoint->state->data);
    $this->assertEquals("node_a", $checkpoint->nodeId);
    $this->assertEquals(1234567890, $checkpoint->createdAt);
    $this->assertEquals("cp_parent", $checkpoint->parentCheckpointId);
    $this->assertEquals(["custom" => "data"], $checkpoint->metadata);
  }

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

    // ID and threadId should be generated.
    $this->assertStringStartsWith("cp_", $checkpoint->id);
    $this->assertStringStartsWith("thread_", $checkpoint->threadId);

    // State should be empty GraphState.
    $this->assertInstanceOf(GraphState::class, $checkpoint->state);

    // NodeId should be empty.
    $this->assertEquals("", $checkpoint->nodeId);

    // CreatedAt should be recent.
    $this->assertGreaterThan(time() - 5, $checkpoint->createdAt);

    // Optional fields.
    $this->assertNull($checkpoint->parentCheckpointId);
    $this->assertEquals([], $checkpoint->metadata);
  }

  /**
   * Tests fromArray with GraphState object in state field.
   *
   * @covers ::fromArray
   */
  public function testFromArrayWithGraphStateObject(): void {
    $state = new GraphState(threadId: "thread_1", data: ["test" => "data"]);

    $data = [
      "id" => "cp_obj",
      "threadId" => "thread_1",
      "state" => $state,
      "nodeId" => "node_1",
      "createdAt" => 1234567890,
    ];

    $checkpoint = Checkpoint::fromArray($data);

    $this->assertSame($state, $checkpoint->state);
  }

  /**
   * Tests toArray serialization.
   *
   * @covers ::toArray
   */
  public function testToArray(): void {
    $state = new GraphState(
      threadId: "thread_1",
      data: ["key" => "value"],
      iterationCount: 5,
    );

    $checkpoint = new Checkpoint(
      id: "cp_toarray",
      threadId: "thread_1",
      state: $state,
      nodeId: "serialize_node",
      createdAt: 1234567890,
      parentCheckpointId: "cp_parent",
      metadata: ["test" => "meta"],
    );

    $array = $checkpoint->toArray();

    $this->assertEquals("cp_toarray", $array["id"]);
    $this->assertEquals("thread_1", $array["threadId"]);
    $this->assertIsArray($array["state"]);
    $this->assertEquals(["key" => "value"], $array["state"]["data"]);
    $this->assertEquals("serialize_node", $array["nodeId"]);
    $this->assertEquals(1234567890, $array["createdAt"]);
    $this->assertEquals("cp_parent", $array["parentCheckpointId"]);
    $this->assertEquals(["test" => "meta"], $array["metadata"]);
  }

  /**
   * Tests JSON serialization.
   *
   * @covers ::jsonSerialize
   */
  public function testJsonSerialize(): void {
    $state = new GraphState(threadId: "thread_json");
    $checkpoint = new Checkpoint(
      id: "cp_json",
      threadId: "thread_json",
      state: $state,
      nodeId: "json_node",
      createdAt: 1234567890,
    );

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

    $decoded = json_decode($json, TRUE);
    $this->assertEquals("cp_json", $decoded["id"]);
    $this->assertEquals("thread_json", $decoded["threadId"]);
    $this->assertEquals("json_node", $decoded["nodeId"]);
  }

  /**
   * Tests round-trip serialization.
   *
   * @covers ::toArray
   * @covers ::fromArray
   */
  public function testRoundTripSerialization(): void {
    $state = new GraphState(
      threadId: "thread_roundtrip",
      data: ["nested" => ["deep" => "value"]],
      iterationCount: 7,
    );

    $original = new Checkpoint(
      id: "cp_roundtrip",
      threadId: "thread_roundtrip",
      state: $state,
      nodeId: "round_node",
      createdAt: 1234567890,
      parentCheckpointId: "cp_parent_rt",
      metadata: ["iteration_count" => 7, "is_complete" => FALSE],
    );

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

    $this->assertEquals($original->id, $restored->id);
    $this->assertEquals($original->threadId, $restored->threadId);
    $this->assertEquals($original->state->data, $restored->state->data);
    $this->assertEquals($original->nodeId, $restored->nodeId);
    $this->assertEquals($original->createdAt, $restored->createdAt);
    $this->assertEquals($original->parentCheckpointId, $restored->parentCheckpointId);
    $this->assertEquals($original->metadata, $restored->metadata);
  }

  /**
   * Tests unique ID generation.
   *
   * @covers ::create
   */
  public function testUniqueIdGeneration(): void {
    $state = new GraphState(threadId: "thread_1");

    $ids = [];
    for ($i = 0; $i < 100; $i++) {
      $checkpoint = Checkpoint::create($state, "node_{$i}");
      $ids[] = $checkpoint->id;
    }

    // All IDs should be unique.
    $this->assertCount(100, array_unique($ids));
  }

}
