<?php

use PHPUnit\Framework\TestCase;
use boru\boruai\Agent\BaseAgent;
use boru\boruai\Models\Tool\ToolContext;
use boru\boruai\Models\File;

class BaseAgentTest extends TestCase
{
    /**
     * Minimal fake Response used to observe interactions from BaseAgent.
     */
    private function createFakeResponse()
    {
        return new class {
            public $model;
            public $messages = [];
            public $files = [];
            public $images = [];
            public $tools = [];
            public $toolContext = null;
            public $eventDispatcher = null;

            public function model($model)
            {
                $this->model = $model;
                return $this;
            }

            public function previousResponseId()
            {
                // Fake a new conversation
                return null;
            }

            public function addMessage($message, $newMessage = false, $prepend = false)
            {
                $this->messages[] = compact('message', 'newMessage', 'prepend');
                return $this;
            }

            public function create()
            {
                // In real life this might persist and return a new instance;
                // for unit tests we can just return $this to simulate success.
                return $this;
            }

            public function toolContext($ctx)
            {
                $this->toolContext = $ctx;
                return $this;
            }

            public function setEventDispatcher($dispatcher)
            {
                $this->eventDispatcher = $dispatcher;
                return $this;
            }

            public function addFile($file)
            {
                $this->files[] = $file;
                return $this;
            }

            public function addImage($image, $detail = null)
            {
                $this->images[] = compact('image', 'detail');
                return $this;
            }

            public function addTool($tool)
            {
                $this->tools[] = $tool;
                return $this;
            }
        };
    }

    /**
     * Concrete test agent that exposes BaseAgent internals for testing.
     */
    private function createTestAgent($reference = null, ToolContext $ctx = null, $fakeResponse = null)
    {
        if ($fakeResponse === null) {
            $fakeResponse = $this->createFakeResponse();
        }

        return new class($reference, $ctx, $fakeResponse) extends BaseAgent {
            private $fakeResponse;

            public function __construct($reference, $ctx, $fakeResponse)
            {
                parent::__construct($reference, $ctx);
                $this->fakeResponse = $fakeResponse;
            }

            public function generateReference()
            {
                return 'TEST_REF_' . uniqid();
            }

            public function getInstructions()
            {
                return 'Test instructions';
            }

            public function getPrompt()
            {
                return 'Test prompt';
            }

            // Override to use our fake response instead of hitting the real model/DB.
            public function response()
            {
                return $this->fakeResponse;
            }

            // Expose some protected properties for assertions
            public function getToolContextInternal()
            {
                return $this->toolContext;
            }

            public function getAttachedFilesInternal()
            {
                return $this->attachedFiles;
            }
        };
    }

    public function testSetReferenceOverridesGeneratedReference()
    {
        $agent = $this->createTestAgent();

        $generated = $agent->getReference();
        $this->assertStringStartsWith('TEST_REF_', $generated);

        $agent->setReference('MY_REF');
        $this->assertSame('MY_REF', $agent->getReference());
    }

    public function testAddContextCreatesAndMutatesToolContext()
    {
        $agent = $this->createTestAgent();

        // No context initially
        $this->assertNull($agent->getToolContextInternal());

        $agent->addContext('foo', 'bar');

        $ctx = $agent->getToolContextInternal();
        $this->assertInstanceOf(ToolContext::class, $ctx);
        $this->assertSame('bar', $ctx->get('foo'));
    }

    public function testBuildToolContextCreatesDefaultContext()
    {
        $agent = $this->createTestAgent();

        // buildToolContext is protected and our test agent does not override it,
        // but we *do* know that addContext() will create a ToolContext when needed.
        $this->assertNull($agent->getToolContextInternal());

        $agent->addContext('foo', 'bar');

        $ctx = $agent->getToolContextInternal();
        $this->assertInstanceOf(ToolContext::class, $ctx);
        $this->assertSame('bar', $ctx->get('foo'));
    }


    public function testAddMessageMarksNoMessagesFalseAndForwardsToResponse()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $agent->addMessage('Hello world', true, false);

        $this->assertCount(1, $fakeResponse->messages);
        $msg = $fakeResponse->messages[0];

        $this->assertSame('Hello world', $msg['message']);
        $this->assertTrue($msg['newMessage']);
        $this->assertFalse($msg['prepend']);
    }

    public function testAddFileWithFileModelUsesFileId()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $file = $this->createMock(File::class);
        $file->method('fileid')->willReturn('file-123');

        $agent->addFile($file);

        $this->assertSame(['file-123'], $fakeResponse->files);
        $this->assertSame(['file-123'], $agent->getAttachedFilesInternal());
    }

    public function testAddFileWithScalarIsForwardedDirectly()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $agent->addFile('raw-file-id');

        $this->assertSame(['raw-file-id'], $fakeResponse->files);
        $this->assertSame(['raw-file-id'], $agent->getAttachedFilesInternal());
    }

    public function testAddImageWithFileModelUsesFileId()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $file = $this->createMock(File::class);
        $file->method('fileid')->willReturn('img-123');

        $agent->addImage($file, 'high');

        $this->assertCount(1, $fakeResponse->images);
        $img = $fakeResponse->images[0];

        $this->assertSame('img-123', $img['image']);
        $this->assertSame('high', $img['detail']);

        $attached = $agent->getAttachedFilesInternal();
        $this->assertCount(1, $attached);
        $this->assertSame(
            ['fileId' => 'img-123', 'type' => 'image'],
            $attached[0]
        );
    }

    public function testAddImageWithScalarIsForwardedDirectly()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $agent->addImage('raw-image-id', 'low');

        $this->assertCount(1, $fakeResponse->images);
        $img = $fakeResponse->images[0];

        $this->assertSame('raw-image-id', $img['image']);
        $this->assertSame('low', $img['detail']);

        $attached = $agent->getAttachedFilesInternal();
        $this->assertSame(['raw-image-id'], $attached);
    }

    public function testAddToolForwardsToResponse()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $tool = ['name' => 'get_weather'];

        $agent->addTool($tool);

        $this->assertCount(1, $fakeResponse->tools);
        $this->assertSame($tool, $fakeResponse->tools[0]);
    }

    public function testGetEventDispatcherReturnsDispatcherInstance()
    {
        $fakeResponse = $this->createFakeResponse();
        $agent = $this->createTestAgent(null, null, $fakeResponse);

        $dispatcher1 = $agent->getEventDispatcher();
        $dispatcher2 = $agent->getEventDispatcher();

        $this->assertNotNull($dispatcher1);
        $this->assertSame(
            $dispatcher1,
            $dispatcher2,
            'getEventDispatcher() should be idempotent and cache the instance'
        );
    }

}
