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

use boru\ai\AI;
use boru\ai\Bootstrap;
use boru\ai\Config\Config;
use boru\ai\Mcp\ClientBackedMcpToolDiscovery;
use boru\ai\Mcp\ClientBackedMcpToolInvoker;
use boru\ai\Mcp\HttpProxyMcpClientFactory;
use boru\ai\Mcp\McpServerConfig;
use boru\ai\Mcp\McpServerRegistry;
use boru\ai\Mcp\McpToolDiscoveryInterface;
use boru\ai\Mcp\McpToolInvokerInterface;
use boru\ai\Mcp\NullMcpClientFactory;
use boru\ai\Mcp\NullMcpToolDiscovery;
use boru\ai\Mcp\NullMcpToolInvoker;
use boru\ai\Mcp\RoutingMcpClientFactory;
use boru\ai\Mcp\TcpMcpClientFactory;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Runtime\ToolDefinition;
use PHPUnit\Framework\TestCase;

class McpBootstrapAndFacadeTest extends TestCase
{
    protected function setUp(): void
    {
        AI::runtimeContext(new RuntimeContext());
    }

    public function testBootstrapRegisterDefaultsRegistersMcpServersWhenEnabled()
    {
        $config = new Config();
        $config->provider('openai', array('api_key' => 'test-key'));
        $config->mcp(array(
            'enabled' => true,
            'client' => array(
                'driver' => 'http_proxy',
            ),
            'servers' => array(
                'filesystem' => array(
                    'transport' => 'http',
                    'url' => 'https://proxy.example.com/mcp',
                ),
            ),
            'discovery' => array(
                'enabled' => true,
            ),
        ));
        $config->persistence(array(
            'enabled' => false,
        ));

        Bootstrap::registerDefaults($config);

        $registry = AI::mcpServers();
        $this->assertInstanceOf(McpServerRegistry::class, $registry);
        $this->assertTrue($registry->has('filesystem'));
        $this->assertSame('filesystem', $registry->get('filesystem')->name());
        $this->assertSame('http', $registry->get('filesystem')->transport());
        $this->assertSame('https://proxy.example.com/mcp', $registry->get('filesystem')->url());
        $this->assertInstanceOf(HttpProxyMcpClientFactory::class, AI::mcpClientFactory());
        $this->assertInstanceOf(ClientBackedMcpToolInvoker::class, AI::mcpToolInvoker());
        $this->assertInstanceOf(ClientBackedMcpToolDiscovery::class, AI::mcpToolDiscovery());
    }

    public function testBootstrapRegisterDefaultsLeavesMcpRegistryEmptyWhenDisabled()
    {
        $config = new Config();
        $config->provider('openai', array('api_key' => 'test-key'));
        $config->mcp(array(
            'enabled' => false,
            'servers' => array(
                'filesystem' => array(
                    'transport' => 'stdio',
                    'command' => 'npx',
                ),
            ),
        ));
        $config->persistence(array(
            'enabled' => false,
        ));

        Bootstrap::registerDefaults($config);

        $this->assertFalse(AI::mcpServers()->has('filesystem'));
    }

    public function testBootstrapDefaultConfigIncludesMcpPlaceholder()
    {
        $config = Bootstrap::defaultConfig();
        $mcp = $config->mcp();

        $this->assertFalse($mcp['enabled']);
        $this->assertSame(array(), $mcp['servers']);
        $this->assertNull($mcp['client']['driver']);
        $this->assertSame(10, $mcp['client']['timeout']);
        $this->assertFalse($mcp['discovery']['enabled']);
        $this->assertFalse($mcp['discovery']['auto_register']);
    }

    public function testBootstrapInfersHttpProxyClientDriverFromHttpServerTransport()
    {
        $config = new Config();
        $config->provider('openai', array('api_key' => 'test-key'));
        $config->mcp(array(
            'enabled' => true,
            'servers' => array(
                'filesystem' => array(
                    'transport' => 'http',
                    'url' => 'https://proxy.example.com/mcp',
                ),
            ),
            'discovery' => array(
                'enabled' => true,
            ),
        ));
        $config->persistence(array(
            'enabled' => false,
        ));

        Bootstrap::registerDefaults($config);

        $this->assertInstanceOf(HttpProxyMcpClientFactory::class, AI::mcpClientFactory());
        $this->assertInstanceOf(ClientBackedMcpToolInvoker::class, AI::mcpToolInvoker());
        $this->assertInstanceOf(ClientBackedMcpToolDiscovery::class, AI::mcpToolDiscovery());
    }

    public function testBootstrapBuildsRoutingFactoryForMixedHttpAndTcpServers()
    {
        $config = new Config();
        $config->provider('openai', array('api_key' => 'test-key'));
        $config->mcp(array(
            'enabled' => true,
            'servers' => array(
                'proxy_server' => array(
                    'transport' => 'http',
                    'url' => 'https://proxy.example.com/mcp',
                ),
                'tcp_server' => array(
                    'transport' => 'tcp',
                    'host' => '127.0.0.1',
                    'port' => 9000,
                ),
            ),
            'discovery' => array(
                'enabled' => true,
            ),
        ));
        $config->persistence(array(
            'enabled' => false,
        ));

        Bootstrap::registerDefaults($config);

        $factory = AI::mcpClientFactory();
        $this->assertInstanceOf(RoutingMcpClientFactory::class, $factory);
        $this->assertInstanceOf(\boru\ai\Mcp\HttpProxyMcpClient::class, $factory->makeClient(AI::mcpServers()->get('proxy_server')));
        $this->assertInstanceOf(\boru\ai\Mcp\TcpMcpClient::class, $factory->makeClient(AI::mcpServers()->get('tcp_server')));
        $this->assertInstanceOf(ClientBackedMcpToolInvoker::class, AI::mcpToolInvoker());
        $this->assertInstanceOf(ClientBackedMcpToolDiscovery::class, AI::mcpToolDiscovery());
    }

    public function testBootstrapHonorsExplicitTcpClientDriver()
    {
        $config = new Config();
        $config->provider('openai', array('api_key' => 'test-key'));
        $config->mcp(array(
            'enabled' => true,
            'client' => array(
                'driver' => 'tcp',
            ),
            'servers' => array(
                'tcp_server' => array(
                    'transport' => 'tcp',
                    'host' => '127.0.0.1',
                    'port' => 9000,
                ),
            ),
            'discovery' => array(
                'enabled' => true,
            ),
        ));
        $config->persistence(array(
            'enabled' => false,
        ));

        Bootstrap::registerDefaults($config);

        $this->assertInstanceOf(TcpMcpClientFactory::class, AI::mcpClientFactory());
        $this->assertInstanceOf(ClientBackedMcpToolInvoker::class, AI::mcpToolInvoker());
        $this->assertInstanceOf(ClientBackedMcpToolDiscovery::class, AI::mcpToolDiscovery());
    }

    public function testAiAddMcpToolRegistersToolDefinitionAndAddsSchemaToRequest()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));
        AI::mcpToolInvoker(new McpBootstrapAndFacadeTestInvoker());

        $ai = AI::provider('openai');
        $ai->addMcpTool('filesystem', array(
            'name' => 'read_file',
            'description' => 'Read a file from disk',
            'inputSchema' => array(
                'type' => 'object',
                'properties' => array(
                    'path' => array('type' => 'string'),
                ),
                'required' => array('path'),
            ),
        ));

        $definition = AI::toolRegistry()->get('read_file');
        $this->assertInstanceOf(ToolDefinition::class, $definition);
        $this->assertTrue($definition->metadata()['mcp']);
        $this->assertSame('filesystem', $definition->metadata()['mcp_server']);
        $this->assertSame('read_file', $definition->metadata()['mcp_tool']);

        $tools = $ai->request()->tools();
        $this->assertCount(1, $tools);
        $this->assertSame('read_file', $tools[0]['name']);
        $this->assertSame('Read a file from disk', $tools[0]['description']);
    }

    public function testAiAddMcpToolsSkipsInvalidEntriesAndRegistersValidTools()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));
        AI::mcpToolInvoker(new McpBootstrapAndFacadeTestInvoker());

        $ai = AI::provider('openai');
        $ai->addMcpTools('filesystem', array(
            'invalid-entry',
            array(
                'name' => 'read_file',
                'inputSchema' => array(
                    'type' => 'object',
                    'properties' => array(),
                ),
            ),
            array(
                'name' => 'write_file',
                'inputSchema' => array(
                    'type' => 'object',
                    'properties' => array(),
                ),
            ),
        ));

        $tools = $ai->request()->tools();
        $this->assertCount(2, $tools);
        $this->assertTrue(AI::toolRegistry()->has('read_file'));
        $this->assertTrue(AI::toolRegistry()->has('write_file'));
    }

    public function testAiAddMcpToolRejectsUnknownServer()
    {
        AI::mcpToolInvoker(new McpBootstrapAndFacadeTestInvoker());

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

        AI::provider('openai')->addMcpTool('filesystem', array(
            'name' => 'read_file',
        ));
    }

    public function testAiAddDiscoveredMcpToolsRegistersDiscoveredToolsAndAddsThemToRequest()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));
        AI::mcpToolInvoker(new McpBootstrapAndFacadeTestInvoker());
        AI::mcpToolDiscovery(new McpBootstrapAndFacadeTestDiscovery());

        $ai = AI::provider('openai');
        $ai->addDiscoveredMcpTools('filesystem');

        $tools = $ai->request()->tools();
        $this->assertCount(2, $tools);
        $this->assertTrue(AI::toolRegistry()->has('read_file'));
        $this->assertTrue(AI::toolRegistry()->has('write_file'));
        $this->assertSame('read_file', $tools[0]['name']);
        $this->assertSame('write_file', $tools[1]['name']);
    }

    public function testAiDiscoverMcpToolsUsesConfiguredDiscoveryService()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));
        AI::mcpToolDiscovery(new McpBootstrapAndFacadeTestDiscovery());

        $tools = AI::provider('openai')->discoverMcpTools('filesystem');

        $this->assertCount(2, $tools);
        $this->assertSame('read_file', $tools[0]['name']);
        $this->assertSame('write_file', $tools[1]['name']);
    }

    public function testAiDiscoverMcpToolsFailsClearlyWithoutConfiguredDiscovery()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('No MCP tool discovery configured for server "filesystem"');

        AI::provider('openai')->discoverMcpTools('filesystem');
    }

    public function testAiTryAddDiscoveredMcpToolsSuppressesDiscoveryFailureAndRecordsMetadata()
    {
        AI::mcpServers()->register(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'stdio',
        )));

        $captured = null;
        $ai = AI::provider('openai');
        $result = $ai->tryAddDiscoveredMcpTools('filesystem', function ($error) use (&$captured) {
            $captured = $error;
        });

        $this->assertSame($ai, $result);
        $this->assertSame(array(), $ai->request()->tools());
        $errors = $ai->request()->metadata('mcp_discovery_errors');
        $this->assertIsArray($errors);
        $this->assertCount(1, $errors);
        $this->assertSame('filesystem', $errors[0]['server']);
        $this->assertSame('No MCP tool discovery configured for server "filesystem"', $errors[0]['message']);
        $this->assertSame($errors[0], $captured);
    }

    public function testRuntimeContextDefaultsStillProvideNullMcpInvoker()
    {
        $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());
    }
}

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

class McpBootstrapAndFacadeTestDiscovery 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'),
                    ),
                ),
            ),
            array(
                'name' => 'write_file',
                'description' => 'Write a file to disk',
                'inputSchema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'path' => array('type' => 'string'),
                        'content' => array('type' => 'string'),
                    ),
                ),
            ),
        );
    }
}
