<?php
namespace boru\ai\Tests\Provider\Gemini;

use boru\ai\Provider\Gemini\GeminiMapper;
use boru\ai\Schema\Request;
use boru\ai\Schema\ToolResult;
use PHPUnit\Framework\TestCase;

class GeminiMapperTest extends TestCase
{
    public function testMapsNormalizedRequestToGeminiGenerateContentPayload()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro')
            ->instructions('You are helpful')
            ->addMessage('What is the weather in Atlanta?', 'user')
            ->addTool(array(
                'name' => 'get_current_temperature',
                'description' => 'Get current temperature',
                'schema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'location' => array('type' => 'string'),
                    ),
                ),
            ))
            ->option('generationConfig', array('temperature' => 0.2))
            ->option('stream', true)
            ->option('previous_response_id', 'ignored_for_now');

        $payload = $mapper->mapRequest($request);

        $this->assertSame('You are helpful', $payload['systemInstruction']['parts'][0]['text']);
        $this->assertSame(0.2, $payload['generationConfig']['temperature']);
        $this->assertArrayNotHasKey('stream', $payload);
        $this->assertArrayNotHasKey('previous_response_id', $payload);
        $this->assertCount(1, $payload['contents']);
        $this->assertSame('user', $payload['contents'][0]['role']);
        $this->assertSame('What is the weather in Atlanta?', $payload['contents'][0]['parts'][0]['text']);
        $this->assertCount(1, $payload['tools']);
        $this->assertSame('get_current_temperature', $payload['tools'][0]['functionDeclarations'][0]['name']);
    }

    public function testNormalizesGeminiFunctionDeclarationSchemasToAllowedSubset()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro')
            ->addTool(array(
                'name' => 'legacy_files_read',
                'description' => 'Read a file from the repository',
                'schema' => array(
                    'type' => 'object',
                    'description' => 'Top-level schema',
                    'additionalProperties' => false,
                    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
                    'properties' => array(
                        'path' => array(
                            'type' => 'string',
                            'description' => 'The file path',
                            'pattern' => '.+\\.php$',
                            'examples' => array('src/AI.php'),
                            'additionalProperties' => false,
                        ),
                        'options' => array(
                            'type' => 'object',
                            'additionalProperties' => false,
                            'properties' => array(
                                'filters' => array(
                                    'type' => 'array',
                                    'items' => array(
                                        'type' => 'object',
                                        'additionalProperties' => false,
                                        'properties' => array(
                                            'kind' => array('type' => 'string'),
                                        ),
                                        'required' => array('kind'),
                                    ),
                                    'contains' => array('type' => 'string'),
                                ),
                            ),
                        ),
                        'mode' => array(
                            'type' => 'string',
                            'enum' => array('full', 'partial'),
                            'default' => 'partial',
                        ),
                    ),
                    'required' => array('path', 'mode'),
                ),
            ));

        $payload = $mapper->mapRequest($request);
        $parameters = $payload['tools'][0]['functionDeclarations'][0]['parameters'];

        $this->assertSame('object', $parameters['type']);
        $this->assertSame('Top-level schema', $parameters['description']);
        $this->assertSame(array('path', 'mode'), $parameters['required']);

        $this->assertArrayNotHasKey('additionalProperties', $parameters);
        $this->assertArrayNotHasKey('$schema', $parameters);
        $this->assertArrayNotHasKey('examples', $parameters['properties']['path']);
        $this->assertArrayNotHasKey('additionalProperties', $parameters['properties']['path']);
        $this->assertArrayNotHasKey('additionalProperties', $parameters['properties']['options']);
        $this->assertArrayNotHasKey(
            'additionalProperties',
            $parameters['properties']['options']['properties']['filters']['items']
        );
        $this->assertArrayNotHasKey(
            'contains',
            $parameters['properties']['options']['properties']['filters']
        );
        $this->assertArrayNotHasKey('x-boruai-gemini-placeholder', $parameters);

        $this->assertSame('.+\\.php$', $parameters['properties']['path']['pattern']);
        $this->assertSame('partial', $parameters['properties']['mode']['default']);
        $this->assertSame('string', $parameters['properties']['options']['properties']['filters']['items']['properties']['kind']['type']);
        $this->assertSame(
            array('kind'),
            $parameters['properties']['options']['properties']['filters']['items']['required']
        );
    }

    public function testDowngradesEmptyObjectSchemasToPlaceholderStringParameterForGemini()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro')
            ->addTool(array(
                'name' => 'parameterless_tool',
                'description' => 'A tool with no usable parameters',
                'schema' => array(
                    'type' => 'object',
                    'description' => 'No arguments expected',
                    'properties' => array(),
                ),
            ));

        $payload = $mapper->mapRequest($request);
        $parameters = $payload['tools'][0]['functionDeclarations'][0]['parameters'];

        $this->assertSame('object', $parameters['type']);
        $this->assertArrayHasKey('__arg', $parameters['properties']);
        $this->assertSame('string', $parameters['properties']['__arg']['type']);
        $this->assertStringContainsString('Gemini rejects empty object parameter schemas', $parameters['properties']['__arg']['description']);
        $this->assertSame(array('__arg'), $parameters['propertyOrdering']);
        $this->assertArrayNotHasKey('x-boruai-gemini-placeholder', $parameters);
    }

    public function testDowngradesNestedEmptyObjectSchemasToPlaceholderStringParameterForGemini()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro')
            ->addTool(array(
                'name' => 'nested_empty_object_tool',
                'description' => 'A tool with an empty nested object',
                'schema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'config' => array(
                            'type' => 'object',
                            'description' => 'Nested config with no remaining supported properties',
                            'additionalProperties' => false,
                            'properties' => array(),
                        ),
                    ),
                ),
            ));

        $payload = $mapper->mapRequest($request);
        $config = $payload['tools'][0]['functionDeclarations'][0]['parameters']['properties']['config'];

        $this->assertSame('object', $config['type']);
        $this->assertArrayHasKey('__arg', $config['properties']);
        $this->assertSame('string', $config['properties']['__arg']['type']);
        $this->assertStringContainsString('Gemini rejects empty object parameter schemas', $config['properties']['__arg']['description']);
        $this->assertSame(array('__arg'), $config['propertyOrdering']);
        $this->assertArrayNotHasKey('x-boruai-gemini-placeholder', $config);
    }

    public function testPreservesAllowedAnyOfBranchSchemasWhenNormalizingForGemini()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro')
            ->addTool(array(
                'name' => 'search_query',
                'description' => 'Search for a query',
                'schema' => array(
                    'type' => 'object',
                    'properties' => array(
                        'term' => array(
                            'anyOf' => array(
                                array('type' => 'string', 'minLength' => 1),
                                array('type' => 'string', 'enum' => array('latest')),
                            ),
                            'oneOf' => array(
                                array('type' => 'string'),
                            ),
                        ),
                    ),
                ),
            ));

        $payload = $mapper->mapRequest($request);
        $termSchema = $payload['tools'][0]['functionDeclarations'][0]['parameters']['properties']['term'];

        $this->assertArrayHasKey('anyOf', $termSchema);
        $this->assertCount(2, $termSchema['anyOf']);
        $this->assertArrayNotHasKey('oneOf', $termSchema);
        $this->assertSame('string', $termSchema['anyOf'][0]['type']);
        $this->assertSame(1, $termSchema['anyOf'][0]['minLength']);
        $this->assertSame(array('latest'), $termSchema['anyOf'][1]['enum']);
    }

    public function testMapsToolResultsIntoGeminiFunctionResponseContent()
    {
        $mapper = new GeminiMapper();
        $request = new Request();
        $request->model('gemini-2.5-pro');

        $toolResult = new ToolResult();
        $toolResult->toolCallId('call_123');
        $toolResult->name('get_current_temperature');
        $toolResult->output('{"temperature":69}');
        $request->addToolResult($toolResult);

        $payload = $mapper->mapRequest($request);

        $this->assertCount(1, $payload['contents']);
        $this->assertSame('user', $payload['contents'][0]['role']);
        $this->assertSame('get_current_temperature', $payload['contents'][0]['parts'][0]['functionResponse']['name']);
        $this->assertSame(69, $payload['contents'][0]['parts'][0]['functionResponse']['response']['temperature']);
    }

    public function testMapsGeminiResponseIntoNormalizedResponseAndToolCalls()
    {
        $mapper = new GeminiMapper();

        $payload = array(
            'responseId' => 'gem_resp_123',
            'modelVersion' => 'gemini-2.5-pro',
            'usageMetadata' => array(
                'promptTokenCount' => 21,
                'candidatesTokenCount' => 23,
                'totalTokenCount' => 44,
            ),
            'candidates' => array(
                array(
                    'content' => array(
                        'parts' => array(
                            array(
                                'functionCall' => array(
                                    'name' => 'get_current_temperature',
                                    'args' => array(
                                        'location' => 'Atlanta, GA',
                                        'unit' => 'Fahrenheit',
                                    ),
                                ),
                            ),
                            array(
                                'text' => 'The current temperature is 69°F.',
                            ),
                        ),
                    ),
                ),
            ),
        );

        $response = $mapper->mapResponse($payload);

        $this->assertSame('gemini', $response->provider());
        $this->assertSame('gem_resp_123', $response->id());
        $this->assertSame('gemini-2.5-pro', $response->model());
        $this->assertSame('completed', $response->status());
        $this->assertSame('The current temperature is 69°F.', $response->text());
        $this->assertCount(1, $response->toolCalls());
        $this->assertSame('get_current_temperature', $response->toolCalls()[0]->name());
        $this->assertSame('Atlanta, GA', $response->toolCalls()[0]->arguments()['location']);
        $this->assertSame(21, $response->inputTokens());
        $this->assertSame(23, $response->outputTokens());
        $this->assertSame(44, $response->totalTokens());
        $this->assertNull($response->cachedInputTokens());
        $this->assertNull($response->reasoningTokens());

        $expectedUsage = array(
            'input_tokens' => 21,
            'output_tokens' => 23,
            'total_tokens' => 44,
            'raw' => $payload['usageMetadata'],
        );
        $actualUsage = $response->usage();
        ksort($expectedUsage);
        ksort($actualUsage);

        $this->assertSame($expectedUsage, $actualUsage);
        $this->assertSame($payload['usageMetadata'], $response->rawUsage());
    }
}
