<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service\Compiler;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Tests\flowdrop_runtime\Unit\Service\Compiler\Fixtures\ToolPassthroughStub;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_runtime\Exception\CompilationException;
use Drupal\flowdrop_runtime\Service\Compiler\WorkflowCompiler;
use Drupal\flowdrop_runtime\Service\Tool\ToolGraphFlattener;

/**
 * Tests WorkflowCompiler tool validation with passthrough (ToolBox) wiring.
 *
 * Extends the special-edge test so every inherited case also runs against a
 * flattener-wired compiler: for graphs without passthrough nodes, flattened
 * validation must behave exactly like the direct-edge fallback.
 *
 * The flattener here resolves the "toolbox" node type to a passthrough
 * processor; every other node type is unresolvable and therefore a leaf tool.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\Compiler\WorkflowCompiler
 * @group flowdrop_runtime
 */
class WorkflowCompilerToolBoxTest extends WorkflowCompilerSpecialEdgeTest {

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $nodeType = $this->createMock(FlowDropNodeTypeInterface::class);
    $nodeType->method("getExecutorPlugin")->willReturn("toolbox_plugin");
    $storage = $this->createMock(EntityStorageInterface::class);
    $storage->method("load")->willReturnCallback(
      static fn(string $id) => $id === "toolbox" ? $nodeType : NULL,
    );
    $etm = $this->createMock(EntityTypeManagerInterface::class);
    $etm->method("getStorage")->with("flowdrop_node_type")->willReturn($storage);

    $pm = $this->createMock(FlowDropNodeProcessorPluginManager::class);
    $pm->method("getDefinition")->willReturnCallback(
      static fn(string $id) => $id === "toolbox_plugin" ? ["class" => ToolPassthroughStub::class] : NULL,
    );

    $flattener = new ToolGraphFlattener($pm, $etm, $this->logger);
    $this->compiler = new WorkflowCompiler($this->loggerFactory, NULL, $flattener);
  }

  /**
   * Colliding tool names reached through a ToolBox fail compilation.
   *
   * Tool_a ("Web Search") and tool_b ("web-search") are wired into a box that
   * feeds the agent; the collision exists only on the flattened set.
   *
   * @covers ::compile
   */
  public function testCollisionThroughToolboxThrows(): void {
    $workflow = $this->createWorkflowMock([
      "id" => "boxed_collision_workflow",
      "nodes" => [
        "agent_1" => $this->createNode("agent_1", "agent"),
        "box_1" => $this->createNode("box_1", "toolbox", "My Tools"),
        "tool_a" => $this->createNode("tool_a", "http_request", "Web Search"),
        "tool_b" => $this->createNode("tool_b", "http_request", "web-search"),
      ],
      "edges" => [
        $this->createEdge("e1", "tool_a", "box_1", "", "", "tool_availability"),
        $this->createEdge("e2", "tool_b", "box_1", "", "", "tool_availability"),
        $this->createEdge("e3", "box_1", "agent_1", "", "", "tool_availability"),
      ],
    ]);

    $this->expectException(CompilationException::class);
    $this->expectExceptionMessage('resolve to "web_search"');
    $this->compiler->compile($workflow);
  }

  /**
   * A ToolBox label never enters the consumer's tool-name set.
   *
   * The box shares its label with a leaf tool — no collision, because only
   * leaves are model-facing — and the box itself is excluded from the
   * execution graph as tool-only.
   *
   * @covers ::compile
   */
  public function testToolboxLabelDoesNotBecomeToolName(): void {
    $workflow = $this->createWorkflowMock([
      "id" => "box_label_workflow",
      "nodes" => [
        "agent_1" => $this->createNode("agent_1", "agent"),
        "box_1" => $this->createNode("box_1", "toolbox", "Web Search"),
        "tool_a" => $this->createNode("tool_a", "http_request", "Web Search"),
        "tool_b" => $this->createNode("tool_b", "calculator", "Calculator"),
      ],
      "edges" => [
        $this->createEdge("e1", "tool_a", "box_1", "", "", "tool_availability"),
        $this->createEdge("e2", "tool_b", "box_1", "", "", "tool_availability"),
        $this->createEdge("e3", "box_1", "agent_1", "", "", "tool_availability"),
      ],
    ]);

    $compiled = $this->compiler->compile($workflow);

    $executionGraph = $compiled->getExecutionGraph();
    $this->assertTrue($executionGraph->isExcluded("box_1"));
    $this->assertTrue($executionGraph->isExcluded("tool_a"));
    $this->assertTrue($executionGraph->isExcluded("tool_b"));
  }

  /**
   * The same tool reached via two boxes is deduped, not a collision.
   *
   * @covers ::compile
   */
  public function testSameToolViaTwoBoxesCompiles(): void {
    $workflow = $this->createWorkflowMock([
      "id" => "diamond_workflow",
      "nodes" => [
        "agent_1" => $this->createNode("agent_1", "agent"),
        "box_1" => $this->createNode("box_1", "toolbox", "Box One"),
        "box_2" => $this->createNode("box_2", "toolbox", "Box Two"),
        "tool_a" => $this->createNode("tool_a", "http_request", "Web Search"),
      ],
      "edges" => [
        $this->createEdge("e1", "tool_a", "box_1", "", "", "tool_availability"),
        $this->createEdge("e2", "tool_a", "box_2", "", "", "tool_availability"),
        $this->createEdge("e3", "box_1", "agent_1", "", "", "tool_availability"),
        $this->createEdge("e4", "box_2", "agent_1", "", "", "tool_availability"),
      ],
    ]);

    $this->assertNotNull($this->compiler->compile($workflow));
  }

  /**
   * A passthrough cycle behind a consumer fails compilation.
   *
   * Tool edges are excluded from general cycle detection (they are not
   * execution dependencies), so the flattener is what must catch this.
   *
   * @covers ::compile
   */
  public function testToolboxCycleFailsCompilation(): void {
    $workflow = $this->createWorkflowMock([
      "id" => "box_cycle_workflow",
      "nodes" => [
        "agent_1" => $this->createNode("agent_1", "agent"),
        "box_a" => $this->createNode("box_a", "toolbox", "Box A"),
        "box_b" => $this->createNode("box_b", "toolbox", "Box B"),
      ],
      "edges" => [
        $this->createEdge("e1", "box_a", "box_b", "", "", "tool_availability"),
        $this->createEdge("e2", "box_b", "box_a", "", "", "tool_availability"),
        $this->createEdge("e3", "box_b", "agent_1", "", "", "tool_availability"),
      ],
    ]);

    $this->expectException(CompilationException::class);
    $this->expectExceptionMessageMatches("/cycle/i");
    $this->compiler->compile($workflow);
  }

}
