<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\flowdrop_stategraph\Checkpointer\EntityCheckpointer;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\DTO\GraphState;
use Drupal\flowdrop_orchestration\DTO\Snapshot\Message;
use Drupal\Tests\flowdrop\Kernel\FlowDropKernelTestTrait;

/**
 * Kernel tests for the EntityCheckpointer.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\Checkpointer\EntityCheckpointer
 * @group flowdrop_stategraph
 */
class EntityCheckpointerTest extends KernelTestBase {

  use FlowDropKernelTestTrait;

  /**
   * {@inheritdoc}
   *
   * Modules listed in dependency order to satisfy the service container
   * compilation requirements for flowdrop_stategraph.orchestrator.
   */
  protected static $modules = [
    'system',
    'user',
    'options',
    'text',
    'flowdrop_ui_components',
    'flowdrop',
    'flowdrop_node_category',
    'flowdrop_node_type',
    'flowdrop_job',
    'flowdrop_workflow',
    'flowdrop_pipeline',
    'flowdrop_runtime',
    'flowdrop_stategraph',
  ];

  /**
   * The entity checkpointer.
   *
   * @var \Drupal\flowdrop_stategraph\Checkpointer\EntityCheckpointer
   */
  private EntityCheckpointer $checkpointer;

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

    $this->installEntitySchema('user');
    $this->installEntitySchema('state_checkpoint');
    $this->installConfig(['flowdrop_stategraph']);

    $this->checkpointer = new EntityCheckpointer(
      $this->container->get('entity_type.manager'),
      $this->container->get('logger.channel.flowdrop_stategraph'),
    );
  }

  /**
   * Creates a test checkpoint.
   *
   * @param string $id
   *   The checkpoint ID.
   * @param string $threadId
   *   The thread ID.
   * @param array<string, mixed> $stateData
   *   Additional state data.
   * @param string|null $parentId
   *   Optional parent checkpoint ID.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\Checkpoint
   *   A test checkpoint.
   */
  private function createTestCheckpoint(
    string $id,
    string $threadId,
    array $stateData = [],
    ?string $parentId = NULL,
  ): Checkpoint {
    $state = new GraphState(
      threadId: $threadId,
      data: array_merge(["test" => "data"], $stateData),
      iterationCount: 3,
    );

    return new Checkpoint(
      id: $id,
      threadId: $threadId,
      state: $state,
      nodeId: "test_node",
      createdAt: time(),
      parentCheckpointId: $parentId,
      metadata: ["iteration_count" => 3],
    );
  }

  /**
   * Tests put creates entity successfully.
   *
   * @covers ::put
   */
  public function testPutCreatesEntity(): void {
    $checkpoint = $this->createTestCheckpoint("cp_put_test", "thread_1");

    $returnedId = $this->checkpointer->put($checkpoint);

    $this->assertEquals("cp_put_test", $returnedId);

    // Verify entity was created.
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');
    $entities = $storage->loadByProperties(['checkpoint_id' => 'cp_put_test']);

    $this->assertCount(1, $entities);
  }

  /**
   * Tests get loads entity and converts to DTO.
   *
   * @covers ::get
   */
  public function testGetLoadsEntity(): void {
    $checkpoint = $this->createTestCheckpoint(
      "cp_get_test",
      "thread_get",
      ["custom" => "value"]
    );
    $this->checkpointer->put($checkpoint);

    $retrieved = $this->checkpointer->get("cp_get_test");

    $this->assertNotNull($retrieved);
    $this->assertEquals("cp_get_test", $retrieved->id);
    $this->assertEquals("thread_get", $retrieved->threadId);
    $this->assertEquals("test_node", $retrieved->nodeId);
    $this->assertEquals("value", $retrieved->state->data["custom"]);
    $this->assertEquals(3, $retrieved->metadata["iteration_count"]);
  }

  /**
   * Tests get returns null for unknown checkpoint.
   *
   * @covers ::get
   */
  public function testGetReturnsNullForUnknown(): void {
    $result = $this->checkpointer->get("nonexistent_checkpoint");

    $this->assertNull($result);
  }

  /**
   * Tests list queries entities by thread ID.
   *
   * @covers ::list
   */
  public function testListQueriesEntities(): void {
    // Create checkpoints for two threads.
    $this->checkpointer->put($this->createTestCheckpoint("cp_a1", "thread_a"));
    $this->checkpointer->put($this->createTestCheckpoint("cp_a2", "thread_a"));
    $this->checkpointer->put($this->createTestCheckpoint("cp_b1", "thread_b"));

    $threadACheckpoints = $this->checkpointer->list("thread_a");

    $this->assertCount(2, $threadACheckpoints);
    foreach ($threadACheckpoints as $cp) {
      $this->assertEquals("thread_a", $cp->threadId);
    }
  }

  /**
   * Tests list returns empty array for unknown thread.
   *
   * @covers ::list
   */
  public function testListReturnsEmptyForUnknownThread(): void {
    $result = $this->checkpointer->list("nonexistent_thread");

    $this->assertEquals([], $result);
  }

  /**
   * Tests list respects limit parameter.
   *
   * @covers ::list
   */
  public function testListRespectsLimit(): void {
    // Create 5 checkpoints.
    for ($i = 0; $i < 5; $i++) {
      $this->checkpointer->put($this->createTestCheckpoint("cp_{$i}", "thread_limit"));
    }

    $result = $this->checkpointer->list("thread_limit", 3);

    $this->assertCount(3, $result);
  }

  /**
   * Tests getLatest returns most recent checkpoint.
   *
   * @covers ::getLatest
   */
  public function testGetLatestReturnsNewest(): void {
    $this->checkpointer->put($this->createTestCheckpoint("cp_old", "thread_latest"));
    sleep(1);
    $this->checkpointer->put($this->createTestCheckpoint("cp_new", "thread_latest"));

    $latest = $this->checkpointer->getLatest("thread_latest");

    $this->assertNotNull($latest);
    $this->assertEquals("cp_new", $latest->id);
  }

  /**
   * Tests getLatest returns null for empty thread.
   *
   * @covers ::getLatest
   */
  public function testGetLatestReturnsNullForEmptyThread(): void {
    $result = $this->checkpointer->getLatest("empty_thread");

    $this->assertNull($result);
  }

  /**
   * Tests delete removes entity.
   *
   * @covers ::delete
   */
  public function testDeleteRemovesEntity(): void {
    $this->checkpointer->put($this->createTestCheckpoint("cp_delete", "thread_del"));

    $result = $this->checkpointer->delete("cp_delete");

    $this->assertTrue($result);
    $this->assertNull($this->checkpointer->get("cp_delete"));
  }

  /**
   * Tests delete returns false for unknown checkpoint.
   *
   * @covers ::delete
   */
  public function testDeleteReturnsFalseForUnknown(): void {
    $result = $this->checkpointer->delete("nonexistent");

    $this->assertFalse($result);
  }

  /**
   * Tests deleteThread removes all thread checkpoints.
   *
   * @covers ::deleteThread
   */
  public function testDeleteThreadRemovesAllEntities(): void {
    $this->checkpointer->put($this->createTestCheckpoint("cp_1", "thread_bulk"));
    $this->checkpointer->put($this->createTestCheckpoint("cp_2", "thread_bulk"));
    $this->checkpointer->put($this->createTestCheckpoint("cp_3", "thread_other"));

    $count = $this->checkpointer->deleteThread("thread_bulk");

    $this->assertEquals(2, $count);
    $this->assertEquals([], $this->checkpointer->list("thread_bulk"));
    // Other thread should be unaffected.
    $this->assertCount(1, $this->checkpointer->list("thread_other"));
  }

  /**
   * Tests deleteThread returns zero for unknown thread.
   *
   * @covers ::deleteThread
   */
  public function testDeleteThreadReturnsZeroForUnknown(): void {
    $count = $this->checkpointer->deleteThread("nonexistent_thread");

    $this->assertEquals(0, $count);
  }

  /**
   * Tests clear removes all checkpoints.
   *
   * @covers ::clear
   */
  public function testClearRemovesAll(): void {
    $this->checkpointer->put($this->createTestCheckpoint("cp_1", "thread_1"));
    $this->checkpointer->put($this->createTestCheckpoint("cp_2", "thread_2"));

    $count = $this->checkpointer->clear();

    $this->assertEquals(2, $count);
    $this->assertEquals([], $this->checkpointer->list("thread_1"));
    $this->assertEquals([], $this->checkpointer->list("thread_2"));
  }

  /**
   * Tests JSON field stores complex state correctly.
   *
   * @covers ::put
   * @covers ::get
   */
  public function testJsonFieldSerialization(): void {
    $message = new Message("user", "Test message", ["meta" => "data"], time());

    $state = new GraphState(
      threadId: "thread_json",
      messages: [$message],
      data: [
        "nested" => [
          "deep" => [
            "value" => "preserved",
          ],
        ],
        "array" => [1, 2, 3],
        "mixed" => ["string", 42, TRUE, NULL],
      ],
      metadata: ["complex" => ["metadata" => "value"]],
      iterationCount: 10,
    );

    $checkpoint = new Checkpoint(
      id: "cp_json_test",
      threadId: "thread_json",
      state: $state,
      nodeId: "json_node",
      createdAt: time(),
      metadata: ["test" => "metadata"],
    );

    $this->checkpointer->put($checkpoint);
    $retrieved = $this->checkpointer->get("cp_json_test");

    $this->assertNotNull($retrieved);

    // Verify complex state was preserved.
    $this->assertCount(1, $retrieved->state->messages);
    $this->assertEquals("Test message", $retrieved->state->messages[0]->content);
    $this->assertEquals("preserved", $retrieved->state->data["nested"]["deep"]["value"]);
    $this->assertEquals([1, 2, 3], $retrieved->state->data["array"]);
    $this->assertEquals(["string", 42, TRUE, NULL], $retrieved->state->data["mixed"]);
    $this->assertEquals(10, $retrieved->state->iterationCount);
  }

  /**
   * Tests put updates existing checkpoint.
   *
   * @covers ::put
   */
  public function testPutUpdatesExisting(): void {
    $original = $this->createTestCheckpoint("cp_update", "thread_update");
    $this->checkpointer->put($original);

    // Update with new state.
    $updatedState = new GraphState(
      threadId: "thread_update",
      data: ["updated" => TRUE],
      iterationCount: 10,
    );

    $updated = new Checkpoint(
      id: "cp_update",
      threadId: "thread_update",
      state: $updatedState,
      nodeId: "updated_node",
      createdAt: time(),
      metadata: ["updated" => TRUE],
    );

    $this->checkpointer->put($updated);

    // Should still only have one entity.
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');
    $entities = $storage->loadByProperties(['checkpoint_id' => 'cp_update']);
    $this->assertCount(1, $entities);

    // Should have updated values.
    $retrieved = $this->checkpointer->get("cp_update");
    $this->assertTrue($retrieved->state->data["updated"]);
    $this->assertEquals("updated_node", $retrieved->nodeId);
    $this->assertEquals(10, $retrieved->state->iterationCount);
  }

  /**
   * Tests parent checkpoint reference.
   *
   * @covers ::put
   * @covers ::get
   */
  public function testParentCheckpointReference(): void {
    // Create parent checkpoint first.
    $parent = $this->createTestCheckpoint("cp_parent", "thread_chain");
    $this->checkpointer->put($parent);

    // Create child with parent reference.
    $child = $this->createTestCheckpoint("cp_child", "thread_chain", [], "cp_parent");
    $this->checkpointer->put($child);

    // Retrieve child and verify parent reference.
    $retrieved = $this->checkpointer->get("cp_child");

    $this->assertNotNull($retrieved);
    $this->assertEquals("cp_parent", $retrieved->parentCheckpointId);
  }

}
