<?php

declare(strict_types=1);

namespace Drupal\languagewire_translation_provider\Test\Service;

use Drupal\languagewire_translation_provider\Adapter\SystemInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\LanguageMapping;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobItemInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtTranslatorInterface;
use Drupal\languagewire_translation_provider\Client\ClientInterface;
use Drupal\languagewire_translation_provider\Client\RecoverableException;
use Drupal\languagewire_translation_provider\Content\HtmlPreview\HtmlPreviewTemporaryArtifactsCleaner;
use Drupal\languagewire_translation_provider\Content\SanitizerInterface;
use Drupal\languagewire_translation_provider\Domain\Document;
use Drupal\languagewire_translation_provider\Domain\DocumentStatus;
use Drupal\languagewire_translation_provider\Domain\Project;
use Drupal\languagewire_translation_provider\Domain\ProjectStatus;
use Drupal\languagewire_translation_provider\Platform\Document\UploadableDocument;
use Drupal\languagewire_translation_provider\Platform\Document\UploadedDocument;
use Drupal\languagewire_translation_provider\Service\HtmlPreviewService;
use Drupal\languagewire_translation_provider\Service\ProjectContentUploader;
use Drupal\languagewire_translation_provider\Service\ProjectContentUploader\DocumentSelectorFactory;
use Drupal\languagewire_translation_provider\Service\ProjectContentUploader\DocumentSelectorInterface;
use Drupal\languagewire_translation_provider\Settings\JobSettings;
use Drupal\languagewire_translation_provider\Settings\OrderType;
use Drupal\languagewire_translation_provider\Test\Builder\Domain\DocumentBuilder;
use Drupal\languagewire_translation_provider\Test\Builder\Domain\ProjectBuilder;
use Drupal\languagewire_translation_provider\Test\Builder\JobItemBuilder;
use Drupal\languagewire_translation_provider\Test\Builder\LanguageMappingBuilder;
use Drupal\languagewire_translation_provider\Test\Builder\UploadableDocumentBuilder;
use Drupal\languagewire_translation_provider\Test\Builder\UploadedDocumentBuilder;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;

/**
 * Project Content Uploader Test .
 */
class ProjectContentUploaderTest extends TestCase {
  use \Prophecy\PhpUnit\ProphecyTrait;

  /**
   * Client .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  protected $client;

  /**
   * System .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  protected $system;

  /**
   * Sanitizer .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  protected $sanitizer;

  /**
   * Logger .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  private $logger;

  /**
   * Document Selector .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  private $documentSelector;

  /**
   * Document Selector Factory .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  private $documentSelectorFactory;

  /**
   * Html Preview Service.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  private $htmlPreviewService;

  /**
   * Html Preview Temporary Artifacts Cleaner.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  private $htmlPreviewTemporaryArtifactsCleaner;

  /**
   * Set Up .
   */
  protected function setUp(): void {
    $this->client = $this->prophesize(ClientInterface::class);
    $this->system = $this->prophesize(SystemInterface::class);
    $this->sanitizer = $this->prophesize(SanitizerInterface::class);
    $this->sanitizer->sanitize(Argument::type('string'))->willReturnArgument(0);
    $this->logger = $this->prophesize(LoggerInterface::class);
    $this->system->logger()->willReturn($this->logger->reveal());
    $this->documentSelector = $this->prophesize(DocumentSelectorInterface::class);
    $this->documentSelectorFactory = $this->prophesize(DocumentSelectorFactory::class);
    $this->htmlPreviewService = $this->prophesize(HtmlPreviewService::class);
    $this->htmlPreviewTemporaryArtifactsCleaner = $this->prophesize(HtmlPreviewTemporaryArtifactsCleaner::class);
  }

  /**
   * When Content Upload Is Successful .
   *
   * @test
   */
  public function whenAllProjectContentUploadIsSuccessfulThenProjectHasValidatingStatusAndDocumentsHaveUploadedContentStatusWithPlatformId(): void {
    $jobSettings = $this->prophesize(JobSettings::class);
    $jobSettings->isItemContentEnabledForTranslation(Argument::cetera())->willReturn(TRUE);
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $languageMapping = $this->newLanguageMapping();
    $job = $this->getSuccessfulJob([$item1, $item2], $jobSettings->reveal(), $languageMapping);
    $document1 = (new DocumentBuilder())->withTmgmtJobItem($item1)->build();
    $document2 = (new DocumentBuilder())->withTmgmtJobItem($item2)->build();
    $project = (new ProjectBuilder())->withTmgmtJob($job)->withDocuments($document1, $document2)->build();
    $uploadableDoc1 = $this->newUploadableDocument($document1);
    $uploadableDoc2 = $this->newUploadableDocument($document2);
    $uploadedDoc1 = $this->newUploadedDocument($document1, '998');
    $uploadedDoc2 = $this->newUploadedDocument($document2, '999');
    $this->languageWireClientSuccessfullyUploadsContent(
      $project,
      [
        $uploadableDoc1,
        $uploadableDoc2,
      ],
      [
        [
          'jobItem' => $item1,
          'document' => $document1,
          'uploadedDocument' => $uploadedDoc1,
        ],
        [
          'jobItem' => $item2,
          'document' => $document2,
          'uploadedDocument' => $uploadedDoc2,
        ],
      ]
    );
    $this->documentSelectorWillNotLimitItems();
    $sut = $this->projectContentUploader();
    $this->client->startProject($project)->willReturn(TRUE);
    $sut->uploadProjectContent($project, [$document1, $document2], $job, $this->client->reveal());

    $this->assertEquals(ProjectStatus::inProgress(), $project->getStatus());
    $this->assertEquals(DocumentStatus::uploadingContent(), $document1->getStatus());
    $this->assertSame('998', $uploadedDoc1->platformId());
    $this->assertEquals(DocumentStatus::uploadingContent(), $document2->getStatus());
    $this->assertSame('999', $uploadedDoc2->platformId());
  }

  /**
   * When Project Content Upload Is Not Complete .
   *
   * @test
   */
  public function whenProjectContentUploadIsNotCompleteThenProjectHasUploadingStatus(): void {
    $jobSettings = $this->prophesize(JobSettings::class);
    $jobSettings->isItemContentEnabledForTranslation(Argument::cetera())->willReturn(TRUE);
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $item3 = $this->newJobItem(3, 'Item 3');
    $languageMapping = $this->newLanguageMapping();
    $pendingDocument1 = (new DocumentBuilder())->withTmgmtJobItem($item1)->build();
    $pendingDocument2 = (new DocumentBuilder())->withTmgmtJobItem($item2)->build();
    $pendingDocument3 = (new DocumentBuilder())->withPlatformId(NULL)->withTmgmtJobItem($item3)->build();
    $job = $this->getIncompleteUploadJob([$item1, $item2, $item3], $jobSettings->reveal(), $languageMapping);
    $project = (new ProjectBuilder())->withTmgmtJob($job)->withDocuments($pendingDocument1, $pendingDocument2, $pendingDocument3)->build();
    $uploadableDoc1 = $this->newUploadableDocument($pendingDocument1);
    $uploadableDoc2 = $this->newUploadableDocument($pendingDocument2);
    $uploadableDoc3 = $this->newUploadableDocument($pendingDocument3);
    $uploadedDocument1 = $this->newUploadedDocument($pendingDocument1, '997');
    $uploadedDocument2 = $this->newUploadedDocument($pendingDocument2, '998');
    // LanguageWireClient only uploads $pendingDocument1 and $pendingDocument2,.
    // But not $pendingDocument3 because $itemUploadBatchSize
    // in TmgmtTranslator settings is set to 2.
    $this->languageWireClientSuccessfullyUploadsContent(
      $project,
      [
        $uploadableDoc1,
        $uploadableDoc2,
        $uploadableDoc3,
      ],
      [
        [
          'jobItem' => $item1,
          'document' => $pendingDocument1,
          'uploadedDocument' => $uploadedDocument1,
        ],
        [
          'jobItem' => $item2,
          'document' => $pendingDocument2,
          'uploadedDocument' => $uploadedDocument2,
        ],
      ]
    );
    $this->documentSelectorWillLimitItems(2);
    $sut = $this->projectContentUploader();
    $sut->uploadProjectContent($project, [$pendingDocument1, $pendingDocument2, $pendingDocument3], $job, $this->client->reveal());
    $this->assertEquals(ProjectStatus::uploading(), $project->getStatus());
    $this->assertEquals(DocumentStatus::uploadingContent(), $uploadedDocument1->getDocument()->getStatus());
    $this->assertSame('997', $uploadedDocument1->platformId());
    $this->assertEquals(DocumentStatus::uploadingContent(), $uploadedDocument2->getDocument()->getStatus());
    $this->assertSame('998', $uploadedDocument2->platformId());
    $this->assertEquals(DocumentStatus::uploadingContent(), $pendingDocument3->getStatus());
    $this->assertSame(NULL, $pendingDocument3->getPlatformId());
    $this->logger->info("There are 1 pending documents for project '{$project->getCorrelationId()}' which will be uploaded in the next cron job run.")->shouldHaveBeenCalled();
  }

  /**
   * When Project Content Upload Was Partially Done .
   *
   * @test
   */
  public function whenProjectContentUploadWasPartiallyDoneThenRerunThenProjectHasUploadedStatus(): void {
    $jobSettings = $this->prophesize(JobSettings::class);
    $jobSettings->isItemContentEnabledForTranslation(Argument::cetera())->willReturn(TRUE);
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $item3 = $this->newJobItem(3, 'Item 3');
    $languageMapping = $this->newLanguageMapping();
    $job = $this->getSuccessfulJob([$item1, $item2, $item3], $jobSettings->reveal(), $languageMapping);
    $previouslyUploadedDocument1 = (new DocumentBuilder())->withPlatformId('997')->withStatus(DocumentStatus::uploadedContent())->withTmgmtJobItem($item1)->build();
    $previouslyUploadedDocument2 = (new DocumentBuilder())->withPlatformId('998')->withStatus(DocumentStatus::uploadedContent())->withTmgmtJobItem($item1)->build();
    $pendingDocument3 = (new DocumentBuilder())->withPlatformId(NULL)->withTmgmtJobItem($item3)->build();
    $project = (new ProjectBuilder())->withTmgmtJob($job)->withDocuments($previouslyUploadedDocument1, $previouslyUploadedDocument2, $pendingDocument3)->build();
    $uploadedDocument3 = $this->newUploadedDocument($pendingDocument3, '999');
    // LanguageWireClient only uploads $pendingDocument3,.
    // As $previouslyUploadedDocument1 and $previouslyUploadedDocument2
    // were uploaded.
    $this->languageWireClientSuccessfullyUploadsContent(
      $project,
      [
        $previouslyUploadedDocument1,
        $previouslyUploadedDocument2,
        $pendingDocument3,
      ],
      [
        [
          'jobItem' => $item3,
          'document' => $pendingDocument3,
          'uploadedDocument' => $uploadedDocument3,
        ],
      ]
    );
    $this->documentSelectorWillLimitItems(2);
    $sut = $this->projectContentUploader();
    $this->client->startProject($project)->willReturn(TRUE);
    $sut->uploadProjectContent($project, [$previouslyUploadedDocument1, $previouslyUploadedDocument2, $pendingDocument3], $job, $this->client->reveal());
    $this->assertEquals(ProjectStatus::inProgress(), $project->getStatus());
    $this->assertEquals(DocumentStatus::uploadingContent(), $uploadedDocument3->getDocument()->getStatus());
    $this->assertSame('999', $uploadedDocument3->platformId());
  }

  /**
   * When Project Has Empty Content .
   *
   * @test
   */
  public function whenProjectHasEmptyContentThenProjectIsMarkedAsErrornousAndTmgmtJobIsMarkedAsUnprocessed(): void {
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $jobSettings = $this->prophesize(JobSettings::class);
    $jobSettings->isItemContentEnabledForTranslation(Argument::cetera())->willReturn(FALSE);
    $languageMapping = $this->newLanguageMapping();
    $job = $this->getAbortedJobDueToAllContentBeingExcluded([$item1, $item2], $jobSettings->reveal(), $languageMapping);
    $document1 = (new DocumentBuilder())->withTmgmtJobItem($item1)->build();
    $document2 = (new DocumentBuilder())->withTmgmtJobItem($item2)->build();
    $project = (new ProjectBuilder())->withTmgmtJob($job->reveal())->withDocuments($document1, $document2)->build();
    $this->documentSelectorWillNotLimitItems();
    $sut = $this->projectContentUploader();
    $sut->uploadProjectContent($project, [$document1, $document2], $job->reveal(), $this->client->reveal());
    $job->addMessage('Nothing to upload to LanguageWire. Please make sure that at least one content field is included in the "Content Settings" section.')->shouldHaveBeenCalled();
    $job->makeUnprocessed()->shouldHaveBeenCalled();
    $this->assertEquals(ProjectStatus::unrecoverableError(), $project->getStatus());
    $this->assertEquals(DocumentStatus::empty(), $document1->getStatus());
    //$this->assertEquals(['This item will not be translated because all of its content has been excluded from translation.'], $document1->getErrors());
    $this->assertEquals(DocumentStatus::empty(), $document2->getStatus());
    //$this->assertEquals(['This item will not be translated because all of its content has been excluded from translation.'], $document2->getErrors());
  }

  /**
   * When Project Has Empty And Non Empty Items .
   *
   * @test
   */
  public function whenProjectHasEmptyAndNonEmptyItemsThenNonEmptyItemsAreUploadedAndProjectAdvancesToUploadedStatus(): void {
    $jobSettings = $this->prophesize(JobSettings::class);
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $jobSettings->isItemContentEnabledForTranslation($item1->itemId(), Argument::cetera())->willReturn(TRUE);
    // Non empty item.
    $jobSettings->isItemContentEnabledForTranslation($item2->itemId(), Argument::cetera())->willReturn(FALSE);
    // Empty item.
    $languageMapping = $this->newLanguageMapping();
    $job = $this->getSuccessfulJob([$item1, $item2], $jobSettings->reveal(), $languageMapping);
    $document1 = (new DocumentBuilder())->withTmgmtJobItem($item1)->build();
    $document2 = (new DocumentBuilder())->withTmgmtJobItem($item2)->build();
    $project = (new ProjectBuilder())->withTmgmtJob($job)->withDocuments($document1, $document2)->build();
    $uploadedDoc1 = $this->newUploadedDocument($document1);
    $this->languageWireClientSuccessfullyUploadsContent(
      $project,
      [
        $uploadedDoc1
      ],
      [
        [
          'jobItem' => $item1,
          'document' => $document1,
          'uploadedDocument' => $uploadedDoc1,
        ],
      ]
    );
    $this->documentSelectorWillNotLimitItems();
    $sut = $this->projectContentUploader();
    $this->client->startProject($project)->willReturn(TRUE);
    $sut->uploadProjectContent($project, [$document1, $document2], $job, $this->client->reveal());
    $this->assertEquals(ProjectStatus::inProgress(), $project->getStatus());
    $this->assertEquals(DocumentStatus::uploadingContent(), $document1->getStatus());
    $this->assertEquals(DocumentStatus::empty(), $document2->getStatus());
    //$this->assertEquals(['This item will not be translated because all of its content has been excluded from translation.'], $document2->getErrors());
  }

  /**
   * When Recoverable Exception Is Thrown During Content Upload .
   *
   * @test
   */
  public function whenRecoverableExceptionIsThrownDuringContentUploadThenWarningIsLoggedAndProjectStatusStaysAsUploading(): void {
    $jobSettings = $this->prophesize(JobSettings::class);
    $jobSettings->isItemContentEnabledForTranslation(Argument::cetera())->willReturn(TRUE);
    $item = $this->newJobItem(1, 'Item 1');
    $languageMapping = $this->newLanguageMapping();
    $job = $this->getTmgmtJobMock([$item], $jobSettings->reveal(), $languageMapping);
    $document = (new DocumentBuilder())->withTmgmtJobItem($item)->build();
    $project = (new ProjectBuilder())->withTmgmtJob($job->reveal())->withDocuments($document)->build();
    $expectedException = new RecoverableException();
    $this->client->uploadDocuments($project, Argument::type('array'))->willThrow($expectedException);
    $this->documentSelectorWillNotLimitItems();
    $sut = $this->projectContentUploader();
    $sut->uploadProjectContent($project, [$document], $job->reveal(), $this->client->reveal());
    $this->logger->warning("$expectedException")->shouldHaveBeenCalled();
    $this->assertEquals(ProjectStatus::uploading(), $project->getStatus());
    $this->assertEquals(DocumentStatus::uploadingContent(), $document->getStatus());
  }

  /**
   * Project Content Uploader .
   */
  private function projectContentUploader(): ProjectContentUploader {
    $this->documentSelectorFactory->createFromTranslator(Argument::cetera())->willReturn($this->documentSelector->reveal());
    return new ProjectContentUploader(
      $this->system->reveal(),
      $this->sanitizer->reveal(),
      $this->documentSelectorFactory->reveal(),
      $this->htmlPreviewService->reveal(),
      $this->htmlPreviewTemporaryArtifactsCleaner->reveal()
    );
  }

  /**
   * Get Successful Job .
   */
  private function getSuccessfulJob(array $jobItems, JobSettings $jobSettings, LanguageMapping $languageMapping): TmgmtJobInterface {
    $job = $this->getTmgmtJobMock($jobItems, $jobSettings, $languageMapping);
    $job->addMessage(Argument::type('string'))->shouldBeCalled();
    return $job->reveal();
  }

  /**
   * Get Incomplete Upload Job .
   */
  private function getIncompleteUploadJob(array $jobItems, JobSettings $jobSettings, LanguageMapping $languageMapping): TmgmtJobInterface {
    $job = $this->getTmgmtJobMock($jobItems, $jobSettings, $languageMapping);
    $job->addMessage(Argument::type('string'))->shouldNotBeCalled();
    return $job->reveal();
  }

  /**
   * New Job Item .
   */
  private function newJobItem(int $itemId, string $label): TmgmtJobItemInterface {
    return (new JobItemBuilder())->withItemId($itemId)->withLabel($label)->build();
  }

  /**
   * New Language Mapping .
   */
  private function newLanguageMapping(): LanguageMapping {
    return (new LanguageMappingBuilder())->build();
  }

  /**
   * New Uploadable Document .
   */
  private function newUploadableDocument(Document $document): UploadableDocument {
    $document->setStatus(DocumentStatus::uploadingContent());
    return (new UploadableDocumentBuilder())->withDocument($document)->build();
  }

  /**
   * New Uploaded Document .
   */
  private function newUploadedDocument(Document $document, string $platformId = '1'): UploadedDocument {
    return (new UploadedDocumentBuilder())->withDocument($document)->withPlatformId($platformId)->build();
  }

  /**
   * Language Wire Client Successfully Uploads Content .
   */
  private function languageWireClientSuccessfullyUploadsContent(Project $project, array $uploadableDocuments, array $expectedContent): void {
    $this->client->uploadDocuments($project, Argument::type('array'))->willReturn(array_map(function (array $item): UploadedDocument {
      return $item['uploadedDocument'];
    }, $expectedContent));
  }

  /**
   * Get Aborted Job Due To All Content Being Excluded .
   */
  private function getAbortedJobDueToAllContentBeingExcluded(array $jobItems, JobSettings $jobSettings, LanguageMapping $languageMapping): ObjectProphecy {
    $job = $this->getTmgmtJobMock($jobItems, $jobSettings, $languageMapping);
    $job->save()->shouldBeCalled();
    return $job;
  }

  /**
   * Get Tmgmt Job Mock .
   */
  private function getTmgmtJobMock(array $jobItems, JobSettings $jobSettings, LanguageMapping $languageMapping): ObjectProphecy {
    $job = $this->prophesize(TmgmtJobInterface::class);
    $job->jobId()->willReturn(1);
    $job->items()->willReturn($jobItems);
    $job->settings()->willReturn($jobSettings);
    $job->sourceLanguage()->willReturn($languageMapping);
    $job->getOrderType()->willReturn(OrderType::ORDER);
    $translator = $this->prophesize(TmgmtTranslatorInterface::class);
    $translator->isHtmlPreviewEnabled()->willReturn(FALSE);
    $job->translator()->willReturn($translator->reveal());
    return $job;
  }

  /**
   * Document Selector Will Limit Items .
   */
  private function documentSelectorWillLimitItems(int $limit): void {
    $this->documentSelector->selectDocuments(Argument::type('array'))->will(function ($args) use ($limit) {
      return array_slice($args[0], 0, $limit);
    });
  }

  /**
   * Document Selector Will Not Limit Items .
   */
  private function documentSelectorWillNotLimitItems(): void {
    $this->documentSelector->selectDocuments(Argument::type('array'))->will(function ($args) {
      return $args[0];
    });
  }

}
