<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_trigger\Unit\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\flowdrop\Service\JsonPath\JsonPathServiceInterface;
use Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager;
use Drupal\flowdrop_trigger\FlowDropEventTypeInterface;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_trigger\Service\OrchestrationRequestBuilder;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the OrchestrationRequestBuilder service.
 *
 * @coversDefaultClass \Drupal\flowdrop_trigger\Service\OrchestrationRequestBuilder
 * @group flowdrop_trigger
 */
class OrchestrationRequestBuilderTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The config factory mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ConfigFactoryInterface
   */
  private $configFactory;

  /**
   * The orchestrator plugin manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager
   */
  private $orchestratorManager;

  /**
   * The JSON path service mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop\Service\JsonPath\JsonPathServiceInterface
   */
  private $jsonPathService;

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

    $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
    $this->orchestratorManager = $this->prophesize(OrchestratorPluginManager::class);
    $this->jsonPathService = $this->prophesize(JsonPathServiceInterface::class);
  }

  /**
   * Creates a request builder instance.
   *
   * @return \Drupal\flowdrop_trigger\Service\OrchestrationRequestBuilder
   *   The request builder.
   */
  private function createBuilder(?JsonPathServiceInterface $jsonPath = NULL): OrchestrationRequestBuilder {
    return new OrchestrationRequestBuilder(
      $this->configFactory->reveal(),
      $this->orchestratorManager->reveal(),
      $jsonPath,
    );
  }

  /**
   * @covers ::buildInitialContext
   */
  public function testBuildInitialContextDefaultFields(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('trigger_1');
    $config->getNodeId()->willReturn('node_start');
    $config->getInitialDataMapping()->willReturn([]);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');
    $entity->id()->willReturn(42);
    $entity->bundle()->willReturn('article');
    $entity->isNew()->willReturn(FALSE);

    $context = $builder->buildInitialContext(
      $config->reveal(),
      'entity.insert',
      $entity->reveal(),
      ['title' => 'Test'],
      ['title' => 'Old'],
    );

    $this->assertEquals('entity.insert', $context['event_type']);
    $this->assertEquals('node', $context['entity_type']);
    $this->assertEquals(42, $context['entity_id']);
    $this->assertEquals('article', $context['bundle']);
    $this->assertEquals(['title' => 'Test'], $context['entity']);
    $this->assertEquals(['title' => 'Old'], $context['original_entity']);
    $this->assertFalse($context['is_new']);
    $this->assertEquals('trigger_1', $context['trigger_config_id']);
    $this->assertEquals('node_start', $context['trigger_node_id']);
  }

  /**
   * @covers ::buildInitialContext
   */
  public function testBuildInitialContextWithJsonPathMapping(): void {
    $this->jsonPathService->applyMappings(Argument::type('array'), ['email' => '$.entity.mail'])
      ->willReturn(['email' => 'test@example.com']);

    $builder = $this->createBuilder($this->jsonPathService->reveal());

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('t1');
    $config->getNodeId()->willReturn('n1');
    $config->getInitialDataMapping()->willReturn(['email' => '$.entity.mail']);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('user');
    $entity->id()->willReturn(1);
    $entity->bundle()->willReturn('user');
    $entity->isNew()->willReturn(FALSE);

    $context = $builder->buildInitialContext(
      $config->reveal(), 'entity.update', $entity->reveal(), [], NULL,
    );

    $this->assertEquals('test@example.com', $context['email']);
  }

  /**
   * @covers ::buildInitialContext
   */
  public function testBuildInitialContextFallbackWithoutJsonPath(): void {
    $builder = $this->createBuilder(NULL);

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('t1');
    $config->getNodeId()->willReturn('n1');
    $config->getInitialDataMapping()->willReturn(['custom_key' => 'static_value']);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');
    $entity->id()->willReturn(1);
    $entity->bundle()->willReturn('page');
    $entity->isNew()->willReturn(TRUE);

    $context = $builder->buildInitialContext(
      $config->reveal(), 'entity.insert', $entity->reveal(), [], NULL,
    );

    // Without JsonPath, mapping is merged directly.
    $this->assertEquals('static_value', $context['custom_key']);
  }

  /**
   * @covers ::getOrchestrator
   */
  public function testGetOrchestratorDefaultUsesEventTypePlugin(): void {
    $builder = $this->createBuilder();

    $plugin = $this->prophesize(FlowDropEventTypeInterface::class);
    $plugin->getDefaultOrchestrator()->willReturn('flowdrop_runtime:synchronous');

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getOrchestratorSettings()->willReturn(['type' => 'default']);
    $config->getEventTypePlugin()->willReturn($plugin->reveal());

    $orchestrator = $this->prophesize(OrchestratorPluginInterface::class);
    $this->orchestratorManager->hasDefinition('flowdrop_runtime:synchronous')->willReturn(TRUE);
    $this->orchestratorManager->createInstance('flowdrop_runtime:synchronous')
      ->willReturn($orchestrator->reveal());

    $result = $builder->getOrchestrator($config->reveal(), 'entity.insert');
    $this->assertSame($orchestrator->reveal(), $result);
  }

  /**
   * @covers ::getOrchestrator
   */
  public function testGetOrchestratorDefaultFallsBackToConfig(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getOrchestratorSettings()->willReturn(['type' => 'default']);
    $config->getEventTypePlugin()->willReturn(NULL);

    $generalConfig = $this->prophesize(ImmutableConfig::class);
    $generalConfig->get('default_orchestrator')->willReturn('flowdrop_runtime:pipeline');
    $this->configFactory->get('flowdrop_trigger.settings')->willReturn($generalConfig->reveal());

    $orchestrator = $this->prophesize(OrchestratorPluginInterface::class);
    $this->orchestratorManager->hasDefinition('flowdrop_runtime:pipeline')->willReturn(TRUE);
    $this->orchestratorManager->createInstance('flowdrop_runtime:pipeline')
      ->willReturn($orchestrator->reveal());

    $result = $builder->getOrchestrator($config->reveal(), 'entity.insert');
    $this->assertSame($orchestrator->reveal(), $result);
  }

  /**
   * @covers ::getOrchestrator
   */
  public function testGetOrchestratorPresaveForcedSynchronous(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getOrchestratorSettings()->willReturn(['type' => 'flowdrop_runtime:asynchronous']);

    $orchestrator = $this->prophesize(OrchestratorPluginInterface::class);
    $this->orchestratorManager->hasDefinition('flowdrop_runtime:synchronous')->willReturn(TRUE);
    $this->orchestratorManager->createInstance('flowdrop_runtime:synchronous')
      ->willReturn($orchestrator->reveal());

    // Presave forces synchronous regardless of config.
    $result = $builder->getOrchestrator($config->reveal(), 'entity.presave');
    $this->assertSame($orchestrator->reveal(), $result);
  }

  /**
   * @covers ::getOrchestrator
   */
  public function testGetOrchestratorInvalidFallsBack(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->getOrchestratorSettings()->willReturn(['type' => 'nonexistent:plugin']);

    $this->orchestratorManager->hasDefinition('nonexistent:plugin')->willReturn(FALSE);
    $orchestrator = $this->prophesize(OrchestratorPluginInterface::class);
    $this->orchestratorManager->createInstance('flowdrop_runtime:asynchronous')
      ->willReturn($orchestrator->reveal());

    $result = $builder->getOrchestrator($config->reveal(), 'entity.insert');
    $this->assertSame($orchestrator->reveal(), $result);
  }

  /**
   * @covers ::determinePipelineId
   */
  public function testDeterminePipelineIdReuse(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('my_trigger');

    $result = $builder->determinePipelineId($config->reveal(), [
      'pipeline_mode' => 'reuse',
      'pipeline_id' => 'custom_pipe',
    ]);

    $this->assertEquals('custom_pipe', $result);
  }

  /**
   * @covers ::determinePipelineId
   */
  public function testDeterminePipelineIdReuseDefaultId(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('my_trigger');

    $result = $builder->determinePipelineId($config->reveal(), [
      'pipeline_mode' => 'reuse',
    ]);

    $this->assertEquals('trigger_my_trigger', $result);
  }

  /**
   * @covers ::determinePipelineId
   */
  public function testDeterminePipelineIdSingleton(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('my_trigger');

    $result = $builder->determinePipelineId($config->reveal(), [
      'pipeline_mode' => 'singleton',
    ]);

    $this->assertEquals('singleton_my_trigger', $result);
  }

  /**
   * @covers ::determinePipelineId
   */
  public function testDeterminePipelineIdNewIsUnique(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('my_trigger');

    $result = $builder->determinePipelineId($config->reveal(), [
      'pipeline_mode' => 'new',
    ]);

    $this->assertStringStartsWith('trigger_my_trigger_', $result);
  }

  /**
   * @covers ::buildOrchestrationOptions
   */
  public function testBuildOrchestrationOptionsBasic(): void {
    $builder = $this->createBuilder();

    $options = $builder->buildOrchestrationOptions(
      [],
      'trigger_1',
      'entity.insert',
      'node',
      42,
    );

    $this->assertEquals('trigger_1', $options['trigger_config_id']);
    $this->assertEquals('entity.insert', $options['trigger_event']);
    $this->assertEquals('node', $options['trigger_entity_type']);
    $this->assertEquals(42, $options['trigger_entity_id']);
    $this->assertEquals('normal', $options['priority']);
    $this->assertEquals(300, $options['timeout']);
  }

  /**
   * @covers ::buildOrchestrationOptions
   */
  public function testBuildOrchestrationOptionsFormTrigger(): void {
    $builder = $this->createBuilder();

    $options = $builder->buildOrchestrationOptions(
      [],
      'trigger_1',
      'form.submit',
      NULL,
      NULL,
      'node_article_form',
    );

    $this->assertEquals('node_article_form', $options['trigger_form_id']);
    $this->assertArrayNotHasKey('trigger_entity_type', $options);
  }

  /**
   * @covers ::buildOrchestrationOptions
   */
  public function testBuildOrchestrationOptionsStateGraphSettings(): void {
    $builder = $this->createBuilder();

    $options = $builder->buildOrchestrationOptions(
      [
        'stategraph' => [
          'checkpointer_type' => 'database',
          'max_iterations' => 50,
          'debug_mode' => TRUE,
        ],
      ],
      'trigger_1',
      'entity.insert',
    );

    $this->assertEquals('database', $options['stategraph']['checkpointerType']);
    $this->assertEquals(50, $options['stategraph']['maxIterations']);
    $this->assertTrue($options['stategraph']['debugMode']);
  }

  /**
   * @covers ::buildEntityRequest
   */
  public function testBuildEntityRequestCompleteness(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('trigger_1');
    $config->getNodeId()->willReturn('start');
    $config->getOrchestratorSettings()->willReturn(['pipeline_mode' => 'singleton']);
    $config->getInitialDataMapping()->willReturn([]);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf_1');
    $workflow->getLabel()->willReturn('My Workflow');
    $workflow->getDescription()->willReturn('A workflow');
    $workflow->getNodes()->willReturn([]);
    $workflow->getEdges()->willReturn([]);
    $workflow->getMetadata()->willReturn([]);

    $entity = $this->prophesize(EntityInterface::class);
    $entity->getEntityTypeId()->willReturn('node');
    $entity->id()->willReturn(42);
    $entity->bundle()->willReturn('article');
    $entity->isNew()->willReturn(FALSE);

    $request = $builder->buildEntityRequest(
      $config->reveal(),
      $workflow->reveal(),
      'entity.insert',
      $entity->reveal(),
      ['title' => 'Test'],
      NULL,
    );

    $this->assertEquals('wf_1', $request->getWorkflowId());
    $this->assertEquals('singleton_trigger_1', $request->getPipelineId());
    $this->assertEquals('entity.insert', $request->getInitialData()['event_type']);
    $this->assertEquals('trigger_1', $request->getOptions()['trigger_config_id']);
  }

  /**
   * @covers ::buildCronRequest
   */
  public function testBuildCronRequestWithState(): void {
    $builder = $this->createBuilder();

    $config = $this->prophesize(FlowDropTriggerConfigInterface::class);
    $config->id()->willReturn('cron_trigger');
    $config->getNodeId()->willReturn('cron_start');
    $config->getOrchestratorSettings()->willReturn(['pipeline_mode' => 'new']);

    $workflow = $this->prophesize(FlowDropWorkflowInterface::class);
    $workflow->id()->willReturn('wf_cron');
    $workflow->getLabel()->willReturn('Cron WF');
    $workflow->getDescription()->willReturn('');
    $workflow->getNodes()->willReturn([]);
    $workflow->getEdges()->willReturn([]);
    $workflow->getMetadata()->willReturn([]);

    $request = $builder->buildCronRequest(
      $config->reveal(),
      $workflow->reveal(),
      'cron.run',
      ['cron_data' => 'value'],
      ['run_count' => 5, 'last_run' => 1000],
    );

    $data = $request->getInitialData();
    $this->assertEquals(6, $data['cron_execution_count']);
    $this->assertEquals(1000, $data['cron_last_run']);
    $this->assertEquals('value', $data['cron_data']);
    $this->assertEquals('cron_trigger', $data['trigger_config_id']);
  }

}
