<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service\WorkflowState;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot;
use Drupal\flowdrop_runtime\Entity\FlowDropWorkflowSnapshotInterface;
use Drupal\flowdrop_runtime\Service\WorkflowState\EntitySnapshotStorage;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Tests the EntitySnapshotStorage service.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\WorkflowState\EntitySnapshotStorage
 * @group flowdrop_runtime
 */
class EntitySnapshotStorageTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The entity snapshot storage under test.
   *
   * @var \Drupal\flowdrop_runtime\Service\WorkflowState\EntitySnapshotStorage
   */
  private EntitySnapshotStorage $storage;

  /**
   * Mock entity type manager.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy<\Drupal\Core\Entity\EntityTypeManagerInterface>
   */
  private $entityTypeManager;

  /**
   * Mock entity storage.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy<\Drupal\Core\Entity\EntityStorageInterface>
   */
  private $entityStorage;

  /**
   * Mock logger.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy<\Psr\Log\LoggerInterface>
   */
  private $logger;

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

    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->entityStorage = $this->prophesize(EntityStorageInterface::class);
    $this->logger = $this->prophesize(LoggerInterface::class);

    // Setup entity type manager to return mock storage.
    $this->entityTypeManager->getStorage("flowdrop_workflow_snapshot")
      ->willReturn($this->entityStorage->reveal());

    // Allow any logging calls (void return type).
    $this->storage = new EntitySnapshotStorage(
      $this->entityTypeManager->reveal(),
      $this->logger->reveal()
    );
  }

  /**
   * Creates a mock snapshot entity.
   *
   * @param \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot $snapshot
   *   The snapshot DTO.
   * @param int $entityId
   *   The entity ID.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\flowdrop_runtime\Entity\FlowDropWorkflowSnapshotInterface>
   *   The mocked entity.
   */
  private function createMockEntity(WorkflowSnapshot $snapshot, int $entityId = 1) {
    $entity = $this->prophesize(FlowDropWorkflowSnapshotInterface::class);
    $entity->id()->willReturn($entityId);
    $entity->toWorkflowSnapshot()->willReturn($snapshot);
    $entity->save()->willReturn(1);
    $entity->delete()->willReturn(NULL);
    return $entity;
  }

  /**
   * @covers ::load
   */
  public function testLoadReturnsSnapshotWhenFound(): void {
    $snapshot = WorkflowSnapshot::create(
      workflowId: "test_workflow",
      workflowVersion: "abc123",
    );

    $mockEntity = $this->createMockEntity($snapshot);
    $this->entityStorage->loadByProperties(["execution_id" => $snapshot->executionId])
      ->willReturn([1 => $mockEntity->reveal()]);

    $result = $this->storage->load($snapshot->executionId);

    $this->assertNotNull($result);
    $this->assertEquals($snapshot->workflowId, $result->workflowId);
  }

  /**
   * @covers ::load
   */
  public function testLoadReturnsNullWhenNotFound(): void {
    $this->entityStorage->loadByProperties(["execution_id" => "nonexistent"])
      ->willReturn([]);

    $result = $this->storage->load("nonexistent");

    $this->assertNull($result);
  }

  /**
   * @covers ::loadByExecutionId
   */
  public function testLoadByExecutionIdReturnsEntity(): void {
    $snapshot = WorkflowSnapshot::create(
      workflowId: "test_workflow",
      workflowVersion: "abc123",
    );

    $mockEntity = $this->createMockEntity($snapshot);
    $this->entityStorage->loadByProperties(["execution_id" => $snapshot->executionId])
      ->willReturn([1 => $mockEntity->reveal()]);

    $result = $this->storage->loadByExecutionId($snapshot->executionId);

    $this->assertInstanceOf(FlowDropWorkflowSnapshotInterface::class, $result);
  }

  /**
   * @covers ::loadByEntityId
   */
  public function testLoadByEntityIdReturnsSnapshot(): void {
    $snapshot = WorkflowSnapshot::create(
      workflowId: "test_workflow",
      workflowVersion: "abc123",
    );

    $mockEntity = $this->createMockEntity($snapshot, 123);
    $this->entityStorage->load(123)->willReturn($mockEntity->reveal());

    $result = $this->storage->loadByEntityId(123);

    $this->assertNotNull($result);
    $this->assertEquals($snapshot->workflowId, $result->workflowId);
  }

  /**
   * @covers ::loadByEntityId
   */
  public function testLoadByEntityIdReturnsNullWhenNotFound(): void {
    $this->entityStorage->load(999)->willReturn(NULL);

    $result = $this->storage->loadByEntityId(999);

    $this->assertNull($result);
  }

  /**
   * @covers ::delete
   */
  public function testDeleteRemovesEntity(): void {
    $snapshot = WorkflowSnapshot::create(
      workflowId: "test_workflow",
      workflowVersion: "abc123",
    );

    $mockEntity = $this->createMockEntity($snapshot);
    $mockEntity->delete()->shouldBeCalled();

    $this->entityStorage->loadByProperties(["execution_id" => $snapshot->executionId])
      ->willReturn([1 => $mockEntity->reveal()]);

    $result = $this->storage->delete($snapshot->executionId);

    $this->assertTrue($result);
  }

  /**
   * @covers ::delete
   */
  public function testDeleteReturnsFalseWhenNotFound(): void {
    $this->entityStorage->loadByProperties(["execution_id" => "nonexistent"])
      ->willReturn([]);

    $result = $this->storage->delete("nonexistent");

    $this->assertFalse($result);
  }

  /**
   * @covers ::exists
   */
  public function testExistsReturnsTrueWhenFound(): void {
    $query = $this->prophesize(QueryInterface::class);
    $query->condition("execution_id", "test_exec")->willReturn($query);
    $query->count()->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn(1);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $result = $this->storage->exists("test_exec");

    $this->assertTrue($result);
  }

  /**
   * @covers ::exists
   */
  public function testExistsReturnsFalseWhenNotFound(): void {
    $query = $this->prophesize(QueryInterface::class);
    $query->condition("execution_id", "nonexistent")->willReturn($query);
    $query->count()->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn(0);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $result = $this->storage->exists("nonexistent");

    $this->assertFalse($result);
  }

  /**
   * @covers ::loadLatestByThread
   */
  public function testLoadLatestByThread(): void {
    $snapshot = WorkflowSnapshot::create(
      workflowId: "test_workflow",
      workflowVersion: "abc123",
      threadId: "thread_001",
    );

    $query = $this->prophesize(QueryInterface::class);
    $query->condition("thread_id", "thread_001")->willReturn($query);
    $query->sort("created", "DESC")->willReturn($query);
    $query->range(0, 1)->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn([42]);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $mockEntity = $this->createMockEntity($snapshot, 42);
    $this->entityStorage->load(42)->willReturn($mockEntity->reveal());

    $result = $this->storage->loadLatestByThread("thread_001");

    $this->assertNotNull($result);
    $this->assertEquals("thread_001", $result->threadId);
  }

  /**
   * @covers ::loadLatestByThread
   */
  public function testLoadLatestByThreadReturnsNullWhenEmpty(): void {
    $query = $this->prophesize(QueryInterface::class);
    $query->condition("thread_id", "thread_999")->willReturn($query);
    $query->sort("created", "DESC")->willReturn($query);
    $query->range(0, 1)->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn([]);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $result = $this->storage->loadLatestByThread("thread_999");

    $this->assertNull($result);
  }

  /**
   * @covers ::listByWorkflow
   */
  public function testListByWorkflow(): void {
    $snapshot1 = WorkflowSnapshot::create(
      workflowId: "my_workflow",
      workflowVersion: "v1",
    );
    $snapshot2 = WorkflowSnapshot::create(
      workflowId: "my_workflow",
      workflowVersion: "v1",
    );

    $query = $this->prophesize(QueryInterface::class);
    $query->condition("workflow_id", "my_workflow")->willReturn($query);
    $query->sort("created", "DESC")->willReturn($query);
    $query->range(0, 50)->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn([1, 2]);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $mockEntity1 = $this->createMockEntity($snapshot1, 1);
    $mockEntity2 = $this->createMockEntity($snapshot2, 2);

    $this->entityStorage->loadMultiple([1, 2])
      ->willReturn([
        1 => $mockEntity1->reveal(),
        2 => $mockEntity2->reveal(),
      ]);

    $results = $this->storage->listByWorkflow("my_workflow", 50, 0);

    $this->assertCount(2, $results);
    foreach ($results as $result) {
      $this->assertEquals("my_workflow", $result->workflowId);
    }
  }

  /**
   * @covers ::cleanup
   */
  public function testCleanup(): void {
    $oldTimestamp = time() - 86400 * 10;

    $query = $this->prophesize(QueryInterface::class);
    $query->condition("created", $oldTimestamp, "<")->willReturn($query);
    $query->condition("snapshot_status", ["completed", "failed"], "IN")->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn([1, 2, 3]);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    // Create mock entities to be deleted.
    $mockEntities = [];
    for ($i = 1; $i <= 3; $i++) {
      $entity = $this->prophesize(FlowDropWorkflowSnapshotInterface::class);
      $mockEntities[$i] = $entity->reveal();
    }

    $this->entityStorage->loadMultiple([1, 2, 3])->willReturn($mockEntities);
    $this->entityStorage->delete($mockEntities)->shouldBeCalled();

    $deleted = $this->storage->cleanup($oldTimestamp, ["completed", "failed"]);

    $this->assertEquals(3, $deleted);
  }

  /**
   * @covers ::cleanup
   */
  public function testCleanupReturnsZeroWhenNothingToDelete(): void {
    $query = $this->prophesize(QueryInterface::class);
    $query->condition("created", Argument::any(), "<")->willReturn($query);
    $query->accessCheck(FALSE)->willReturn($query);
    $query->execute()->willReturn([]);

    $this->entityStorage->getQuery()->willReturn($query->reveal());

    $deleted = $this->storage->cleanup(time() - 86400);

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

}
