<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_processor\Kernel;

use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;

/**
 * Pins the ToolBox path: tools wired once, shared by multiple consumers.
 *
 * Topology (real workflow JSON through the real compiler and projector):
 *
 *   adder ──tool──┐
 *                 ├─→ [box] ──tool──→ caller_1
 *   multiplier ───┘       └──tool──→ caller_2
 *
 * Each consumer must see BOTH tools in its manifest as if wired directly,
 * invoke its own choice by name, and neither the box nor the leaf tools may
 * ever be scheduled as ordinary jobs.
 *
 * @group flowdrop_node_processor
 */
class ToolBoxTest extends WorkflowExecutionTestBase {

  /**
   * Tools in a box are projected into every consumer wired to the box.
   */
  public function testToolboxSharesToolsAcrossConsumers(): void {
    // The ToolCaller reference processor ships without a node type entity;
    // create one for the fixture (same shape as WorkflowAsToolTest).
    $this->container->get('entity_type.manager')
      ->getStorage('flowdrop_node_type')
      ->create([
        'id' => 'tool_caller',
        'label' => 'Tool Caller',
        'category' => 'tools',
        'enabled' => TRUE,
        'parameters' => [
          // The reserved tool input port; tool nodes are wired into it.
          'tool' => ['configurable' => FALSE, 'connectable' => TRUE, 'required' => FALSE],
          'tool_name' => ['configurable' => TRUE, 'connectable' => TRUE, 'required' => FALSE],
          'arguments' => ['configurable' => TRUE, 'connectable' => TRUE, 'required' => FALSE],
        ],
        'outputs' => [
          'available_tools' => ['exposed' => TRUE],
          'called' => ['exposed' => TRUE],
          'success' => ['exposed' => TRUE],
          'result' => ['exposed' => TRUE],
          'error' => ['exposed' => TRUE],
        ],
        'executor_plugin' => 'flowdrop_node_processor:tool_caller',
      ])->save();

    // Two calculator tools with distinct, model-facing labels.
    $adder = $this->node('calc_add', 'calculator', ['operation' => 'add', 'precision' => 2], [
      'name' => 'Calculator',
      'executor_plugin' => 'flowdrop_node_processor:calculator',
    ]);
    $adder['data']['label'] = 'Adder';
    $multiplier = $this->node('calc_mult', 'calculator', ['operation' => 'multiply', 'precision' => 2], [
      'name' => 'Calculator',
      'executor_plugin' => 'flowdrop_node_processor:calculator',
    ]);
    $multiplier['data']['label'] = 'Multiplier';

    // The shared box (node type installed from flowdrop_node_processor
    // config) and two independent consumers.
    $box = $this->node('box', 'toolbox');
    $box['data']['label'] = 'Shared Tools';

    $workflow = $this->createStateGraphWorkflow('toolbox_sharing', [
      $adder,
      $multiplier,
      $box,
      $this->node('caller_1', 'tool_caller', [
        'tool_name' => 'adder',
        'arguments' => ['values' => [2, 3]],
      ], ['name' => 'Tool Caller']),
      $this->node('caller_2', 'tool_caller', [
        'tool_name' => 'multiplier',
        'arguments' => ['values' => [2, 3]],
      ], ['name' => 'Tool Caller']),
    ], [
      $this->edge('calc_add', 'result', 'box', 'tool'),
      $this->edge('calc_mult', 'result', 'box', 'tool'),
      $this->edge('box', 'tool', 'caller_1', 'tool'),
      $this->edge('box', 'tool', 'caller_2', 'tool'),
    ]);

    $result = $this->launchAndWait($workflow);

    $pipeline = $this->loadPipeline($result->pipelineId);
    $this->assertSame(PipelineStatus::COMPLETED, $result->status, $this->jobTrail($pipeline));

    // Both consumers see the box's full tool set in their manifests.
    foreach (['caller_1', 'caller_2'] as $caller) {
      $output = $this->jobOutputForNode($pipeline, $caller);
      $names = array_map(
        static fn(array $definition): string => $definition['name'] ?? '',
        $output['available_tools'] ?? [],
      );
      sort($names);
      $this->assertSame(['adder', 'multiplier'], $names,
        "{$caller} must see both boxed tools: " . json_encode($output['available_tools'] ?? NULL));
    }

    // Each consumer invoked its own tool from the shared set.
    $output1 = $this->jobOutputForNode($pipeline, 'caller_1');
    $this->assertTrue($output1['success'] ?? NULL, json_encode($output1));
    $this->assertEquals(5, $output1['result']['result'] ?? NULL,
      'caller_1 must get adder(2, 3) = 5: ' . json_encode($output1['result'] ?? NULL));

    $output2 = $this->jobOutputForNode($pipeline, 'caller_2');
    $this->assertTrue($output2['success'] ?? NULL, json_encode($output2));
    $this->assertEquals(6, $output2['result']['result'] ?? NULL,
      'caller_2 must get multiplier(2, 3) = 6: ' . json_encode($output2['result'] ?? NULL));

    // Neither the box nor the leaf tools are scheduled as ordinary jobs. Tool
    // invocations are recorded as their own jobs (metadata.tool_invocation),
    // which is expected and distinct from graph scheduling — exclude those
    // before asserting the tool nodes never appear as ordinary scheduled jobs.
    $ordinaryNodeIds = array_map(
      static fn ($job) => $job->getNodeId(),
      array_filter(
        $pipeline->getJobs(),
        static fn ($job) => $job->getMetadataValue('tool_invocation', FALSE) !== TRUE,
      ),
    );
    foreach (['box', 'calc_add', 'calc_mult'] as $excluded) {
      $this->assertNotContains($excluded, $ordinaryNodeIds,
        "Tool-only node {$excluded} must not be scheduled as an ordinary job.");
    }
  }

}
