<?php

namespace Drupal\Tests\eb\Kernel;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\eb\Traits\YamlFixtureTrait;
use Drupal\user\Entity\User;
use Psr\Log\NullLogger;
use Symfony\Component\ErrorHandler\BufferingLogger;

/**
 * Base class for Entity Builder kernel tests.
 *
 * Provides common setup, helper methods, and assertions for testing
 * Entity Builder services and plugins with a real Drupal environment.
 *
 * @group eb
 */
abstract class EbKernelTestBase extends KernelTestBase {

  use YamlFixtureTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'eb',
    'system',
    'user',
    'field',
    'field_ui',
    'node',
    'text',
    'taxonomy',
    'options',
    'datetime',
    'link',
    'file',
    'image',
    'menu_link_content',
  ];

  /**
   * The service name for a logger that collects anything logged.
   *
   * @var string
   */
  protected static string $testLogServiceName = 'eb_test.logger';

  /**
   * The admin user.
   */
  protected User $adminUser;

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

    // Install entity schemas.
    $this->installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installEntitySchema('taxonomy_term');
    $this->installEntitySchema('menu_link_content');
    $this->installEntitySchema('file');
    $this->installEntitySchema('eb_rollback');
    $this->installEntitySchema('eb_rollback_operation');
    $this->installEntitySchema('eb_log');

    // Install configs.
    $this->installConfig([
      'system',
      'field',
      'node',
      'user',
      'taxonomy',
      'eb',
    ]);

    // Install additional schemas.
    $this->installSchema('node', ['node_access']);
    $this->installSchema('file', ['file_usage']);

    // Create user 1 (admin).
    $this->adminUser = User::create([
      'uid' => 1,
      'name' => 'admin',
      'mail' => 'admin@test.com',
      'status' => 1,
    ]);
    $this->adminUser->save();

    // Set current user.
    $this->container->get('current_user')->setAccount($this->adminUser);

    // Clean test logger.
    if ($this->container->has(self::$testLogServiceName)) {
      $this->container->get(self::$testLogServiceName)->cleanLogs();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function register(ContainerBuilder $container): void {
    parent::register($container);

    // Register a buffering logger for test assertions.
    $container
      ->register(self::$testLogServiceName, BufferingLogger::class)
      ->addTag('logger');

    // Override the eb logger channel with a NullLogger
    // to prevent output during tests (which PHPUnit treats as errors).
    $container
      ->register('logger.channel.eb', NullLogger::class);
  }

  /**
   * Creates a node type for testing.
   *
   * @param string $type
   *   The node type machine name.
   * @param string $name
   *   The human-readable name.
   * @param array<string, mixed> $values
   *   Optional additional values.
   *
   * @return \Drupal\node\Entity\NodeType
   *   The created node type.
   */
  protected function createNodeType(string $type, string $name, array $values = []): NodeType {
    $nodeType = NodeType::create([
      'type' => $type,
      'name' => $name,
      'description' => $values['description'] ?? "Test node type: $name",
    ] + $values);
    $nodeType->save();
    return $nodeType;
  }

  /**
   * Creates a taxonomy vocabulary for testing.
   *
   * @param string $vid
   *   The vocabulary machine name.
   * @param string $name
   *   The human-readable name.
   * @param array<string, mixed> $values
   *   Optional additional values.
   *
   * @return \Drupal\taxonomy\Entity\Vocabulary
   *   The created vocabulary.
   */
  protected function createVocabulary(string $vid, string $name, array $values = []): Vocabulary {
    $vocabulary = Vocabulary::create([
      'vid' => $vid,
      'name' => $name,
      'description' => $values['description'] ?? "Test vocabulary: $name",
    ] + $values);
    $vocabulary->save();
    return $vocabulary;
  }

  /**
   * Asserts that a node type exists.
   *
   * @param string $type
   *   The node type machine name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertNodeTypeExists(string $type, string $message = ''): void {
    $nodeType = NodeType::load($type);
    $message = $message ?: "Node type '$type' should exist.";
    $this->assertNotNull($nodeType, $message);
  }

  /**
   * Asserts that a node type does not exist.
   *
   * @param string $type
   *   The node type machine name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertNodeTypeNotExists(string $type, string $message = ''): void {
    $nodeType = NodeType::load($type);
    $message = $message ?: "Node type '$type' should not exist.";
    $this->assertNull($nodeType, $message);
  }

  /**
   * Asserts that a vocabulary exists.
   *
   * @param string $vid
   *   The vocabulary machine name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertVocabularyExists(string $vid, string $message = ''): void {
    $vocabulary = Vocabulary::load($vid);
    $message = $message ?: "Vocabulary '$vid' should exist.";
    $this->assertNotNull($vocabulary, $message);
  }

  /**
   * Asserts that a vocabulary does not exist.
   *
   * @param string $vid
   *   The vocabulary machine name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertVocabularyNotExists(string $vid, string $message = ''): void {
    $vocabulary = Vocabulary::load($vid);
    $message = $message ?: "Vocabulary '$vid' should not exist.";
    $this->assertNull($vocabulary, $message);
  }

  /**
   * Asserts that a field storage exists.
   *
   * @param string $entityType
   *   The entity type ID.
   * @param string $fieldName
   *   The field name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertFieldStorageExists(string $entityType, string $fieldName, string $message = ''): void {
    $storage = $this->container
      ->get('entity_type.manager')
      ->getStorage('field_storage_config')
      ->load("$entityType.$fieldName");
    $message = $message ?: "Field storage '$entityType.$fieldName' should exist.";
    $this->assertNotNull($storage, $message);
  }

  /**
   * Asserts that a field storage does not exist.
   *
   * @param string $entityType
   *   The entity type ID.
   * @param string $fieldName
   *   The field name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertFieldStorageNotExists(string $entityType, string $fieldName, string $message = ''): void {
    $storage = $this->container
      ->get('entity_type.manager')
      ->getStorage('field_storage_config')
      ->load("$entityType.$fieldName");
    $message = $message ?: "Field storage '$entityType.$fieldName' should not exist.";
    $this->assertNull($storage, $message);
  }

  /**
   * Asserts that a field config exists.
   *
   * @param string $entityType
   *   The entity type ID.
   * @param string $bundle
   *   The bundle name.
   * @param string $fieldName
   *   The field name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertFieldConfigExists(string $entityType, string $bundle, string $fieldName, string $message = ''): void {
    $config = $this->container
      ->get('entity_type.manager')
      ->getStorage('field_config')
      ->load("$entityType.$bundle.$fieldName");
    $message = $message ?: "Field config '$entityType.$bundle.$fieldName' should exist.";
    $this->assertNotNull($config, $message);
  }

  /**
   * Asserts that a field config does not exist.
   *
   * @param string $entityType
   *   The entity type ID.
   * @param string $bundle
   *   The bundle name.
   * @param string $fieldName
   *   The field name.
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertFieldConfigNotExists(string $entityType, string $bundle, string $fieldName, string $message = ''): void {
    $config = $this->container
      ->get('entity_type.manager')
      ->getStorage('field_config')
      ->load("$entityType.$bundle.$fieldName");
    $message = $message ?: "Field config '$entityType.$bundle.$fieldName' should not exist.";
    $this->assertNull($config, $message);
  }

  /**
   * Gets collected log messages from the test logger.
   *
   * @return array<array<mixed>>
   *   Array of log entries.
   */
  protected function getLogMessages(): array {
    if ($this->container->has(self::$testLogServiceName)) {
      return $this->container->get(self::$testLogServiceName)->cleanLogs();
    }
    return [];
  }

  /**
   * Asserts that no errors were logged.
   *
   * @param string $message
   *   Optional assertion message.
   */
  protected function assertNoLogErrors(string $message = ''): void {
    $logs = $this->getLogMessages();
    $errors = array_filter($logs, fn($log) => $log[0] <= 3);
    $message = $message ?: 'No errors should be logged.';
    $this->assertEmpty($errors, $message);
  }

  /**
   * Clears all caches.
   */
  protected function clearCaches(): void {
    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
    $this->container->get('plugin.manager.field.field_type')->clearCachedDefinitions();
    \Drupal::service('cache.discovery')->deleteAll();
  }

}
