<?php

namespace Drupal\languagewire_translation_provider\Test\Service;

use Drupal\languagewire_translation_provider\Adapter\Database\ProjectRepository;
use Drupal\languagewire_translation_provider\Adapter\SystemInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface;
use Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobItemInterface;
use Drupal\languagewire_translation_provider\Client\ClientInterface;
use Drupal\languagewire_translation_provider\Client\Project as PlatformProject;
use Drupal\languagewire_translation_provider\Client\ProjectStatus as PlatformProjectStatus;
use Drupal\languagewire_translation_provider\Client\RecoverableException;
use Drupal\languagewire_translation_provider\Content\VariablePlaceholderPrefixingConventionInterface;
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\AvailableServices;
use Drupal\languagewire_translation_provider\Service\ProjectMonitor;
use Drupal\languagewire_translation_provider\Settings\JobSettings;
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 Laminas\Diactoros\Uri;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;

/**
 * Project Monitor Test .
 */
class ProjectMonitorTest extends TestCase {
  use \Prophecy\PhpUnit\ProphecyTrait;

  /**
   * Project Repository .
   *
   * @var \Prophecy\Prophecy\ObjectProphecy
   */
  protected $projectRepository;

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

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

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

  /**
   * Prefixing Convention .
   *
   * @var \Drupal\languagewire_translation_provider\Content\VariablePlaceholderPrefixingConventionInterface
   */
  private $prefixingConvention;

  /**
   * Job .
   *
   * @var \Drupal\languagewire_translation_provider\Adapter\TMGMT\TmgmtJobInterface
   */
  private $job;

  /**
   * Project .
   *
   * @var \Drupal\languagewire_translation_provider\Domain\Project
   */
  private $project;

  /**
   * Set Up .
   */
  protected function setUp(): void {
    $this->projectRepository = $this->prophesize(ProjectRepository::class);
    $this->client = $this->prophesize(ClientInterface::class);
    $this->system = $this->prophesize(SystemInterface::class);
    $this->logger = $this->prophesize(LoggerInterface::class);
    $this->system->logger()->willReturn($this->logger->reveal());

    $this->prefixingConvention = $this->prophesize(VariablePlaceholderPrefixingConventionInterface::class);
    $this->prefixingConvention
      ->prefix('url', VariablePlaceholderPrefixingConventionInterface::TYPE_INSECURE)
      ->willReturn(':url');
    $this->prefixingConvention
      ->prefix('projectId', VariablePlaceholderPrefixingConventionInterface::TYPE_SECURE)
      ->willReturn('@projectId');

    $this->job = $this->getSuccessfulTmgmtJobMock();
  }

  /**
   * When Project Is Finished Then Project Status Is Updated .
   *
   * @test
   */
  public function whenProjectIsFinishedInThePlatformThenProjectStatusIsUpdatedToImporting(): void {
    $this->setReturnValueOfHasMessageForJob(TRUE);
    $this->project = $this->createProject($this->job);

    $this->languageWireClientChangesProjectStatus(
          $this->project,
          PlatformProjectStatus::finished()
      );

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $expectedProjectStatus = ProjectStatus::importing();

    $this->assertEquals($expectedProjectStatus, $this->project->getStatus());
    $this->logger->info("Project " . $this->project->getPlatformId() . " has finished in the platform.")->shouldHaveBeenCalled();
  }

  /**
   * When Project Is Cancelled Then Project Status Is Updated .
   *
   * @test
   */
  public function whenProjectIsCancelledInThePlatformThenProjectStatusIsUpdatedToCancelledAndTmgmtJobIsCancelled(): void {
    $this->setReturnValueOfHasMessageForJob(TRUE);
    $this->project = $this->createProject($this->job);

    $this->languageWireClientChangesProjectStatus(
          $this->project,
          PlatformProjectStatus::cancelled()
      );

    $this->job->title()->willReturn('title');
    $this->job->jobId()->willReturn(1);

    $this->projectRepository->getAllByTmgmtJobId(1)->willReturn([$this->project]);
    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $expectedProjectStatus = ProjectStatus::cancelled();

    $this->assertEquals($expectedProjectStatus, $this->project->getStatus());
    $this->job->cancel(Argument::cetera())->shouldHaveBeenCalled();
    $this->logger->info("Project " . $this->project->getPlatformId() . " was cancelled in the platform.")->shouldHaveBeenCalled();
  }

  /**
   * When Project Is Under Creation Then Warning Message Logged .
   *
   * @test
   */
  public function whenProjectIsUnderCreationInThePlatformThenWarningMessageIsLogged(): void {
    $this->setReturnValueOfHasMessageForJob(TRUE);
    $this->project = $this->createProject($this->job);

    $this->languageWireClientChangesProjectStatus(
      $this->project,
      PlatformProjectStatus::underCreation()
    );

    $this->job->jobId()->willReturn(1);

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $expectedProjectStatus = ProjectStatus::inProgress();
    $this->assertEquals($expectedProjectStatus, $this->project->getStatus());

    $this->job->cancel(Argument::cetera())->shouldNotHaveBeenCalled();
    $this->logger->warning("Project {$this->project->getCorrelationId()} (TMGMT job id {$this->job->reveal()->jobId()}) is in the 'Under creation' state in the platform.")->shouldHaveBeenCalled();
  }

  /**
   * When Project Gets New Platform Url Then Job Message Is Sent .
   *
   * @test
   */
  public function whenProjectGetsNewPlatformUrlThenJobMessageIsSent(): void {
    $this->setReturnValueOfHasMessageForJob(FALSE);
    $this->project = $this->createProject($this->job, FALSE);

    $expectedPlatformUrl = new Uri('https://example.com/new_platform_url');
    $this->languageWireClientChangesPlatformUrl(
      $this->project,
      $expectedPlatformUrl
    );

    $this->job->jobId()->willReturn(1);

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $this->assertEquals((string) $expectedPlatformUrl, (string) $this->project->getPlatformUrl());

    $this->job->addMessage(
          Argument::containingString("The project ID in the LanguageWire platform is"),
          Argument::type('array')
      )->shouldHaveBeenCalled();
  }

  /**
   * When Project Gets Url And Has Message .
   *
   * @test
   */
  public function whenProjectGetsNewPlatformUrlAndHasMessageAlreadyThenJobMessageIsNotSent(): void {
    $this->setReturnValueOfHasMessageForJob(TRUE);
    $this->project = $this->createProject($this->job, FALSE);

    $expectedPlatformUrl = new Uri('https://example.com/new_platform_url');
    $this->languageWireClientChangesPlatformUrl(
      $this->project,
      $expectedPlatformUrl
    );

    $this->job->jobId()->willReturn(1);

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $this->assertEquals((string) $expectedPlatformUrl, (string) $this->project->getPlatformUrl());

    $this->job->addMessage(
          Argument::containingString("The project ID in the LanguageWire platform is"),
          Argument::type('array')
      )->shouldNotHaveBeenCalled();
  }

  /**
   * When Client Throws Recoverable Exception .
   *
   * @test
   */
  public function whenClientThrowsRecoverableExceptionThenProjectStatusIsNotChanged(): void {
    $this->project = $this->createProject($this->job);

    $expectedException = new RecoverableException();
    $this->languageWireClientThrowsException($this->project, $expectedException);

    $this->job->title()->willReturn('title');
    $this->job->jobId()->willReturn(123);

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $expectedProjectStatus = ProjectStatus::inProgress();

    $this->assertEquals($expectedProjectStatus, $this->project->getStatus());
  }

  /**
   * When Client Throws An Exception Then Project Status Is Set To Error .
   *
   * @test
   */
  public function whenClientThrowsAnExceptionThenProjectStatusIsSetToError(): void {
    $this->project = $this->createProject($this->job);

    $expectedException = new \Exception();
    $this->languageWireClientThrowsException($this->project, $expectedException);

    $this->job->title()->willReturn('title');
    $this->job->jobId()->willReturn(123);

    $spc = $this->projectMonitor();
    $spc->monitorProject($this->project, $this->client->reveal());

    $this->assertEquals(ProjectStatus::error(), $this->project->getStatus());
    $this->job->addMessage(Argument::containingString("Project monitoring has failed. Failure reason:"))->shouldHaveBeenCalled();
  }

  /**
   * Get Successful Tmgmt Job Mock .
   */
  private function getSuccessfulTmgmtJobMock(): ObjectProphecy {
    $job = $this->prophesize(TmgmtJobInterface::class);
    $settingsMock = $this->prophesize(JobSettings::class);
    $settingsMock->getService()->willReturn(AvailableServices::MACHINE_TRANSLATION_SERVICE_ID);

    $job->settings()->willReturn($settingsMock->reveal());

    return $job;
  }

  /**
   * Create Project .
   */
  private function createProject(ObjectProphecy $job, bool $existsInPlatform = TRUE): Project {
    $item1 = $this->newJobItem(1, 'Item 1');
    $item2 = $this->newJobItem(2, 'Item 2');
    $document1 = (new DocumentBuilder())->withTmgmtJobItem($item1)->withStatus(DocumentStatus::uploadingContent())->build();
    $document2 = (new DocumentBuilder())->withTmgmtJobItem($item2)->withStatus(DocumentStatus::uploadingContent())->build();
    $project = (new ProjectBuilder())
      ->withTmgmtJob($job->reveal())
      ->withStatus(ProjectStatus::inProgress())
      ->withDocuments($document1, $document2);

    if (!$existsInPlatform) {
      $project = $project
        ->withPlatformId(0)
        ->withPlatformUrl(NULL);
    }

    return $project->build();
  }

  /**
   * Language Wire Client Changes Project Status .
   */
  private function languageWireClientChangesProjectStatus(
    Project $project,
    PlatformProjectStatus $desiredStatus
  ): void {
    $this->client->getPlatformProject(Argument::type(Project::class))->willReturn(
          $this->platformProjectWithStatus($project, $desiredStatus)
      );
  }

  /**
   * Language Wire Client Changes Platform Url .
   */
  private function languageWireClientChangesPlatformUrl(
    Project $project,
    Uri $desiredPlatformUrl
  ): void {
    $this->client->getPlatformProject(Argument::type(Project::class))->willReturn(
          $this->platformProjectWithPlatformUrl($project, $desiredPlatformUrl)
      );
  }

  /**
   * Language Wire Client Throws Exception .
   */
  private function languageWireClientThrowsException(
    Project $project,
    \Exception $exception
  ): void {
    $this->client->getPlatformProject(Argument::type(Project::class))->willThrow($exception);
  }

  /**
   * Project Monitor .
   */
  private function projectMonitor(): ProjectMonitor {
    return new ProjectMonitor(
          $this->projectRepository->reveal(),
          $this->logger->reveal(),
          $this->prefixingConvention->reveal()
      );
  }

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

  /**
   * Platform Project With Status .
   */
  private function platformProjectWithStatus(Project $project, PlatformProjectStatus $desiredStatus): PlatformProject {
    $platformProject = $this->prophesize(PlatformProject::class);
    $platformProject->id()->willReturn($project->getPlatformId());
    $platformProject->platformUrl()->willReturn((string) $project->getPlatformUrl());
    $platformProject->status()->willReturn($desiredStatus);
    $platformProject->assignments()->willReturn([]);

    return $platformProject->reveal();
  }

  /**
   * Platform Project With Platform Url .
   */
  private function platformProjectWithPlatformUrl(Project $project, Uri $desiredPlatformUrl): PlatformProject {
    $platformProject = $this->prophesize(PlatformProject::class);
    $platformProject->id()->willReturn($project->getPlatformId());
    $platformProject->platformUrl()->willReturn((string) $desiredPlatformUrl);
    $platformProject->status()->willReturn(PlatformProjectStatus::finished());
    $platformProject->assignments()->willReturn([]);

    return $platformProject->reveal();
  }

  /**
   * Set Return Value Of Has Message For Job .
   */
  private function setReturnValueOfHasMessageForJob(bool $value): void {
    $this->job->hasMessage(Argument::type('string'), 'like')->willReturn($value);
  }

}
