<?php
namespace boru\ai\Tests\Persistence\Db;

use boru\ai\Conversation\Conversation;
use boru\ai\Conversation\ConversationMemory;
use boru\ai\Conversation\ConversationNote;
use boru\ai\Conversation\ConversationReference;
use boru\ai\Conversation\Turn;
use boru\ai\Persistence\Db\DbConfig;
use boru\ai\Persistence\Db\DbConnection;
use boru\ai\Persistence\Db\DbPersistenceProvider;
use boru\ai\Runtime\Runtime;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Schema\Request;
use boru\ai\Schema\Response;
use boru\ai\Schema\ToolCall;
use boru\ai\Schema\ToolResult;
use PHPUnit\Framework\TestCase;

class DbPersistenceHydrationTest extends TestCase
{
    public function testLoadConversationHydratesTurnsInOrder()
    {
        $provider = new TestableDbPersistenceProvider(new FakeDbConnection(), array(
            array(
                'id' => 10,
                'conversation_id' => 'conv_123',
                'external_reference' => 'chat_abc',
                'user_id' => 'user_1',
                'provider' => 'openai',
                'model' => 'gpt-test',
                'metadata' => json_encode(array('topic' => 'support')),
                'title' => 'Support conversation',
                'summary' => 'Customer needs help with billing.',
                'memory_metadata' => json_encode(array('memory_version' => 1)),
            ),
            array(
                array(
                    'turn_number' => 1,
                    'status' => 'completed',
                    'request_json' => json_encode(array(
                        'provider' => 'openai',
                        'model' => 'gpt-test',
                        'messages' => array(
                            array('role' => 'user', 'content' => array('Hello')),
                        ),
                        'metadata' => array('request_key' => 'request_value'),
                    )),
                    'response_json' => json_encode(array(
                        'id' => 'resp_1',
                        'provider' => 'openai',
                        'model' => 'gpt-test',
                        'status' => 'completed',
                        'text' => 'Hi there',
                        'tool_calls' => array(
                            array(
                                'id' => 'tool_1',
                                'name' => 'lookup_weather',
                                'arguments' => array('location' => 'Atlanta'),
                                'arguments_json' => '{"location":"Atlanta"}',
                                'status' => 'completed',
                                'metadata' => array('source' => 'test'),
                            ),
                        ),
                        'usage' => array('input_tokens' => 10),
                        'metadata' => array('response_key' => 'response_value'),
                    )),
                    'tool_results_json' => json_encode(array(
                        array(
                            'tool_call_id' => 'tool_1',
                            'name' => 'lookup_weather',
                            'output' => array('temperature' => 70),
                            'success' => true,
                            'metadata' => array('unit' => 'f'),
                        ),
                    )),
                    'metadata' => json_encode(array('turn_key' => 'turn_value')),
                    'started_at' => '2026-03-19T10:00:00+00:00',
                    'completed_at' => '2026-03-19T10:00:05+00:00',
                ),
                array(
                    'turn_number' => 2,
                    'status' => 'completed',
                    'request_json' => json_encode(array(
                        'provider' => 'openai',
                        'model' => 'gpt-test',
                    )),
                    'response_json' => json_encode(array(
                        'id' => 'resp_2',
                        'provider' => 'openai',
                        'model' => 'gpt-test',
                        'status' => 'completed',
                        'text' => 'Follow-up',
                    )),
                    'tool_results_json' => json_encode(array()),
                    'metadata' => json_encode(array()),
                    'started_at' => '2026-03-19T10:01:00+00:00',
                    'completed_at' => '2026-03-19T10:01:05+00:00',
                ),
            ),
            array(
                array(
                    'note_id' => 'note_1',
                    'kind' => 'preference',
                    'content' => 'Prefers email follow-up',
                    'confidence' => '0.9500',
                    'source_turn_number' => 1,
                    'metadata' => json_encode(array('source' => 'agent')),
                    'created_at' => '2026-03-19 10:00:05',
                    'updated_at' => '2026-03-19 10:00:05',
                ),
            ),
        ));

        $reference = new ConversationReference();
        $reference->externalReference('chat_abc');
        $reference->userId('user_1');

        $conversation = $provider->loadConversation($reference);

        $this->assertInstanceOf(Conversation::class, $conversation);
        $this->assertSame('conv_123', $conversation->reference()->id());
        $this->assertSame('support', $conversation->metadata('topic'));
        $this->assertSame('Support conversation', $conversation->memory()->title());
        $this->assertSame('Customer needs help with billing.', $conversation->memory()->summary());
        $this->assertSame(1, $conversation->memory()->metadata('memory_version'));
        $this->assertCount(1, $conversation->memory()->notes());
        $this->assertSame('preference', $conversation->memory()->notes()[0]->kind());
        $this->assertSame('Prefers email follow-up', $conversation->memory()->notes()[0]->text());
        $this->assertCount(2, $conversation->turns());
        $this->assertSame(1, $conversation->turns()[0]->number());
        $this->assertSame(2, $conversation->turns()[1]->number());
        $this->assertSame('turn_value', $conversation->turns()[0]->metadata('turn_key'));
        $this->assertSame('request_value', $conversation->turns()[0]->request()->metadata('request_key'));
        $this->assertSame('Hi there', $conversation->turns()[0]->response()->text());
        $this->assertCount(1, $conversation->turns()[0]->response()->toolCalls());
        $this->assertSame('lookup_weather', $conversation->turns()[0]->response()->toolCalls()[0]->name());
        $this->assertCount(1, $conversation->turns()[0]->toolResults());
        $this->assertSame(array('temperature' => 70), $conversation->turns()[0]->toolResults()[0]->output());
    }

    public function testRuntimeStartTurnUsesLoadedTurnCountForNextTurnNumber()
    {
        $runtime = new TestableRuntime(new RuntimeContext());
        $conversation = new Conversation();
        $conversation->addTurn((new Turn())->number(1));
        $conversation->addTurn((new Turn())->number(2));

        $request = new Request();
        $request->provider('openai')->model('gpt-test');

        $turn = $runtime->publicStartTurn($request, $conversation);

        $this->assertSame(3, $turn->number());
        $this->assertSame('started', $turn->status());
        $this->assertInstanceOf(Request::class, $turn->request());
    }

    public function testSaveTurnUsesStoredConversationRowIdWhenLookupMisses()
    {
        $provider = new SaveTurnRecoveryDbPersistenceProvider();

        $conversation = new Conversation();
        $conversation->reference()->id('conv_123');
        $conversation->storage(DbPersistenceProvider::CONVERSATION_ROW_STORAGE_KEY, 42);

        $turn = new Turn();
        $turn->number(1);
        $turn->status('started');

        $provider->saveTurn($conversation, $turn);

        $this->assertSame(1, $provider->db->insertCount);
        $this->assertSame(42, $provider->db->lastInsertParams[0]);
        $this->assertSame(1, $provider->db->lastInsertParams[1]);
    }
}

class TestableDbPersistenceProvider extends DbPersistenceProvider
{
    /** @var array */
    protected $fixtures;

    public function __construct(DbConnection $connection, array $fixtures)
    {
        parent::__construct($connection);
        $this->fixtures = $fixtures;
    }

    protected function loadTurnsForConversationPk($conversationPk)
    {
        return parent::loadTurnsForConversationPk($conversationPk);
    }

    public function loadConversation(ConversationReference $reference)
    {
        if ($reference->externalReference() !== 'chat_abc') {
            return false;
        }

        $conversationRow = new FakeRow($this->fixtures[0]);
        $conversation = new Conversation();
        $conversation->reference()->id($conversationRow->get('conversation_id'));
        $conversation->reference()->externalReference($conversationRow->get('external_reference'));
        $conversation->reference()->userId($conversationRow->get('user_id'));
        $conversation->provider($conversationRow->get('provider'));
        $conversation->model($conversationRow->get('model'));
        $conversation->metadata($this->decodeJsonArray($conversationRow->get('metadata')));
        $memory = new ConversationMemory();
        $memory->title($conversationRow->get('title'));
        $memory->summary($conversationRow->get('summary'));
        $memory->metadata($this->decodeJsonArray($conversationRow->get('memory_metadata')));
        foreach ($this->fixtures[2] as $noteRow) {
            $note = new ConversationNote();
            $note->id($noteRow['note_id']);
            $note->kind($noteRow['kind']);
            $note->text($noteRow['content']);
            $note->confidence($noteRow['confidence']);
            $note->sourceTurnNumber($noteRow['source_turn_number']);
            $note->metadata($this->decodeJsonArray($noteRow['metadata']));
            $note->createdAt($noteRow['created_at']);
            $note->updatedAt($noteRow['updated_at']);
            $memory->addNote($note);
        }
        $conversation->memory($memory);

        $turns = array();
        foreach ($this->fixtures[1] as $turnRow) {
            $turns[] = $this->hydrateTurn(new FakeRow($turnRow));
        }
        $conversation->turns($turns);

        return $conversation;
    }
}

class TestableRuntime extends Runtime
{
    public function publicStartTurn(Request $request, Conversation $conversation)
    {
        return $this->startTurn($request, $conversation);
    }
}

class FakeDbConnection extends DbConnection
{
    public function __construct()
    {
        parent::__construct(new DbConfig());
    }

    public function db($dbConfig = null)
    {
        return null;
    }
}

class FakeRow
{
    /** @var array */
    protected $data;

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

    public function get($key)
    {
        return isset($this->data[$key]) ? $this->data[$key] : null;
    }
}

class SaveTurnRecoveryDbPersistenceProvider extends DbPersistenceProvider
{
    /** @var FakeSaveTurnDb */
    public $db;

    public function __construct()
    {
        $this->db = new FakeSaveTurnDb();
        parent::__construct(new SaveTurnRecoveryDbConnection($this->db));
    }

    protected function findConversationRowId($conversationId)
    {
        return null;
    }

    protected function findTurnRowId($conversationPk, $turnNumber)
    {
        return null;
    }

    protected function dbNow()
    {
        return '2026-04-01 12:00:00';
    }
}

class SaveTurnRecoveryDbConnection extends DbConnection
{
    /** @var FakeSaveTurnDb */
    protected $fakeDb;

    public function __construct($fakeDb)
    {
        parent::__construct(new DbConfig());
        $this->fakeDb = $fakeDb;
    }

    public function db($dbConfig = null)
    {
        return $this->fakeDb;
    }
}

class FakeSaveTurnDb
{
    /** @var int */
    public $insertCount = 0;

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

    public function run($sql, $params = array(), $options = array())
    {
        if (strpos($sql, 'insert into') === 0) {
            $this->insertCount++;
            $this->lastInsertParams = $params;
        }

        return new FakeStatementResult();
    }
}

class FakeStatementResult
{
    public function next()
    {
        return false;
    }
}
