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

use boru\ai\Mcp\McpServerConfig;
use boru\ai\Mcp\NativeTcpMcpConnector;
use boru\ai\Mcp\RoutingMcpClientFactory;
use boru\ai\Mcp\TcpMcpClient;
use boru\ai\Mcp\TcpMcpClientFactory;
use boru\ai\Mcp\TcpMcpConnectionInterface;
use boru\ai\Mcp\TcpMcpConnectorInterface;
use PHPUnit\Framework\TestCase;

class TcpMcpClientTest extends TestCase
{
    public function testInitializeListToolsAndCallToolUseDirectTcpJsonRpcFlow()
    {
        $connection = new FakeTcpMcpConnection(array(
            '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18"}}' . "\n",
            '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"read_file"}]}}' . "\n",
            '{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"ok"}]}}' . "\n",
        ));
        $connector = new FakeTcpMcpConnector($connection);
        $client = new TcpMcpClient($connector);
        $server = new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'tcp',
            'host' => '127.0.0.1',
            'port' => 9000,
            'auth' => array(
                'type' => 'bearer',
                'token' => 'secret-token',
            ),
        ));

        $initialize = $client->initialize($server);
        $tools = $client->listTools();
        $result = $client->callTool('read_file', array('path' => '/tmp/example.txt'));

        $this->assertSame('2025-06-18', $initialize['protocolVersion']);
        $this->assertSame('read_file', $tools[0]['name']);
        $this->assertSame('ok', $result['content'][0]['text']);
        $this->assertCount(3, $connection->writes);
        $this->assertStringContainsString('"method":"initialize"', $connection->writes[0]);
        $this->assertStringContainsString('"authToken":"secret-token"', $connection->writes[0]);
        $this->assertStringContainsString('"method":"tools/list"', $connection->writes[1]);
        $this->assertStringContainsString('"method":"tools/call"', $connection->writes[2]);
    }

    public function testCallToolFailsOnJsonRpcError()
    {
        $connection = new FakeTcpMcpConnection(array(
            '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18"}}' . "\n",
            '{"jsonrpc":"2.0","id":2,"error":{"code":401,"message":"Unauthorized: invalid token"}}' . "\n",
        ));
        $client = new TcpMcpClient(new FakeTcpMcpConnector($connection));
        $client->initialize(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'tcp',
            'host' => '127.0.0.1',
            'port' => 9000,
        )));

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Unauthorized: invalid token');

        $client->callTool('read_file', array());
    }

    public function testFactoryCreatesTcpClientInstances()
    {
        $factory = new TcpMcpClientFactory(new FakeTcpMcpConnector(new FakeTcpMcpConnection()));
        $client = $factory->makeClient(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'tcp',
        )));

        $this->assertInstanceOf(TcpMcpClient::class, $client);
    }

    public function testRoutingFactorySelectsRegisteredTransportFactory()
    {
        $routing = new RoutingMcpClientFactory();
        $routing->register('tcp', new TcpMcpClientFactory(new FakeTcpMcpConnector(new FakeTcpMcpConnection())));

        $client = $routing->makeClient(new McpServerConfig(array(
            'name' => 'filesystem',
            'transport' => 'tcp',
        )));

        $this->assertInstanceOf(TcpMcpClient::class, $client);
    }
}

class FakeTcpMcpConnector implements TcpMcpConnectorInterface
{
    /** @var FakeTcpMcpConnection */
    protected $connection;

    public function __construct(FakeTcpMcpConnection $connection)
    {
        $this->connection = $connection;
    }

    public function connect(McpServerConfig $serverConfig)
    {
        return $this->connection;
    }
}

class FakeTcpMcpConnection implements TcpMcpConnectionInterface
{
    /** @var array */
    protected $reads;

    /** @var array */
    public $writes = array();

    /** @var bool */
    public $closed = false;

    public function __construct(array $reads = array())
    {
        $this->reads = $reads;
    }

    public function writeLine($line)
    {
        $this->writes[] = $line;
    }

    public function readLine()
    {
        if (empty($this->reads)) {
            return false;
        }

        return array_shift($this->reads);
    }

    public function close()
    {
        $this->closed = true;
    }
}
