<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop\Unit\DTO\Tool;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\Tool\ToolBinding;
use Drupal\flowdrop\DTO\Tool\ToolBindingInterface;
use Drupal\flowdrop\DTO\Tool\ToolCollection;
use Drupal\flowdrop\DTO\Tool\ToolResult;
use Drupal\flowdrop\Service\Tool\ToolInvokerInterface;

/**
 * Tests the tool DTO value objects.
 *
 * @group flowdrop
 */
class ToolCollectionTest extends UnitTestCase {

  /**
   * Builds a binding wired to a no-op invoker.
   */
  private function binding(string $name, string $nodeId): ToolBindingInterface {
    $invoker = $this->prophesize(ToolInvokerInterface::class);
    $invoker->invokeTool($nodeId, [])->willReturn(ToolResult::success(["ok" => TRUE]));
    return new ToolBinding(
      $name,
      $nodeId,
      "Desc of {$name}",
      ["type" => "object"],
      "{$name} ({$nodeId})",
      $invoker->reveal(),
    );
  }

  /**
   * @covers \Drupal\flowdrop\DTO\Tool\ToolResult
   */
  public function testToolResult(): void {
    $ok = ToolResult::success(["x" => 1]);
    $this->assertTrue($ok->isSuccess());
    $this->assertSame(["x" => 1], $ok->getData());
    $this->assertNull($ok->getError());

    $err = ToolResult::error("boom");
    $this->assertFalse($err->isSuccess());
    $this->assertSame([], $err->getData());
    $this->assertSame("boom", $err->getError());
    $this->assertSame(
      ["success" => FALSE, "data" => [], "error" => "boom"],
      $err->toArray(),
    );
  }

  /**
   * @covers \Drupal\flowdrop\DTO\Tool\ToolBinding
   */
  public function testToolBindingExposesDefinitionAndRoutesInvoke(): void {
    $binding = $this->binding("web_search", "search_1");

    $this->assertSame("web_search", $binding->getName());
    $this->assertSame("search_1", $binding->getNodeId());
    $this->assertSame("web_search (search_1)", $binding->getDisplayTitle());
    $this->assertSame(
      ["name" => "web_search", "description" => "Desc of web_search", "input_schema" => ["type" => "object"]],
      $binding->toDefinition(),
    );

    // invoke() routes through the invoker keyed by the node ID.
    $result = $binding->invoke([]);
    $this->assertTrue($result->isSuccess());
    $this->assertSame(["ok" => TRUE], $result->getData());
  }

  /**
   * @covers \Drupal\flowdrop\DTO\Tool\ToolCollection
   */
  public function testCollectionLookupAndDefinitions(): void {
    $a = $this->binding("alpha", "node_a");
    $b = $this->binding("beta", "node_b");
    $collection = new ToolCollection([$a, $b]);

    $this->assertCount(2, $collection);
    $this->assertTrue($collection->has("alpha"));
    $this->assertFalse($collection->has("missing"));
    $this->assertSame($a, $collection->get("alpha"));
    $this->assertNull($collection->get("missing"));
    $this->assertSame([$a, $b], $collection->all());
    $this->assertSame([$a, $b], iterator_to_array($collection));
    $this->assertCount(2, $collection->getDefinitions());
    $this->assertSame("alpha", $collection->getDefinitions()[0]["name"]);
  }

  /**
   * The collection guards against duplicate names rather than dropping a tool.
   *
   * @covers \Drupal\flowdrop\DTO\Tool\ToolCollection
   */
  public function testDuplicateNamesThrow(): void {
    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Duplicate tool name "dup"');
    new ToolCollection([
      $this->binding("dup", "node_a"),
      $this->binding("dup", "node_b"),
    ]);
  }

  /**
   * An empty collection is valid and iterable.
   *
   * @covers \Drupal\flowdrop\DTO\Tool\ToolCollection
   */
  public function testEmptyCollection(): void {
    $collection = new ToolCollection();
    $this->assertCount(0, $collection);
    $this->assertSame([], $collection->all());
    $this->assertSame([], $collection->getDefinitions());
  }

}
