<?php
namespace boru\ai\Tests\Mcp;

use boru\ai\Config\Config;
use boru\ai\Mcp\ClientBackedMcpToolDiscovery;
use boru\ai\Mcp\ClientBackedMcpToolInvoker;
use boru\ai\Mcp\McpClientFactoryInterface;
use boru\ai\Mcp\McpClientInterface;
use boru\ai\Mcp\McpServerConfig;
use boru\ai\Mcp\McpServerRegistry;
use boru\ai\Mcp\McpToolDiscoveryInterface;
use boru\ai\Mcp\McpToolAdapter;
use boru\ai\Mcp\McpToolRegistrar;
use boru\ai\Mcp\McpToolInvokerInterface;
use boru\ai\Mcp\NullMcpClientFactory;
use boru\ai\Mcp\NullMcpToolDiscovery;
use boru\ai\Mcp\NullMcpToolInvoker;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Runtime\ToolDefinition;
use boru\ai\Runtime\ToolRegistry;
use PHPUnit\Framework\TestCase;

class McpRuntimeSkeletonTest extends TestCase
{
    public function testConfigSupportsMcpSection()
    {
        $config = new Config();
        $config->mcp(array(
            'enabled' => true,
            'servers' => array(
                'filesystem' => array(
                    'transport' => 'tcp',
                    'protocol_version' => '2025-06-18',
                    'host' => '127.0.0.1',
                    'port' => 9000,
                    'auth' => array(
                        'type' => 'bearer',
                        'token' => 'secret',
                    ),
                ),
            ),
        ));

        $mcp = $config->mcp();
        $this->assertTrue($mcp['enabled']);
        $this->assertSame('tcp', $mcp['servers']['filesystem']['transport']);
        $this->assertSame('2025-06-18', $mcp['servers']['filesystem']['protocol_version']);
        $this->assertSame('bearer', $mcp['servers']['filesystem']['auth']['type']);
        $this->assertSame('secret', $mcp['servers']['filesystem']['auth']['token']);
        $this->assertSame('127.0.0.1', $mcp['servers']['filesystem']['host']);
        $this->assertSame(9000, $mcp['servers']['filesystem']['port']);
    }

    public function testRuntimeContextProvidesDefaultMcpServices()
    {
        $context = new RuntimeContext();

        $this->assertInstanceOf(McpServerRegistry::class, $context->mcpServerRegistry());
        $this->assertInstanceOf(NullMcpClientFactory::class, $context->mcpClientFactory());
        $this->assertInstanceOf(NullMcpToolInvoker::class, $context->mcpToolInvoker());
        $this->assertInstanceOf(NullMcpToolDiscovery::class, $context->mcpToolDiscovery());
    }

    public function testRuntimeContextAcceptsCustomMcpServices()
    {
        $registry = new McpServerRegistry();
        $clientFactory = new SkeletonTestMcpClientFactory();
        $invoker = new SkeletonTestMcpToolInvoker();
        $discovery = new SkeletonTestMcpToolDiscovery();
        $context = new RuntimeContext(null, null, null, null, null, null, null, $registry, $clientFactory, $invoker, $discovery);

        $this->assertSame($registry, $context->mcpServerRegistry());
        $this->assertSame($clientFactory, $context->mcpClientFactory());
        $this->assertSame($invoker, $context->mcpToolInvoker());
        $this->assertSame($discovery, $context->mcpToolDiscovery());
    }

    public function testMcpServerRegistryRegistersAndReturnsServers()
    {
        $registry = new McpServerRegistry();
        $server = new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
            'command' => 'npx',
        ));

        $registry->register($server);

        $this->assertTrue($registry->has('filesystem'));
        $this->assertSame($server, $registry->get('filesystem'));
        $this->assertCount(1, $registry->all());
    }

    public function testMcpServerConfigIncludesProtocolVersionAndAuth()
    {
        $server = new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'tcp',
            'protocol_version' => '2025-06-18',
            'host' => '127.0.0.1',
            'port' => 9000,
            'auth' => array(
                'type' => 'bearer',
                'token' => 'secret',
            ),
        ));

        $this->assertSame('2025-06-18', $server->protocolVersion());
        $this->assertSame('bearer', $server->auth()['type']);
        $this->assertSame('secret', $server->auth()['token']);
        $this->assertSame('127.0.0.1', $server->host());
        $this->assertSame(9000, $server->port());
    }

    public function testMcpToolAdapterBuildsToolDefinitionWithMetadataAndInvokerCallback()
    {
        $invoker = new SkeletonTestMcpToolInvoker();
        $definition = McpToolAdapter::toToolDefinition('filesystem', array(
            'name' => 'read_file',
            'description' => 'Read a file from disk',
            'inputSchema' => array(
                'type' => 'object',
                'properties' => array(
                    'path' => array('type' => 'string'),
                ),
            ),
        ), $invoker);

        $this->assertInstanceOf(ToolDefinition::class, $definition);
        $this->assertSame('read_file', $definition->name());
        $this->assertSame('Read a file from disk', $definition->description());
        $this->assertTrue($definition->metadata()['mcp']);
        $this->assertSame('filesystem', $definition->metadata()['mcp_server']);
        $this->assertSame('read_file', $definition->metadata()['mcp_tool']);

        $result = $definition->invoke(array('path' => '/tmp/example.txt'));
        $this->assertSame('filesystem', $result['server']);
        $this->assertSame('read_file', $result['tool']);
        $this->assertSame('/tmp/example.txt', $result['arguments']['path']);
    }

    public function testMcpToolAdapterFallsBackToEmptyObjectSchema()
    {
        $definition = McpToolAdapter::toToolDefinition('filesystem', array(
            'name' => 'read_file',
        ), new SkeletonTestMcpToolInvoker());

        $this->assertSame('object', $definition->schema()['type']);
        $this->assertSame(array(), $definition->schema()['properties']);
    }

    public function testMcpToolRegistrarRegistersToolIntoRuntimeRegistry()
    {
        $serverRegistry = new McpServerRegistry();
        $serverRegistry->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));

        $toolRegistry = new ToolRegistry();
        $registrar = new McpToolRegistrar($toolRegistry, new SkeletonTestMcpToolInvoker(), $serverRegistry);

        $definition = $registrar->register('filesystem', array(
            'name' => 'read_file',
            'description' => 'Read a file from disk',
        ));

        $this->assertInstanceOf(ToolDefinition::class, $definition);
        $this->assertTrue($toolRegistry->has('read_file'));
        $this->assertSame($definition, $toolRegistry->get('read_file'));
    }

    public function testMcpToolRegistrarRejectsUnknownServer()
    {
        $registrar = new McpToolRegistrar(new ToolRegistry(), new SkeletonTestMcpToolInvoker(), new McpServerRegistry());

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Unknown MCP server: missing');

        $registrar->register('missing', array(
            'name' => 'read_file',
        ));
    }

    public function testClientBackedMcpToolDiscoveryInitializesAndListsTools()
    {
        $factory = new SkeletonTestMcpClientFactory();
        $discovery = new ClientBackedMcpToolDiscovery($factory);
        $server = new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'http',
        ));

        $tools = $discovery->discover($server);

        $this->assertCount(1, $tools);
        $this->assertSame('read_file', $tools[0]['name']);
        $this->assertSame(array('initialize', 'list'), $factory->client()->calls());
    }

    public function testClientBackedMcpToolInvokerInitializesAndCallsTool()
    {
        $serverRegistry = new McpServerRegistry();
        $serverRegistry->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'http',
        )));
        $factory = new SkeletonTestMcpClientFactory();
        $invoker = new ClientBackedMcpToolInvoker($serverRegistry, $factory);

        $result = $invoker->invoke('filesystem', 'read_file', array('path' => '/tmp/example.txt'));

        $this->assertSame('read_file', $result['tool']);
        $this->assertSame('/tmp/example.txt', $result['arguments']['path']);
        $this->assertSame(array('initialize', 'call:read_file'), $factory->client()->calls());
    }
}

class SkeletonTestMcpToolInvoker implements McpToolInvokerInterface
{
    public function invoke($serverName, $toolName, array $arguments)
    {
        return array(
            'server' => $serverName,
            'tool' => $toolName,
            'arguments' => $arguments,
        );
    }
}

class SkeletonTestMcpToolDiscovery implements McpToolDiscoveryInterface
{
    public function discover(McpServerConfig $serverConfig)
    {
        return array(
            array(
                'name' => 'read_file',
                'description' => 'Read a file from disk',
                'inputSchema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'path' => array('type' => 'string'),
                    ),
                ),
            ),
        );
    }
}

class SkeletonTestMcpClientFactory implements McpClientFactoryInterface
{
    /** @var SkeletonTestMcpClient */
    protected $client;

    public function __construct()
    {
        $this->client = new SkeletonTestMcpClient();
    }

    public function makeClient(McpServerConfig $serverConfig)
    {
        $this->client->serverConfig($serverConfig);
        $this->client->resetCalls();

        return $this->client;
    }

    public function client()
    {
        return $this->client;
    }
}

class SkeletonTestMcpClient implements McpClientInterface
{
    /** @var McpServerConfig|null */
    protected $serverConfig;

    /** @var array */
    protected $calls = array();

    public function serverConfig(McpServerConfig $serverConfig = null)
    {
        if ($serverConfig !== null) {
            $this->serverConfig = $serverConfig;
            return $this;
        }
        return $this->serverConfig;
    }

    public function resetCalls()
    {
        $this->calls = array();
        return $this;
    }

    public function calls()
    {
        return $this->calls;
    }

    public function initialize(McpServerConfig $serverConfig)
    {
        $this->calls[] = 'initialize';

        return array(
            'protocolVersion' => $serverConfig->protocolVersion(),
        );
    }

    public function listTools()
    {
        $this->calls[] = 'list';

        return array(
            array(
                'name' => 'read_file',
                'description' => 'Read a file from disk',
                'inputSchema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'path' => array('type' => 'string'),
                    ),
                ),
            ),
        );
    }

    public function callTool($toolName, array $arguments)
    {
        $this->calls[] = 'call:' . $toolName;

        return array(
            'server' => $this->serverConfig ? $this->serverConfig->name() : null,
            'tool' => $toolName,
            'arguments' => $arguments,
        );
    }
}
