<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_processor\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop\DTO\Tool\ToolBindingInterface;
use Drupal\flowdrop\DTO\Tool\ToolCollection;
use Drupal\flowdrop\DTO\Tool\ToolResult;
use Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\ToolCaller;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the ToolCaller reference processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\ToolCaller
 * @group flowdrop_node_processor
 */
class ToolCallerTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * Builds a ToolCaller with one wired tool.
   *
   * @param \Prophecy\Prophecy\ObjectProphecy $binding
   *   The (unrevealed) tool binding prophecy, so callers can add expectations.
   */
  private function caller($binding): ToolCaller {
    $binding->getName()->willReturn("web_search");
    $binding->toDefinition()->willReturn([
      "name" => "web_search",
      "description" => "Search the web",
      "input_schema" => ["type" => "object"],
    ]);
    $caller = new ToolCaller([], "tool_caller", []);
    $caller->setTools(new ToolCollection([$binding->reveal()]));
    return $caller;
  }

  /**
   * With no tool requested, the processor lists available tools.
   *
   * @covers ::process
   */
  public function testListsAvailableToolsWhenNoneRequested(): void {
    $binding = $this->prophesize(ToolBindingInterface::class);
    $binding->invoke(Argument::any())->shouldNotBeCalled();
    $caller = $this->caller($binding);

    $output = $caller->process(new ParameterBag([]));

    $this->assertNull($output["called"]);
    $this->assertCount(1, $output["available_tools"]);
    $this->assertSame("web_search", $output["available_tools"][0]["name"]);
  }

  /**
   * A known tool is invoked and its result returned.
   *
   * @covers ::process
   */
  public function testInvokesKnownTool(): void {
    $binding = $this->prophesize(ToolBindingInterface::class);
    $binding->invoke(["q" => "drupal"])
      ->willReturn(ToolResult::success(["hits" => 3]))
      ->shouldBeCalled();
    $caller = $this->caller($binding);

    $output = $caller->process(new ParameterBag([
      "tool_name" => "web_search",
      "arguments" => ["q" => "drupal"],
    ]));

    $this->assertSame("web_search", $output["called"]);
    $this->assertTrue($output["success"]);
    $this->assertSame(["hits" => 3], $output["result"]);
    $this->assertNull($output["error"]);
  }

  /**
   * An unknown tool name yields a recoverable error, not an invocation.
   *
   * @covers ::process
   */
  public function testUnknownToolReturnsError(): void {
    $binding = $this->prophesize(ToolBindingInterface::class);
    $binding->invoke(Argument::any())->shouldNotBeCalled();
    $caller = $this->caller($binding);

    $output = $caller->process(new ParameterBag(["tool_name" => "does_not_exist"]));

    $this->assertSame("does_not_exist", $output["called"]);
    $this->assertFalse($output["success"]);
    $this->assertStringContainsString("does_not_exist", $output["error"]);
  }

  /**
   * A failed tool result surfaces as an error, not an exception.
   *
   * @covers ::process
   */
  public function testFailedToolResultSurfacesError(): void {
    $binding = $this->prophesize(ToolBindingInterface::class);
    $binding->invoke(Argument::any())->willReturn(ToolResult::error("boom"));
    $caller = $this->caller($binding);

    $output = $caller->process(new ParameterBag(["tool_name" => "web_search"]));

    $this->assertFalse($output["success"]);
    $this->assertSame("boom", $output["error"]);
  }

}
