<?php

namespace Drupal\Tests\acquia_contenthub_subscriber\Kernel\EventSubscriber;

use Acquia\ContentHubClient\CDF\CDFObject;
use Acquia\ContentHubClient\CDFAttribute;
use Drupal\acquia_contenthub\AcquiaContentHubEntityTrackerInterface;
use Drupal\acquia_contenthub\Event\ParseCdfEntityEvent;
use Drupal\acquia_contenthub\Event\Queue\QueueItemProcessFinishedEvent;
use Drupal\acquia_contenthub\Exception\ContentHubFileException;
use Drupal\acquia_contenthub\Plugin\FileSchemeHandler\FileSchemeHandlerManagerInterface;
use Drupal\acquia_contenthub_subscriber\ContentHubImportQueue;
use Drupal\acquia_contenthub_subscriber\EventSubscriber\ParseCdf\File;
use Drupal\acquia_contenthub_subscriber\SubscriberTracker;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\File\FileSystemInterface;
use Drupal\depcalc\DependencyStack;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\acquia_contenthub\Traits\CdfMockerTrait;
use Drupal\Tests\acquia_contenthub\Unit\Helpers\LoggerMock;
use Prophecy\Argument;

/**
 * @coversDefaultClass \Drupal\acquia_contenthub_subscriber\EventSubscriber\ParseCdf\File
 *
 * @group acquia_contenthub_subscriber
 *
 * @requires module depcalc
 */
class FileTest extends KernelTestBase {

  use CdfMockerTrait;

  /**
   * A mocked logger.
   *
   * @var \Drupal\Tests\acquia_contenthub\Unit\Helpers\LoggerMock
   */
  private $logger;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'acquia_contenthub',
    'acquia_contenthub_subscriber',
    'depcalc',
    'file',
    'user',
  ];

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

    $this->installSchema('acquia_contenthub_subscriber', [SubscriberTracker::IMPORT_TRACKING_TABLE]);

    $this->logger = new LoggerMock();
  }

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

    $container->register('stream_wrapper.private', 'Drupal\Core\StreamWrapper\PrivateStream')
      ->addTag('stream_wrapper', ['scheme' => 'private']);
  }

  /**
   * Tests different cases of import queue item event.
   */
  public function testOnParseCdfWithPublicHandler(): void {
    // We'll incrementally introduce the attributes.
    $attributes = [new CDFAttribute('label', CDFAttribute::TYPE_STRING, 'File label')];
    // Without file_scheme attribute.
    $manager = $this->prophesize(FileSchemeHandlerManagerInterface::class);
    $manager->createInstance(Argument::type('string'))->shouldNotBeCalled();
    $sut = $this->newFile($manager->reveal());
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);

    $this->assertEquals('File File label does not have a scheme and therefore was not copied. This likely 
    means that its scheme is remote, unsupported or that it should already exist within a module, theme or library in 
    the subscriber.', $this->logger->takeLastWarningMessage(), 'No file_scheme attribute, the file is logged with warning.');

    // With file_scheme attribute.
    $attributes[] = new CDFAttribute('file_scheme', CDFAttribute::TYPE_STRING, 'public');
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    $this->assertEquals(
      sprintf(
        'The file (%s | n/a) cannot be saved: file_location is missing from file CDF | code: %s',
        $cdf->getUuid(), ContentHubFileException::MISSING_ATTRIBUTE
      ),
      $this->logger->takeLastErrorMessage(),
    );

    $path = $this->getPathTo('tests/assets/emptyfile.txt');
    $attributes['file_location'] = new CDFAttribute('file_location', CDFAttribute::TYPE_STRING, $path);
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    $this->assertEquals(
      sprintf(
        'The file (%s | n/a) cannot be saved: file_uri is missing from file CDF | code: %s',
        $cdf->getUuid(), ContentHubFileException::MISSING_ATTRIBUTE
      ),
      $this->logger->takeLastErrorMessage(),
    );

    $uri = 'public://2023-03/emptyfile.txt';
    $attributes['file_uri'] = new CDFAttribute('file_uri', CDFAttribute::TYPE_STRING, $uri);
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    $contents = file_get_contents($this->getPathTo('tests/assets/emptyfile.txt'));
    $private_contents = file_get_contents($uri);
    $this->assertEquals($contents, $private_contents, 'The file has been successfully saved without any contents.');

    $path = $this->getPathTo('tests/assets/nonemptyfile.txt');
    $uri = 'public://2023-03/nonemptyfile.txt.txt';
    $attributes['file_location'] = new CDFAttribute('file_location', CDFAttribute::TYPE_STRING, $path);
    $attributes['file_uri'] = new CDFAttribute('file_uri', CDFAttribute::TYPE_STRING, $uri);
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    $contents = file_get_contents($path);
    $this->assertEquals('I have some content', trim($contents), 'The file has been successfully saved with content.');

    // The file is not available.
    $location = 'wrong/path/to/file.txt';
    $attributes['file_location'] = new CDFAttribute('file_location', CDFAttribute::TYPE_STRING, $location);
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    // @todo Can be removed after we support php8 or higher.
    if (version_compare(phpversion(), '8', '<')) {
      $message = 'Error during file save (%s): file_get_contents(%s): failed to open stream: No such file or directory';
    }
    else {
      $message = 'The file (%s | %s) cannot be saved: could not fetch contents of the file | code: 1';
    }
    $this->assertEquals(
      sprintf(
        $message,
        $cdf->getUuid(), $location,
      ),
      $this->logger->takeLastErrorMessage(),
    );

    // Simulate an exception on the file system level.
    $fs = $this->prophesize(FileSystemInterface::class);
    $dir = 'some dir';
    $fs->dirname(Argument::type('string'))->willReturn($dir);
    $fs->prepareDirectory($dir, 1)->willReturn(FALSE);
    $this->container->set('file_system', $fs->reveal());

    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $this->dispatchParseCdfEvent($cdf, $sut);
    $this->assertEquals(
      sprintf(
        'The file (%s | n/a) cannot be saved: target directory (%s) does not exist or not writable | code: %s',
        $cdf->getUuid(), $dir, ContentHubFileException::DIRECTORY_NOT_WRITABLE
      ),
      $this->logger->takeLastErrorMessage(),
    );
  }

  /**
   * @covers ::onQueueFinished
   */
  public function testOnQueueFinished(): void {
    $db = $this->container->get('database');

    $path = 'non/existing/file.txt';
    $uri = 'public://2023-03/nonemptyfile.txt';
    $attributes = [
      new CDFAttribute('label', CDFAttribute::TYPE_STRING, 'File label'),
      new CDFAttribute('file_scheme', CDFAttribute::TYPE_STRING, 'public'),
      new CDFAttribute('file_location', CDFAttribute::TYPE_STRING, $path),
      new CDFAttribute('file_uri', CDFAttribute::TYPE_STRING, $uri),
    ];
    $sut = $this->newFile();
    $cdf = $this->generateFileCdf($attributes);
    $db->insert(SubscriberTracker::IMPORT_TRACKING_TABLE)
      ->fields([
        'entity_uuid' => $cdf->getUuid(),
        'entity_type' => 'file',
        'entity_id' => 1,
        'hash' => 'somehash',
        'status' => 'queued',
        'first_imported' => time(),
        'last_imported' => time(),
        'queue_id' => '123',
      ])
      ->execute();
    $this->dispatchParseCdfEvent($cdf, $sut);
    $property = new \ReflectionProperty($sut, 'unsavedFiles');
    $property->setAccessible(TRUE);
    $unsaved_files = $property->getValue($sut);
    $this->assertEquals($cdf->getUuid(), $unsaved_files[0]);

    $this->dispatchQueueItemProcessFinishedEvent(ContentHubImportQueue::QUEUE_NAME, $sut);
    $msg = $this->logger->takeLastWarningMessage();
    $this->assertEquals(sprintf(
      'Some files could not be saved during the queue run, therefore they were deleted from subscriber tracking table: %s. Queue items: %s',
      $cdf->getUuid(), 'uuid1,uuid2'
    ), $msg);
    $count = $db->select(SubscriberTracker::IMPORT_TRACKING_TABLE, 't')
      ->condition('hash', '')
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEquals(1, $count);

    // Queue is different, will be omitted, hash will not be nullified.
    $db->update(SubscriberTracker::IMPORT_TRACKING_TABLE)
      ->fields(['hash' => 'somehash'])
      ->execute();
    $this->dispatchQueueItemProcessFinishedEvent('some_other_queue', $sut);
    $count = $db->select(SubscriberTracker::IMPORT_TRACKING_TABLE, 't')
      ->condition('hash', '')
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEquals(0, $count);
  }

  /**
   * Returns the relative path to the file.
   *
   * @param string $sub_path
   *   The path to the file.
   *
   * @return string
   *   The full path to the file from the root.
   */
  protected function getPathTo(string $sub_path): string {
    $module_root = \Drupal::service('extension.list.module')
      ->getPath('acquia_contenthub_subscriber');
    return $module_root . '/' . $sub_path;
  }

  /**
   * Instantiates a new File subscriber.
   *
   * @param mixed ...$constructor_args
   *   The constructor arguments to pass to the object.
   *
   * @return \Drupal\acquia_contenthub_subscriber\EventSubscriber\ParseCdf\File
   *   The file object.
   *
   * @throws \Exception
   */
  protected function newFile(...$constructor_args): File {
    $manager = $this->container->get('acquia_contenthub.file_scheme_handler.manager');
    $tracker = $this->container->get('acquia_contenthub_subscriber.tracker');
    foreach ($constructor_args as $arg) {
      if ($arg instanceof FileSchemeHandlerManagerInterface) {
        $manager = $arg;
        continue;
      }

      if ($arg instanceof AcquiaContentHubEntityTrackerInterface) {
        $tracker = $arg;
      }
    }

    return new File($manager, $tracker, $this->logger);
  }

  /**
   * Dispatches the event with the provided params.
   *
   * @param \Acquia\ContentHubClient\CDF\CDFObject $cdf
   *   The CDF object to pass to the event.
   * @param \Drupal\acquia_contenthub_subscriber\EventSubscriber\ParseCdf\File $sut
   *   The SUT, file subscriber.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function dispatchParseCdfEvent(CDFObject $cdf, File $sut) {
    $stack = new DependencyStack();
    $event = new ParseCdfEntityEvent($cdf, $stack, NULL);
    $sut->onParseCdf($event);
  }

  /**
   * Dispatches the event with the provided params.
   *
   * @param string $queue_name
   *   The name of the queue.
   * @param \Drupal\acquia_contenthub_subscriber\EventSubscriber\ParseCdf\File $sut
   *   The SUT, file subscriber.
   */
  protected function dispatchQueueItemProcessFinishedEvent(string $queue_name, File $sut) {
    $event = new QueueItemProcessFinishedEvent($queue_name, ['uuid1', 'uuid2'], '');
    $sut->onQueueFinished($event);
  }

}
