<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_processor\Kernel;

use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop\DTO\Tool\ToolBinding;
use Drupal\flowdrop\DTO\Tool\ToolCollection;
use Drupal\flowdrop\DTO\Tool\ToolResult;
use Drupal\flowdrop\DTO\Tool\ToolResultInterface;
use Drupal\flowdrop\Service\Tool\ToolInvokerInterface;
use Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\ToolInvoke;
use Drupal\KernelTests\KernelTestBase;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests the ToolInvoke node — runs a batch of tool calls via the tool runtime.
 *
 * No real graph: a stub ToolInvokerInterface stands in for FlowDrop's
 * ScopedToolInvoker (whose authorize/validate/execute path is FlowDrop's own,
 * unit-tested code). We assert the node's contract — it resolves each call by
 * name against the wired tools, invokes it with the call's args, and emits a
 * tool-role message per call (paired by tool_call_id) — and that an
 * unknown/unwired tool is a recoverable error, not a throw.
 *
 * @coversDefaultClass \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\ToolInvoke
 * @group flowdrop_node_processor
 */
#[RunTestsInSeparateProcesses]
final class ToolInvokeTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['flowdrop', 'flowdrop_node_processor'];

  /**
   * A tool collection whose one tool echoes the args it was invoked with.
   *
   * The stub result shape ({ok, result}) mirrors a node result whose data
   * carries a `result` key; the recorded args let us assert the ToolInvoke
   * node forwarded them.
   */
  private function collectionRecording(array &$recorded): ToolCollection {
    $invoker = new class($recorded) implements ToolInvokerInterface {

      public function __construct(private array &$recorded) {}

      /**
       * {@inheritdoc}
       */
      public function invokeTool(string $nodeId, array $args): ToolResultInterface {
        $this->recorded = ['nodeId' => $nodeId, 'args' => $args];
        return ToolResult::success(['ok' => TRUE, 'result' => 'Created node 5 (draft).']);
      }

    };
    $binding = new ToolBinding(
      'create_content',
      'cap_create',
      'Create a node',
      ['type' => 'object', 'properties' => []],
      'Create content (cap_create)',
      $invoker,
    );
    return new ToolCollection([$binding]);
  }

  /**
   * @covers ::process
   *
   * A wired tool is invoked with the call's args and its result becomes a
   * tool-role message paired to the call id.
   */
  public function testInvokesWiredToolAndPairsResult(): void {
    $recorded = [];
    $node = new ToolInvoke([], 'flowdrop_node_processor:tool_invoke', []);
    $node->setTools($this->collectionRecording($recorded));

    $result = $node->process(new ParameterBag([
      'tool_calls' => [
        ['name' => 'create_content', 'args' => ['title' => 'Hello'], 'tool_call_id' => 'c1'],
      ],
    ]));

    // Forwarded the args to the wired node.
    $this->assertSame('cap_create', $recorded['nodeId']);
    $this->assertSame(['title' => 'Hello'], $recorded['args']);

    // Emitted one tool-role message, paired by id, carrying the result string.
    $this->assertTrue($result['ok']);
    $this->assertCount(1, $result['tool_messages']);
    $message = $result['tool_messages'][0];
    $this->assertSame('tool', $message['role']);
    $this->assertSame('c1', $message['tool_call_id']);
    $this->assertSame('Created node 5 (draft).', $message['content']);
  }

  /**
   * @covers ::process
   *
   * A batch of calls runs each in order; one unknown tool flips `ok` to FALSE
   * while the known calls still resolve.
   */
  public function testBatchWithUnknownToolIsRecoverable(): void {
    $recorded = [];
    $node = new ToolInvoke([], 'flowdrop_node_processor:tool_invoke', []);
    $node->setTools($this->collectionRecording($recorded));

    $result = $node->process(new ParameterBag([
      'tool_calls' => [
        ['name' => 'create_content', 'args' => [], 'tool_call_id' => 'c1'],
        ['name' => 'launch_rocket', 'args' => [], 'tool_call_id' => 'c9'],
      ],
    ]));

    // One unknown call in the batch is a recoverable error, not a throw.
    $this->assertFalse($result['ok']);
    $this->assertCount(2, $result['tool_messages']);
    $this->assertSame('c1', $result['tool_messages'][0]['tool_call_id']);
    $this->assertSame('c9', $result['tool_messages'][1]['tool_call_id']);
    $this->assertStringContainsString('not an available tool', $result['tool_messages'][1]['content']);
  }

}
