<?php

namespace Drupal\Tests\acquia_contenthub\Kernel\Traits;

use Acquia\ContentHubClient\CDF\CDFObject;
use Acquia\ContentHubClient\CDFDocument;
use Drupal\Component\Serialization\Json;
use Drupal\Core\File\Exception\FileNotExistsException;
use Drupal\Tests\acquia_contenthub\Kernel\Stubs\DrupalVersion;

/**
 * Trait containing helper methods to create a CDF document.
 */
trait CdfDocumentCreatorTrait {

  use DrupalVersion;

  /**
   * Creates CDF document from fixture.
   *
   * @param string $fixture_filename
   *   Fixture file name.
   * @param string $module_name
   *   Module name.
   *
   * @return \Acquia\ContentHubClient\CDFDocument
   *   CDF document.
   *
   * @throws \ReflectionException
   */
  protected function createCdfDocumentFromFixtureFile(string $fixture_filename, string $module_name = 'acquia_contenthub'): CDFDocument {
    $data = $this->getCdfData($fixture_filename, $module_name);
    $document_parts = [];
    foreach ($data['entities'] as $entity) {
      $document_parts[] = $this->populateCdfObject($entity);
    }

    return new CDFDocument(...$document_parts);
  }

  /**
   * Loads a fixture cdf.
   *
   * Attempts to start the lookup from the current drupal version, if it exists
   * loads the fixture otherwise tries one minor version lower until it reaches
   * the specified arbitrary threshold.
   *
   * @param string $fixture_filename
   *   Fixture filename.
   * @param string $module_name
   *   Module name.
   *
   * @return mixed
   *   Decoded data.
   */
  public function getCdfData(string $fixture_filename, string $module_name = 'acquia_contenthub') {
    $fixture_path = $this->getAvailableFixturePath($fixture_filename, $module_name);
    if (!extension_loaded('zlib')) {
      throw new \RuntimeException('zlib extension is missing, it is required for importing fixtures.');
    }
    $content = file_get_contents('compress.zlib://' . $fixture_path);
    return Json::decode($content);
  }

  /**
   * Returns the next available path for the provided fixture name.
   *
   * The process starts from the current version. If it cannot find fixtures
   * for the current version it takes the next available in descending order.
   *
   * @param string $fixture_filename
   *   The name of the fixture file.
   * @param string $module_name
   *   The name of the module.
   *
   * @return string
   *   The fixture path with the fixture name.
   *
   * @throws \Drupal\Core\File\Exception\FileNotExistsException
   *   At the end of the process if the fixture could not be located.
   */
  public function getAvailableFixturePath(string $fixture_filename, string $module_name = 'acquia_contenthub'): string {
    if (substr($fixture_filename, -4) !== '.php') {
      $fixture_filename = $this->attachZipExtension($fixture_filename);
    }
    $current_version = $this->getDrupalVersion(TRUE);
    $available_version_fixtures = $this->getImportFixtureDirContent($module_name);
    foreach ($available_version_fixtures as $path) {
      if (version_compare($path['version'], $current_version, '>')) {
        continue;
      }

      $fixture_path = $path['path'] . '/' . $fixture_filename;
      if (is_file($fixture_path)) {
        return $fixture_path;
      }
    }

    throw new FileNotExistsException(sprintf('Could not find fixture: %s', $fixture_filename));
  }

  /**
   * Appends .gz extension to filename if not already appended.
   *
   * @param string $filename
   *   Filename to look for.
   *
   * @return string
   *   Appended string.
   */
  protected function attachZipExtension(string $filename): string {
    return substr($filename, -3) === '.gz' ? $filename : $filename . '.gz';
  }

  /**
   * Returns all directories from the fixture folder.
   *
   * @param string $module_name
   *   The module's name to identify the path to fixtures.
   *
   * @return array
   *   The available drupal version specific fixture directories.
   */
  public function getImportFixtureDirContent(string $module_name): array {
    $module_path = \Drupal::service('module_handler')
      ->getModule($module_name)
      ->getPath();
    $path = sprintf('%s/tests/fixtures/import', $module_path);
    $iterator = new \DirectoryIterator($path);
    $dirs = [];
    while ($iterator->valid()) {
      $current = $iterator->current();
      if ($current->isDot()) {
        $iterator->next();
        continue;
      }
      $dirs[] = [
        'version' => substr($current->getBasename(), strlen('drupal-')),
        'path' => $current->getPathname(),
      ];
      $iterator->next();
    }

    // Descending order.
    usort($dirs, function ($a, $b) {
      if (version_compare($a['version'], $b['version'], '>')) {
        return -1;
      }
      if (version_compare($a['version'], $b['version'], '<')) {
        return 1;
      }
      return 0;
    });

    return $dirs;
  }

  /**
   * Populates CDF object from array.
   *
   * @param array $entity
   *   Entity.
   *
   * @return \Acquia\ContentHubClient\CDF\CDFObject
   *   Populated CDF object.
   *
   * @throws \Exception
   * @throws \ReflectionException
   *
   * @see \Acquia\ContentHubClient\ContentHubClient::getEntities()
   */
  protected function populateCdfObject(array $entity): CDFObject {
    $object = new CDFObject($entity['type'], $entity['uuid'], $entity['created'], $entity['modified'], $entity['origin'], $entity['metadata']);

    foreach ($entity['attributes'] as $attribute_name => $values) {
      // Refactor ClientHub.php: get rid of duplicated code blocks.
      if (!$attribute = $object->getAttribute($attribute_name)) {
        $class = !empty($object->getMetadata()['attributes'][$attribute_name]) ? $object->getMetadata()['attributes'][$attribute_name]['class'] : FALSE;
        if ($class && class_exists($class)) {
          $object->addAttribute($attribute_name, $values['type'], NULL, 'und', $class);
        }
        else {
          $object->addAttribute($attribute_name, $values['type'], NULL);
        }
        $attribute = $object->getAttribute($attribute_name);
      }

      $value_property = (new \ReflectionClass($attribute))->getProperty('value');
      $value_property->setAccessible(TRUE);
      $value_property->setValue($attribute, $values['value']);
    }

    return $object;
  }

}
