<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_interrupt\Unit\Service;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\flowdrop_interrupt\DTO\InterruptCreationData;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptDirection;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Drupal\flowdrop_interrupt\Event\InterruptCancelledEvent;
use Drupal\flowdrop_interrupt\Event\InterruptCreatedEvent;
use Drupal\flowdrop_interrupt\Event\InterruptResolvedEvent;
use Drupal\flowdrop_interrupt\Service\InterruptFactory;
use Drupal\flowdrop_interrupt\Service\InterruptManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Tests the InterruptManager service.
 *
 * @coversDefaultClass \Drupal\flowdrop_interrupt\Service\InterruptManager
 * @group flowdrop_interrupt
 */
class InterruptManagerTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The interrupt manager under test.
   *
   * @var \Drupal\flowdrop_interrupt\Service\InterruptManager
   */
  private InterruptManager $manager;

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

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

  /**
   * The current user mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Session\AccountProxyInterface
   */
  private $currentUser;

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

  /**
   * The event dispatcher mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  private $eventDispatcher;

  /**
   * The lock backend mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Lock\LockBackendInterface
   */
  private $lock;

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

    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->storage = $this->prophesize(EntityStorageInterface::class);
    $this->currentUser = $this->prophesize(AccountProxyInterface::class);
    $this->logger = $this->prophesize(LoggerInterface::class);
    $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
    $this->lock = $this->prophesize(LockBackendInterface::class);

    $this->entityTypeManager->getStorage('flowdrop_interrupt')
      ->willReturn($this->storage->reveal());

    $factory = new InterruptFactory(
      $this->entityTypeManager->reveal(),
      $this->currentUser->reveal(),
      $this->logger->reveal(),
      $this->eventDispatcher->reveal(),
    );
    $this->manager = new InterruptManager(
      $this->entityTypeManager->reveal(),
      $this->currentUser->reveal(),
      $this->logger->reveal(),
      $this->eventDispatcher->reveal(),
      $this->lock->reveal(),
      $factory,
    );
  }

  /**
   * @covers ::create
   */
  public function testCreateInterruptSetsFieldsAndSaves(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->setSchema([])->willReturn($interrupt->reveal());
    $interrupt->setOptions([])->willReturn($interrupt->reveal());
    $interrupt->setContext(['key' => 'val'])->willReturn($interrupt->reveal());
    $interrupt->setDefaultValue(NULL)->willReturn($interrupt->reveal());
    $interrupt->setDirection(InterruptDirection::Outward)->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();
    $interrupt->uuid()->willReturn('uuid-123');

    $this->currentUser->id()->willReturn('5');

    $this->storage->create([
      'workflow_id' => 'wf_1',
      'node_id' => 'node_1',
      'type' => 'confirmation',
      'status' => 'pending',
      'message' => 'Please confirm',
      'uid' => '5',
    ])->willReturn($interrupt->reveal());

    $this->eventDispatcher->dispatch(
      Argument::type(InterruptCreatedEvent::class),
      InterruptCreatedEvent::NAME
    )->shouldBeCalled();

    $result = $this->manager->create(new InterruptCreationData(
      type: InterruptType::Confirmation,
      workflowId: 'wf_1',
      nodeId: 'node_1',
      message: 'Please confirm',
      context: ['key' => 'val'],
    ));

    $this->assertSame($interrupt->reveal(), $result);
  }

  /**
   * @covers ::create
   */
  public function testCreateInterruptWithPipelineAndSession(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->setSchema([])->willReturn($interrupt->reveal());
    $interrupt->setOptions([])->willReturn($interrupt->reveal());
    $interrupt->setContext([])->willReturn($interrupt->reveal());
    $interrupt->setDefaultValue(NULL)->willReturn($interrupt->reveal());
    $interrupt->setPipelineId('pipe_1')->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->setSessionId(42)->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->setDirection(InterruptDirection::Outward)->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();
    $interrupt->uuid()->willReturn('uuid-456');

    $this->currentUser->id()->willReturn('1');

    $this->storage->create(Argument::any())->willReturn($interrupt->reveal());
    $this->eventDispatcher->dispatch(Argument::cetera())->shouldBeCalled();

    $this->manager->create(new InterruptCreationData(
      type: InterruptType::Text,
      workflowId: 'wf_1',
      nodeId: 'node_1',
      message: 'Enter text',
      pipelineId: 'pipe_1',
      sessionId: 42,
    ));
  }

  /**
   * @covers ::create
   */
  public function testCreateInterruptWithExpiration(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->setSchema([])->willReturn($interrupt->reveal());
    $interrupt->setOptions([])->willReturn($interrupt->reveal());
    $interrupt->setContext([])->willReturn($interrupt->reveal());
    $interrupt->setDefaultValue(NULL)->willReturn($interrupt->reveal());
    $interrupt->setExpires(Argument::type('int'))->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->setDirection(InterruptDirection::Outward)->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();
    $interrupt->uuid()->willReturn('uuid-789');

    $this->currentUser->id()->willReturn('1');

    $this->storage->create(Argument::any())->willReturn($interrupt->reveal());
    $this->eventDispatcher->dispatch(Argument::cetera())->shouldBeCalled();

    $this->manager->create(new InterruptCreationData(
      type: InterruptType::Confirmation,
      workflowId: 'wf_1',
      nodeId: 'node_1',
      message: 'Confirm',
      expiresIn: 3600,
    ));
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptSuccess(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(TRUE);
    $interrupt->isExpired()->willReturn(FALSE);
    $interrupt->resolve('yes', 5)->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();

    $this->storage->loadByProperties(['uuid' => 'int-1'])
      ->willReturn([$interrupt->reveal()]);

    $this->lock->acquire('flowdrop_interrupt_resolve_int-1', 30)->willReturn(TRUE);
    $this->lock->release('flowdrop_interrupt_resolve_int-1')->shouldBeCalled();

    $this->eventDispatcher->dispatch(
      Argument::type(InterruptResolvedEvent::class),
      InterruptResolvedEvent::NAME
    )->shouldBeCalled();

    $result = $this->manager->resolveInterrupt('int-1', 'yes', 5);
    $this->assertSame($interrupt->reveal(), $result);
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptLockNotAcquiredThrows(): void {
    $this->lock->acquire('flowdrop_interrupt_resolve_int-1', 30)->willReturn(FALSE);

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('already in progress');

    $this->manager->resolveInterrupt('int-1', 'yes');
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptNotFoundReturnsNull(): void {
    $this->lock->acquire(Argument::cetera())->willReturn(TRUE);
    $this->lock->release(Argument::any())->shouldBeCalled();

    $this->storage->loadByProperties(['uuid' => 'missing'])
      ->willReturn([]);

    $result = $this->manager->resolveInterrupt('missing', 'data');
    $this->assertNull($result);
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptNotPendingThrows(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(FALSE);
    $interrupt->getStatus()->willReturn(InterruptStatus::Resolved);

    $this->storage->loadByProperties(['uuid' => 'int-2'])
      ->willReturn([$interrupt->reveal()]);

    $this->lock->acquire(Argument::cetera())->willReturn(TRUE);
    $this->lock->release(Argument::any())->shouldBeCalled();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('not in a resolvable state');

    $this->manager->resolveInterrupt('int-2', 'data');
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptExpiredThrows(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(TRUE);
    $interrupt->isExpired()->willReturn(TRUE);
    $interrupt->expire()->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();

    $this->storage->loadByProperties(['uuid' => 'int-3'])
      ->willReturn([$interrupt->reveal()]);

    $this->lock->acquire(Argument::cetera())->willReturn(TRUE);
    $this->lock->release(Argument::any())->shouldBeCalled();

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('has expired');

    $this->manager->resolveInterrupt('int-3', 'data');
  }

  /**
   * @covers ::resolveInterrupt
   */
  public function testResolveInterruptUsesCurrentUserWhenNoUserId(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(TRUE);
    $interrupt->isExpired()->willReturn(FALSE);
    $interrupt->resolve('data', 10)->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();

    $this->storage->loadByProperties(['uuid' => 'int-4'])
      ->willReturn([$interrupt->reveal()]);

    $this->currentUser->isAuthenticated()->willReturn(TRUE);
    $this->currentUser->id()->willReturn('10');

    $this->lock->acquire(Argument::cetera())->willReturn(TRUE);
    $this->lock->release(Argument::any())->shouldBeCalled();
    $this->eventDispatcher->dispatch(Argument::cetera())->shouldBeCalled();

    $this->manager->resolveInterrupt('int-4', 'data');
  }

  /**
   * @covers ::cancelInterrupt
   */
  public function testCancelInterruptPendingSuccess(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(TRUE);
    $interrupt->cancel()->shouldBeCalled()->willReturn($interrupt->reveal());
    $interrupt->save()->shouldBeCalled();

    $this->storage->loadByProperties(['uuid' => 'int-5'])
      ->willReturn([$interrupt->reveal()]);

    $this->eventDispatcher->dispatch(
      Argument::type(InterruptCancelledEvent::class),
      InterruptCancelledEvent::NAME
    )->shouldBeCalled();

    $this->assertTrue($this->manager->cancelInterrupt('int-5'));
  }

  /**
   * @covers ::cancelInterrupt
   */
  public function testCancelInterruptNotFoundReturnsFalse(): void {
    $this->storage->loadByProperties(['uuid' => 'missing'])
      ->willReturn([]);

    $this->assertFalse($this->manager->cancelInterrupt('missing'));
  }

  /**
   * @covers ::cancelInterrupt
   */
  public function testCancelInterruptNotPendingReturnsFalse(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);
    $interrupt->isPending()->willReturn(FALSE);
    $interrupt->getStatus()->willReturn(InterruptStatus::Resolved);

    $this->storage->loadByProperties(['uuid' => 'int-6'])
      ->willReturn([$interrupt->reveal()]);

    $this->assertFalse($this->manager->cancelInterrupt('int-6'));
  }

  /**
   * @covers ::getInterrupt
   */
  public function testGetInterruptReturnsEntity(): void {
    $interrupt = $this->prophesize(FlowDropInterruptInterface::class);

    $this->storage->loadByProperties(['uuid' => 'int-7'])
      ->willReturn([$interrupt->reveal()]);

    $result = $this->manager->getInterrupt('int-7');
    $this->assertSame($interrupt->reveal(), $result);
  }

  /**
   * @covers ::getInterrupt
   */
  public function testGetInterruptReturnsNullWhenNotFound(): void {
    $this->storage->loadByProperties(['uuid' => 'missing'])
      ->willReturn([]);

    $this->assertNull($this->manager->getInterrupt('missing'));
  }

}
