<?php

namespace Drupal\Tests\eb\Unit\Plugin\EbOperation;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\eb\Plugin\EbOperation\CreateBundleOperation;
use Drupal\eb\Service\DiscoveryServiceInterface;
use Drupal\Tests\eb\Traits\OperationTestTrait;
use Drupal\Tests\eb\Traits\ValidationAssertionsTrait;
use Drupal\Tests\eb\Unit\EbUnitTestBase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for CreateBundleOperation plugin.
 *
 * @coversDefaultClass \Drupal\eb\Plugin\EbOperation\CreateBundleOperation
 * @group eb
 */
class CreateBundleOperationTest extends EbUnitTestBase {

  use OperationTestTrait;
  use ValidationAssertionsTrait;

  /**
   * Mock entity type manager.
   */
  protected EntityTypeManagerInterface|MockObject $entityTypeManager;

  /**
   * Mock entity field manager.
   */
  protected EntityFieldManagerInterface|MockObject $entityFieldManager;

  /**
   * Mock discovery service.
   */
  protected DiscoveryServiceInterface|MockObject $discoveryService;

  /**
   * Mock config factory.
   */
  protected ConfigFactoryInterface|MockObject $configFactory;

  /**
   * Mock logger.
   */
  protected LoggerInterface|MockObject $logger;

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

    $this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $this->entityFieldManager = $this->createMock(EntityFieldManagerInterface::class);
    $this->discoveryService = $this->createMock(DiscoveryServiceInterface::class);
    $this->configFactory = $this->createMock(ConfigFactoryInterface::class);
    $this->logger = $this->createMock(LoggerInterface::class);
  }

  /**
   * Creates a CreateBundleOperation instance for testing.
   *
   * @param array<string, mixed> $data
   *   The operation data.
   *
   * @return \Drupal\eb\Plugin\EbOperation\CreateBundleOperation
   *   The operation instance.
   */
  protected function createOperation(array $data): CreateBundleOperation {
    return new CreateBundleOperation(
      ['data' => $data],
      'create_bundle',
      [
        'id' => 'create_bundle',
        'label' => 'Create Bundle',
        'operationType' => 'create',
      ],
      $this->entityTypeManager,
      $this->logger,
      $this->configFactory,
      $this->discoveryService,
      $this->entityFieldManager
    );
  }

  /**
   * Gets valid bundle data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional overrides.
   *
   * @return array<string, mixed>
   *   The bundle data.
   */
  protected function getValidData(array $overrides = []): array {
    return array_merge([
      'entity_type' => 'node',
      'bundle_id' => 'test_article',
      'label' => 'Test Article',
      'description' => 'A test article bundle.',
    ], $overrides);
  }

  /**
   * Tests validation passes with valid data.
   *
   * @covers ::validate
   */
  public function testValidatePassesWithValidData(): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->with('node')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('isBundleable')
      ->with('node')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('bundleExists')
      ->with('node', 'test_article')
      ->willReturn(FALSE);

    $operation = $this->createOperation($this->getValidData());
    $result = $operation->validate();

    $this->assertValidationResultPasses($result);
  }

  /**
   * Tests validation fails when entity_type is missing.
   *
   * @covers ::validate
   */
  public function testValidateFailsWithMissingEntityType(): void {
    $data = $this->getValidData();
    unset($data['entity_type']);

    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasError($result, 'entity_type');
  }

  /**
   * Tests validation fails when bundle_id is missing.
   *
   * @covers ::validate
   */
  public function testValidateFailsWithMissingBundleId(): void {
    $data = $this->getValidData();
    unset($data['bundle_id']);

    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasError($result, 'bundle_id');
  }

  /**
   * Tests validation fails when label is missing.
   *
   * @covers ::validate
   */
  public function testValidateFailsWithMissingLabel(): void {
    $data = $this->getValidData();
    unset($data['label']);

    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasError($result, 'label');
  }

  /**
   * Tests validation fails when entity type does not exist.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenEntityTypeDoesNotExist(): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->with('nonexistent_type')
      ->willReturn(FALSE);

    $data = $this->getValidData(['entity_type' => 'nonexistent_type']);
    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasErrorCode($result, 'invalid_entity_type');
  }

  /**
   * Tests validation fails when entity type is not bundleable.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenEntityTypeNotBundleable(): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('isBundleable')
      ->with('user')
      ->willReturn(FALSE);

    $data = $this->getValidData(['entity_type' => 'user']);
    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasErrorCode($result, 'not_bundleable');
  }

  /**
   * Tests validation fails when bundle already exists.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenBundleAlreadyExists(): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('isBundleable')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('bundleExists')
      ->with('node', 'existing_bundle')
      ->willReturn(TRUE);

    $data = $this->getValidData(['bundle_id' => 'existing_bundle']);
    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasErrorCode($result, 'bundle_already_exists');
  }

  /**
   * Tests validation fails with invalid bundle_id format.
   *
   * @covers ::validate
   * @dataProvider invalidBundleIdProvider
   */
  public function testValidateFailsWithInvalidBundleIdFormat(string $bundleId): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('isBundleable')
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('bundleExists')
      ->willReturn(FALSE);

    $data = $this->getValidData(['bundle_id' => $bundleId]);
    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultFails($result);
    $this->assertValidationHasErrorCode($result, 'invalid_bundle_id');
  }

  /**
   * Data provider for invalid bundle IDs.
   *
   * @return array<string, array<string>>
   *   Test cases.
   */
  public static function invalidBundleIdProvider(): array {
    return [
      'starts_with_number' => ['123bundle'],
      'contains_uppercase' => ['TestBundle'],
      'contains_spaces' => ['test bundle'],
      'contains_hyphens' => ['test-bundle'],
      'starts_with_underscore' => ['_test'],
    ];
  }

  /**
   * Tests preview returns operation details.
   *
   * @covers ::preview
   */
  public function testPreviewReturnsOperationDetails(): void {
    $data = $this->getValidData();
    $operation = $this->createOperation($data);
    $result = $operation->preview();

    $this->assertPreviewHasOperations($result);
    $this->assertPreviewHasOperationType($result, 'create');
    $this->assertPreviewHasDetails($result);
  }

  /**
   * Tests preview includes bundle information in description.
   *
   * @covers ::preview
   */
  public function testPreviewIncludesBundleInfo(): void {
    $data = $this->getValidData([
      'label' => 'My Article',
      'description' => 'Article description',
    ]);

    $operation = $this->createOperation($data);
    $result = $operation->preview();

    // Details are added to the description string via addDetails().
    $description = $result->getDescription();
    $this->assertStringContainsString('Entity Type: node', $description);
    $this->assertStringContainsString('Bundle ID: test_article', $description);
    $this->assertStringContainsString('Label: My Article', $description);
  }

  /**
   * Tests getPluginId returns correct ID.
   *
   * @covers ::getPluginId
   */
  public function testGetPluginIdReturnsCorrectId(): void {
    $operation = $this->createOperation($this->getValidData());
    $this->assertEquals('create_bundle', $operation->getPluginId());
  }

  /**
   * Tests getData returns operation data.
   *
   * @covers ::getData
   */
  public function testGetDataReturnsOperationData(): void {
    $data = $this->getValidData();
    $operation = $this->createOperation($data);

    $retrievedData = $operation->getData();

    $this->assertEquals($data['entity_type'], $retrievedData['entity_type']);
    $this->assertEquals($data['bundle_id'], $retrievedData['bundle_id']);
    $this->assertEquals($data['label'], $retrievedData['label']);
  }

  /**
   * Tests validation with all supported entity types.
   *
   * @covers ::validate
   * @dataProvider supportedEntityTypeProvider
   */
  public function testValidationWithSupportedEntityTypes(string $entityType): void {
    $this->discoveryService
      ->method('entityTypeExists')
      ->with($entityType)
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('isBundleable')
      ->with($entityType)
      ->willReturn(TRUE);

    $this->discoveryService
      ->method('bundleExists')
      ->willReturn(FALSE);

    $data = $this->getValidData(['entity_type' => $entityType]);
    $operation = $this->createOperation($data);
    $result = $operation->validate();

    $this->assertValidationResultPasses($result);
  }

  /**
   * Data provider for supported entity types.
   *
   * @return array<string, array<string>>
   *   Test cases.
   */
  public static function supportedEntityTypeProvider(): array {
    return [
      'node' => ['node'],
      'taxonomy_term' => ['taxonomy_term'],
      'media' => ['media'],
      'paragraph' => ['paragraph'],
    ];
  }

}
