<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow_executor\Kernel;

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

/**
 * Pins the workflow-as-tool path: a workflow node wired to a tool port.
 *
 * "Tools ARE workflows": a `workflow:<id>` derivative node placed on the
 * canvas and wired into a consumer's tool port must surface as a callable
 * tool — its input_schema derived from the sub-workflow's declared input
 * ports, its invocation running the sub-flow and returning the declared
 * output ports. No hand-built wrapper processor per flow.
 *
 * The consumer is the deterministic ToolCaller reference processor (no LLM):
 * it lists the wired tools and invokes one by name with static arguments.
 *
 * @group flowdrop_workflow_executor
 */
class WorkflowAsToolTest extends WorkflowExecutionTestBase {

  /**
   * {@inheritdoc}
   *
   * Known gap: WorkflowSchemaBuilder copies JSON-Schema per-property
   * `required: bool` fragments (e.g. from the calculator plugin) into the
   * workflow's parameter_schema snapshot, but the config schema declares
   * `required` as a sequence — the strict checker rejects the save.
   * Production saves are unaffected (no strict checker); fix tracked with
   * the workflows-as-tools work.
   */
  protected $strictConfigSchema = FALSE;

  /**
   * A workflow node wired to a tool port is a callable tool.
   *
   * Sub-workflow: a single calculator node, with `values` declared as the
   * workflow input port and `result` as the output port. The consumer
   * invokes it as the tool `echo_sub` with values [2, 3] and must get the
   * sub-flow's declared output back: result = 5.
   */
  public function testWorkflowNodeWiredToToolPortIsInvokable(): void {
    // The sub-workflow that should become the tool.
    $sub = $this->createWorkflow('echo_sub', [
      $this->node('calculator.1', 'calculator', ['operation' => 'add', 'precision' => 2], [
        'name' => 'Calculator',
        'category' => 'tools',
        // The schema snapshotter resolves declared ports through the node's
        // executor plugin.
        'executor_plugin' => 'flowdrop_node_processor:calculator',
      ]),
    ], []);
    $sub->setInputPorts([
      ['name' => 'values', 'node_id' => 'calculator.1', 'port' => 'values'],
    ]);
    $sub->setOutputPorts([
      ['name' => 'result', 'node_id' => 'calculator.1', 'port' => 'result'],
    ]);
    $sub->save();

    // The node type that makes the workflow placeable on a canvas — today
    // this is the manual step (no auto-sync from workflow save yet).
    $this->container->get('entity_type.manager')
      ->getStorage('flowdrop_node_type')
      ->create([
        'id' => 'workflow_echo_sub',
        'label' => 'Echo Sub',
        'category' => 'tools',
        'enabled' => TRUE,
        'parameters' => [
          'values' => ['configurable' => FALSE, 'connectable' => TRUE, 'required' => FALSE],
        ],
        'outputs' => [
          'result' => ['exposed' => TRUE],
        ],
        'executor_plugin' => 'flowdrop_workflow_executor:flowdrop_workflow:echo_sub',
      ])->save();

    // The ToolCaller reference processor ships without a node type entity;
    // create one for the fixture.
    $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();

    // Consumer workflow: ToolCaller invokes the wired workflow tool.
    $subToolNode = $this->node('sub_tool', 'workflow_echo_sub');
    // The tool's model-facing name derives from the node label.
    $subToolNode['data']['label'] = 'echo_sub';

    $workflow = $this->createStateGraphWorkflow('tool_consumer', [
      $this->node('caller', 'tool_caller', [
        'tool_name' => 'echo_sub',
        'arguments' => ['values' => [2, 3]],
      ], ['name' => 'Tool Caller']),
      $subToolNode,
    ], [
      $this->edge('sub_tool', 'result', 'caller', 'tool'),
    ]);

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

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

    $output = $this->jobOutputForNode($pipeline, 'caller');

    // The workflow tool appears in the manifest with a usable input_schema.
    $this->assertNotEmpty($output['available_tools'] ?? [],
      'The wired workflow node must surface in the tool manifest.');
    $definition = NULL;
    foreach ($output['available_tools'] as $candidate) {
      if (($candidate['name'] ?? '') === 'echo_sub') {
        $definition = $candidate;
      }
    }
    $this->assertNotNull($definition, 'Tool manifest entry named after the node label: '
      . json_encode($output['available_tools']));
    $this->assertArrayHasKey('values', $definition['input_schema']['properties'] ?? [],
      'The tool input_schema must expose the sub-workflow\'s declared input ports: '
      . json_encode($definition['input_schema'] ?? NULL));

    // The invocation ran the sub-flow and returned its declared outputs.
    $this->assertSame('echo_sub', $output['called'] ?? NULL);
    $this->assertTrue($output['success'] ?? NULL,
      'Workflow tool invocation must succeed: ' . json_encode($output));
    $this->assertEquals(5, $output['result']['result'] ?? NULL,
      'The tool result must carry the sub-workflow\'s declared output port: '
      . json_encode($output['result'] ?? NULL));

    // The workflow tool node itself must never be SCHEDULED as an ordinary
    // job. It appears exactly once as a recorded tool invocation — the job
    // ScopedToolInvoker creates around the call (tool_invocation metadata)
    // so tool calls show up in the pipeline's job trail.
    $subToolJobs = array_filter(
      $pipeline->getJobs(),
      static fn ($job) => $job->getNodeId() === 'sub_tool',
    );
    foreach ($subToolJobs as $job) {
      $this->assertTrue(
        $job->getMetadata()['tool_invocation'] ?? FALSE,
        'A tool-only workflow node may appear only as a recorded tool '
        . 'invocation, never as a graph-scheduled job.',
      );
    }
    $this->assertCount(1, $subToolJobs,
      'The tool call must be recorded as exactly one pipeline job item.');
  }

}
