using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.FileDatabase.Services;
using DeepDrftContent.FileDatabase.Utils;

namespace DeepDrftTests;

/// <summary>
/// SOLID, DRY tests for MediaVault implementations
/// Follows Single Responsibility: Each test class tests one vault concern
/// Follows Liskov Substitution: Tests that all vault implementations behave consistently
/// Follows Dependency Inversion: Tests through abstractions where possible
/// </summary>
[TestFixture]
public class MediaVaultTests
{
    /// <summary>
    /// Base class for MediaVault tests - DRY principle
    /// </summary>
    public abstract class MediaVaultTestBase
    {
        protected string TestDirectory { get; private set; } = null!;
        protected string IndexPath => Path.Combine(TestDirectory, "index");

        [SetUp]
        public virtual void SetUp()
        {
            TestDirectory = Path.Combine(Path.GetTempPath(), "DeepDrftTests", "MediaVault", Guid.NewGuid().ToString());
            Directory.CreateDirectory(TestDirectory);
        }

        [TearDown]
        public virtual void TearDown()
        {
            if (Directory.Exists(TestDirectory))
            {
                try { Directory.Delete(TestDirectory, true); } catch { /* Ignore cleanup errors */ }
            }
        }

        /// <summary>
        /// Helper method to create test entry IDs - DRY principle  
        /// </summary>
        protected static string CreateTestEntryId(string key)
            => key;

        /// <summary>
        /// Helper method to create test media files - DRY principle
        /// </summary>
        protected string CreateTestMediaFile(string fileName, byte[]? content = null)
        {
            content ??= TestData.TestPngBytes;
            var filePath = Path.Combine(TestDirectory, fileName);
            File.WriteAllBytes(filePath, content);
            return filePath;
        }

        /// <summary>
        /// Helper method to verify media file exists and has correct content - DRY principle
        /// </summary>
        protected static void AssertMediaFileExists(string filePath, byte[] expectedContent)
        {
            Assert.That(File.Exists(filePath), Is.True, $"Media file should exist at {filePath}");
            var actualContent = File.ReadAllBytes(filePath);
            Assert.That(actualContent, Is.EqualTo(expectedContent), "File content should match expected");
        }
    }

    /// <summary>
    /// Tests for ImageVault - Single Responsibility
    /// </summary>
    [TestFixture]
    public class ImageVaultTests : MediaVaultTestBase
    {
        private ImageVault _imageVault = null!;

        [SetUp]
        public async Task SetUpAsync()
        {
            base.SetUp(); // Call base synchronous setup first
            _imageVault = await ImageVault.FromAsync(TestDirectory);
            Assert.That(_imageVault, Is.Not.Null, "ImageVault should be created for tests");
        }

        [Test]
        public async Task ImageVault_FromAsync_CreatesVaultWithIndex()
        {
            // Act
            var vault = await ImageVault.FromAsync(TestDirectory);

            // Assert
            Assert.That(vault, Is.Not.Null, "Should create ImageVault");
            Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided directory");
            Assert.That(File.Exists(IndexPath), Is.True, "Should create index file");
        }

        [Test]
        public async Task ImageVault_FromAsync_NonExistentDirectory_CreatesDirectoryAndVault()
        {
            // Arrange
            var newDirectory = Path.Combine(TestDirectory, "new-vault");
            Assert.That(Directory.Exists(newDirectory), Is.False, "Directory should not exist initially");

            // Act
            var vault = await ImageVault.FromAsync(newDirectory);

            // Assert
            Assert.That(vault, Is.Not.Null, "Should create ImageVault");
            Assert.That(Directory.Exists(newDirectory), Is.True, "Should create directory");
            Assert.That(File.Exists(Path.Combine(newDirectory, "index")), Is.True, "Should create index");
        }

        [Test]
        public async Task AddEntryAsync_ImageBinary_AddsToIndexAndCreatesFile()
        {
            // Arrange
            var entryKey = CreateTestEntryId("test-image");
            var imageBinary = TestData.CreateTestImageBinary(1.5);

            // Act
            await _imageVault.AddEntryAsync(entryKey, imageBinary);

            // Assert
            Assert.That(await _imageVault.HasIndexEntry(entryKey), Is.True, "Should add to index");
            
            var expectedFilePath = Path.Combine(TestDirectory, "test-image.png");
            AssertMediaFileExists(expectedFilePath, imageBinary.Buffer);
        }

        [Test]
        public async Task AddEntryAsync_MultipleImages_AddsAllToIndexAndCreatesFiles()
        {
            // Arrange
            var entries = new[]
            {
                (CreateTestEntryId("image1"), TestData.CreateTestImageBinary(1.0)),
                (CreateTestEntryId("image2"), TestData.CreateTestImageBinary(1.5)),
                (CreateTestEntryId("image3"), TestData.CreateTestImageBinary(2.0))
            };

            // Act
            foreach (var (key, binary) in entries)
            {
                await _imageVault.AddEntryAsync(key, binary);
            }

            // Assert
            Assert.That(_imageVault.GetIndexSize(), Is.EqualTo(3), "Should have three entries in index");
            
            foreach (var (key, binary) in entries)
            {
                Assert.That(await _imageVault.HasIndexEntry(key), Is.True, $"Should contain {key} in index");
                var expectedFilePath = Path.Combine(TestDirectory, $"{key}.png");
                AssertMediaFileExists(expectedFilePath, binary.Buffer);
            }
        }

        [Test]
        public async Task GetEntryAsync_ExistingImage_ReturnsImageBinary()
        {
            // Arrange
            var entryKey = CreateTestEntryId("existing-image");
            var originalImage = TestData.CreateTestImageBinary(1.77);
            await _imageVault.AddEntryAsync(entryKey, originalImage);

            // Act
            var retrievedImage = await _imageVault.GetEntryAsync<ImageBinary>(entryKey);

            // Assert
            Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image");
            Assert.That(retrievedImage!.Buffer, Is.EqualTo(originalImage.Buffer), "Buffer should match");
            Assert.That(retrievedImage.Extension, Is.EqualTo(originalImage.Extension), "Extension should match");
            Assert.That(retrievedImage.AspectRatio, Is.EqualTo(originalImage.AspectRatio), "Aspect ratio should match");
        }

        [Test]
        public async Task GetEntryAsync_NonExistentImage_ReturnsNull()
        {
            // Arrange
            var nonExistentKey = CreateTestEntryId("non-existent");

            // Act
            var retrievedImage = await _imageVault.GetEntryAsync<ImageBinary>(nonExistentKey);

            // Assert
            Assert.That(retrievedImage, Is.Null, "Should return null for non-existent image");
        }

        [Test]
        public async Task GetEntryAsync_IndexEntryExistsButFileDeleted_ReturnsNull()
        {
            // Arrange
            var entryKey = CreateTestEntryId("deleted-file");
            var imageBinary = TestData.CreateTestImageBinary(1.0);
            await _imageVault.AddEntryAsync(entryKey, imageBinary);

            // Delete the physical file but leave index entry
            var filePath = Path.Combine(TestDirectory, "deleted-file.png");
            File.Delete(filePath);

            // Act
            var retrievedImage = await _imageVault.GetEntryAsync<ImageBinary>(entryKey);

            // Assert
            Assert.That(retrievedImage, Is.Null, "Should return null when file is missing");
        }

        [Test]
        public async Task AddEntryAsync_DuplicateKey_UpdatesExistingEntry()
        {
            // Arrange
            var entryKey = CreateTestEntryId("duplicate-key");
            var originalImage = TestData.CreateTestImageBinary(1.0);
            var updatedImage = TestData.CreateTestImageBinary(2.0);

            // Act
            await _imageVault.AddEntryAsync(entryKey, originalImage);
            await _imageVault.AddEntryAsync(entryKey, updatedImage);

            // Assert
            Assert.That(_imageVault.GetIndexSize(), Is.EqualTo(1), "Should still have only one entry");
            
            var retrievedImage = await _imageVault.GetEntryAsync<ImageBinary>(entryKey);
            Assert.That(retrievedImage, Is.Not.Null, "Should retrieve updated image");
            Assert.That(retrievedImage!.AspectRatio, Is.EqualTo(2.0), "Should have updated aspect ratio");
        }
    }

    /// <summary>
    /// Tests for AudioVault - Single Responsibility (following same patterns as ImageVault)
    /// </summary>
    [TestFixture]
    public class AudioVaultTests : MediaVaultTestBase
    {
        private AudioVault _audioVault = null!;

        [SetUp]
        public async Task SetUpAsync()
        {
            base.SetUp(); // Call base synchronous setup first
            _audioVault = await AudioVault.FromAsync(TestDirectory);
            Assert.That(_audioVault, Is.Not.Null, "AudioVault should be created for tests");
        }

        [Test]
        public async Task AudioVault_FromAsync_CreatesVaultWithIndex()
        {
            // Act
            var vault = await AudioVault.FromAsync(TestDirectory);

            // Assert
            Assert.That(vault, Is.Not.Null, "Should create AudioVault");
            Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided directory");
            Assert.That(File.Exists(IndexPath), Is.True, "Should create index file");
        }

        [Test]
        public async Task AddEntryAsync_AudioBinary_AddsToIndexAndCreatesFile()
        {
            // Arrange
            var entryKey = CreateTestEntryId("test-audio");
            var audioBinary = TestData.CreateTestAudioBinary(120.0, 320);

            // Act
            await _audioVault.AddEntryAsync(entryKey, audioBinary);

            // Assert
            Assert.That(await _audioVault.HasIndexEntry(entryKey), Is.True, "Should add to index");
            
            var expectedFilePath = Path.Combine(TestDirectory, "test-audio.mp3");
            AssertMediaFileExists(expectedFilePath, audioBinary.Buffer);
        }

        [Test]
        public async Task GetEntryAsync_ExistingAudio_ReturnsAudioBinary()
        {
            // Arrange
            var entryKey = CreateTestEntryId("existing-audio");
            var originalAudio = TestData.CreateTestAudioBinary(180.5, 256);
            await _audioVault.AddEntryAsync(entryKey, originalAudio);

            // Act
            var retrievedAudio = await _audioVault.GetEntryAsync<AudioBinary>(entryKey);

            // Assert
            Assert.That(retrievedAudio, Is.Not.Null, "Should retrieve audio");
            Assert.That(retrievedAudio!.Buffer, Is.EqualTo(originalAudio.Buffer), "Buffer should match");
            Assert.That(retrievedAudio.Extension, Is.EqualTo(originalAudio.Extension), "Extension should match");
            Assert.That(retrievedAudio.Duration, Is.EqualTo(originalAudio.Duration), "Duration should match");
            Assert.That(retrievedAudio.Bitrate, Is.EqualTo(originalAudio.Bitrate), "Bitrate should match");
        }
    }

    /// <summary>
    /// Tests for MediaVault abstract base class behavior - Liskov Substitution Principle
    /// </summary>
    [TestFixture]
    public class MediaVaultBaseTests : MediaVaultTestBase
    {
        /// <summary>
        /// Test implementation of MediaVault for testing abstract functionality
        /// Uses ImageVault as concrete implementation to avoid creating test-specific vault
        /// </summary>
        private class TestMediaVaultWrapper
        {
            private readonly ImageVault _vault;

            public TestMediaVaultWrapper(ImageVault vault)
            {
                _vault = vault;
            }

            public static async Task<TestMediaVaultWrapper?> FromAsync(string rootPath)
            {
                var vault = await ImageVault.FromAsync(rootPath);
                return vault != null ? new TestMediaVaultWrapper(vault) : null;
            }

            // Expose protected methods for testing using reflection
            public string GetMediaKey(string entryKey, string extension)
            {
                var method = typeof(MediaVault).GetMethod("GetMediaKey", 
                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                return (string)method!.Invoke(_vault, new object[] { entryKey, extension })!;
            }

            public string GetMediaPathFromEntryKey(string entryKey, string extension)
            {
                var method = typeof(MediaVault).GetMethod("GetMediaPathFromEntryKey", 
                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                return (string)method!.Invoke(_vault, new object[] { entryKey, extension })!;
            }

            public string GetMediaPathFromMediaKey(string mediaKey)
            {
                var method = typeof(MediaVault).GetMethod("GetMediaPathFromMediaKey", 
                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                return (string)method!.Invoke(_vault, new object[] { mediaKey })!;
            }

            public Task<bool> HasIndexEntry(string entryId) => _vault.HasIndexEntry(entryId);
            public Task AddEntryAsync(string entryId, FileBinary media) => 
                _vault.AddEntryAsync(entryId, media);
        }

        [Test]
        public async Task GetMediaKey_NormalKey_SanitizesCorrectly()
        {
            // Arrange
            var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory);
            Assert.That(vault, Is.Not.Null, "Vault should be created");

            // Act & Assert - Test various sanitization scenarios
            Assert.That(vault!.GetMediaKey("normal-key", ".png"), Is.EqualTo("normal-key.png"), 
                "Normal key should pass through unchanged");
            
            Assert.That(vault.GetMediaKey("key with spaces", ".jpg"), Is.EqualTo("key-with-spaces.jpg"), 
                "Spaces should be replaced with dashes");
            
            Assert.That(vault.GetMediaKey("key@#$%special", ".gif"), Is.EqualTo("key----special.gif"), 
                "Special characters should be replaced with dashes");
            
            Assert.That(vault.GetMediaKey("key123ABC", ".png"), Is.EqualTo("key123ABC.png"), 
                "Alphanumeric characters should be preserved");
        }

        [Test]
        public async Task GetMediaPathFromEntryKey_ValidInputs_ReturnsCorrectPath()
        {
            // Arrange
            var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory);
            Assert.That(vault, Is.Not.Null, "Vault should be created");

            // Act
            var path = vault!.GetMediaPathFromEntryKey("test-key", ".png");

            // Assert
            var expectedPath = Path.Combine(TestDirectory, "test-key.png");
            Assert.That(path, Is.EqualTo(expectedPath), "Should combine directory and sanitized filename");
        }

        [Test]
        public async Task GetMediaPathFromMediaKey_ValidKey_ReturnsCorrectPath()
        {
            // Arrange
            var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory);
            Assert.That(vault, Is.Not.Null, "Vault should be created");

            // Act
            var path = vault!.GetMediaPathFromMediaKey("media-file.png");

            // Assert
            var expectedPath = Path.Combine(TestDirectory, "media-file.png");
            Assert.That(path, Is.EqualTo(expectedPath), "Should combine directory and media key");
        }

        [Test]
        public async Task AddEntryAsync_BaseFileBinary_ThrowsArgumentException()
        {
            // Arrange
            var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory);
            Assert.That(vault, Is.Not.Null, "Vault should be created");
            
            var entryKey = CreateTestEntryId("test");
            var baseFileBinary = new FileBinary(new FileBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length)); // Base FileBinary, not a specific media type

            // Act & Assert
            Assert.ThrowsAsync<ArgumentException>(async () =>
                await vault!.AddEntryAsync(entryKey, baseFileBinary),
                "Should throw for base FileBinary type - must be specific media type");
        }

        [Test]
        public async Task AddEntryAsync_ValidMedia_UpdatesIndexAndCreatesFile()
        {
            // Arrange
            var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory);
            Assert.That(vault, Is.Not.Null, "Vault should be created");
            
            var entryKey = CreateTestEntryId("test-media");
            var imageBinary = TestData.CreateTestImageBinary(1.0); // Use existing test data helper

            // Act
            await vault!.AddEntryAsync(entryKey, imageBinary);

            // Assert
            Assert.That(await vault.HasIndexEntry(entryKey), Is.True, "Should add entry to index");
            
            var expectedFilePath = Path.Combine(TestDirectory, "test-media.png");
            AssertMediaFileExists(expectedFilePath, imageBinary.Buffer);
        }
    }

    /// <summary>
    /// Tests for MediaVault error handling and edge cases - Interface Segregation
    /// </summary>
    [TestFixture]
    public class MediaVaultErrorHandlingTests : MediaVaultTestBase
    {
        [Test]
        public async Task ImageVault_FromAsync_CorruptedIndexFile_RecreatesIndex()
        {
            // Arrange - Create a corrupted index file
            var indexPath = Path.Combine(TestDirectory, "index");
            await File.WriteAllTextAsync(indexPath, "{ corrupted json }");

            // Act - Should handle corruption gracefully by recreating
            var vault = await ImageVault.FromAsync(TestDirectory);

            // Assert
            Assert.That(vault, Is.Not.Null, "Should create vault even with corrupted index");
            Assert.That(vault!.GetIndexSize(), Is.EqualTo(0), "Should have empty index after recreation");
        }

        [Test]
        public async Task GetEntryAsync_CorruptedMediaFile_HandlesGracefully()
        {
            // Arrange
            var vault = await ImageVault.FromAsync(TestDirectory);
            var entryKey = CreateTestEntryId("corrupted-file");
            var imageBinary = TestData.CreateTestImageBinary(1.0);
            
            await vault!.AddEntryAsync(entryKey, imageBinary);
            
            // Corrupt the media file
            var filePath = Path.Combine(TestDirectory, "corrupted-file.png");
            await File.WriteAllTextAsync(filePath, "corrupted data");

            // Act & Assert - Should not throw, but behavior may vary
            Assert.DoesNotThrowAsync(async () =>
            {
                await vault.GetEntryAsync<ImageBinary>(entryKey);
            }, "Should handle corrupted files gracefully");
        }

        [Test]
        public async Task AddEntryAsync_DiskSpaceIssue_HandlesGracefully()
        {
            // This test is difficult to simulate reliably across platforms
            // Instead, we test with very large buffers that might cause issues
            
            // Arrange
            var vault = await ImageVault.FromAsync(TestDirectory);
            var entryKey = CreateTestEntryId("large-file");
            
            // Create a reasonably large buffer (not too large to cause test issues)
            var largeBuffer = new byte[1024 * 1024]; // 1MB
            Array.Fill<byte>(largeBuffer, 0xFF);
            
            var largeBinary = new ImageBinary(new ImageBinaryParams(largeBuffer, largeBuffer.Length, ".png", 1.0));

            // Act & Assert - Should not throw exceptions
            Assert.DoesNotThrowAsync(async () =>
            {
                await vault!.AddEntryAsync(entryKey, largeBinary);
            }, "Should handle large files gracefully");
        }

        [Test]
        public async Task GetEntryAsync_ConcurrentAccess_HandlesGracefully()
        {
            // Arrange
            var vault = await ImageVault.FromAsync(TestDirectory);
            var entryKey = CreateTestEntryId("concurrent-test");
            var imageBinary = TestData.CreateTestImageBinary(1.0);
            
            await vault!.AddEntryAsync(entryKey, imageBinary);

            // Act - Multiple concurrent reads
            var tasks = new List<Task<ImageBinary?>>();
            for (int i = 0; i < 10; i++)
            {
                tasks.Add(vault.GetEntryAsync<ImageBinary>(entryKey));
            }

            var results = await Task.WhenAll(tasks);

            // Assert
            Assert.That(results.Length, Is.EqualTo(10), "Should complete all concurrent reads");
            foreach (var result in results)
            {
                Assert.That(result, Is.Not.Null, "Each concurrent read should succeed");
                Assert.That(result!.Buffer, Is.EqualTo(imageBinary.Buffer), "Each result should have correct data");
            }
        }
    }

    /// <summary>
    /// Integration tests for MediaVault with FileDatabase - Dependency Inversion
    /// </summary>
    [TestFixture]
    public class MediaVaultIntegrationTests : MediaVaultTestBase
    {
        [Test]
        public async Task MediaVault_IntegratesWithFileDatabase_WorksEndToEnd()
        {
            // This test verifies that MediaVault works correctly when used through FileDatabase
            
            // Arrange
            var database = await FileDatabase.FromAsync(TestDirectory);
            var vaultKey = "test-vault";
            var entryKey = "test-image";
            var imageBinary = TestData.CreateTestImageBinary(1.5);

            // Act
            await database!.CreateVaultAsync(vaultKey, MediaVaultType.Image);
            await database.RegisterResourceAsync(vaultKey, entryKey, imageBinary);
            var retrievedImage = await database.LoadResourceAsync<ImageBinary>(vaultKey, entryKey);

            // Assert
            Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image through database");
            Assert.That(retrievedImage!.Buffer, Is.EqualTo(imageBinary.Buffer), "Retrieved data should match original");
            Assert.That(retrievedImage.AspectRatio, Is.EqualTo(imageBinary.AspectRatio), "Metadata should be preserved");

            // Verify vault was created correctly
            var vault = database.GetVault(vaultKey);
            Assert.That(vault, Is.Not.Null, "Vault should exist in database");
            Assert.That(vault, Is.TypeOf<ImageVault>(), "Should be ImageVault type");
        }

        [Test]
        public async Task MediaVault_PersistenceAcrossRestarts_MaintainsData()
        {
            // Test that vault data persists when database is reloaded
            
            // Arrange - Create and populate vault
            var database1 = await FileDatabase.FromAsync(TestDirectory);
            var vaultKey = "persistent-vault";
            var entryKey = "persistent-image";
            var imageBinary = TestData.CreateTestImageBinary(2.0);

            await database1!.CreateVaultAsync(vaultKey, MediaVaultType.Image);
            await database1.RegisterResourceAsync(vaultKey, entryKey, imageBinary);

            // Act - Reload database
            var database2 = await FileDatabase.FromAsync(TestDirectory);
            var retrievedImage = await database2!.LoadResourceAsync<ImageBinary>(vaultKey, entryKey);

            // Assert
            Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image after database reload");
            Assert.That(retrievedImage!.Buffer, Is.EqualTo(imageBinary.Buffer), "Data should persist across restarts");
            Assert.That(retrievedImage.AspectRatio, Is.EqualTo(imageBinary.AspectRatio), "Metadata should persist");
        }
    }
}
