<?php

namespace Drupal\Tests\acquia_contenthub\Kernel\EventSubscriber\ParseCdf;

use Acquia\ContentHubClient\CDF\CDFObject;
use Drupal\acquia_contenthub\AcquiaContentHubEvents;
use Drupal\acquia_contenthub\Event\ParseCdfEntityEvent;
use Drupal\acquia_contenthub\Event\UnserializeAdditionalMetadataEvent;
use Drupal\acquia_contenthub\EventSubscriber\ParseCdf\ContentEntityParseCdfHandler;
use Drupal\acquia_contenthub_test\Entity\EntityWithoutLangcode;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\depcalc\DependencyStack;
use Drupal\depcalc\DependentEntityWrapper;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\path_alias\Entity\PathAlias;
use Drupal\Tests\acquia_contenthub\Kernel\Traits\AssetHandlerTrait;
use Drupal\Tests\acquia_contenthub\Kernel\Traits\CdfDocumentCreatorTrait;
use Drupal\Tests\acquia_contenthub\Kernel\Traits\StringFormatterTrait;

/**
 * Tests content entity parse cdf handler.
 *
 * @group acquia_contenthub
 * @coversDefaultClass \Drupal\acquia_contenthub\EventSubscriber\ParseCdf\ContentEntityParseCdfHandler
 *
 * @requires module depcalc
 *
 * @package Drupal\Tests\acquia_contenthub\Kernel\EventSubscriber\ParseCdf
 */
class ContentEntityParseCdfHandlerTest extends KernelTestBase {

  use AssetHandlerTrait, CdfDocumentCreatorTrait {
    AssetHandlerTrait::attachZipExtension insteadof CdfDocumentCreatorTrait;
  }
  use StringFormatterTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'acquia_contenthub',
    'acquia_contenthub_test',
    'depcalc',
    'user',
    'field',
    'filter',
    'node',
    'text',
    'system',
    'path',
    'language',
    'content_translation',
  ];

  /**
   * Event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcher
   */
  protected $dispatcher;

  /**
   * ContentEntityHandler object.
   *
   * @var \Drupal\acquia_contenthub\EventSubscriber\ParseCdf\ContentEntityParseCdfHandler
   */
  protected $entityHandler;

  /**
   * Config object.
   *
   * @var \Drupal\acquia_contenthub\Client\ClientFactory
   */
  protected $clientFactory;

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  protected function setUp() : void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installSchema('user', ['users_data']);
    $this->installEntitySchema('node');
    $this->installEntitySchema('configurable_language');
    $this->installConfig([
      'field',
      'filter',
      'node',
      'language',
    ]);
    $this->dispatcher = $this->container->get('event_dispatcher');
    $this->entityHandler = new ContentEntityParseCdfHandler(
      $this->container->get('event_dispatcher')
    );
    $this->clientFactory = $this->container->get('acquia_contenthub.client.factory');
    ConfigurableLanguage::createFromLangcode('hu')->save();
    // Unserialize the dependencies before parsing the actual node.
    $cdf_document = $this->createCdfDocumentFromFixtureFile('node/node_article_dependencies_onparsecdf.json');
    $serializer = $this->container->get('entity.cdf.serializer');
    $serializer->unserializeEntities($cdf_document, new DependencyStack());
  }

  /**
   * Tests the OnParseCdf() method.
   */
  public function testOnParseCdf(): void {
    $data = $this->getCdfArray('node_article_onparsecdf.json');
    $cdf = CDFObject::fromArray($data);
    $event = $this->triggerOnParseCdfEvent($cdf);

    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $event->getEntity();
    // Entity has just been created, not saved into database yet.
    $this->assertTrue($entity->isNew());

    $this->assertEquals($entity->uuid(), $cdf->getUuid());
    $this->assertEquals($entity->label(), $cdf->getAttribute('label')->getValue()[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
    $fields = $this->entityHandler->decodeMetadataContent($cdf->getMetadata()['data']);
    unset($fields['content_translation_source'], $fields['content_translation_outdated']);
    $translations = ['en', 'hu'];
    foreach ($translations as $langcode) {
      $translation = $entity->getTranslation($langcode);
      foreach ($fields as $field_name => $value) {
        $field = $translation->get($field_name);
        if ($field instanceof EntityReferenceFieldItemListInterface) {
          $referenced_entity = $field->referencedEntities()[0];
          $entity_value = $referenced_entity ? $referenced_entity->uuid() : '';
        }
        else {
          $entity_value = $field->first()->getValue();
          $entity_value = $entity_value['value'] ?? $entity_value;
          $entity_value = $entity_value['target_id'] ?? $entity_value;
        }
        $value = $this->getFieldValue($value, $langcode);
        $this->assertTrue($entity_value == $value, $this->stringPrintFormat(
          'Field: %s. Actual: %s - Expected: %s',
          $field_name, $entity_value, $value,
        ));
      }
    }
  }

  /**
   * Tests onParseCdf on non-translatable entities.
   *
   * Non-translatable entities such as path_alias don't have translations.
   * Therefore, during the import process we have to make sure that we don't
   * ADD translation, but SET the language.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   * @throws \ReflectionException
   */
  public function testOnParseCdfWithNonTranslatableEntity(): void {
    $this->enableModules(['path_alias']);
    $this->installEntitySchema('path_alias');

    // Used to get unserialize the new path.
    $node = Node::create([
      'title' => 'Some title',
      'type' => 'article',
      // Referred in path_alias.json metadata.
      'uuid' => 'a8c4504b-1935-4fd0-b53b-9a8e489c2197',
    ]);
    $node->save();

    $data = $this->getCdfArray('path_alias.json');
    $cdf = CDFObject::fromArray($data);

    $path_alias = PathAlias::create([
      'path' => '/user/1',
      'alias' => '/user-alias',
      'uuid' => $cdf->getUuid(),
    ]);
    $path_alias->save();

    $this->assertEquals('und', $path_alias->language()->getId(), 'Original entity has a und language code');

    $stack = new DependencyStack();
    // In order for the path unserialization to be successful, we need to insert
    // the node into the stack.
    $stack->addDependency(new DependentEntityWrapper($node));
    $event = new ParseCdfEntityEvent($cdf, $stack);
    $event->setEntity($path_alias);
    $this->entityHandler->onParseCdf($event);

    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $event->getEntity();

    $this->assertFalse($entity->isNew());
    $this->assertEquals($path_alias->uuid(), $entity->uuid());
    $this->assertEquals($entity->label(), $cdf->getAttribute('label')->getValue()[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
    $this->assertEquals('hu', $path_alias->language()->getId(), 'The original entity has been overridden');

    $fields = $this->entityHandler->decodeMetadataContent($cdf->getMetadata()['data']);
    // At this point the path field should be set to the node's path.
    $fields['path']['value']['hu']['value'] = '/node/1';
    foreach ($fields as $field_name => $value) {
      $entity_value = $entity->get($field_name)->first()->getValue()['value'];
      $value = $this->getFieldValue($value, 'hu');
      $this->assertTrue($entity_value == $value, $this->stringPrintFormat(
        'Field: %s. Actual: %s - Expected: %s',
        $field_name, $entity_value, $value,
      ));
    }
  }

  /**
   * Tests onParseCdf on non-translatable entities with und langcode.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \ReflectionException
   */
  public function testOnParseCdfWithNonTranslatableUndLangcodeEntity(): void {
    $this->installEntitySchema('custom_entity_wo_langcode');
    $entity_wo_langcode = EntityWithoutLangcode::create([
      'label' => 'Some label',
    ]);
    $entity_wo_langcode->save();

    $this->assertEquals('und', $entity_wo_langcode->language()->getId(), 'Original entity has a und language code');

    $data = $this->getCdfArray('custom_entity_wo_langcode.json');
    $cdf = CDFObject::fromArray($data);

    $stack = new DependencyStack();
    $stack->addDependency(new DependentEntityWrapper($entity_wo_langcode));
    $event = new ParseCdfEntityEvent($cdf, $stack);
    $event->setEntity($entity_wo_langcode);
    $this->entityHandler->onParseCdf($event);

    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $event->getEntity();
    $this->assertFalse($entity->isNew());
    $this->assertEquals($entity_wo_langcode->uuid(), $entity->uuid());
    $this->assertEquals($entity->label(), $cdf->getAttribute('label')->getValue()[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
    $this->assertEquals('und', $entity_wo_langcode->language()->getId(), 'The original entity has been overridden');
  }

  /**
   * Tests UnserializeAdditionalMetadataEvent.
   */
  public function testOnParseCdfUnserializeAdditionalMetadataEvent(): void {
    $data = $this->getCdfArray('node_article_onparsecdf.json');
    $cdf = CDFObject::fromArray($data);

    // Add additional metadata.
    $this->dispatcher->addListener(
      AcquiaContentHubEvents::UNSERIALIZE_ADDITIONAL_METADATA,
      [$this, 'unserializeAdditionalMetadata']
    );
    $event = $this->triggerOnParseCdfEvent($cdf);
    $entity = $event->getEntity();
    $this->assertTrue($entity->label() === 'Change label to this one');
  }

  /**
   * Tests immutable event.
   */
  public function testOnParseCdfImmutableEvent(): void {
    $data = $this->getCdfArray('node_article_onparsecdf.json');
    $cdf = CDFObject::fromArray($data);

    $event = new ParseCdfEntityEvent($cdf, new DependencyStack());
    $event->setMutable(FALSE);
    $this->entityHandler->onParseCdf($event);
    $this->assertFalse($event->hasEntity(), 'Entity has not been created, the event is not mutable.');
  }

  /**
   * Tests onParseCdf for exceptions.
   */
  public function testOnParseCdfException(): void {
    $data = $this->getCdfArray('node_article_onparsecdf.json', [
      'metadata' => [
        'default_language' => [],
      ],
    ]);

    $cdf = CDFObject::fromArray($data);
    $this->expectException(\Exception::class);
    $this->expectExceptionMessage(sprintf("No language available for entity with UUID %s.", $cdf->getUuid()));

    $event = $this->triggerOnParseCdfEvent($cdf);
    $this->entityHandler->onParseCdf($event);
  }

  /**
   * Unserialize additional metadata on the fly event subscriber.
   *
   * @param \Drupal\acquia_contenthub\Event\UnserializeAdditionalMetadataEvent $event
   *   The event object.
   */
  public function unserializeAdditionalMetadata(UnserializeAdditionalMetadataEvent $event): void {
    $entity = $event->getEntity();
    $entity->set('title', 'Change label to this one');
    $event->setEntity($entity);
  }

  /**
   * Returns field value.
   *
   * @param array $value
   *   The array to fetch the value from.
   * @param string $langcode
   *   The langcode to use to get field value.
   *
   * @return mixed
   *   The fetched value.
   */
  protected function getFieldValue(array $value, string $langcode = 'en') {
    if ($value) {
      $value = $value['value'][$langcode]['value'] ?? $value['value'][$langcode];
      return is_array($value) ? current($value) : $value;
    }
    return '';
  }

  /**
   * Triggers PARSE_CDF event.
   *
   * @param \Acquia\ContentHubClient\CDF\CDFObject $cdf
   *   The CDF to pass to the event.
   *
   * @return \Drupal\acquia_contenthub\Event\ParseCdfEntityEvent
   *   The dispatched event object.
   *
   * @throws \Exception
   */
  protected function triggerOnParseCdfEvent(CDFObject $cdf): ParseCdfEntityEvent {
    $event = new ParseCdfEntityEvent($cdf, new DependencyStack());
    $this->entityHandler->onParseCdf($event);
    return $event;
  }

}
