<?php

declare(strict_types=1);

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\LiveComponents\ComponentCacheManager;
use App\Framework\LiveComponents\Contracts\LiveComponent;
use App\Framework\LiveComponents\ValueObjects\CacheConfig;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

describe('ComponentCacheManager', function () {
    beforeEach(function () {
        $this->cache = Mockery::mock(Cache::class);
        $this->cacheManager = new ComponentCacheManager($this->cache);
    });

    afterEach(function () {
        Mockery::close();
    });

    it('caches component with basic config', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-123'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromMinutes(5),
            varyBy: [],
            staleWhileRevalidate: false
        );

        $html = '<div class="stats">Views: 100</div>';

        $this->cache
            ->shouldReceive('set')
            ->once()
            ->with(Mockery::on(function (CacheItem $item) use ($html) {
                return $item->value === $html
                    && $item->ttl->toSeconds() === 300;
            }))
            ->andReturn(true);

        $result = $this->cacheManager->set($component, $html, $config);

        expect($result)->toBeTrue();
    });

    it('generates cache key with varyBy parameters', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('products:filter'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray([
            'category' => 'electronics',
            'price_min' => 100,
            'price_max' => 500,
            'sort' => 'price_asc',
        ]));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromMinutes(10),
            varyBy: ['category', 'price_min', 'price_max'],
            staleWhileRevalidate: false
        );

        $html = '<div>Product list</div>';

        $this->cache
            ->shouldReceive('set')
            ->once()
            ->with(Mockery::on(function (CacheItem $item) {
                $keyString = $item->key->toString();

                // Key should include component ID and varyBy parameters
                return str_contains($keyString, 'products:filter')
                    && str_contains($keyString, 'electronics')
                    && str_contains($keyString, '100')
                    && str_contains($keyString, '500');
            }))
            ->andReturn(true);

        $this->cacheManager->set($component, $html, $config);
    });

    it('retrieves cached component by exact key match', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-123'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromMinutes(5)
        );

        $cachedHtml = '<div class="stats">Views: 100</div>';

        $this->cache
            ->shouldReceive('get')
            ->once()
            ->andReturn(CacheItem::forGetting(
                key: CacheKey::fromString('livecomponent:stats:user-123'),
                value: $cachedHtml
            ));

        $result = $this->cacheManager->get($component, $config);

        expect($result)->toBe($cachedHtml);
    });

    it('returns null when cache miss', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-999'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));

        $config = new CacheConfig(enabled: true, ttl: Duration::fromMinutes(5));

        $this->cache
            ->shouldReceive('get')
            ->once()
            ->andReturn(null);

        $result = $this->cacheManager->get($component, $config);

        expect($result)->toBeNull();
    });

    it('invalidates component cache', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('cart:user-123'));

        $this->cache
            ->shouldReceive('forget')
            ->once()
            ->with(Mockery::on(function ($key) {
                return str_contains($key->toString(), 'cart:user-123');
            }))
            ->andReturn(true);

        $result = $this->cacheManager->invalidate($component);

        expect($result)->toBeTrue();
    });

    it('supports stale-while-revalidate pattern', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('news:feed'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromMinutes(5),
            varyBy: [],
            staleWhileRevalidate: true,
            staleWhileRevalidateTtl: Duration::fromMinutes(60)
        );

        $html = '<div>News feed</div>';

        $this->cache
            ->shouldReceive('set')
            ->once()
            ->with(Mockery::on(function (CacheItem $item) {
                // SWR should use extended TTL
                return $item->ttl->toSeconds() === 3600; // 60 minutes
            }))
            ->andReturn(true);

        $this->cacheManager->set($component, $html, $config);
    });

    it('varyBy with different values creates different cache keys', function () {
        $component1 = Mockery::mock(LiveComponent::class);
        $component1->shouldReceive('getId')->andReturn(ComponentId::fromString('search:results'));
        $component1->shouldReceive('getData')->andReturn(ComponentData::fromArray([
            'query' => 'laptop',
            'page' => 1,
        ]));

        $component2 = Mockery::mock(LiveComponent::class);
        $component2->shouldReceive('getId')->andReturn(ComponentId::fromString('search:results'));
        $component2->shouldReceive('getData')->andReturn(ComponentData::fromArray([
            'query' => 'laptop',
            'page' => 2,
        ]));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromMinutes(10),
            varyBy: ['query', 'page']
        );

        $capturedKeys = [];

        $this->cache
            ->shouldReceive('set')
            ->twice()
            ->with(Mockery::on(function (CacheItem $item) use (&$capturedKeys) {
                $capturedKeys[] = $item->key->toString();

                return true;
            }))
            ->andReturn(true);

        $this->cacheManager->set($component1, '<div>Page 1</div>', $config);
        $this->cacheManager->set($component2, '<div>Page 2</div>', $config);

        // Keys should be different because page number differs
        expect($capturedKeys[0])->not->toBe($capturedKeys[1]);
        expect($capturedKeys[0])->toContain('laptop');
        expect($capturedKeys[1])->toContain('laptop');
    });

    it('ignores cache when config disabled', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('realtime:feed'));

        $config = new CacheConfig(enabled: false, ttl: Duration::fromMinutes(5));

        $this->cache->shouldNotReceive('set');
        $this->cache->shouldNotReceive('get');

        $result = $this->cacheManager->set($component, '<div>Feed</div>', $config);
        expect($result)->toBeFalse();

        $cached = $this->cacheManager->get($component, $config);
        expect($cached)->toBeNull();
    });

    it('handles empty varyBy array correctly', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('static:banner'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['message' => 'Welcome']));

        $config = new CacheConfig(
            enabled: true,
            ttl: Duration::fromHours(1),
            varyBy: [] // No variation
        );

        $this->cache
            ->shouldReceive('set')
            ->once()
            ->with(Mockery::on(function (CacheItem $item) {
                // Should only include component ID, no variation parameters
                return str_contains($item->key->toString(), 'static:banner');
            }))
            ->andReturn(true);

        $this->cacheManager->set($component, '<div>Banner</div>', $config);
    });

    it('respects custom TTL durations', function () {
        $component = Mockery::mock(LiveComponent::class);
        $component->shouldReceive('getId')->andReturn(ComponentId::fromString('test:component'));
        $component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));

        $testCases = [
            Duration::fromSeconds(30) => 30,
            Duration::fromMinutes(15) => 900,
            Duration::fromHours(2) => 7200,
            Duration::fromDays(1) => 86400,
        ];

        foreach ($testCases as $duration => $expectedSeconds) {
            $config = new CacheConfig(enabled: true, ttl: $duration);

            $this->cache
                ->shouldReceive('set')
                ->once()
                ->with(Mockery::on(function (CacheItem $item) use ($expectedSeconds) {
                    return $item->ttl->toSeconds() === $expectedSeconds;
                }))
                ->andReturn(true);

            $this->cacheManager->set($component, '<div>Test</div>', $config);
        }
    });
});
