<?php

declare(strict_types=1);

namespace ContextPaging\Tests;

use ContextPaging\ContextPaging;
use ContextPaging\TokenCounter;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;

class ContextPagingTest extends TestCase
{
    private ContextPaging $contextPaging;

    protected function setUp(): void
    {
        $this->contextPaging = new ContextPaging();
    }

    /**
     * Test basic fit with a small payload that doesn't need summarization.
     */
    public function testFitWithSmallPayload(): void
    {
        $messages = [
            ['role' => 'user', 'content' => 'Hello, how are you?'],
        ];

        $request = $this->createRequest($messages);

        $fitted = $this->contextPaging->fit($request);

        $this->assertTrue($fitted->getAttribute('context_fitted'));

        $fittedMessages = $fitted->getParsedBody()['messages'];
        $this->assertCount(1, $fittedMessages);
        $this->assertEquals('Hello, how are you?', $fittedMessages[0]['content']);
    }

    /**
     * Test fit with a larger payload that exceeds context limit.
     */
    public function testFitWithLargePayloadTriggersSummarization(): void
    {
        // Set a limit low enough to force summarization but high enough for last message
        $this->contextPaging->setMaxContextTokens(100)->setResponseReserve(20);

        $messages = [
            ['role' => 'user', 'content' => str_repeat('This is a long message that should be summarized. ', 50)],
            ['role' => 'assistant', 'content' => 'I understand your message.'],
            ['role' => 'user', 'content' => 'Short question'],
        ];

        $request = $this->createRequest($messages);

        $fitted = $this->contextPaging->fit($request);

        $fittedMessages = $fitted->getParsedBody()['messages'];

        // First message should be summarized
        $this->assertTrue($fittedMessages[0]['_summarized'] ?? false);
        $this->assertStringContainsString('[md5:', $fittedMessages[0]['content']);

        // Last message should NOT be summarized
        $lastIndex = count($fittedMessages) - 1;
        $this->assertFalse($fittedMessages[$lastIndex]['_summarized'] ?? false);

        // Should be under budget
        $tokens = $fitted->getAttribute('context_tokens');
        $this->assertLessThanOrEqual(80, $tokens);
    }

    /**
     * Test execute with no tool calls returns response as-is.
     */
    public function testExecuteWithNoToolCalls(): void
    {
        $messages = [
            ['role' => 'user', 'content' => 'Hello!'],
        ];

        $request = $this->createRequest($messages);

        $response = $this->contextPaging->execute($request, function (array $msgs, $req) {
            return new Response(200, [], json_encode(['choices' => [[
                'message' => ['role' => 'assistant', 'content' => 'Hi there!'],
            ]]]));
        });

        $body = json_decode($response->getBody()->getContents(), true);
        $this->assertEquals('Hi there!', $body['choices'][0]['message']['content']);
    }

    /**
     * Test that original messages are stored for dereferencing.
     */
    public function testOriginalMessagesStoredForDereferencing(): void
    {
        $longContent = str_repeat('This is a long message. ', 100);
        $messages = [
            ['role' => 'user', 'content' => $longContent],
            ['role' => 'user', 'content' => 'Short question'],
        ];

        $this->contextPaging->setMaxContextTokens(100)->setResponseReserve(20);

        $request = $this->createRequest($messages);
        $fitted = $this->contextPaging->fit($request);

        // The MD5 hash in the summarized message should reference the original
        $fittedMessages = $fitted->getParsedBody()['messages'];
        $this->assertMatchesRegularExpression('/\[md5:([a-f0-9]{32})\]/', $fittedMessages[0]['content']);

        // Extract MD5
        preg_match('/\[md5:([a-f0-9]{32})\]/', $fittedMessages[0]['content'], $matches);
        $md5 = $matches[1];

        // Verify the message store has the original
        // (We'd need to expose this or use reflection in a real test)
        $this->assertNotEmpty($md5);
    }

    /**
     * Test that last message is never summarized.
     */
    public function testLastMessageNeverSummarized(): void
    {
        $this->contextPaging->setMaxContextTokens(80)->setResponseReserve(15);

        $messages = [
            ['role' => 'user', 'content' => 'First message that is quite long and should be summarized'],
            ['role' => 'user', 'content' => 'Second message that is also quite long'],
            ['role' => 'user', 'content' => 'Short'],
        ];

        $request = $this->createRequest($messages);
        $fitted = $this->contextPaging->fit($request);

        $fittedMessages = $fitted->getParsedBody()['messages'];
        $lastIndex = count($fittedMessages) - 1;

        // Last message must not be summarized
        $this->assertFalse($fittedMessages[$lastIndex]['_summarized'] ?? false);
        $this->assertEquals('Short', $fittedMessages[$lastIndex]['content']);
    }

    /**
     * Test error when last message itself exceeds context.
     */
    public function testErrorWhenLastMessageTooLarge(): void
    {
        $this->contextPaging->setMaxContextTokens(20)->setResponseReserve(5);

        // Single message that's way too big
        $messages = [
            ['role' => 'user', 'content' => str_repeat('This is a massive message that will never fit. ', 100)],
        ];

        $request = $this->createRequest($messages);

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('too large');

        $this->contextPaging->fit($request);
    }

    /**
     * Test that max_tokens in request is used for budget calculation.
     */
    public function testMaxTokensUsedForBudgetCalculation(): void
    {
        $this->contextPaging->setMaxContextTokens(65536);

        $messages = [
            ['role' => 'user', 'content' => 'Hello'],
        ];

        // Request with max_tokens: 8000
        $request = $this->createRequest($messages, ['max_tokens' => 8000]);

        $fitted = $this->contextPaging->fit($request);

        // Budget should be 65536 - 8000 = 57536
        $this->assertEquals(57536, $fitted->getAttribute('context_budget'));
    }

    /**
     * Test fallback to responseReserve when max_tokens not provided.
     */
    public function testFallbackToResponseReserveWhenNoMaxTokens(): void
    {
        $this->contextPaging->setMaxContextTokens(65536)->setResponseReserve(4096);

        $messages = [
            ['role' => 'user', 'content' => 'Hello'],
        ];

        // Request WITHOUT max_tokens
        $request = $this->createRequest($messages);

        $fitted = $this->contextPaging->fit($request);

        // Budget should be 65536 - 4096 = 61440
        $this->assertEquals(61440, $fitted->getAttribute('context_budget'));
    }

    // -----------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------

    private function createRequest(array $messages, array $extraBody = []): ServerRequest
    {
        $body = array_merge(['messages' => $messages], $extraBody);

        return new ServerRequest(
            method: 'POST',
            uri: 'test://localhost',
            headers: ['Content-Type' => 'application/json'],
            body: json_encode($body),
            version: '1.1',
            serverParams: []
        )->withParsedBody($body);
    }
}
