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

use boru\ai\Conversation\ConversationReference;
use boru\ai\Provider\ProviderRegistry;
use boru\ai\Runtime\Runtime;
use boru\ai\Runtime\RuntimeContext;
use boru\ai\Runtime\ToolExecutor;
use boru\ai\Runtime\ToolRegistry;
use boru\ai\Schema\Request;
use boru\ai\Stream\StreamEvent;
use boru\ai\Tests\Fakes\FakePersistenceProvider;
use boru\ai\Tests\Fakes\FakeProvider;
use PHPUnit\Framework\TestCase;

class RuntimeToolLoopTest extends TestCase
{
    public function testRuntimeExecutesToolLoopAndContinuesProvider()
    {
        $provider = new FakeProvider();
        $providerRegistry = new ProviderRegistry();
        $providerRegistry->register($provider);
        $providerRegistry->mapModelPrefix('fake-', 'fake');

        $toolRegistry = new ToolRegistry();
        $toolRegistry->define(
            'get_current_temperature',
            'Get the current temperature',
            array(
                'type' => 'object',
                'properties' => array(
                    'location' => array('type' => 'string'),
                    'unit' => array('type' => 'string'),
                ),
            ),
            function (array $arguments) {
                return json_encode(array(
                    'location' => $arguments['location'],
                    'unit' => $arguments['unit'],
                    'temperature' => 69,
                ));
            }
        );

        $persistence = new FakePersistenceProvider();
        $context = new RuntimeContext(
            $providerRegistry,
            $persistence,
            null,
            new ToolExecutor($toolRegistry),
            $toolRegistry
        );

        $runtime = new Runtime($context);
        $request = new Request();
        $request->provider('fake')->model('fake-weather-v1')->addMessage('What is the weather in Atlanta?');

        $reference = new ConversationReference();
        $reference->externalReference('weatherbot_test');

        $response = $runtime->execute($request, $reference);

        $this->assertSame(1, $provider->executeCalls);
        $this->assertSame(1, $provider->continueCalls);
        $this->assertStringContainsString('temperature', $provider->receivedToolResults[0]->output());
        $this->assertSame('completed', $response->status());
        $this->assertStringContainsString('Weather resolved:', $response->text());
        $this->assertGreaterThanOrEqual(2, count($persistence->savedConversations));
        $this->assertGreaterThanOrEqual(4, count($persistence->savedTurns));
        $this->assertSame('started', $persistence->savedTurns[0]->status());
        $this->assertSame('provider_completed', $persistence->savedTurns[1]->status());
        $this->assertGreaterThanOrEqual(1, count($persistence->savedTurns[2]->toolCalls()));
        $this->assertCount(1, $persistence->savedTurns[count($persistence->savedTurns) - 1]->toolResults());
    }

    public function testRuntimeEmitsCoreToolExecutionEvents()
    {
        $provider = new FakeProvider();
        $providerRegistry = new ProviderRegistry();
        $providerRegistry->register($provider);
        $providerRegistry->mapModelPrefix('fake-', 'fake');

        $toolRegistry = new ToolRegistry();
        $toolRegistry->define(
            'get_current_temperature',
            'Get the current temperature',
            array('type' => 'object'),
            function () {
                return json_encode(array('temperature' => 69));
            }
        );

        $context = new RuntimeContext(
            $providerRegistry,
            new FakePersistenceProvider(),
            null,
            new ToolExecutor($toolRegistry),
            $toolRegistry
        );

        $runtime = new Runtime($context);
        $request = new Request();
        $request->provider('fake')->model('fake-weather-v1')->addMessage('What is the weather in Atlanta?');

        $events = array();
        $runtime->execute($request, new ConversationReference(), function ($event) use (&$events) {
            $events[] = $event;
        });

        $types = array();
        foreach ($events as $event) {
            $types[] = $event->type();
        }

        $this->assertContains(StreamEvent::TOOL_EXECUTION_STARTED, $types);
        $this->assertContains(StreamEvent::TOOL_EXECUTION_COMPLETED, $types);
    }

    public function testRuntimeEmitsNormalizedToolLifecyclePayloadsForStartedAndCompletedEvents()
    {
        $provider = new FakeProvider();
        $providerRegistry = new ProviderRegistry();
        $providerRegistry->register($provider);
        $providerRegistry->mapModelPrefix('fake-', 'fake');

        $toolRegistry = new ToolRegistry();
        $toolRegistry->define(
            'get_current_temperature',
            'Get the current temperature',
            array('type' => 'object'),
            function () {
                return json_encode(array('temperature' => 69));
            }
        );

        $context = new RuntimeContext(
            $providerRegistry,
            new FakePersistenceProvider(),
            null,
            new ToolExecutor($toolRegistry),
            $toolRegistry
        );

        $runtime = new Runtime($context);
        $request = new Request();
        $request->provider('fake')->model('fake-weather-v1')->addMessage('What is the weather in Atlanta?');

        $events = array();
        $runtime->execute($request, new ConversationReference(), function ($event) use (&$events) {
            $events[] = $event;
        });

        $started = null;
        $completed = null;
        foreach ($events as $event) {
            if ($event->type() === StreamEvent::TOOL_EXECUTION_STARTED) {
                $started = $event;
            }
            if ($event->type() === StreamEvent::TOOL_EXECUTION_COMPLETED) {
                $completed = $event;
            }
        }

        $this->assertInstanceOf(StreamEvent::class, $started);
        $this->assertInstanceOf(StreamEvent::class, $completed);

        $this->assertNotNull($started->data('tool_call_id'));
        $this->assertSame('get_current_temperature', $started->data('tool_name'));
        $this->assertIsArray($started->data('arguments'));
        $this->assertIsString($started->data('arguments_json'));
        $this->assertNull($started->data('output'));
        $this->assertSame(array(), $started->data('metadata'));
        $this->assertSame('started', $started->data('status'));
        $this->assertNull($started->data('success'));

        $this->assertSame($started->data('tool_call_id'), $completed->data('tool_call_id'));
        $this->assertSame('get_current_temperature', $completed->data('tool_name'));
        $this->assertSame($started->data('arguments'), $completed->data('arguments'));
        $this->assertSame($started->data('arguments_json'), $completed->data('arguments_json'));
        $this->assertIsString($completed->data('output'));
        $this->assertSame(array(), $completed->data('metadata'));
        $this->assertSame('completed', $completed->data('status'));
        $this->assertTrue($completed->data('success'));
    }

    public function testRuntimeEmitsNormalizedToolLifecyclePayloadForFailedEvents()
    {
        $provider = new FakeProvider();
        $providerRegistry = new ProviderRegistry();
        $providerRegistry->register($provider);
        $providerRegistry->mapModelPrefix('fake-', 'fake');

        $toolRegistry = new ToolRegistry();
        $toolRegistry->define(
            'get_current_temperature',
            'Get the current temperature',
            array('type' => 'object'),
            function () {
                throw new \RuntimeException('Tool execution blew up');
            }
        );

        $context = new RuntimeContext(
            $providerRegistry,
            new FakePersistenceProvider(),
            null,
            new ToolExecutor($toolRegistry),
            $toolRegistry
        );

        $runtime = new Runtime($context);
        $request = new Request();
        $request->provider('fake')->model('fake-weather-v1')->addMessage('What is the weather in Atlanta?');

        $events = array();
        $response = $runtime->execute($request, new ConversationReference(), function ($event) use (&$events) {
            $events[] = $event;
        });

        $failed = null;
        foreach ($events as $event) {
            if ($event->type() === StreamEvent::TOOL_EXECUTION_FAILED) {
                $failed = $event;
                break;
            }
        }

        $this->assertInstanceOf(StreamEvent::class, $failed);
        $this->assertSame('get_current_temperature', $failed->data('tool_name'));
        $this->assertIsArray($failed->data('arguments'));
        $this->assertIsString($failed->data('arguments_json'));
        $this->assertSame('Tool execution blew up', $failed->data('output'));
        $this->assertSame('failed', $failed->data('status'));
        $this->assertFalse($failed->data('success'));
        $this->assertSame('RuntimeException', $failed->data('metadata')['exception_class']);
        $this->assertSame(0, $failed->data('metadata')['exception_code']);
        $this->assertSame('completed', $response->status());
    }

    public function testRuntimePersistsFailedTurnWhenProviderThrows()
    {
        $provider = new FailingFakeProvider();
        $providerRegistry = new ProviderRegistry();
        $providerRegistry->register($provider);
        $providerRegistry->mapModelPrefix('fake-', 'fake');

        $persistence = new FakePersistenceProvider();
        $context = new RuntimeContext(
            $providerRegistry,
            $persistence,
            null,
            new ToolExecutor(new ToolRegistry()),
            new ToolRegistry()
        );

        $runtime = new Runtime($context);
        $request = new Request();
        $request->provider('fake')->model('fake-weather-v1')->addMessage('Trigger failure');

        try {
            $runtime->execute($request, new ConversationReference());
            $this->fail('Expected runtime to throw');
        } catch (\RuntimeException $e) {
            $this->assertSame('Provider execution failed', $e->getMessage());
        }

        $lastTurn = $persistence->savedTurns[count($persistence->savedTurns) - 1];
        $this->assertSame('failed', $lastTurn->status());
        $this->assertSame('Provider execution failed', $lastTurn->metadata('exception_message'));
        $this->assertSame('RuntimeException', $lastTurn->metadata('exception_class'));
    }
}

class FailingFakeProvider extends FakeProvider
{
    public function execute(Request $request, $eventCallback = null)
    {
        throw new \RuntimeException('Provider execution failed');
    }
}
