<?php

declare(strict_types=1);

namespace ContextPaging\Tests;

use ContextPaging\CacheInterface;
use ContextPaging\ContextPaging;
use ContextPaging\InMemoryCache;
use ContextPaging\LLMSummarizer;
use ContextPaging\OpenAICompatibleClient;
use ContextPaging\RedisCache;
use ContextPaging\TokenCounter;
use ContextPaging\ToolCallMode;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;

/**
 * Tests for Redis-backed caching.
 */
class RedisCacheTest extends TestCase
{
    private CacheInterface $redisCache;
    private string $redisUrl = 'REDIS_URL_FROM_ENV';

    protected function setUp(): void
    {
        $this->redisCache = RedisCache::fromUrl($this->redisUrl, 'test_ctx:');
    }

    protected function tearDown(): void
    {
        // Clean up test keys
        $this->redisCache->clear();
    }

    public function testSetAndGet(): void
    {
        $this->redisCache->set('foo', ['bar' => 'baz']);

        $value = $this->redisCache->get('foo');

        $this->assertIsArray($value);
        $this->assertEquals('baz', $value['bar']);
    }

    public function testHasReturnsTrueForExistingKey(): void
    {
        $this->redisCache->set('exists', 'value');

        $this->assertTrue($this->redisCache->has('exists'));
        $this->assertFalse($this->redisCache->has('nonexistent'));
    }

    public function testDelete(): void
    {
        $this->redisCache->set('to_delete', 'value');
        $this->assertTrue($this->redisCache->has('to_delete'));

        $this->redisCache->delete('to_delete');
        $this->assertFalse($this->redisCache->has('to_delete'));
    }

    public function testGetReturnsNullForMissingKey(): void
    {
        $this->assertNull($this->redisCache->get('missing_key'));
    }

    public function testTtl(): void
    {
        // Set with 1 second TTL
        $this->redisCache->set('expires_soon', 'value', 1);

        $this->assertTrue($this->redisCache->has('expires_soon'));
        $this->assertEquals('value', $this->redisCache->get('expires_soon'));

        // Wait for expiry
        sleep(2);

        $this->assertFalse($this->redisCache->has('expires_soon'));
    }

    public function testContextPagingWithRedisCache(): void
    {
        $messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg:');
        $summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum:');

        $contextPaging = new ContextPaging(
            tokenCounter: new TokenCounter(),
            messageStore: $messageStore,
            summaryCache: $summaryCache
        );

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

        $longContent = str_repeat('This is a long message that will be summarized. ', 30);
        $md5 = md5($longContent);

        $messages = [
            ['role' => 'user', 'content' => $longContent],
            ['role' => 'user', 'content' => 'Short question'],
        ];

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

        // Verify message was stored in Redis
        $storedMessage = $messageStore->get("msg:{$md5}");
        $this->assertNotNull($storedMessage, 'Message should be stored in Redis');
        $this->assertEquals($longContent, $storedMessage['content']);

        // Verify summary was cached
        $summaryKey = "summary:{$md5}";
        $this->assertTrue($summaryCache->has($summaryKey), 'Summary should be cached');

        // Clean up
        $messageStore->clear();
        $summaryCache->clear();
    }

    public function testSummaryPersistsBetweenRequests(): void
    {
        $messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg2:');
        $summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum2:');

        $longContent = str_repeat('Persist test content. ', 50);
        $md5 = md5($longContent);

        // First request: create summary
        $contextPaging1 = new ContextPaging(
            tokenCounter: new TokenCounter(),
            messageStore: $messageStore,
            summaryCache: $summaryCache
        );
        $contextPaging1->setMaxContextTokens(100)->setResponseReserve(20);

        $request1 = $this->createRequest([
            ['role' => 'user', 'content' => $longContent],
            ['role' => 'user', 'content' => 'Short'],
        ]);

        $fitted1 = $contextPaging1->fit($request1);
        $fittedMessages1 = $fitted1->getParsedBody()['messages'];

        // Get the summary from cache
        $cachedSummary = $summaryCache->get("summary:{$md5}");

        // Second request: should use cached summary
        $contextPaging2 = new ContextPaging(
            tokenCounter: new TokenCounter(),
            messageStore: $messageStore,
            summaryCache: $summaryCache
        );
        $contextPaging2->setMaxContextTokens(100)->setResponseReserve(20);

        $request2 = $this->createRequest([
            ['role' => 'user', 'content' => $longContent],
            ['role' => 'user', 'content' => 'Short'],
        ]);

        $fitted2 = $contextPaging2->fit($request2);
        $fittedMessages2 = $fitted2->getParsedBody()['messages'];

        // Summaries should be identical (from cache)
        $this->assertEquals(
            $fittedMessages1[0]['content'],
            $fittedMessages2[0]['content'],
            'Summary should be identical from cache'
        );

        // Clean up
        $messageStore->clear();
        $summaryCache->clear();
    }

    public function testInMemoryVsRedisParity(): void
    {
        $content = 'Test content for parity check';
        $md5 = md5($content);

        // In-memory
        $inMemory = new InMemoryCache();
        $inMemory->set("msg:{$md5}", ['role' => 'user', 'content' => $content]);

        // Redis
        $redis = RedisCache::fromUrl($this->redisUrl, 'test_parity:');
        $redis->set("msg:{$md5}", ['role' => 'user', 'content' => $content]);

        // Both should return same data
        $this->assertEquals(
            $inMemory->get("msg:{$md5}"),
            $redis->get("msg:{$md5}"),
            'In-memory and Redis should return same data'
        );

        $redis->clear();
    }

    public function testMessageStorePersistsAcrossInstances(): void
    {
        $messageStore = RedisCache::fromUrl($this->redisUrl, 'test_msg3:');
        $summaryCache = RedisCache::fromUrl($this->redisUrl, 'test_sum3:');

        $longContent = str_repeat('Cross-instance test. ', 40);
        $md5 = md5($longContent);

        // First instance: fit and store
        $instance1 = new ContextPaging(
            tokenCounter: new TokenCounter(),
            messageStore: $messageStore,
            summaryCache: $summaryCache
        );
        $instance1->setMaxContextTokens(100)->setResponseReserve(20);

        $request = $this->createRequest([
            ['role' => 'user', 'content' => $longContent],
            ['role' => 'user', 'content' => 'Query'],
        ]);

        $fitted = $instance1->fit($request);

        // Second instance: should be able to dereference from shared Redis
        $instance2 = new ContextPaging(
            tokenCounter: new TokenCounter(),
            messageStore: $messageStore,
            summaryCache: $summaryCache
        );

        // Access the message store directly
        $retrievedMessage = $instance2->getMessageStore()->get("msg:{$md5}");

        $this->assertNotNull($retrievedMessage, 'Second instance should see stored message');
        $this->assertEquals($longContent, $retrievedMessage['content']);

        // Clean up
        $messageStore->clear();
        $summaryCache->clear();
    }

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

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