<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_session\Unit\Service;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_session\Constants\MessageStatus;
use Drupal\flowdrop_session\Constants\SessionStatus;
use Drupal\flowdrop_session\Entity\FlowDropSessionInterface;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Drupal\flowdrop_session\Service\DeferredMessageProcessor;
use Drupal\flowdrop_session\Service\MessageProcessorInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Tests the DeferredMessageProcessor service.
 *
 * @coversDefaultClass \Drupal\flowdrop_session\Service\DeferredMessageProcessor
 * @group flowdrop_session
 */
class DeferredMessageProcessorTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The deferred message processor under test.
   *
   * @var \Drupal\flowdrop_session\Service\DeferredMessageProcessor
   */
  private DeferredMessageProcessor $processor;

  /**
   * The entity type manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private $entityTypeManager;

  /**
   * The service container mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\DependencyInjection\ContainerInterface
   */
  private $container;

  /**
   * The logger mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * The message storage mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Entity\EntityStorageInterface
   */
  private $messageStorage;

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

    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->container = $this->prophesize(ContainerInterface::class);
    $this->logger = $this->prophesize(LoggerInterface::class);
    $this->messageStorage = $this->prophesize(EntityStorageInterface::class);

    $this->entityTypeManager->getStorage('flowdrop_session_message')
      ->willReturn($this->messageStorage->reveal());

    // Allow all logger calls by default (void methods - no willReturn).
    $this->logger->info(Argument::cetera())->will(function () {});
    $this->logger->warning(Argument::cetera())->will(function () {});
    $this->logger->error(Argument::cetera())->will(function () {});

    $this->processor = new DeferredMessageProcessor(
      $this->entityTypeManager->reveal(),
      $this->container->reveal(),
      $this->logger->reveal(),
    );
  }

  /**
   * @covers ::schedule
   */
  public function testScheduleAddsMessageId(): void {
    $message = $this->prophesize(FlowDropSessionMessageInterface::class);
    $message->id()->willReturn(42);

    $this->processor->schedule($message->reveal());

    $this->assertTrue($this->processor->hasPendingMessages());
  }

  /**
   * @covers ::schedule
   */
  public function testScheduleNullIdIgnored(): void {
    $message = $this->prophesize(FlowDropSessionMessageInterface::class);
    $message->id()->willReturn(NULL);

    $this->processor->schedule($message->reveal());

    $this->assertFalse($this->processor->hasPendingMessages());
  }

  /**
   * @covers ::hasPendingMessages
   */
  public function testHasPendingMessagesInitiallyFalse(): void {
    $this->assertFalse($this->processor->hasPendingMessages());
  }

  /**
   * @covers ::destruct
   */
  public function testDestructNoPendingDoesNothing(): void {
    // No messages scheduled - storage should not be accessed.
    $this->entityTypeManager->getStorage('flowdrop_session_message')
      ->shouldNotBeCalled();

    $this->processor->destruct();
  }

  /**
   * @covers ::destruct
   */
  public function testDestructProcessesAllPendingMessages(): void {
    $executionService = $this->prophesize(MessageProcessorInterface::class);
    $this->container->get('flowdrop_session.execution_service')
      ->willReturn($executionService->reveal());

    $msg1 = $this->prophesize(FlowDropSessionMessageInterface::class);
    $msg2 = $this->prophesize(FlowDropSessionMessageInterface::class);

    $this->messageStorage->load(1)->willReturn($msg1->reveal());
    $this->messageStorage->load(2)->willReturn($msg2->reveal());

    $executionService->processMessage($msg1->reveal())->shouldBeCalledOnce();
    $executionService->processMessage($msg2->reveal())->shouldBeCalledOnce();

    // Schedule two messages.
    $schedMsg1 = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg1->id()->willReturn(1);
    $schedMsg2 = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg2->id()->willReturn(2);

    $this->processor->schedule($schedMsg1->reveal());
    $this->processor->schedule($schedMsg2->reveal());

    $this->processor->destruct();

    // After destruct, pending list should be cleared.
    $this->assertFalse($this->processor->hasPendingMessages());
  }

  /**
   * @covers ::destruct
   */
  public function testDestructMessageNotFoundLogsWarning(): void {
    $this->messageStorage->load(999)->willReturn(NULL);

    $this->logger->warning(Argument::containingString('not found'), Argument::type('array'))
      ->shouldBeCalled();

    $schedMsg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg->id()->willReturn(999);

    $this->processor->schedule($schedMsg->reveal());
    $this->processor->destruct();
  }

  /**
   * @covers ::destruct
   */
  public function testDestructProcessingFailureUpdatesStatuses(): void {
    $executionService = $this->prophesize(MessageProcessorInterface::class);
    $this->container->get('flowdrop_session.execution_service')
      ->willReturn($executionService->reveal());

    $session = $this->prophesize(FlowDropSessionInterface::class);
    $session->setStatus(SessionStatus::IDLE)->willReturn($session->reveal());
    $session->save()->shouldBeCalled();

    $msg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $msg->setStatus(MessageStatus::FAILED)->willReturn($msg->reveal());
    $msg->save()->shouldBeCalled();
    $msg->getSession()->willReturn($session->reveal());

    // First load for processing, second load for failure handling.
    $this->messageStorage->load(10)->willReturn($msg->reveal());

    $executionService->processMessage($msg->reveal())
      ->willThrow(new \RuntimeException('Processing error'));

    $schedMsg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg->id()->willReturn(10);

    $this->processor->schedule($schedMsg->reveal());
    $this->processor->destruct();
  }

  /**
   * @covers ::destruct
   */
  public function testDestructProcessingFailureNoSession(): void {
    $executionService = $this->prophesize(MessageProcessorInterface::class);
    $this->container->get('flowdrop_session.execution_service')
      ->willReturn($executionService->reveal());

    $msg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $msg->setStatus(MessageStatus::FAILED)->willReturn($msg->reveal());
    $msg->save()->shouldBeCalled();
    $msg->getSession()->willReturn(NULL);

    $this->messageStorage->load(11)->willReturn($msg->reveal());

    $executionService->processMessage($msg->reveal())
      ->willThrow(new \RuntimeException('Error'));

    $schedMsg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg->id()->willReturn(11);

    $this->processor->schedule($schedMsg->reveal());
    $this->processor->destruct();
  }

  /**
   * @covers ::destruct
   */
  public function testDestructCalledTwiceSecondIsNoop(): void {
    $executionService = $this->prophesize(MessageProcessorInterface::class);
    $this->container->get('flowdrop_session.execution_service')
      ->willReturn($executionService->reveal());

    $msg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $this->messageStorage->load(1)->willReturn($msg->reveal());
    $executionService->processMessage($msg->reveal())->shouldBeCalledOnce();

    $schedMsg = $this->prophesize(FlowDropSessionMessageInterface::class);
    $schedMsg->id()->willReturn(1);

    $this->processor->schedule($schedMsg->reveal());
    $this->processor->destruct();
    // Second call should be a no-op since pending list was cleared.
    $this->processor->destruct();
  }

}
