using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.Xz;
using SharpCompress.Crypto;
using SharpCompress.Test.Mocks;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;

namespace SharpCompress.Test.Zip;

public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests
{
    public ZipTypesLevelsWithCrcRatioAsyncTests() => UseExtensionInsteadOfNameToVerify = true;

    [Theory]
    [InlineData(CompressionType.Deflate, 1, 1, 0.11f)] // was 0.8f, actual 0.104
    [InlineData(CompressionType.Deflate, 3, 1, 0.08f)] // was 0.8f, actual 0.078
    [InlineData(CompressionType.Deflate, 6, 1, 0.05f)] // was 0.8f, actual ~0.042
    [InlineData(CompressionType.Deflate, 9, 1, 0.04f)] // was 0.7f, actual 0.038
    [InlineData(CompressionType.ZStandard, 1, 1, 0.025f)] // was 0.8f, actual 0.023
    [InlineData(CompressionType.ZStandard, 3, 1, 0.015f)] // was 0.7f, actual 0.013
    [InlineData(CompressionType.ZStandard, 9, 1, 0.006f)] // was 0.7f, actual 0.005
    [InlineData(CompressionType.ZStandard, 22, 1, 0.005f)] // was 0.7f, actual 0.004
    [InlineData(CompressionType.BZip2, 0, 1, 0.035f)] // was 0.8f, actual 0.033
    [InlineData(CompressionType.LZMA, 0, 1, 0.005f)] // was 0.8f, actual 0.004
    [InlineData(CompressionType.None, 0, 1, 1.001f)] // was 1.1f, actual 1.000
    [InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8f, actual 0.042
    [InlineData(CompressionType.ZStandard, 3, 2, 0.012f)] // was 0.7f, actual 0.010
    [InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8f, actual 0.032
    [InlineData(CompressionType.Deflate, 9, 3, 0.04f)] // was 0.7f, actual 0.038
    [InlineData(CompressionType.ZStandard, 9, 3, 0.003f)] // was 0.7f, actual 0.002
    public async ValueTask Zip_Create_Archive_With_3_Files_Crc32_Test_Async(
        CompressionType compressionType,
        int compressionLevel,
        int sizeMb,
        float expectedRatio
    )
    {
        const int OneMiB = 1024 * 1024;
        var baseSize = sizeMb * OneMiB;

        // Generate test content for files with sizes based on the sizeMb parameter
        var file1Data = TestPseudoTextStream.Create(baseSize);
        var file2Data = TestPseudoTextStream.Create(baseSize * 2);
        var file3Data = TestPseudoTextStream.Create(baseSize * 3);

        var expectedFiles = new Dictionary<string, (byte[] data, uint crc)>
        {
            [$"file1_{sizeMb}MiB.txt"] = (file1Data, CalculateCrc32(file1Data)),
            [$"data/file2_{sizeMb * 2}MiB.txt"] = (file2Data, CalculateCrc32(file2Data)),
            [$"deep/nested/file3_{sizeMb * 3}MiB.txt"] = (file3Data, CalculateCrc32(file3Data)),
        };

        // Create zip archive in memory
        using var zipStream = new MemoryStream();
        await using (
            var writer = await CreateWriterWithLevelAsync(
                zipStream,
                compressionType,
                compressionLevel
            )
        )
        {
            await writer.WriteAsync($"file1_{sizeMb}MiB.txt", new MemoryStream(file1Data));
            await writer.WriteAsync($"data/file2_{sizeMb * 2}MiB.txt", new MemoryStream(file2Data));
            await writer.WriteAsync(
                $"deep/nested/file3_{sizeMb * 3}MiB.txt",
                new MemoryStream(file3Data)
            );
        }

        // Calculate and output actual compression ratio
        var originalSize = file1Data.Length + file2Data.Length + file3Data.Length;
        var actualRatio = (double)zipStream.Length / originalSize;

        // Verify compression occurred (except for None compression type)
        if (compressionType != CompressionType.None)
        {
            Assert.True(
                zipStream.Length < originalSize,
                $"Compression failed: compressed={zipStream.Length}, original={originalSize}"
            );
        }

        // Verify compression ratio
        VerifyCompressionRatio(
            originalSize,
            zipStream.Length,
            expectedRatio,
            $"{compressionType} level {compressionLevel}"
        );

        // Verify archive content and CRC32
        await VerifyArchiveContentAsync(zipStream, expectedFiles);

        // Verify compression type is correctly set
        VerifyCompressionType(zipStream, compressionType);
    }

    [Theory]
    [InlineData(CompressionType.Deflate, 1, 4, 0.11f)] // was 0.8, actual 0.105
    [InlineData(CompressionType.Deflate, 3, 4, 0.08f)] // was 0.8, actual 0.077
    [InlineData(CompressionType.Deflate, 6, 4, 0.045f)] // was 0.8, actual 0.042
    [InlineData(CompressionType.Deflate, 9, 4, 0.04f)] // was 0.8, actual 0.037
    [InlineData(CompressionType.ZStandard, 1, 4, 0.025f)] // was 0.8, actual 0.022
    [InlineData(CompressionType.ZStandard, 3, 4, 0.012f)] // was 0.8, actual 0.010
    [InlineData(CompressionType.ZStandard, 9, 4, 0.003f)] // was 0.8, actual 0.002
    [InlineData(CompressionType.ZStandard, 22, 4, 0.003f)] // was 0.8, actual 0.002
    [InlineData(CompressionType.BZip2, 0, 4, 0.035f)] // was 0.8, actual 0.032
    [InlineData(CompressionType.LZMA, 0, 4, 0.003f)] // was 0.8, actual 0.002
    public async ValueTask Zip_WriterFactory_Crc32_Test_Async(
        CompressionType compressionType,
        int compressionLevel,
        int sizeMb,
        float expectedRatio
    )
    {
        var fileSize = sizeMb * 1024 * 1024;

        var testData = TestPseudoTextStream.Create(fileSize);
        var expectedCrc = CalculateCrc32(testData);

        // Create archive with specified compression level
        using var zipStream = new MemoryStream();
        var writerOptions = new ZipWriterOptions(compressionType)
        {
            CompressionLevel = compressionLevel,
        };

        await using (
            var writer = await WriterFactory.OpenAsyncWriter(
                new AsyncOnlyStream(zipStream),
                ArchiveType.Zip,
                writerOptions
            )
        )
        {
            await writer.WriteAsync(
                $"{compressionType}_level_{compressionLevel}_{sizeMb}MiB.txt",
                new MemoryStream(testData)
            );
        }

        // Calculate and output actual compression ratio
        var actualRatio = (double)zipStream.Length / testData.Length;

        VerifyCompressionRatio(
            testData.Length,
            zipStream.Length,
            expectedRatio,
            $"{compressionType} level {compressionLevel}"
        );

        // Verify the archive
        zipStream.Position = 0;
        using var archive = ZipArchive.OpenArchive(zipStream);

        var entry = archive.Entries.Single(e => !e.IsDirectory);
        using var entryStream = await entry.OpenEntryStreamAsync();
        using var extractedStream = new MemoryStream();
        await entryStream.CopyToAsync(extractedStream);

        var extractedData = extractedStream.ToArray();
        var actualCrc = CalculateCrc32(extractedData);

        Assert.Equal(compressionType, entry.CompressionType);
        Assert.Equal(expectedCrc, actualCrc);
        Assert.Equal(testData.Length, extractedData.Length);
        Assert.Equal(testData, extractedData);
    }

    [Theory]
    [InlineData(CompressionType.Deflate, 1, 2, 0.11f)] // was 0.8, actual 0.104
    [InlineData(CompressionType.Deflate, 3, 2, 0.08f)] // was 0.8, actual 0.077
    [InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8, actual 0.042
    [InlineData(CompressionType.Deflate, 9, 2, 0.04f)] // was 0.7, actual 0.038
    [InlineData(CompressionType.ZStandard, 1, 2, 0.025f)] // was 0.8, actual 0.023
    [InlineData(CompressionType.ZStandard, 3, 2, 0.015f)] // was 0.7, actual 0.012
    [InlineData(CompressionType.ZStandard, 9, 2, 0.006f)] // was 0.7, actual 0.005
    [InlineData(CompressionType.ZStandard, 22, 2, 0.005f)] // was 0.7, actual 0.004
    [InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8, actual 0.032
    [InlineData(CompressionType.LZMA, 0, 2, 0.005f)] // was 0.8, actual 0.004
    public async ValueTask Zip_ZipArchiveOpen_Crc32_Test_Async(
        CompressionType compressionType,
        int compressionLevel,
        int sizeMb,
        float expectedRatio
    )
    {
        var fileSize = sizeMb * 1024 * 1024;

        var testData = TestPseudoTextStream.Create(fileSize);
        var expectedCrc = CalculateCrc32(testData);

        // Create archive with specified compression and level
        using var zipStream = new MemoryStream();
        await using (
            var writer = await CreateWriterWithLevelAsync(
                zipStream,
                compressionType,
                compressionLevel
            )
        )
        {
            await writer.WriteAsync(
                $"{compressionType}_{compressionLevel}_{sizeMb}MiB.txt",
                new MemoryStream(testData)
            );
        }

        // Calculate and output actual compression ratio
        var actualRatio = (double)zipStream.Length / testData.Length;

        // Verify the archive
        zipStream.Position = 0;
        using var archive = ZipArchive.OpenArchive(zipStream);

        var entry = archive.Entries.Single(e => !e.IsDirectory);
        using var entryStream = await entry.OpenEntryStreamAsync();
        using var extractedStream = new MemoryStream();
        await entryStream.CopyToAsync(extractedStream);

        var extractedData = extractedStream.ToArray();
        var actualCrc = CalculateCrc32(extractedData);

        Assert.Equal(compressionType, entry.CompressionType);
        Assert.Equal(expectedCrc, actualCrc);
        Assert.Equal(testData.Length, extractedData.Length);

        // For smaller files, verify full content; for larger, spot check
        if (testData.Length <= sizeMb * 2)
        {
            Assert.Equal(testData, extractedData);
        }
        else
        {
            VerifyDataSpotCheck(testData, extractedData);
        }

        VerifyCompressionRatio(
            testData.Length,
            zipStream.Length,
            expectedRatio,
            $"{compressionType} Level {compressionLevel}"
        );
    }

    // Helper method for async archive content verification
    private async ValueTask VerifyArchiveContentAsync(
        MemoryStream zipStream,
        Dictionary<string, (byte[] data, uint crc)> expectedFiles
    )
    {
        zipStream.Position = 0;
        using var archive = ZipArchive.OpenArchive(zipStream);

        foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
        {
            Assert.True(
                expectedFiles.ContainsKey(entry.Key!),
                $"Unexpected file in archive: {entry.Key}"
            );

            var expected = expectedFiles[entry.Key!];
            using var entryStream = await entry.OpenEntryStreamAsync();
            using var extractedStream = new MemoryStream();
            await entryStream.CopyToAsync(extractedStream);

            var extractedData = extractedStream.ToArray();
            var actualCrc = CalculateCrc32(extractedData);

            Assert.Equal(expected.crc, actualCrc);
            Assert.Equal(expected.data.Length, extractedData.Length);

            // For larger files, just spot check, for smaller verify full content
            var expectedData = expected.data;
            if (expectedData.Length <= 2 * 1024 * 1024)
            {
                Assert.Equal(expectedData, extractedData);
            }
            else
            {
                VerifyDataSpotCheck(expectedData, extractedData);
            }
        }

        Assert.Equal(expectedFiles.Count, archive.Entries.Count(e => !e.IsDirectory));
    }
}
