<?php

namespace Drupal\Tests\eb\Unit\Service;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\eb\PluginInterfaces\EbExtensionInterface;
use Drupal\eb\Service\DefinitionGenerator;
use Drupal\Tests\eb\Unit\EbUnitTestBase;

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

  /**
   * The definition generator service under test.
   */
  protected DefinitionGenerator $definitionGenerator;

  /**
   * Bundle entities registry for mocking.
   *
   * @var array<string, array<string, object>>
   */
  protected array $bundleEntities = [];

  /**
   * Field definitions registry for mocking.
   *
   * @var array<string, array<string, array<string, mixed>>>
   */
  protected array $fieldDefinitionsRegistry = [];

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

    $this->definitionGenerator = new DefinitionGenerator(
      $this->discoveryService,
      $this->entityTypeManager,
      $this->entityFieldManager,
      $this->extensionPluginManager
    );
  }

  /**
   * Tests generateBundle extracts bundle definition.
   *
   * @covers ::generateBundle
   * @covers ::extractBundleDefinition
   */
  public function testGenerateBundleExtractsBundleDefinition(): void {
    $this->setUpMockBundle('node', 'article', 'Article', 'Article content type');
    $this->setUpEmptyFieldDefinitions('node', 'article');
    $this->setUpEmptyDisplays('node', 'article');

    $result = $this->definitionGenerator->generateBundle('node', 'article');

    $this->assertArrayHasKey('bundle_definitions', $result);
    $this->assertCount(1, $result['bundle_definitions']);

    $bundleDef = $result['bundle_definitions'][0];
    $this->assertEquals('node', $bundleDef['entity_type']);
    $this->assertEquals('article', $bundleDef['bundle_id']);
    $this->assertEquals('Article', $bundleDef['label']);
    $this->assertEquals('Article content type', $bundleDef['description']);
  }

  /**
   * Tests generateBundle extracts field definitions.
   *
   * @covers ::generateBundle
   * @covers ::extractFieldDefinitions
   */
  public function testGenerateBundleExtractsFieldDefinitions(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpMockFieldDefinitions('node', 'article', [
      'field_body' => [
        'type' => 'text_long',
        'label' => 'Body',
        'required' => TRUE,
        'cardinality' => 1,
        'settings' => [],
      ],
      'field_tags' => [
        'type' => 'entity_reference',
        'label' => 'Tags',
        'required' => FALSE,
        'cardinality' => -1,
        'settings' => [
          'target_type' => 'taxonomy_term',
          'handler' => 'default:taxonomy_term',
          'handler_settings' => ['target_bundles' => ['tags' => 'tags']],
        ],
      ],
    ]);
    $this->setUpEmptyDisplays('node', 'article');

    $result = $this->definitionGenerator->generateBundle('node', 'article');

    $this->assertArrayHasKey('field_definitions', $result);
    $this->assertCount(2, $result['field_definitions']);

    // Find body field.
    $bodyField = $this->findFieldByName($result['field_definitions'], 'field_body');
    $this->assertNotNull($bodyField);
    $this->assertEquals('text_long', $bodyField['field_type']);
    $this->assertEquals('Body', $bodyField['label']);
    $this->assertTrue($bodyField['required']);

    // Find tags field.
    $tagsField = $this->findFieldByName($result['field_definitions'], 'field_tags');
    $this->assertNotNull($tagsField);
    $this->assertEquals('entity_reference', $tagsField['field_type']);
    $this->assertEquals(-1, $tagsField['cardinality']);
    $this->assertArrayHasKey('settings', $tagsField);
    $this->assertEquals('taxonomy_term', $tagsField['settings']['target_type']);
  }

  /**
   * Tests generate with multiple bundles.
   *
   * @covers ::generate
   */
  public function testGenerateMultipleBundles(): void {
    $this->setUpMultipleBundles([
      'article' => ['label' => 'Article', 'description' => ''],
      'page' => ['label' => 'Basic Page', 'description' => ''],
    ]);
    $this->setUpEmptyExtensions();

    $result = $this->definitionGenerator->generate([
      'node' => ['article', 'page'],
    ]);

    $this->assertArrayHasKey('bundle_definitions', $result);
    $this->assertCount(2, $result['bundle_definitions']);
  }

  /**
   * Tests generate calls extension extractConfig.
   *
   * @covers ::generate
   */
  public function testGenerateCallsExtensionExtractConfig(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpEmptyFieldDefinitions('node', 'article');
    $this->setUpEmptyDisplays('node', 'article');

    // Create mock extension.
    $mockExtension = $this->createMock(EbExtensionInterface::class);
    $mockExtension->expects($this->once())
      ->method('extractConfig')
      ->with('node', 'article')
      ->willReturn([
        'field_group_definitions' => [
          [
            'entity_type' => 'node',
            'bundle' => 'article',
            'group_name' => 'group_content',
            'label' => 'Content',
          ],
        ],
      ]);

    $this->extensionPluginManager
      ->method('getExtensions')
      ->willReturn(['field_group' => $mockExtension]);

    $result = $this->definitionGenerator->generate([
      'node' => ['article'],
    ]);

    $this->assertArrayHasKey('field_group_definitions', $result);
    $this->assertCount(1, $result['field_group_definitions']);
  }

  /**
   * Tests generate with include_extensions option disabled.
   *
   * @covers ::generate
   */
  public function testGenerateWithExtensionsDisabled(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpEmptyFieldDefinitions('node', 'article');
    $this->setUpEmptyDisplays('node', 'article');

    // Extensions should not be called when disabled.
    $this->extensionPluginManager
      ->expects($this->never())
      ->method('getExtensions');

    $result = $this->definitionGenerator->generate([
      'node' => ['article'],
    ], ['include_extensions' => FALSE]);

    $this->assertArrayHasKey('bundle_definitions', $result);
  }

  /**
   * Tests generate with include_fields option disabled.
   *
   * @covers ::generateBundle
   */
  public function testGenerateWithFieldsDisabled(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpEmptyDisplays('node', 'article');
    $this->setUpEmptyExtensions();

    // Field manager should not be called when fields disabled.
    $this->entityFieldManager
      ->expects($this->never())
      ->method('getFieldDefinitions');

    $result = $this->definitionGenerator->generate([
      'node' => ['article'],
    ], ['include_fields' => FALSE]);

    $this->assertArrayHasKey('bundle_definitions', $result);
    $this->assertArrayNotHasKey('field_definitions', $result);
  }

  /**
   * Tests empty arrays are filtered from output.
   *
   * @covers ::generate
   */
  public function testEmptyArraysAreFiltered(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpEmptyFieldDefinitions('node', 'article');
    $this->setUpEmptyDisplays('node', 'article');
    $this->setUpEmptyExtensions();

    $result = $this->definitionGenerator->generate([
      'node' => ['article'],
    ]);

    // Empty arrays should be filtered out.
    $this->assertArrayNotHasKey('role_definitions', $result);
    $this->assertArrayNotHasKey('menu_definitions', $result);
  }

  /**
   * Tests settings normalization for entity reference fields.
   *
   * @covers ::extractEntityReferenceSettings
   */
  public function testEntityReferenceSettingsNormalization(): void {
    $this->setUpMockBundle('node', 'article', 'Article');
    $this->setUpMockFieldDefinitions('node', 'article', [
      'field_reference' => [
        'type' => 'entity_reference',
        'label' => 'Reference',
        'required' => FALSE,
        'cardinality' => 1,
        'settings' => [
          'target_type' => 'node',
          'handler' => 'default:node',
          'handler_settings' => ['target_bundles' => ['page' => 'page']],
        ],
      ],
    ]);
    $this->setUpEmptyDisplays('node', 'article');

    $result = $this->definitionGenerator->generateBundle('node', 'article');

    $refField = $this->findFieldByName($result['field_definitions'], 'field_reference');
    $this->assertNotNull($refField);
    $this->assertArrayHasKey('settings', $refField);
    $this->assertEquals('node', $refField['settings']['target_type']);
    $this->assertArrayHasKey('handler_settings', $refField['settings']);
    $this->assertArrayHasKey('target_bundles', $refField['settings']['handler_settings']);
  }

  /**
   * Sets up mock bundle entity.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   * @param string $label
   *   The bundle label.
   * @param string $description
   *   The bundle description.
   */
  protected function setUpMockBundle(string $entityTypeId, string $bundleId, string $label, string $description = ''): void {
    // Create mock entity type.
    $entityType = $this->createMock(EntityTypeInterface::class);
    $entityType->method('getBundleEntityType')->willReturn('node_type');

    // Create an anonymous class that implements the needed methods.
    // @codingStandardsIgnoreStart
    $bundleEntity = new class($label, $description) {

      public function __construct(
        private string $label,
        private string $description,
      ) {}

      public function label(): string {
        return $this->label;
      }

      public function getDescription(): string {
        return $this->description;
      }

    };
    // @codingStandardsIgnoreEnd

    // Create mock storage.
    $storage = $this->createMock(EntityStorageInterface::class);
    $storage->method('load')
      ->with($bundleId)
      ->willReturn($bundleEntity);

    // Set up entity type manager.
    $this->entityTypeManager->method('getDefinition')
      ->willReturnCallback(function ($id) use ($entityTypeId, $entityType) {
        return $id === $entityTypeId ? $entityType : NULL;
      });

    $this->entityTypeManager->method('getStorage')
      ->willReturnCallback(function ($type) use ($storage) {
        return $storage;
      });
  }

  /**
   * Sets up mock field definitions.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   * @param array<string, array<string, mixed>> $fields
   *   Array of field configurations keyed by field name.
   */
  protected function setUpMockFieldDefinitions(string $entityTypeId, string $bundleId, array $fields): void {
    $definitions = [];

    foreach ($fields as $fieldName => $config) {
      $storageDefinition = $this->createMock(FieldStorageDefinitionInterface::class);
      $storageDefinition->method('isBaseField')->willReturn(FALSE);
      $storageDefinition->method('getCardinality')->willReturn($config['cardinality'] ?? 1);
      $storageDefinition->method('getSettings')->willReturn([]);

      $fieldDefinition = $this->createMock(FieldDefinitionInterface::class);
      $fieldDefinition->method('getFieldStorageDefinition')->willReturn($storageDefinition);
      $fieldDefinition->method('getType')->willReturn($config['type']);
      $fieldDefinition->method('getLabel')->willReturn($config['label']);
      $fieldDefinition->method('isRequired')->willReturn($config['required'] ?? FALSE);
      $fieldDefinition->method('getDescription')->willReturn($config['description'] ?? '');
      $fieldDefinition->method('getSettings')->willReturn($config['settings'] ?? []);
      $fieldDefinition->method('getSetting')
        ->willReturnCallback(function ($key) use ($config) {
          return $config['settings'][$key] ?? NULL;
        });

      $definitions[$fieldName] = $fieldDefinition;
    }

    $this->entityFieldManager->method('getFieldDefinitions')
      ->with($entityTypeId, $bundleId)
      ->willReturn($definitions);
  }

  /**
   * Sets up empty field definitions.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   */
  protected function setUpEmptyFieldDefinitions(string $entityTypeId, string $bundleId): void {
    $this->entityFieldManager->method('getFieldDefinitions')
      ->with($entityTypeId, $bundleId)
      ->willReturn([]);
  }

  /**
   * Sets up empty display entities.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   */
  protected function setUpEmptyDisplays(string $entityTypeId, string $bundleId): void {
    $formDisplayStorage = $this->createMock(EntityStorageInterface::class);
    $formDisplayStorage->method('loadByProperties')->willReturn([]);

    $viewDisplayStorage = $this->createMock(EntityStorageInterface::class);
    $viewDisplayStorage->method('loadByProperties')->willReturn([]);

    $this->entityTypeManager->method('getStorage')
      ->willReturnCallback(function ($type) use ($formDisplayStorage, $viewDisplayStorage) {
        return match ($type) {
          'entity_form_display' => $formDisplayStorage,
          'entity_view_display' => $viewDisplayStorage,
          default => $this->createMock(EntityStorageInterface::class),
        };
      });
  }

  /**
   * Sets up empty extensions.
   */
  protected function setUpEmptyExtensions(): void {
    $this->extensionPluginManager
      ->method('getExtensions')
      ->willReturn([]);
  }

  /**
   * Sets up multiple bundles with callback-based mocking.
   *
   * This method is designed to support multiple bundles in a single test by
   * using callbacks instead of specific argument matchers.
   *
   * @param array<string, array{label: string, description: string}> $bundles
   *   Array of bundles keyed by bundle ID with label and description.
   */
  protected function setUpMultipleBundles(array $bundles): void {
    // Create bundle entities for each bundle.
    $bundleEntities = [];
    foreach ($bundles as $bundleId => $config) {
      // @codingStandardsIgnoreStart
      $bundleEntities[$bundleId] = new class($config['label'], $config['description']) {

        public function __construct(
          private string $label,
          private string $description,
        ) {}

        public function label(): string {
          return $this->label;
        }

        public function getDescription(): string {
          return $this->description;
        }

      };
      // @codingStandardsIgnoreEnd
    }

    // Create mock entity type.
    $entityType = $this->createMock(EntityTypeInterface::class);
    $entityType->method('getBundleEntityType')->willReturn('node_type');

    // Create mock storage for bundles.
    $bundleStorage = $this->createMock(EntityStorageInterface::class);
    $bundleStorage->method('load')
      ->willReturnCallback(function ($bundleId) use ($bundleEntities) {
        return $bundleEntities[$bundleId] ?? NULL;
      });

    // Create mock storage for displays.
    $formDisplayStorage = $this->createMock(EntityStorageInterface::class);
    $formDisplayStorage->method('loadByProperties')->willReturn([]);

    $viewDisplayStorage = $this->createMock(EntityStorageInterface::class);
    $viewDisplayStorage->method('loadByProperties')->willReturn([]);

    // Set up entity type manager with callback-based getStorage.
    $this->entityTypeManager->method('getDefinition')
      ->willReturnCallback(function ($id) use ($entityType) {
        return $id === 'node' ? $entityType : NULL;
      });

    $this->entityTypeManager->method('getStorage')
      ->willReturnCallback(function ($type) use ($bundleStorage, $formDisplayStorage, $viewDisplayStorage) {
        return match ($type) {
          'node_type' => $bundleStorage,
          'entity_form_display' => $formDisplayStorage,
          'entity_view_display' => $viewDisplayStorage,
          default => $this->createMock(EntityStorageInterface::class),
        };
      });

    // Set up empty field definitions for all bundles.
    $this->entityFieldManager->method('getFieldDefinitions')
      ->willReturnCallback(function ($entityTypeId, $bundleId) use ($bundles) {
        if ($entityTypeId === 'node' && isset($bundles[$bundleId])) {
          return [];
        }
        return [];
      });
  }

  /**
   * Finds a field by name in field definitions array.
   *
   * @param array<int, array<string, mixed>> $fieldDefinitions
   *   The field definitions array.
   * @param string $fieldName
   *   The field name to find.
   *
   * @return array<string, mixed>|null
   *   The field definition or NULL if not found.
   */
  protected function findFieldByName(array $fieldDefinitions, string $fieldName): ?array {
    foreach ($fieldDefinitions as $fieldDef) {
      if (($fieldDef['field_name'] ?? '') === $fieldName) {
        return $fieldDef;
      }
    }
    return NULL;
  }

}
