<?php

namespace Drupal\Tests\eb\Unit\Service;

use Drupal\eb\PluginInterfaces\OperationInterface;
use Drupal\eb\PluginManager\EbOperationPluginManager;
use Drupal\eb\Service\OperationBuilder;
use Drupal\Tests\eb\Unit\EbUnitTestBase;
use PHPUnit\Framework\MockObject\MockObject;

/**
 * Unit tests for OperationBuilder service.
 *
 * @coversDefaultClass \Drupal\eb\Service\OperationBuilder
 * @group eb
 */
class OperationBuilderTest extends EbUnitTestBase {

  /**
   * The operation builder service under test.
   */
  protected OperationBuilder $operationBuilder;

  /**
   * Mock operation plugin manager.
   */
  protected EbOperationPluginManager|MockObject $mockOperationManager;

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

    $this->mockOperationManager = $this->createMock(EbOperationPluginManager::class);
    $this->operationBuilder = new OperationBuilder($this->mockOperationManager);
  }

  /**
   * Tests buildOperation creates an operation instance.
   *
   * @covers ::buildOperation
   */
  public function testBuildOperationCreatesInstance(): void {
    $data = $this->getValidBundleData();
    $mockOperation = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->expects($this->once())
      ->method('createInstance')
      ->with('create_bundle', ['data' => $data])
      ->willReturn($mockOperation);

    $result = $this->operationBuilder->buildOperation('create_bundle', $data);

    $this->assertSame($mockOperation, $result);
  }

  /**
   * Tests buildOperation with different operation types.
   *
   * @covers ::buildOperation
   * @dataProvider operationTypeProvider
   */
  public function testBuildOperationWithDifferentTypes(string $operationType, array $data): void {
    $mockOperation = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->expects($this->once())
      ->method('createInstance')
      ->with($operationType, ['data' => $data])
      ->willReturn($mockOperation);

    $result = $this->operationBuilder->buildOperation($operationType, $data);

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

  /**
   * Data provider for operation types.
   *
   * @return array<string, array<mixed>>
   *   Test data.
   */
  public static function operationTypeProvider(): array {
    return [
      'create_bundle' => [
        'create_bundle',
        [
          'entity_type' => 'node',
          'bundle_id' => 'article',
          'label' => 'Article',
        ],
      ],
      'create_field' => [
        'create_field',
        [
          'entity_type' => 'node',
          'bundle' => 'article',
          'field_name' => 'field_test',
          'field_type' => 'text',
          'label' => 'Test Field',
        ],
      ],
      'create_role' => [
        'create_role',
        [
          'role_id' => 'editor',
          'label' => 'Editor',
          'permissions' => ['access content'],
        ],
      ],
      'create_menu' => [
        'create_menu',
        [
          'menu_id' => 'main',
          'label' => 'Main Menu',
        ],
      ],
    ];
  }

  /**
   * Tests buildBatch creates multiple operations.
   *
   * @covers ::buildBatch
   */
  public function testBuildBatchCreatesMultipleOperations(): void {
    $batchData = [
      ['operation' => 'create_bundle', 'entity_type' => 'node', 'bundle_id' => 'article', 'label' => 'Article'],
      [
        'operation' => 'create_field',
        'entity_type' => 'node',
        'bundle' => 'article',
        'field_name' => 'field_test',
        'field_type' => 'text',
        'label' => 'Test',
      ],
    ];

    $mockOperation1 = $this->createMock(OperationInterface::class);
    $mockOperation2 = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->expects($this->exactly(2))
      ->method('createInstance')
      ->willReturnOnConsecutiveCalls($mockOperation1, $mockOperation2);

    $results = $this->operationBuilder->buildBatch($batchData);

    $this->assertCount(2, $results);
    $this->assertSame($mockOperation1, $results[0]);
    $this->assertSame($mockOperation2, $results[1]);
  }

  /**
   * Tests buildBatch supports 'operation_type' key for backward compatibility.
   *
   * @covers ::buildBatch
   */
  public function testBuildBatchSupportsOperationTypeKey(): void {
    $batchData = [
      ['operation_type' => 'create_bundle', 'entity_type' => 'node', 'bundle_id' => 'article', 'label' => 'Article'],
    ];

    $mockOperation = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->expects($this->once())
      ->method('createInstance')
      ->with('create_bundle', $this->anything())
      ->willReturn($mockOperation);

    $results = $this->operationBuilder->buildBatch($batchData);

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

  /**
   * Tests buildBatch skips entries without operation key.
   *
   * @covers ::buildBatch
   */
  public function testBuildBatchSkipsEntriesWithoutOperationKey(): void {
    $batchData = [
      ['operation' => 'create_bundle', 'entity_type' => 'node', 'bundle_id' => 'article', 'label' => 'Article'],
      ['entity_type' => 'node', 'bundle_id' => 'page'],
      [
        'operation' => 'create_field',
        'entity_type' => 'node',
        'field_name' => 'field_test',
        'field_type' => 'text',
        'label' => 'Test',
      ],
    ];

    $mockOperation1 = $this->createMock(OperationInterface::class);
    $mockOperation2 = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->expects($this->exactly(2))
      ->method('createInstance')
      ->willReturnOnConsecutiveCalls($mockOperation1, $mockOperation2);

    $results = $this->operationBuilder->buildBatch($batchData);

    $this->assertCount(2, $results);
    $this->assertArrayHasKey(0, $results);
    $this->assertArrayNotHasKey(1, $results);
    $this->assertArrayHasKey(2, $results);
  }

  /**
   * Tests buildBatch handles plugin exceptions gracefully.
   *
   * @covers ::buildBatch
   */
  public function testBuildBatchHandlesPluginExceptions(): void {
    $batchData = [
      ['operation' => 'create_bundle', 'entity_type' => 'node', 'bundle_id' => 'article', 'label' => 'Article'],
      ['operation' => 'invalid_operation', 'data' => 'test'],
      [
        'operation' => 'create_field',
        'entity_type' => 'node',
        'field_name' => 'field_test',
        'field_type' => 'text',
        'label' => 'Test',
      ],
    ];

    $mockOperation1 = $this->createMock(OperationInterface::class);
    $mockOperation2 = $this->createMock(OperationInterface::class);

    $this->mockOperationManager
      ->method('createInstance')
      ->willReturnCallback(function ($pluginId) use ($mockOperation1, $mockOperation2) {
        if ($pluginId === 'invalid_operation') {
          throw new \Exception('Plugin not found');
        }
        static $callCount = 0;
        return $callCount++ === 0 ? $mockOperation1 : $mockOperation2;
      });

    $results = $this->operationBuilder->buildBatch($batchData);

    $this->assertCount(2, $results);
    $this->assertArrayNotHasKey(1, $results);
  }

  /**
   * Tests buildBatch with empty data.
   *
   * @covers ::buildBatch
   */
  public function testBuildBatchWithEmptyData(): void {
    $this->mockOperationManager
      ->expects($this->never())
      ->method('createInstance');

    $results = $this->operationBuilder->buildBatch([]);

    $this->assertEmpty($results);
  }

  /**
   * Tests getAvailableOperations returns definitions.
   *
   * @covers ::getAvailableOperations
   */
  public function testGetAvailableOperationsReturnsDefinitions(): void {
    $definitions = [
      'create_bundle' => ['id' => 'create_bundle', 'label' => 'Create Bundle'],
      'create_field' => ['id' => 'create_field', 'label' => 'Create Field'],
      'create_menu' => ['id' => 'create_menu', 'label' => 'Create Menu'],
    ];

    $this->mockOperationManager
      ->expects($this->once())
      ->method('getDefinitions')
      ->willReturn($definitions);

    $results = $this->operationBuilder->getAvailableOperations();

    $this->assertEquals($definitions, $results);
    $this->assertArrayHasKey('create_bundle', $results);
    $this->assertArrayHasKey('create_field', $results);
    $this->assertArrayHasKey('create_menu', $results);
  }

  /**
   * Tests operationExists returns true for existing operations.
   *
   * @covers ::operationExists
   */
  public function testOperationExistsReturnsTrueForExisting(): void {
    $definitions = [
      'create_bundle' => ['id' => 'create_bundle'],
      'create_field' => ['id' => 'create_field'],
    ];

    $this->mockOperationManager
      ->method('getDefinitions')
      ->willReturn($definitions);

    $this->assertTrue($this->operationBuilder->operationExists('create_bundle'));
    $this->assertTrue($this->operationBuilder->operationExists('create_field'));
  }

  /**
   * Tests operationExists returns false for non-existing operations.
   *
   * @covers ::operationExists
   */
  public function testOperationExistsReturnsFalseForNonExisting(): void {
    $definitions = [
      'create_bundle' => ['id' => 'create_bundle'],
    ];

    $this->mockOperationManager
      ->method('getDefinitions')
      ->willReturn($definitions);

    $this->assertFalse($this->operationBuilder->operationExists('non_existing_operation'));
    $this->assertFalse($this->operationBuilder->operationExists('delete_everything'));
  }

}
