<?php

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

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\eb\Plugin\EbOperation\CreateFieldOperation;
use Drupal\eb\Service\DiscoveryServiceInterface;
use Drupal\eb\Service\DisplayConfigurationService;
use Drupal\eb\Service\FieldManagementServiceInterface;
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 CreateFieldOperation plugin.
 *
 * @coversDefaultClass \Drupal\eb\Plugin\EbOperation\CreateFieldOperation
 * @group eb
 */
class CreateFieldOperationTest extends EbUnitTestBase {

  use OperationTestTrait;
  use ValidationAssertionsTrait;

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

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

  /**
   * Mock field management service.
   */
  protected FieldManagementServiceInterface|MockObject $fieldManagementService;

  /**
   * Mock display configuration service.
   */
  protected DisplayConfigurationService|MockObject $displayConfigurationService;

  /**
   * 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->discoveryService = $this->createMock(DiscoveryServiceInterface::class);
    $this->fieldManagementService = $this->createMock(FieldManagementServiceInterface::class);
    $this->displayConfigurationService = $this->createMock(DisplayConfigurationService::class);
    $this->logger = $this->createMock(LoggerInterface::class);

    // Set up config factory mock with eb.settings.
    $mockConfig = $this->createMock(ImmutableConfig::class);
    $mockConfig->method('get')
      ->with('cardinality_unlimited')
      ->willReturn(-1);
    $this->configFactory = $this->createMock(ConfigFactoryInterface::class);
    $this->configFactory->method('get')
      ->with('eb.settings')
      ->willReturn($mockConfig);
  }

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

  /**
   * Gets valid field data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional overrides.
   *
   * @return array<string, mixed>
   *   The field data.
   */
  protected function getValidData(array $overrides = []): array {
    return array_merge([
      'entity_type' => 'node',
      'bundle' => 'article',
      'field_name' => 'field_subtitle',
      'field_type' => 'text',
      'label' => 'Subtitle',
    ], $overrides);
  }

  /**
   * Sets up mocks for valid field creation.
   */
  protected function setUpValidMocks(): void {
    // Field doesn't exist yet.
    $this->fieldManagementService
      ->method('fieldConfigExists')
      ->willReturn(FALSE);

    // Field configuration is valid.
    $this->discoveryService
      ->method('validateFieldConfiguration')
      ->willReturn([
        'valid' => TRUE,
        'errors' => [],
        'warnings' => [],
      ]);
  }

  /**
   * Tests validation passes with valid data.
   *
   * @covers ::validate
   */
  public function testValidatePassesWithValidData(): void {
    $this->setUpValidMocks();

    $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 is missing.
   *
   * @covers ::validate
   */
  public function testValidateFailsWithMissingBundle(): void {
    $data = $this->getValidData();
    unset($data['bundle']);

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

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

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

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

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

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

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

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

  /**
   * 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->fieldManagementService
      ->method('fieldConfigExists')
      ->willReturn(FALSE);

    // Return validation failure for entity type.
    $this->discoveryService
      ->method('validateFieldConfiguration')
      ->willReturn([
        'valid' => FALSE,
        'errors' => ['Entity type does not exist.'],
        'warnings' => [],
      ]);

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

    $this->assertValidationResultFails($result);
  }

  /**
   * Tests validation fails when bundle does not exist.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenBundleDoesNotExist(): void {
    $this->fieldManagementService
      ->method('fieldConfigExists')
      ->willReturn(FALSE);

    // Return validation failure for bundle.
    $this->discoveryService
      ->method('validateFieldConfiguration')
      ->willReturn([
        'valid' => FALSE,
        'errors' => ['Bundle does not exist.'],
        'warnings' => [],
      ]);

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

    $this->assertValidationResultFails($result);
  }

  /**
   * Tests validation fails when field type is invalid.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenFieldTypeInvalid(): void {
    $this->fieldManagementService
      ->method('fieldConfigExists')
      ->willReturn(FALSE);

    // Return validation failure for invalid field type.
    $this->discoveryService
      ->method('validateFieldConfiguration')
      ->willReturn([
        'valid' => FALSE,
        'errors' => ['Field type "invalid_type" does not exist.'],
        'warnings' => [],
      ]);

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

    $this->assertValidationResultFails($result);
  }

  /**
   * Tests validation fails when field config already exists.
   *
   * @covers ::validate
   */
  public function testValidateFailsWhenFieldConfigExists(): void {
    // Field already exists.
    $this->fieldManagementService
      ->method('fieldConfigExists')
      ->with('node', 'article', 'field_subtitle')
      ->willReturn(TRUE);

    // Field configuration is valid.
    $this->discoveryService
      ->method('validateFieldConfiguration')
      ->willReturn([
        'valid' => TRUE,
        'errors' => [],
        'warnings' => [],
      ]);

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

    $this->assertValidationResultFails($result);
  }

  /**
   * Tests validation with invalid field_name format.
   *
   * @covers ::validate
   * @dataProvider invalidFieldNameProvider
   */
  public function testValidateFailsWithInvalidFieldNameFormat(string $fieldName): void {
    $this->setUpValidMocks();

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

    // Field names must start with "field_" and contain only lowercase letters,
    // numbers, and underscores.
    $this->assertValidationResultFails($result);
  }

  /**
   * Data provider for invalid field names.
   *
   * @return array<string, array<string>>
   *   Test cases.
   */
  public static function invalidFieldNameProvider(): array {
    return [
      'too_short' => ['f'],
      'starts_with_number' => ['123field'],
      'contains_uppercase' => ['Field_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 field information in description.
   *
   * @covers ::preview
   */
  public function testPreviewIncludesFieldInfo(): void {
    $data = $this->getValidData([
      'field_name' => 'field_custom',
      'label' => 'Custom Field',
      'field_type' => 'text_long',
    ]);

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

    // Details are added to the description string via addDetails().
    $description = $result->getDescription();
    $this->assertStringContainsString('Field Name: field_custom', $description);
    $this->assertStringContainsString('Field Type: text_long', $description);
  }

  /**
   * Tests validation with various field types.
   *
   * @covers ::validate
   * @dataProvider fieldTypeProvider
   */
  public function testValidationWithVariousFieldTypes(string $fieldType): void {
    $this->setUpValidMocks();

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

    $this->assertValidationResultPasses($result);
  }

  /**
   * Data provider for common field types.
   *
   * @return array<string, array<string>>
   *   Test cases.
   */
  public static function fieldTypeProvider(): array {
    return [
      'text' => ['text'],
      'text_long' => ['text_long'],
      'text_with_summary' => ['text_with_summary'],
      'string' => ['string'],
      'integer' => ['integer'],
      'boolean' => ['boolean'],
      'entity_reference' => ['entity_reference'],
      'datetime' => ['datetime'],
      'link' => ['link'],
      'list_string' => ['list_string'],
    ];
  }

  /**
   * Tests getPluginId returns correct ID.
   *
   * @covers ::getPluginId
   */
  public function testGetPluginIdReturnsCorrectId(): void {
    $operation = $this->createOperation($this->getValidData());
    $this->assertEquals('create_field', $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'], $retrievedData['bundle']);
    $this->assertEquals($data['field_name'], $retrievedData['field_name']);
    $this->assertEquals($data['field_type'], $retrievedData['field_type']);
  }

}
