<?php

namespace Drupal\Tests\eb\Unit;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\eb\PluginInterfaces\OperationInterface;
use Drupal\eb\PluginManager\EbExtensionPluginManager;
use Drupal\eb\PluginManager\EbOperationPluginManager;
use Drupal\eb\PluginManager\EbValidatorPluginManager;
use Drupal\eb\Result\ValidationResult;
use Drupal\eb\Service\DiscoveryServiceInterface;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;

/**
 * Base class for Entity Builder unit tests.
 *
 * Provides common mock factories, reflection utilities, and helper methods
 * for testing Entity Builder components in isolation.
 *
 * @group eb
 */
abstract class EbUnitTestBase extends UnitTestCase {

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

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

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

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

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

  /**
   * Mock validator plugin manager.
   */
  protected EbValidatorPluginManager|MockObject $validatorPluginManager;

  /**
   * Mock extension plugin manager.
   */
  protected EbExtensionPluginManager|MockObject $extensionPluginManager;

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

    // Set up a container with string translation service for unit tests.
    $this->setUpContainer();

    // Create common mocks.
    $this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $this->entityFieldManager = $this->createMock(EntityFieldManagerInterface::class);
    $this->logger = $this->createMock(LoggerInterface::class);
    $this->discoveryService = $this->createMock(DiscoveryServiceInterface::class);
    $this->operationPluginManager = $this->createMock(EbOperationPluginManager::class);
    $this->validatorPluginManager = $this->createMock(EbValidatorPluginManager::class);
    $this->extensionPluginManager = $this->createMock(EbExtensionPluginManager::class);
  }

  /**
   * Sets up a minimal container with string translation.
   *
   * Needed for unit tests that use components with StringTranslationTrait.
   */
  protected function setUpContainer(): void {
    $container = new ContainerBuilder();

    // Create a mock translation service that returns untranslated strings.
    $stringTranslation = $this->createMock(TranslationInterface::class);
    $stringTranslation->method('translateString')
      ->willReturnCallback(function (TranslatableMarkup $string) {
        return $string->getUntranslatedString();
      });

    $container->set('string_translation', $stringTranslation);
    \Drupal::setContainer($container);
  }

  /**
   * Gets a private or protected method for testing.
   *
   * @param string $class
   *   The fully qualified class name.
   * @param string $method
   *   The method name.
   *
   * @return \ReflectionMethod
   *   The accessible reflection method.
   */
  protected function getPrivateMethod(string $class, string $method): \ReflectionMethod {
    $reflection = new \ReflectionClass($class);
    $method = $reflection->getMethod($method);
    $method->setAccessible(TRUE);
    return $method;
  }

  /**
   * Gets a private or protected property for testing.
   *
   * @param string $className
   *   The fully qualified class name.
   * @param string $propertyName
   *   The property name.
   *
   * @return \ReflectionProperty
   *   The accessible reflection property.
   */
  protected function getPrivateProperty(string $className, string $propertyName): \ReflectionProperty {
    $reflection = new \ReflectionClass($className);
    $property = $reflection->getProperty($propertyName);
    $property->setAccessible(TRUE);
    return $property;
  }

  /**
   * Sets a private or protected property value.
   *
   * @param object $object
   *   The object instance.
   * @param string $propertyName
   *   The property name.
   * @param mixed $value
   *   The value to set.
   */
  protected function setPrivateProperty(object $object, string $propertyName, mixed $value): void {
    $property = $this->getPrivateProperty(get_class($object), $propertyName);
    $property->setValue($object, $value);
  }

  /**
   * Creates a mock operation with specified data.
   *
   * @param array<string, mixed> $data
   *   The operation data.
   * @param string $operationType
   *   The operation type (plugin ID).
   *
   * @return \Drupal\eb\PluginInterfaces\OperationInterface|\PHPUnit\Framework\MockObject\MockObject
   *   The mock operation.
   */
  protected function createMockOperation(array $data, string $operationType = 'create_bundle'): OperationInterface|MockObject {
    $operation = $this->createMock(OperationInterface::class);
    $operation->method('getData')->willReturn($data);
    $operation->method('getPluginId')->willReturn($operationType);
    return $operation;
  }

  /**
   * Creates a mock validation result.
   *
   * @param bool $isValid
   *   Whether the result should be valid.
   * @param array<string> $errors
   *   Optional array of error messages.
   * @param array<string> $warnings
   *   Optional array of warning messages.
   *
   * @return \Drupal\eb\Result\ValidationResult
   *   The validation result.
   */
  protected function createValidationResult(bool $isValid = TRUE, array $errors = [], array $warnings = []): ValidationResult {
    $result = new ValidationResult();

    foreach ($errors as $error) {
      $result->addError($error, '', 'test_error');
    }

    foreach ($warnings as $warning) {
      $result->addWarning($warning);
    }

    return $result;
  }

  /**
   * Creates a mock entity storage.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param array<mixed> $entities
   *   Optional array of entities to return from load().
   *
   * @return \PHPUnit\Framework\MockObject\MockObject
   *   The mock storage.
   */
  protected function createMockStorage(string $entityTypeId, array $entities = []): MockObject {
    $storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');

    $storage->method('load')
      ->willReturnCallback(function ($id) use ($entities) {
        return $entities[$id] ?? NULL;
      });

    $storage->method('loadMultiple')
      ->willReturnCallback(function ($ids = NULL) use ($entities) {
        if ($ids === NULL) {
          return $entities;
        }
        return array_intersect_key($entities, array_flip($ids));
      });

    return $storage;
  }

  /**
   * Sets up entity type manager to return a storage.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param \PHPUnit\Framework\MockObject\MockObject $storage
   *   The mock storage to return.
   */
  protected function setUpStorage(string $entityTypeId, MockObject $storage): void {
    $this->entityTypeManager
      ->method('getStorage')
      ->with($entityTypeId)
      ->willReturn($storage);
  }

  /**
   * Creates valid bundle operation data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional data overrides.
   *
   * @return array<string, mixed>
   *   The operation data.
   */
  protected function getValidBundleData(array $overrides = []): array {
    return array_merge([
      'operation' => 'create_bundle',
      'entity_type' => 'node',
      'bundle_id' => 'test_bundle',
      'label' => 'Test Bundle',
      'description' => 'A test bundle for testing.',
    ], $overrides);
  }

  /**
   * Creates valid field operation data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional data overrides.
   *
   * @return array<string, mixed>
   *   The operation data.
   */
  protected function getValidFieldData(array $overrides = []): array {
    return array_merge([
      'operation' => 'create_field',
      'entity_type' => 'node',
      'bundle' => 'article',
      'field_name' => 'field_test',
      'field_type' => 'text',
      'label' => 'Test Field',
    ], $overrides);
  }

  /**
   * Creates valid role operation data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional data overrides.
   *
   * @return array<string, mixed>
   *   The operation data.
   */
  protected function getValidRoleData(array $overrides = []): array {
    return array_merge([
      'operation' => 'create_role',
      'role_id' => 'test_role',
      'label' => 'Test Role',
      'permissions' => ['access content'],
    ], $overrides);
  }

  /**
   * Creates valid menu operation data for testing.
   *
   * @param array<string, mixed> $overrides
   *   Optional data overrides.
   *
   * @return array<string, mixed>
   *   The operation data.
   */
  protected function getValidMenuData(array $overrides = []): array {
    return array_merge([
      'operation' => 'create_menu',
      'menu_id' => 'test_menu',
      'label' => 'Test Menu',
      'description' => 'A test menu.',
    ], $overrides);
  }

  /**
   * Asserts that a TranslatableMarkup contains expected text.
   *
   * @param string $expected
   *   The expected string.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|string $actual
   *   The actual value.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertTranslatableContains(string $expected, TranslatableMarkup|string $actual, string $message = ''): void {
    $actualString = (string) $actual;
    $this->assertStringContainsString($expected, $actualString, $message);
  }

}
