<?php

declare(strict_types=1);

use App\Framework\Filesystem\CachedFileValidator;
use App\Framework\Filesystem\FileValidator;
use App\Framework\Filesystem\Exceptions\FileValidationException;
use App\Framework\Core\ValueObjects\FileSize;

describe('CachedFileValidator', function () {
    beforeEach(function () {
        $this->validator = FileValidator::createDefault();
        $this->cachedValidator = new CachedFileValidator(
            validator: $this->validator,
            cacheTtl: 60,
            maxCacheSize: 10
        );
    });

    describe('Path Validation Caching', function () {
        it('caches successful path validation', function () {
            $path = '/valid/path/file.txt';

            // First call - cache miss
            $this->cachedValidator->validatePath($path);

            // Second call - cache hit (should not throw)
            $this->cachedValidator->validatePath($path);

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['path_cache_size'])->toBe(1);
        });

        it('caches failed path validation', function () {
            $invalidPath = '/path/with/../traversal';

            // First call - cache miss, throws exception
            try {
                $this->cachedValidator->validatePath($invalidPath);
                $this->fail('Expected FileValidationException');
            } catch (FileValidationException $e) {
                expect($e->getMessage())->toContain('Path traversal');
            }

            // Second call - cache hit, throws same exception
            try {
                $this->cachedValidator->validatePath($invalidPath);
                $this->fail('Expected FileValidationException from cache');
            } catch (FileValidationException $e) {
                expect($e->getMessage())->toContain('Path traversal');
            }

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['path_cache_size'])->toBe(1);
        });

        it('detects path traversal on first call and caches result', function () {
            $traversalPath = '/etc/../passwd';

            // Should throw on first call
            expect(fn() => $this->cachedValidator->validatePath($traversalPath))
                ->toThrow(FileValidationException::class);

            // Should throw on second call (from cache)
            expect(fn() => $this->cachedValidator->validatePath($traversalPath))
                ->toThrow(FileValidationException::class);
        });

        it('detects null bytes and caches result', function () {
            $nullBytePath = "/path/with\0nullbyte";

            // Should throw on first call
            expect(fn() => $this->cachedValidator->validatePath($nullBytePath))
                ->toThrow(FileValidationException::class);

            // Should throw on second call (from cache)
            expect(fn() => $this->cachedValidator->validatePath($nullBytePath))
                ->toThrow(FileValidationException::class);
        });
    });

    describe('Extension Validation Caching', function () {
        it('caches successful extension validation', function () {
            $path = '/valid/path/file.txt';

            // First call - cache miss
            $this->cachedValidator->validateExtension($path);

            // Second call - cache hit
            $this->cachedValidator->validateExtension($path);

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['extension_cache_size'])->toBe(1);
        });

        it('caches failed extension validation', function () {
            $strictValidator = FileValidator::createStrict(['json', 'txt']);
            $cached = new CachedFileValidator($strictValidator);

            $phpPath = '/path/file.php';

            // First call - cache miss, throws exception
            expect(fn() => $cached->validateExtension($phpPath))
                ->toThrow(FileValidationException::class);

            // Second call - cache hit, throws same exception
            expect(fn() => $cached->validateExtension($phpPath))
                ->toThrow(FileValidationException::class);

            $stats = $cached->getCacheStats();
            expect($stats['extension_cache_size'])->toBe(1);
        });

        it('validates different extensions separately', function () {
            $this->cachedValidator->validateExtension('/file1.txt');
            $this->cachedValidator->validateExtension('/file2.json');
            $this->cachedValidator->validateExtension('/file3.csv');

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['extension_cache_size'])->toBe(3);
        });
    });

    describe('LRU Cache Eviction', function () {
        it('evicts oldest entry when cache is full', function () {
            $cached = new CachedFileValidator(
                validator: $this->validator,
                maxCacheSize: 3
            );

            // Fill cache with 3 entries
            $cached->validatePath('/path1');
            $cached->validatePath('/path2');
            $cached->validatePath('/path3');

            $stats = $cached->getCacheStats();
            expect($stats['path_cache_size'])->toBe(3);

            // Add 4th entry - should evict oldest (/path1)
            $cached->validatePath('/path4');

            $stats = $cached->getCacheStats();
            expect($stats['path_cache_size'])->toBe(3); // Still 3 (max size)
        });

        it('maintains LRU order on cache hit', function () {
            $cached = new CachedFileValidator(
                validator: $this->validator,
                maxCacheSize: 3
            );

            // Fill cache
            $cached->validatePath('/path1');
            $cached->validatePath('/path2');
            $cached->validatePath('/path3');

            // Access /path1 again (should move to end)
            $cached->validatePath('/path1');

            // Add new entry - should evict /path2 (oldest)
            $cached->validatePath('/path4');

            $stats = $cached->getCacheStats();
            expect($stats['path_cache_size'])->toBe(3);
        });
    });

    describe('Cache TTL Expiration', function () {
        it('expires cache entries after TTL', function () {
            $cached = new CachedFileValidator(
                validator: $this->validator,
                cacheTtl: 1 // 1 second TTL
            );

            $path = '/valid/path.txt';

            // Cache the validation
            $cached->validatePath($path);

            // Wait for TTL to expire
            sleep(2);

            // This should be a cache miss (expired)
            $cached->validatePath($path);

            // Cache should still have 1 entry (re-cached)
            $stats = $cached->getCacheStats();
            expect($stats['path_cache_size'])->toBe(1);
        });
    });

    describe('Non-Cached Validations', function () {
        it('does not cache file size validation', function () {
            $fileSize = FileSize::fromBytes(1024);

            // Multiple calls should not affect cache
            $this->cachedValidator->validateFileSize($fileSize);
            $this->cachedValidator->validateFileSize($fileSize);

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['path_cache_size'])->toBe(0);
            expect($stats['extension_cache_size'])->toBe(0);
        });

        it('does not cache existence checks', function () {
            // Create a temporary test file
            $testFile = sys_get_temp_dir() . '/test_cached_validator_' . uniqid() . '.txt';
            file_put_contents($testFile, 'test');

            try {
                $this->cachedValidator->validateExists($testFile);
                $this->cachedValidator->validateExists($testFile);

                $stats = $this->cachedValidator->getCacheStats();
                expect($stats['path_cache_size'])->toBe(0);
            } finally {
                if (file_exists($testFile)) {
                    unlink($testFile);
                }
            }
        });

        it('does not cache permission checks', function () {
            // Use temp directory for permission test
            $testDir = sys_get_temp_dir();

            $this->cachedValidator->validateWritable($testDir);
            $this->cachedValidator->validateWritable($testDir);

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['path_cache_size'])->toBe(0);
        });
    });

    describe('Composite Validations', function () {
        it('caches path and extension in validateUpload', function () {
            $path = '/upload/file.jpg';
            $fileSize = FileSize::fromBytes(1024);

            $this->cachedValidator->validateUpload($path, $fileSize);

            $stats = $this->cachedValidator->getCacheStats();
            expect($stats['path_cache_size'])->toBe(1);
            expect($stats['extension_cache_size'])->toBe(1);
        });

        it('caches path in validateRead', function () {
            $testFile = sys_get_temp_dir() . '/test_read_' . uniqid() . '.txt';
            file_put_contents($testFile, 'test');

            try {
                $this->cachedValidator->validateRead($testFile);

                $stats = $this->cachedValidator->getCacheStats();
                expect($stats['path_cache_size'])->toBe(1);
            } finally {
                if (file_exists($testFile)) {
                    unlink($testFile);
                }
            }
        });

        it('caches path and extension in validateWrite', function () {
            $testDir = sys_get_temp_dir() . '/test_write_dir_' . uniqid();
            mkdir($testDir);

            try {
                $path = $testDir . '/file.txt';

                $this->cachedValidator->validateWrite($path);

                $stats = $this->cachedValidator->getCacheStats();
                expect($stats['path_cache_size'])->toBe(1);
                expect($stats['extension_cache_size'])->toBe(1);
            } finally {
                if (is_dir($testDir)) {
                    rmdir($testDir);
                }
            }
        });
    });

    describe('Cache Management', function () {
        it('can clear all caches', function () {
            $this->cachedValidator->validatePath('/path1');
            $this->cachedValidator->validatePath('/path2');
            $this->cachedValidator->validateExtension('/file.txt');

            $statsBefore = $this->cachedValidator->getCacheStats();
            expect($statsBefore['path_cache_size'])->toBe(2);
            expect($statsBefore['extension_cache_size'])->toBe(1);

            $this->cachedValidator->clearCache();

            $statsAfter = $this->cachedValidator->getCacheStats();
            expect($statsAfter['path_cache_size'])->toBe(0);
            expect($statsAfter['extension_cache_size'])->toBe(0);
        });

        it('provides accurate cache statistics', function () {
            $stats = $this->cachedValidator->getCacheStats();

            expect($stats)->toHaveKeys([
                'path_cache_size',
                'extension_cache_size',
                'max_cache_size',
                'cache_ttl_seconds'
            ]);

            expect($stats['max_cache_size'])->toBe(10);
            expect($stats['cache_ttl_seconds'])->toBe(60);
        });
    });

    describe('Delegation Methods', function () {
        it('delegates isExtensionAllowed to wrapped validator', function () {
            expect($this->cachedValidator->isExtensionAllowed('txt'))->toBeTrue();
            expect($this->cachedValidator->isExtensionAllowed('exe'))->toBeFalse();
        });

        it('delegates getAllowedExtensions to wrapped validator', function () {
            $result = $this->cachedValidator->getAllowedExtensions();
            expect($result)->toBeNull(); // Default validator has no whitelist
        });

        it('delegates getBlockedExtensions to wrapped validator', function () {
            $result = $this->cachedValidator->getBlockedExtensions();
            expect($result)->toBeArray();
            expect($result)->toContain('exe');
        });

        it('delegates getMaxFileSize to wrapped validator', function () {
            $result = $this->cachedValidator->getMaxFileSize();
            expect($result)->toBeInstanceOf(FileSize::class);
        });
    });

    describe('Performance Characteristics', function () {
        it('maintains cache hits for repeated validations', function () {
            $path = '/repeated/path/file.txt';

            // First call - cache miss
            $this->cachedValidator->validatePath($path);

            $statsBefore = $this->cachedValidator->getCacheStats();
            expect($statsBefore['path_cache_size'])->toBe(1);

            // Repeated calls - cache hits
            for ($i = 0; $i < 100; $i++) {
                $this->cachedValidator->validatePath($path);
            }

            // Cache size should remain 1 (same path cached)
            $statsAfter = $this->cachedValidator->getCacheStats();
            expect($statsAfter['path_cache_size'])->toBe(1);
        });
    });
});
