<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\DTO\GraphState;
use Drupal\flowdrop_orchestration\DTO\Snapshot\Message;
use Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpoint;
use Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface;
use Drupal\Tests\flowdrop\Kernel\FlowDropKernelTestTrait;

/**
 * Kernel tests for the FlowDropStateCheckpoint entity.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpoint
 * @group flowdrop_stategraph
 */
class FlowDropStateCheckpointEntityTest 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_orchestration',
    'flowdrop_runtime',
    'flowdrop_stategraph',
  ];

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

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

  /**
   * Tests entity creation.
   *
   * @covers ::baseFieldDefinitions
   */
  public function testEntityCreation(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_entity_test',
      'thread_id' => 'thread_test',
      'state_data' => '{"threadId":"thread_test","data":{"key":"value"}}',
      'node_id' => 'create_node',
      'metadata' => '{"test":"metadata"}',
    ]);

    $entity->save();

    $this->assertNotNull($entity->id());
    $this->assertEquals('cp_entity_test', $entity->getCheckpointId());
    $this->assertEquals('thread_test', $entity->getThreadId());
  }

  /**
   * Tests state data JSON field stores and retrieves correctly.
   *
   * @covers ::getStateData
   * @covers ::setStateData
   */
  public function testStateDataJsonField(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    $stateData = [
      'threadId' => 'thread_json',
      'messages' => [
        ['role' => 'user', 'content' => 'Hello'],
      ],
      'data' => [
        'nested' => ['deep' => 'value'],
        'array' => [1, 2, 3],
      ],
      'iterationCount' => 5,
      'isComplete' => FALSE,
    ];

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_state_data',
      'thread_id' => 'thread_json',
      'node_id' => 'state_node',
    ]);

    $entity->setStateData($stateData);
    $entity->save();

    // Reload entity.
    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);

    $retrievedData = $loaded->getStateData();

    $this->assertEquals('thread_json', $retrievedData['threadId']);
    $this->assertCount(1, $retrievedData['messages']);
    $this->assertEquals('Hello', $retrievedData['messages'][0]['content']);
    $this->assertEquals('value', $retrievedData['data']['nested']['deep']);
    $this->assertEquals([1, 2, 3], $retrievedData['data']['array']);
    $this->assertEquals(5, $retrievedData['iterationCount']);
    $this->assertFalse($retrievedData['isComplete']);
  }

  /**
   * Tests metadata JSON field stores and retrieves correctly.
   *
   * @covers ::getMetadata
   * @covers ::setMetadata
   */
  public function testMetadataJsonField(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    $metadata = [
      'iteration_count' => 10,
      'is_complete' => FALSE,
      'custom' => [
        'nested' => 'value',
        'array' => ['a', 'b', 'c'],
      ],
    ];

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_metadata',
      'thread_id' => 'thread_meta',
      'state_data' => '{}',
      'node_id' => 'meta_node',
    ]);

    $entity->setMetadata($metadata);
    $entity->save();

    // Reload entity.
    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);

    $retrievedMetadata = $loaded->getMetadata();

    $this->assertEquals(10, $retrievedMetadata['iteration_count']);
    $this->assertFalse($retrievedMetadata['is_complete']);
    $this->assertEquals('value', $retrievedMetadata['custom']['nested']);
    $this->assertEquals(['a', 'b', 'c'], $retrievedMetadata['custom']['array']);
  }

  /**
   * Tests thread_id field stores correctly.
   *
   * @covers ::getThreadId
   * @covers ::setThreadId
   */
  public function testThreadIdField(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_thread_test',
      'thread_id' => 'original_thread',
      'state_data' => '{}',
      'node_id' => 'thread_node',
    ]);

    $entity->save();

    $this->assertEquals('original_thread', $entity->getThreadId());

    // Update thread ID.
    $entity->setThreadId('updated_thread');
    $entity->save();

    // Reload and verify.
    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);
    $this->assertEquals('updated_thread', $loaded->getThreadId());
  }

  /**
   * Tests parent checkpoint entity reference field.
   *
   * @covers ::getParentCheckpointId
   */
  public function testParentCheckpointField(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    // Create parent entity.
    $parent = $storage->create([
      'checkpoint_id' => 'cp_parent_ref',
      'thread_id' => 'thread_ref',
      'state_data' => '{}',
      'node_id' => 'parent_node',
    ]);
    $parent->save();

    // Create child entity with reference to parent.
    $child = $storage->create([
      'checkpoint_id' => 'cp_child_ref',
      'thread_id' => 'thread_ref',
      'state_data' => '{}',
      'node_id' => 'child_node',
      'parent_checkpoint' => $parent->id(),
    ]);
    $child->save();

    // Reload and verify parent reference.
    $storage->resetCache();
    $loaded = $storage->load($child->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);
    $this->assertEquals('cp_parent_ref', $loaded->getParentCheckpointId());
  }

  /**
   * Tests entity deletion.
   *
   * @covers ::baseFieldDefinitions
   */
  public function testEntityDeletion(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_delete_test',
      'thread_id' => 'thread_delete',
      'state_data' => '{}',
      'node_id' => 'delete_node',
    ]);

    $entity->save();
    $entityId = $entity->id();

    // Verify entity exists.
    $this->assertNotNull($storage->load($entityId));

    // Delete entity.
    $entity->delete();

    // Verify entity is gone.
    $storage->resetCache();
    $this->assertNull($storage->load($entityId));
  }

  /**
   * Tests getGraphState returns proper GraphState DTO.
   *
   * @covers ::getGraphState
   * @covers ::setGraphState
   */
  public function testGetGraphState(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    $graphState = new GraphState(
      threadId: 'thread_graph',
      messages: [new Message('user', 'Test', [], time())],
      data: ['key' => 'value'],
      iterationCount: 7,
      isComplete: TRUE,
    );

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_graph_state',
      'thread_id' => 'thread_graph',
      'node_id' => 'graph_node',
    ]);

    $entity->setGraphState($graphState);
    $entity->save();

    // Reload and get GraphState.
    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);

    $retrievedState = $loaded->getGraphState();

    $this->assertInstanceOf(GraphState::class, $retrievedState);
    $this->assertEquals('thread_graph', $retrievedState->threadId);
    $this->assertCount(1, $retrievedState->messages);
    $this->assertEquals('Test', $retrievedState->messages[0]->content);
    $this->assertEquals(['key' => 'value'], $retrievedState->data);
    $this->assertEquals(7, $retrievedState->iterationCount);
    $this->assertTrue($retrievedState->isComplete);
  }

  /**
   * Tests toCheckpointDto creates complete Checkpoint.
   *
   * @covers ::toCheckpointDto
   */
  public function testToCheckpointDto(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    $graphState = new GraphState(
      threadId: 'thread_dto',
      data: ['test' => 'data'],
      iterationCount: 3,
    );

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_to_dto',
      'thread_id' => 'thread_dto',
      'node_id' => 'dto_node',
      'metadata' => '{"iteration_count":3}',
    ]);

    $entity->setGraphState($graphState);
    $entity->save();

    // Convert to DTO.
    $dto = $entity->toCheckpointDto();

    $this->assertInstanceOf(Checkpoint::class, $dto);
    $this->assertEquals('cp_to_dto', $dto->id);
    $this->assertEquals('thread_dto', $dto->threadId);
    $this->assertEquals('dto_node', $dto->nodeId);
    $this->assertInstanceOf(GraphState::class, $dto->state);
    $this->assertEquals(['test' => 'data'], $dto->state->data);
    $this->assertEquals(['iteration_count' => 3], $dto->metadata);
  }

  /**
   * Tests fromCheckpointDto creates entity from DTO.
   *
   * @covers ::fromCheckpointDto
   */
  public function testFromCheckpointDto(): void {
    $graphState = new GraphState(
      threadId: 'thread_from_dto',
      data: ['from' => 'dto'],
      iterationCount: 5,
    );

    $checkpoint = new Checkpoint(
      id: 'cp_from_dto_test',
      threadId: 'thread_from_dto',
      state: $graphState,
      nodeId: 'from_dto_node',
      createdAt: time(),
      metadata: ['test' => 'metadata'],
    );

    $entity = FlowDropStateCheckpoint::fromCheckpointDto($checkpoint);

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $entity);
    $this->assertEquals('cp_from_dto_test', $entity->getCheckpointId());
    $this->assertEquals('thread_from_dto', $entity->getThreadId());
    $this->assertEquals('from_dto_node', $entity->getNodeId());
    $this->assertEquals(['from' => 'dto'], $entity->getGraphState()->data);
    $this->assertEquals(['test' => 'metadata'], $entity->getMetadata());
  }

  /**
   * Tests node_id field.
   *
   * @covers ::getNodeId
   * @covers ::setNodeId
   */
  public function testNodeIdField(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_node_test',
      'thread_id' => 'thread_node',
      'state_data' => '{}',
      'node_id' => 'original_node',
    ]);

    $entity->save();
    $this->assertEquals('original_node', $entity->getNodeId());

    $entity->setNodeId('updated_node');
    $entity->save();

    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);
    $this->assertEquals('updated_node', $loaded->getNodeId());
  }

  /**
   * Tests created timestamp field.
   *
   * @covers ::getCreatedTime
   */
  public function testCreatedTimestamp(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_created_test',
      'thread_id' => 'thread_created',
      'state_data' => '{}',
      'node_id' => 'created_node',
    ]);

    $entity->save();

    $createdTime = $entity->getCreatedTime();

    // Drupal's created field uses Unix timestamps (1-second resolution).
    // Verify the timestamp is set and is recent (within last 10 seconds).
    $this->assertGreaterThan(0, $createdTime);
    $this->assertLessThanOrEqual(time(), $createdTime);
    $this->assertGreaterThanOrEqual(time() - 10, $createdTime);
  }

  /**
   * Tests checkpoint_id uniqueness constraint.
   *
   * @covers ::baseFieldDefinitions
   */
  public function testCheckpointIdUniqueness(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    // Create first entity.
    $entity1 = $storage->create([
      'checkpoint_id' => 'cp_unique_test',
      'thread_id' => 'thread_1',
      'state_data' => '{}',
      'node_id' => 'node_1',
    ]);
    $entity1->save();

    // Try to create second with same checkpoint_id.
    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity2 */
    $entity2 = $storage->create([
      'checkpoint_id' => 'cp_unique_test',
      'thread_id' => 'thread_2',
      'state_data' => '{}',
      'node_id' => 'node_2',
    ]);

    $violations = $entity2->validate();

    // Should have uniqueness violation.
    $this->assertGreaterThan(0, $violations->count());
  }

  /**
   * Tests empty state data handling.
   *
   * @covers ::getStateData
   */
  public function testEmptyStateDataHandling(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_empty_state',
      'thread_id' => 'thread_empty',
      'state_data' => '',
      'node_id' => 'empty_node',
    ]);

    $entity->save();

    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);

    // Should return empty array for empty/null state_data.
    $this->assertEquals([], $loaded->getStateData());
  }

  /**
   * Tests empty metadata handling.
   *
   * @covers ::getMetadata
   */
  public function testEmptyMetadataHandling(): void {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');

    /** @var \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface $entity */
    $entity = $storage->create([
      'checkpoint_id' => 'cp_empty_meta',
      'thread_id' => 'thread_meta',
      'state_data' => '{}',
      'node_id' => 'meta_node',
      // No metadata field set.
    ]);

    $entity->save();

    $storage->resetCache();
    $loaded = $storage->load($entity->id());

    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $loaded);

    // Should return empty array for null/empty metadata.
    $this->assertEquals([], $loaded->getMetadata());
  }

}
