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

use boru\ai\AI;
use boru\ai\Config\Config;
use boru\ai\Conversation\Conversation;
use boru\ai\Conversation\ConversationReference;
use boru\ai\Conversation\Turn;
use boru\ai\Conversation\Tool\UpdateConversationTitleTool;
use boru\ai\Persistence\NullPersistenceProvider;
use boru\ai\Persistence\PersistenceProviderInterface;
use boru\ai\Runtime\AbstractTool;
use boru\ai\Provider\Capabilities;
use boru\ai\Provider\ProviderInterface;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Schema\Request;
use boru\ai\Schema\Response;
use PHPUnit\Framework\TestCase;

class AIConversationMemoryToolConfigTest extends TestCase
{
    /** @var CapturingConversationMemoryProvider */
    protected $provider;

    protected function setUp(): void
    {
        AI::runtimeContext(new RuntimeContext());
        AI::config(null);
        AI::persistence(new NullPersistenceProvider());

        $this->provider = new CapturingConversationMemoryProvider();
        AI::registry()->register($this->provider);
    }

    public function testRunAutoRegistersConfiguredConversationMemoryTools()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => false,
                    'only_after_turn' => 1,
                ),
            ),
        ));
        AI::config($config);

        AI::provider('fake')
            ->model('fake-model')
            ->addMessage('Hello')
            ->run();

        $names = $this->toolNamesFromLastRequest();
        $this->assertSame(
            array(
                'update_conversation_title',
                'update_conversation_summary',
                'append_conversation_note',
            ),
            $names
        );
    }

    public function testRunRespectsProviderSpecificConversationMemoryToolOverrides()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'append_note' => true,
                    'replace_notes' => false,
                    'require_persistence' => false,
                    'only_after_turn' => 1,
                ),
            ),
        ));
        $config->provider('fake', array(
            'conversation_memory' => array(
                'tools' => array(
                    'append_note' => false,
                    'replace_notes' => true,
                ),
            ),
        ));
        AI::config($config);

        AI::provider('fake')
            ->model('fake-model')
            ->addMessage('Hello')
            ->run();

        $names = $this->toolNamesFromLastRequest();
        $this->assertContains('update_conversation_title', $names);
        $this->assertContains('update_conversation_summary', $names);
        $this->assertNotContains('append_conversation_note', $names);
        $this->assertContains('replace_conversation_notes', $names);
    }

    public function testRunDoesNotDuplicateExplicitConversationMemoryTools()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => false,
                    'only_after_turn' => 1,
                ),
            ),
        ));
        AI::config($config);

        AI::provider('fake')
            ->model('fake-model')
            ->addMessage('Hello')
            ->addTool(new UpdateConversationTitleTool())
            ->run();

        $names = $this->toolNamesFromLastRequest();
        $titleCount = 0;
        foreach ($names as $name) {
            if ($name === 'update_conversation_title') {
                $titleCount++;
            }
        }

        $this->assertSame(1, $titleCount);
        $this->assertContains('update_conversation_summary', $names);
        $this->assertContains('append_conversation_note', $names);
    }

    public function testRunMergesPromptAdditionsFromActiveConversationMemoryTools()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => false,
                    'only_after_turn' => 1,
                ),
            ),
        ));
        AI::config($config);

        AI::provider('fake')
            ->model('fake-model')
            ->instructions('Answer the user directly.')
            ->addMessage('Hello')
            ->run();

        $instructions = $this->provider->lastRequest->instructions();
        $this->assertStringContainsString('Answer the user directly.', $instructions);
        $this->assertStringContainsString('keep the conversation title updated', $instructions);
        $this->assertStringContainsString('maintain a concise rolling summary', $instructions);
        $this->assertStringContainsString('append a structured conversation note', $instructions);
    }

    public function testRunOnlyMergesPromptAdditionsForToolsThatOptIn()
    {
        AI::provider('fake')
            ->model('fake-model')
            ->instructions('Base instructions.')
            ->addTool(new SilentTool())
            ->addTool(new PromptedTool())
            ->addMessage('Hello')
            ->run();

        $instructions = $this->provider->lastRequest->instructions();
        $this->assertStringContainsString('Base instructions.', $instructions);
        $this->assertStringContainsString('Use the prompted tool when it clearly fits.', $instructions);
        $this->assertStringNotContainsString('Silent tool', $instructions);
    }

    public function testRunSkipsConversationMemoryToolsWhenPersistenceIsRequiredButUnavailable()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => true,
                    'only_after_turn' => 1,
                ),
            ),
        ));
        AI::config($config);

        AI::provider('fake')
            ->model('fake-model')
            ->addMessage('Hello')
            ->run();

        $this->assertSame(array(), $this->toolNamesFromLastRequest());
    }

    public function testRunAutoRegistersConversationMemoryToolsOnlyAfterConfiguredTurn()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => true,
                    'only_after_turn' => 2,
                ),
            ),
        ));
        AI::config($config);
        AI::persistence(new FakeConversationPersistenceProvider(1));

        AI::provider('fake')
            ->model('fake-model')
            ->reference('memory-ref')
            ->addMessage('Hello again')
            ->run();

        $this->assertSame(
            array(
                'update_conversation_title',
                'update_conversation_summary',
                'append_conversation_note',
            ),
            $this->toolNamesFromLastRequest()
        );
    }

    public function testRunSkipsConversationMemoryToolsBeforeConfiguredTurnThreshold()
    {
        $config = new Config();
        $config->runtime(array(
            'conversation_memory' => array(
                'tools' => array(
                    'enabled' => true,
                    'require_persistence' => true,
                    'only_after_turn' => 2,
                ),
            ),
        ));
        AI::config($config);
        AI::persistence(new FakeConversationPersistenceProvider(0));

        AI::provider('fake')
            ->model('fake-model')
            ->reference('memory-ref')
            ->addMessage('First turn')
            ->run();

        $this->assertSame(array(), $this->toolNamesFromLastRequest());
    }

    protected function toolNamesFromLastRequest()
    {
        $this->assertInstanceOf(Request::class, $this->provider->lastRequest);

        $names = array();
        foreach ($this->provider->lastRequest->tools() as $tool) {
            if (is_array($tool) && isset($tool['name'])) {
                $names[] = $tool['name'];
            }
        }

        return $names;
    }
}

class CapturingConversationMemoryProvider implements ProviderInterface
{
    /** @var Request|null */
    public $lastRequest;

    public function name()
    {
        return 'fake';
    }

    public function capabilities()
    {
        return new Capabilities(array(
            'tools' => true,
        ));
    }

    public function supportsModel($model)
    {
        return strpos((string)$model, 'fake-') === 0;
    }

    public function execute(Request $request, $eventCallback = null)
    {
        $this->lastRequest = $request->copy();

        $response = new Response();
        $response->provider('fake');
        $response->model($request->model());
        $response->status('completed');
        $response->text('ok');
        return $response;
    }

    public function continueResponse(Request $request, Response $previousResponse, array $toolResults, $eventCallback = null)
    {
        $this->lastRequest = $request->copy();
        return $previousResponse;
    }
}

class SilentTool extends AbstractTool
{
    public function name()
    {
        return 'silent_tool';
    }

    public function handle(array $arguments, \boru\ai\Runtime\ToolExecutionContext $context = null)
    {
        return 'ok';
    }
}

class PromptedTool extends AbstractTool implements \boru\ai\Runtime\ToolPromptAdditionInterface
{
    public function name()
    {
        return 'prompted_tool';
    }

    public function promptAddition()
    {
        return 'Use the prompted tool when it clearly fits.';
    }

    public function handle(array $arguments, \boru\ai\Runtime\ToolExecutionContext $context = null)
    {
        return 'ok';
    }
}

class FakeConversationPersistenceProvider implements PersistenceProviderInterface
{
    /** @var int */
    protected $existingTurnCount;

    public function __construct($existingTurnCount)
    {
        $this->existingTurnCount = (int)$existingTurnCount;
    }

    public function loadConversation(ConversationReference $reference)
    {
        $conversation = new Conversation($reference);
        for ($i = 1; $i <= $this->existingTurnCount; $i++) {
            $conversation->addTurn((new Turn())->number($i));
        }
        return $conversation;
    }

    public function saveConversation(Conversation $conversation)
    {
        return $conversation;
    }

    public function saveTurn(Conversation $conversation, Turn $turn)
    {
        return $turn;
    }
}
