<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_trigger\Unit\Hook;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_trigger\Hook\WorkflowHooks;
use Drupal\flowdrop_trigger\Service\TriggerConfigSchemaBuilder;
use Drupal\flowdrop_trigger\Service\TriggerManagerInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the WorkflowHooks class.
 *
 * @coversDefaultClass \Drupal\flowdrop_trigger\Hook\WorkflowHooks
 * @group flowdrop_trigger
 */
class WorkflowHooksTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The hooks instance under test.
   *
   * @var \Drupal\flowdrop_trigger\Hook\WorkflowHooks
   */
  private WorkflowHooks $hooks;

  /**
   * The trigger manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\TriggerManagerInterface
   */
  private $triggerManager;

  /**
   * The node metadata resolver mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_node_type\Service\NodeMetadataResolver
   */
  private $nodeMetadataResolver;

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

  /**
   * The schema builder mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_trigger\Service\TriggerConfigSchemaBuilder
   */
  private $schemaBuilder;

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

    $this->triggerManager = $this->prophesize(TriggerManagerInterface::class);
    $this->nodeMetadataResolver = $this->prophesize(NodeMetadataResolver::class);
    $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
    $this->schemaBuilder = $this->prophesize(TriggerConfigSchemaBuilder::class);

    // By default, the 'trigger' node type resolves to the trigger processor.
    $this->nodeMetadataResolver->resolveExecutorPlugin('trigger')
      ->willReturn('flowdrop_trigger:trigger');

    $this->hooks = new WorkflowHooks(
      $this->triggerManager->reveal(),
      $this->nodeMetadataResolver->reveal(),
      $this->entityTypeManager->reveal(),
      $this->schemaBuilder->reveal(),
    );
  }

  /**
   * Builds a stored trigger node with the given id and config buffer.
   *
   * @param string $id
   *   The node id.
   * @param array<string, mixed> $config
   *   The node's data.config buffer.
   *
   * @return array<string, mixed>
   *   The stored node array.
   */
  private function triggerNode(string $id, array $config): array {
    return [
      'id' => $id,
      'data' => [
        'metadata' => ['node_type_id' => 'trigger'],
        'config' => $config,
      ],
    ];
  }

  /**
   * @covers ::workflowPredelete
   */
  public function testPredeleteDeletesAllConfigsForWorkflow(): void {
    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');

    $a = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $b = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $a->delete()->shouldBeCalledOnce();
    $b->delete()->shouldBeCalledOnce();

    $this->triggerManager->getTriggersForWorkflow('wf')
      ->willReturn([$a->reveal(), $b->reveal()]);

    $this->hooks->workflowPredelete($workflow->reveal());
  }

  /**
   * @covers ::workflowPredelete
   */
  public function testPredeleteIgnoresNonWorkflowEntity(): void {
    $entity = $this->prophesize(EntityInterface::class);

    $this->triggerManager->getTriggersForWorkflow(Argument::cetera())
      ->shouldNotBeCalled();

    $this->hooks->workflowPredelete($entity->reveal());
  }

  /**
   * @covers ::workflowUpdate
   */
  public function testUpdateDeletesOnlyOrphanedConfigs(): void {
    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    // Only "kept" node remains in the saved graph.
    $workflow->getNodes()->willReturn([
      ['id' => 'kept', 'data' => []],
      ['id' => 'other', 'data' => []],
    ]);

    $kept = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $kept->getNodeId()->willReturn('kept');
    $kept->delete()->shouldNotBeCalled();

    $orphan = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $orphan->getNodeId()->willReturn('removed');
    $orphan->delete()->shouldBeCalledOnce();

    $this->triggerManager->getTriggersForWorkflow('wf')
      ->willReturn([$kept->reveal(), $orphan->reveal()]);

    $this->hooks->workflowUpdate($workflow->reveal());
  }

  /**
   * @covers ::workflowUpdate
   */
  public function testUpdateNoConfigsIsNoOp(): void {
    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->getNodes()->shouldNotBeCalled();

    $this->triggerManager->getTriggersForWorkflow('wf')->willReturn([]);

    $this->hooks->workflowUpdate($workflow->reveal());
  }

  /**
   * @covers ::workflowUpdate
   */
  public function testUpdateIgnoresNonWorkflowEntity(): void {
    $entity = $this->prophesize(EntityInterface::class);

    $this->triggerManager->getTriggersForWorkflow(Argument::cetera())
      ->shouldNotBeCalled();

    $this->hooks->workflowUpdate($entity->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveFlushesBufferedConfigThenStripsIt(): void {
    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getNodeId()->willReturn('trigger-1');
    $config->getEventType()->willReturn('entity.insert');
    $config->getConditions()->willReturn([]);
    // The event type flushes, and the flat condition key overlays conditions;
    // the entity is then saved.
    $config->setEventType('user.login')->willReturn($config->reveal())
      ->shouldBeCalledOnce();
    $config->setConditions(['roles' => ['editor']])->willReturn($config->reveal())
      ->shouldBeCalledOnce();
    $config->save()->shouldBeCalledOnce();

    $this->triggerManager->getTriggersForWorkflow('wf')
      ->willReturn([$config->reveal()]);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->isSyncing()->willReturn(FALSE);
    $workflow->getNodes()->willReturn([
      $this->triggerNode('trigger-1', [
        'event_type' => 'user.login',
        'roles' => ['editor'],
        'label' => 'keep me',
      ]),
    ]);
    // The buffered keys are stripped from the stored node; other config stays.
    $workflow->setNodes([
      [
        'id' => 'trigger-1',
        'data' => [
          'metadata' => ['node_type_id' => 'trigger'],
          'config' => ['label' => 'keep me'],
        ],
      ],
    ])->willReturn($workflow->reveal())->shouldBeCalledOnce();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveDoesNotSaveWhenBufferMatchesEntity(): void {
    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getNodeId()->willReturn('trigger-1');
    $config->getEventType()->willReturn('entity.insert');
    $config->getConditions()->willReturn(['bundles' => ['article']]);
    // Buffer equals the entity → no setter, no save.
    $config->setEventType(Argument::any())->shouldNotBeCalled();
    $config->setConditions(Argument::any())->shouldNotBeCalled();
    $config->save()->shouldNotBeCalled();

    $this->triggerManager->getTriggersForWorkflow('wf')
      ->willReturn([$config->reveal()]);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->isSyncing()->willReturn(FALSE);
    $workflow->getNodes()->willReturn([
      $this->triggerNode('trigger-1', [
        'event_type' => 'entity.insert',
        'bundles' => ['article'],
      ]),
    ]);
    // Buffered keys were the only config → node collapses to an empty buffer.
    $workflow->setNodes([
      [
        'id' => 'trigger-1',
        'data' => [
          'metadata' => ['node_type_id' => 'trigger'],
          'config' => [],
        ],
      ],
    ])->willReturn($workflow->reveal())->shouldBeCalledOnce();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveIsNoOpWhenNodeCarriesNoBuffer(): void {
    // A trigger node with an existing config but no buffered keys in
    // data.config (only the reserved variant): nothing to flush or strip.
    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getNodeId()->willReturn('trigger-1');
    $config->getEventType()->shouldNotBeCalled();
    $config->getConditions()->willReturn([]);
    $config->save()->shouldNotBeCalled();

    $this->triggerManager->getTriggersForWorkflow('wf')
      ->willReturn([$config->reveal()]);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->isSyncing()->willReturn(FALSE);
    $workflow->getNodes()->willReturn([
      $this->triggerNode('trigger-1', ['variant' => 'start']),
    ]);
    // Nothing buffered → nodes are not rewritten.
    $workflow->setNodes(Argument::any())->shouldNotBeCalled();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveCreatesConfigForInlineConfiguredNewNode(): void {
    // No existing config for this workflow — the node was configured inline
    // before an entity existed, so presave must create one.
    $this->triggerManager->getTriggersForWorkflow('wf')->willReturn([]);

    $this->schemaBuilder->getDefaultValues('user.login')->willReturn([
      'conditions' => ['roles' => []],
      'orchestrator_settings' => ['type' => 'async'],
    ]);

    $created = $this->prophesize(FlowDropTriggerConfigInterface::class);
    // Created with the event type from the buffer; the baseline is persisted.
    $created->getEventType()->willReturn('user.login');
    $created->getConditions()->willReturn(['roles' => []]);
    // Flush overlays the author's roles edit, then saves again.
    $created->setConditions(['roles' => ['editor']])->willReturn($created->reveal())
      ->shouldBeCalledOnce();
    $created->setEventType(Argument::any())->shouldNotBeCalled();
    $created->save()->shouldBeCalled();

    $storage = $this->prophesize(EntityStorageInterface::class);
    $storage->create(Argument::that(function ($values): bool {
      // generateId() sanitises the node id (hyphen → underscore).
      return $values['id'] === 'wf__trigger_1'
        && $values['workflow_id'] === 'wf'
        && $values['node_id'] === 'trigger-1'
        && $values['event_type'] === 'user.login';
    }))->willReturn($created->reveal())->shouldBeCalledOnce();
    $this->entityTypeManager->getStorage('flowdrop_trigger_config')
      ->willReturn($storage->reveal());

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->isSyncing()->willReturn(FALSE);
    $workflow->getNodes()->willReturn([
      $this->triggerNode('trigger-1', [
        'event_type' => 'user.login',
        'roles' => ['editor'],
      ]),
    ]);
    $workflow->setNodes([
      [
        'id' => 'trigger-1',
        'data' => [
          'metadata' => ['node_type_id' => 'trigger'],
          'config' => [],
        ],
      ],
    ])->willReturn($workflow->reveal())->shouldBeCalledOnce();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveIgnoresNonTriggerNodes(): void {
    // A non-trigger node is left untouched, even if it carries a config buffer.
    $this->nodeMetadataResolver->resolveExecutorPlugin('http_request')
      ->willReturn('flowdrop:http_request');
    $this->triggerManager->getTriggersForWorkflow('wf')->willReturn([]);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf');
    $workflow->isSyncing()->willReturn(FALSE);
    $workflow->getNodes()->willReturn([
      [
        'id' => 'http-1',
        'data' => [
          'metadata' => ['node_type_id' => 'http_request'],
          'config' => ['event_type' => 'nonsense', 'roles' => ['x']],
        ],
      ],
    ]);
    // Not a trigger node → no create, no flush, no strip.
    $this->entityTypeManager->getStorage(Argument::any())->shouldNotBeCalled();
    $workflow->setNodes(Argument::any())->shouldNotBeCalled();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveSkipsDuringConfigSync(): void {
    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->isSyncing()->willReturn(TRUE);
    $workflow->getNodes()->shouldNotBeCalled();

    $this->triggerManager->getTriggersForWorkflow(Argument::cetera())
      ->shouldNotBeCalled();

    $this->hooks->workflowPresave($workflow->reveal());
  }

  /**
   * @covers ::workflowPresave
   */
  public function testPresaveIgnoresNonWorkflowEntity(): void {
    $entity = $this->prophesize(EntityInterface::class);

    $this->triggerManager->getTriggersForWorkflow(Argument::cetera())
      ->shouldNotBeCalled();

    $this->hooks->workflowPresave($entity->reveal());
  }

}
