<?php

namespace Drupal\Tests\eb\Kernel\Service;

use Drupal\eb\Entity\EbRollback;
use Drupal\eb\Entity\EbRollbackOperation;
use Drupal\eb\Service\RollbackManagerInterface;
use Drupal\Tests\eb\Kernel\EbKernelTestBase;
use Drupal\Tests\eb\Traits\OperationTestTrait;

/**
 * Kernel tests for RollbackManager service.
 *
 * @coversDefaultClass \Drupal\eb\Service\RollbackManager
 * @group eb
 */
class RollbackManagerKernelTest extends EbKernelTestBase {

  use OperationTestTrait;

  /**
   * The rollback manager service.
   */
  protected RollbackManagerInterface $rollbackManager;

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

    $this->rollbackManager = $this->container->get('eb.rollback_manager');
  }

  /**
   * Tests startRollback() creates parent entity.
   *
   * @covers ::startRollback
   */
  public function testStartRollbackCreatesParentEntity(): void {
    $rollback = $this->rollbackManager->startRollback('test_def', 'Apply: test_def');

    $this->assertNotNull($rollback);
    $this->assertNotNull($rollback->id());
    $this->assertEquals('test_def', $rollback->getDefinitionId());
    $this->assertEquals('Apply: test_def', $rollback->label());
    $this->assertEquals('pending', $rollback->getStatus());
    $this->assertEquals($this->adminUser->id(), $rollback->getOwnerId());
  }

  /**
   * Tests storeRollbackData() creates operation entities.
   *
   * Note: OperationProcessor already calls storeRollbackData internally,
   * so executing an operation will create an operation entity automatically.
   *
   * @covers ::storeRollbackData
   */
  public function testStoreRollbackDataCreatesOperationEntities(): void {
    // Start rollback.
    $rollback = $this->rollbackManager->startRollback('test_def', 'Apply: test_def');
    $rollbackId = (int) $rollback->id();

    // Create a bundle - OperationProcessor will call storeRollbackData.
    $operationBuilder = $this->container->get('eb.operation_builder');
    $operationProcessor = $this->container->get('eb.operation_processor');

    $data = $this->createBundleOperationData('node', 'test_article', 'Test Article');
    $operation = $operationBuilder->buildOperation('create_bundle', $data);
    $operationProcessor->executeOperation($operation);

    $this->rollbackManager->finalizeRollback();

    // Verify operation was created.
    $operations = $this->rollbackManager->getOperationsForRollback($rollbackId);
    $this->assertCount(1, $operations);

    $opEntity = reset($operations);
    $this->assertNotNull($opEntity);
    $this->assertNotNull($opEntity->id());
    $this->assertEquals('test_def', $opEntity->getDefinitionId());
    $this->assertEquals('create_bundle', $opEntity->getOperationType());
    $this->assertNotEmpty($opEntity->getOriginalData());
    $this->assertEquals(0, $opEntity->getSequence());
  }

  /**
   * Tests finalizeRollback() updates operation count.
   *
   * @covers ::finalizeRollback
   */
  public function testFinalizeRollbackUpdatesOperationCount(): void {
    // Start rollback.
    $rollback = $this->rollbackManager->startRollback('test_def', 'Apply: test_def');
    $rollbackId = (int) $rollback->id();

    // Create bundles - OperationProcessor will call storeRollbackData.
    $operationBuilder = $this->container->get('eb.operation_builder');
    $operationProcessor = $this->container->get('eb.operation_processor');

    for ($i = 1; $i <= 3; $i++) {
      $data = $this->createBundleOperationData('node', "bundle_$i", "Bundle $i");
      $operation = $operationBuilder->buildOperation('create_bundle', $data);
      $operationProcessor->executeOperation($operation);
    }

    // Finalize.
    $this->rollbackManager->finalizeRollback();

    // Reload and verify.
    $loaded = EbRollback::load($rollbackId);
    $this->assertEquals(3, $loaded->getOperationCount());
  }

  /**
   * Tests loadRollback() returns entity.
   *
   * @covers ::loadRollback
   */
  public function testLoadRollbackReturnsEntity(): void {
    $rollback = $this->rollbackManager->startRollback('test_def', 'Apply: test_def');
    $this->rollbackManager->finalizeRollback();

    $loaded = $this->rollbackManager->loadRollback((int) $rollback->id());

    $this->assertNotNull($loaded);
    $this->assertEquals($rollback->id(), $loaded->id());
  }

  /**
   * Tests loadRollback() returns NULL for non-existent ID.
   *
   * @covers ::loadRollback
   */
  public function testLoadRollbackReturnsNullForNonExistent(): void {
    $loaded = $this->rollbackManager->loadRollback(99999);
    $this->assertNull($loaded);
  }

  /**
   * Tests getOperationsForRollback() returns child entities.
   *
   * @covers ::getOperationsForRollback
   */
  public function testGetOperationsForRollbackReturnsChildEntities(): void {
    // Start rollback.
    $rollback = $this->rollbackManager->startRollback('test_def', 'Apply: test_def');
    $rollbackId = (int) $rollback->id();

    // Create bundles - OperationProcessor will call storeRollbackData.
    $operationBuilder = $this->container->get('eb.operation_builder');
    $operationProcessor = $this->container->get('eb.operation_processor');

    for ($i = 1; $i <= 2; $i++) {
      $data = $this->createBundleOperationData('node', "op_bundle_$i", "OP Bundle $i");
      $operation = $operationBuilder->buildOperation('create_bundle', $data);
      $operationProcessor->executeOperation($operation);
    }

    $this->rollbackManager->finalizeRollback();

    // Get operations.
    $operations = $this->rollbackManager->getOperationsForRollback($rollbackId);

    $this->assertCount(2, $operations);
    foreach ($operations as $op) {
      $this->assertEquals($rollbackId, $op->getRollbackId());
    }
  }

  /**
   * Tests listRollbacks() with conditions.
   *
   * @covers ::listRollbacks
   */
  public function testListRollbacksWithConditions(): void {
    // Create multiple rollbacks.
    $this->rollbackManager->startRollback('def_a', 'Apply: def_a');
    $this->rollbackManager->finalizeRollback();

    $this->rollbackManager->startRollback('def_b', 'Apply: def_b');
    $this->rollbackManager->finalizeRollback();

    // List all.
    $all = $this->rollbackManager->listRollbacks();
    $this->assertCount(2, $all);

    // List with condition.
    $filtered = $this->rollbackManager->listRollbacks(['definition_id' => 'def_a']);
    $this->assertCount(1, $filtered);
    $this->assertEquals('def_a', reset($filtered)->getDefinitionId());
  }

  /**
   * Tests listRollbacksByDefinition().
   *
   * @covers ::listRollbacksByDefinition
   */
  public function testListRollbacksByDefinition(): void {
    // Create rollbacks for different definitions.
    $this->rollbackManager->startRollback('def_x', 'Apply: def_x 1');
    $this->rollbackManager->finalizeRollback();

    $this->rollbackManager->startRollback('def_x', 'Apply: def_x 2');
    $this->rollbackManager->finalizeRollback();

    $this->rollbackManager->startRollback('def_y', 'Apply: def_y');
    $this->rollbackManager->finalizeRollback();

    // List by definition.
    $defXRollbacks = $this->rollbackManager->listRollbacksByDefinition('def_x');
    $this->assertCount(2, $defXRollbacks);

    $defYRollbacks = $this->rollbackManager->listRollbacksByDefinition('def_y');
    $this->assertCount(1, $defYRollbacks);
  }

  /**
   * Tests listRollbacksByDefinition() with status filter.
   *
   * @covers ::listRollbacksByDefinition
   */
  public function testListRollbacksByDefinitionWithStatusFilter(): void {
    // Create a pending rollback.
    $this->rollbackManager->startRollback('filtered_def', 'Apply: filtered_def');
    $this->rollbackManager->finalizeRollback();

    // Create a completed rollback.
    $rollback2 = $this->rollbackManager->startRollback('filtered_def', 'Apply: filtered_def 2');
    $this->rollbackManager->finalizeRollback();
    $rollback2->setStatus('completed')->save();

    // Filter by status.
    $pending = $this->rollbackManager->listRollbacksByDefinition('filtered_def', 'pending');
    $this->assertCount(1, $pending);

    $completed = $this->rollbackManager->listRollbacksByDefinition('filtered_def', 'completed');
    $this->assertCount(1, $completed);
  }

  /**
   * Tests purgeOldRollbacks() cleanup.
   *
   * @covers ::purgeOldRollbacks
   */
  public function testPurgeOldRollbacksCleanup(): void {
    // Create a rollback.
    $rollback = $this->rollbackManager->startRollback('old_def', 'Apply: old_def');
    $this->rollbackManager->finalizeRollback();

    // Manually backdate the rollback.
    $oldTimestamp = time() - (40 * 86400);
    \Drupal::database()->update('eb_rollback')
      ->fields(['created' => $oldTimestamp])
      ->condition('id', $rollback->id())
      ->execute();

    // Purge with 30 day retention.
    $deleted = $this->rollbackManager->purgeOldRollbacks(30);

    $this->assertEquals(1, $deleted);
    $this->assertNull(EbRollback::load($rollback->id()));
  }

  /**
   * Tests purgeOldRollbacks() also deletes operations.
   *
   * @covers ::purgeOldRollbacks
   */
  public function testPurgeOldRollbacksAlsoDeletesOperations(): void {
    // Start rollback and add operation.
    $rollback = $this->rollbackManager->startRollback('purge_def', 'Apply: purge_def');
    $rollbackId = (int) $rollback->id();

    // Create an operation entity directly.
    $opEntity = EbRollbackOperation::create([
      'rollback_id' => $rollbackId,
      'definition_id' => 'purge_def',
      'operation_type' => 'create_bundle',
      'description' => 'Test operation',
      'original_data' => ['test' => 'data'],
      'sequence' => 0,
    ]);
    $opEntity->save();
    $opId = $opEntity->id();

    $this->rollbackManager->finalizeRollback();

    // Backdate the rollback.
    $oldTimestamp = time() - (40 * 86400);
    \Drupal::database()->update('eb_rollback')
      ->fields(['created' => $oldTimestamp])
      ->condition('id', $rollbackId)
      ->execute();

    // Purge.
    $this->rollbackManager->purgeOldRollbacks(30);

    // Both should be deleted.
    $this->assertNull(EbRollback::load($rollbackId));
    $this->assertNull(EbRollbackOperation::load($opId));
  }

  /**
   * Tests validateRollback() returns valid for pending rollback with ops.
   *
   * @covers ::validateRollback
   */
  public function testValidateRollbackValid(): void {
    // Create rollback with operations.
    $rollback = $this->rollbackManager->startRollback('valid_def', 'Apply: valid_def');
    $rollbackId = (int) $rollback->id();

    $opEntity = EbRollbackOperation::create([
      'rollback_id' => $rollbackId,
      'definition_id' => 'valid_def',
      'operation_type' => 'create_bundle',
      'description' => 'Test operation',
      'original_data' => ['test' => 'data'],
      'sequence' => 0,
    ]);
    $opEntity->save();

    $this->rollbackManager->finalizeRollback();

    $result = $this->rollbackManager->validateRollback($rollbackId);

    $this->assertTrue($result['valid']);
    $this->assertEmpty($result['errors']);
  }

  /**
   * Tests validateRollback() returns invalid for non-existent rollback.
   *
   * @covers ::validateRollback
   */
  public function testValidateRollbackNotFound(): void {
    $result = $this->rollbackManager->validateRollback(99999);

    $this->assertFalse($result['valid']);
    $this->assertContains('Rollback entity not found', $result['errors']);
  }

  /**
   * Tests validateRollback() returns invalid for completed rollback.
   *
   * @covers ::validateRollback
   */
  public function testValidateRollbackAlreadyCompleted(): void {
    $rollback = $this->rollbackManager->startRollback('completed_def', 'Apply: completed_def');
    $rollbackId = (int) $rollback->id();
    $this->rollbackManager->finalizeRollback();

    // Mark as completed.
    $rollback->setStatus('completed')->save();

    $result = $this->rollbackManager->validateRollback($rollbackId);

    $this->assertFalse($result['valid']);
    $this->assertContains('Rollback already executed', $result['errors']);
  }

  /**
   * Tests validateRollback() returns invalid for rollback with no operations.
   *
   * @covers ::validateRollback
   */
  public function testValidateRollbackNoOperations(): void {
    $rollback = $this->rollbackManager->startRollback('empty_def', 'Apply: empty_def');
    $rollbackId = (int) $rollback->id();
    $this->rollbackManager->finalizeRollback();

    $result = $this->rollbackManager->validateRollback($rollbackId);

    $this->assertFalse($result['valid']);
    $this->assertContains('No operations found for rollback', $result['errors']);
  }

  /**
   * Tests executeRollback() processes operations in reverse order.
   *
   * @covers ::executeRollback
   */
  public function testExecuteRollbackProcessesInReverseOrder(): void {
    // Start rollback and create bundles.
    $rollback = $this->rollbackManager->startRollback('reverse_def', 'Apply: reverse_def');
    $rollbackId = (int) $rollback->id();

    $operationBuilder = $this->container->get('eb.operation_builder');
    $operationProcessor = $this->container->get('eb.operation_processor');

    // Create bundles in sequence.
    $bundles = ['reverse_bundle_1', 'reverse_bundle_2', 'reverse_bundle_3'];
    foreach ($bundles as $bundle) {
      $data = $this->createBundleOperationData('node', $bundle, ucfirst($bundle));
      $operation = $operationBuilder->buildOperation('create_bundle', $data);
      $result = $operationProcessor->executeOperation($operation);
      $this->rollbackManager->storeRollbackData($operation, $result);
    }

    $this->rollbackManager->finalizeRollback();

    // Verify bundles exist.
    foreach ($bundles as $bundle) {
      $this->assertNodeTypeExists($bundle);
    }

    // Execute rollback.
    $result = $this->rollbackManager->executeRollback($rollbackId);

    $this->assertTrue($result->isSuccess());

    // Verify bundles are deleted.
    foreach ($bundles as $bundle) {
      $this->assertNodeTypeNotExists($bundle);
    }

    // Verify status is completed.
    $loaded = EbRollback::load($rollbackId);
    $this->assertEquals('completed', $loaded->getStatus());
    $this->assertNotNull($loaded->getCompletedTime());
  }

  /**
   * Tests deleteByDefinition() removes all rollbacks for a definition.
   *
   * @covers ::deleteByDefinition
   */
  public function testDeleteByDefinition(): void {
    // Create multiple rollbacks for same definition.
    $this->rollbackManager->startRollback('delete_def', 'Apply 1');
    $this->rollbackManager->finalizeRollback();

    $this->rollbackManager->startRollback('delete_def', 'Apply 2');
    $this->rollbackManager->finalizeRollback();

    $this->rollbackManager->startRollback('keep_def', 'Keep me');
    $this->rollbackManager->finalizeRollback();

    // Delete by definition.
    $deleted = $this->rollbackManager->deleteByDefinition('delete_def');

    $this->assertEquals(2, $deleted);
    $this->assertEmpty($this->rollbackManager->listRollbacksByDefinition('delete_def'));
    $this->assertCount(1, $this->rollbackManager->listRollbacksByDefinition('keep_def'));
  }

  /**
   * Tests sequence counter increments for each operation.
   *
   * @covers ::storeRollbackData
   */
  public function testSequenceCounterIncrements(): void {
    // Start rollback.
    $rollback = $this->rollbackManager->startRollback('seq_def', 'Apply: seq_def');
    $rollbackId = (int) $rollback->id();

    $operationBuilder = $this->container->get('eb.operation_builder');
    $operationProcessor = $this->container->get('eb.operation_processor');

    // Create operations - OperationProcessor calls storeRollbackData.
    for ($i = 0; $i < 3; $i++) {
      $data = $this->createBundleOperationData('node', "seq_bundle_$i", "Seq Bundle $i");
      $operation = $operationBuilder->buildOperation('create_bundle', $data);
      $operationProcessor->executeOperation($operation);
    }

    $this->rollbackManager->finalizeRollback();

    // Check sequences.
    $operations = $this->rollbackManager->getOperationsForRollback($rollbackId);
    $sequences = array_map(fn($op) => $op->getSequence(), $operations);
    sort($sequences);

    $this->assertEquals([0, 1, 2], $sequences);
  }

}
