using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Rar;
using SharpCompress.Test.Mocks;
using Xunit;

namespace SharpCompress.Test.Rar;

public class RarReaderAsyncTests : ReaderTests
{
    [Theory]
    [InlineData("Rar15.rar")]
    [InlineData("Rar.rar")]
    [InlineData("Rar.Audio_program.rar")]
    [InlineData("Rar5.rar")]
    [InlineData("Rar5.solid.rar")]
    public async ValueTask Rar_Reader_Async_Uses_Only_Async_Stream_Operations(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        await using var reader = await ReaderFactory.OpenAsyncReader(
            new AsyncOnlyStream(stream),
            new ReaderOptions { LookForHeader = true }
        );

        while (await reader.MoveToNextEntryAsync())
        {
            if (!reader.Entry.IsDirectory)
            {
                using var output = new AsyncOnlyStream(new MemoryStream());
                await reader.WriteEntryToAsync(output);
            }
        }
    }

    [Fact]
    public async ValueTask Rar_Multi_Reader_Async() =>
        await DoRar_Multi_Reader_Async([
            "Rar.multi.part01.rar",
            "Rar.multi.part02.rar",
            "Rar.multi.part03.rar",
            "Rar.multi.part04.rar",
            "Rar.multi.part05.rar",
            "Rar.multi.part06.rar",
        ]);

    [Fact]
    public async ValueTask Rar5_Multi_Reader_Async() =>
        await DoRar_Multi_Reader_Async([
            "Rar5.multi.part01.rar",
            "Rar5.multi.part02.rar",
            "Rar5.multi.part03.rar",
            "Rar5.multi.part04.rar",
            "Rar5.multi.part05.rar",
            "Rar5.multi.part06.rar",
        ]);

    private async ValueTask DoRar_Multi_Reader_Async(string[] archives)
    {
        using (
            IReader baseReader = RarReader.OpenReader(
                archives
                    .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
                    .Select(p => File.OpenRead(p))
            )
        )
        {
            IAsyncReader reader = (IAsyncReader)baseReader;
            while (await reader.MoveToNextEntryAsync())
            {
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public async ValueTask Rar_Multi_Reader_Encrypted_Async() =>
        await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
        {
            string[] archives =
            [
                "Rar.EncryptedParts.part01.rar",
                "Rar.EncryptedParts.part02.rar",
                "Rar.EncryptedParts.part03.rar",
                "Rar.EncryptedParts.part04.rar",
                "Rar.EncryptedParts.part05.rar",
                "Rar.EncryptedParts.part06.rar",
            ];
            using (
                IReader baseReader = RarReader.OpenReader(
                    archives
                        .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
                        .Select(p => File.OpenRead(p)),
                    ReaderOptions.ForExternalStream with
                    {
                        Password = "test",
                    }
                )
            )
            {
                IAsyncReader reader = (IAsyncReader)baseReader;
                while (await reader.MoveToNextEntryAsync())
                {
                    await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
                }
            }
            VerifyFiles();
        });

    [Fact]
    public async ValueTask Rar_Multi_Reader_Delete_Files_Async() =>
        await DoRar_Multi_Reader_Delete_Files_Async([
            "Rar.multi.part01.rar",
            "Rar.multi.part02.rar",
            "Rar.multi.part03.rar",
            "Rar.multi.part04.rar",
            "Rar.multi.part05.rar",
            "Rar.multi.part06.rar",
        ]);

    [Fact]
    public async ValueTask Rar5_Multi_Reader_Delete_Files_Async() =>
        await DoRar_Multi_Reader_Delete_Files_Async([
            "Rar5.multi.part01.rar",
            "Rar5.multi.part02.rar",
            "Rar5.multi.part03.rar",
            "Rar5.multi.part04.rar",
            "Rar5.multi.part05.rar",
            "Rar5.multi.part06.rar",
        ]);

    private async ValueTask DoRar_Multi_Reader_Delete_Files_Async(string[] archives)
    {
        foreach (var file in archives)
        {
            File.Copy(
                Path.Combine(TEST_ARCHIVES_PATH, file),
                Path.Combine(SCRATCH2_FILES_PATH, file)
            );
        }
        var streams = archives
            .Select(s => Path.Combine(SCRATCH2_FILES_PATH, s))
            .Select(File.OpenRead)
            .ToList();
        using (IReader baseReader = RarReader.OpenReader(streams))
        {
            IAsyncReader reader = (IAsyncReader)baseReader;
            while (await reader.MoveToNextEntryAsync())
            {
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
        foreach (var stream in streams)
        {
            stream.Dispose();
        }
        VerifyFiles();

        foreach (var file in archives.Select(s => Path.Combine(SCRATCH2_FILES_PATH, s)))
        {
            File.Delete(file);
        }
    }

    [Fact]
    public async ValueTask Rar_None_Reader_Async() =>
        await ReadAsync("Rar.none.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar5_None_Reader_Async() =>
        await ReadAsync("Rar5.none.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar_Reader_Async() => await ReadAsync("Rar.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar5_Reader_Async() => await ReadAsync("Rar5.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar5_CRC_Blake2_Reader_Async() =>
        await ReadAsync("Rar5.crc_blake2.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar_EncryptedFileAndHeader_Reader_Async() =>
        await ReadRar_Async("Rar.encrypted_filesAndHeader.rar", "test");

    [Fact]
    public async ValueTask Rar5_EncryptedFileAndHeader_Reader_Async() =>
        await ReadRar_Async("Rar5.encrypted_filesAndHeader.rar", "test");

    [Fact]
    public async ValueTask Rar_EncryptedFileOnly_Reader_Async() =>
        await ReadRar_Async("Rar.encrypted_filesOnly.rar", "test");

    [Fact]
    public async ValueTask Rar5_EncryptedFileOnly_Reader_Async() =>
        await ReadRar_Async("Rar5.encrypted_filesOnly.rar", "test");

    [Fact]
    public async ValueTask Rar_Encrypted_Reader_Async() =>
        await ReadRar_Async("Rar.Encrypted.rar", "test");

    [Fact]
    public async ValueTask Rar5_Encrypted_Reader_Async() =>
        await ReadRar_Async("Rar5.encrypted_filesOnly.rar", "test");

    private async ValueTask ReadRar_Async(string testArchive, string password) =>
        await ReadAsync(
            testArchive,
            CompressionType.Rar,
            ReaderOptions.ForFilePath with
            {
                Password = password,
            }
        );

    [Fact]
    public async ValueTask Rar_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar.rar");

    [Fact]
    public async ValueTask Rar5_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar5.rar");

    private async ValueTask DoRar_Entry_Stream_Async(string filename)
    {
        using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)))
        await using (var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream)))
        {
            while (await reader.MoveToNextEntryAsync())
            {
                if (!reader.Entry.IsDirectory)
                {
                    Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType);
                    var entryStream = await reader.OpenEntryStreamAsync();
                    try
                    {
                        var file = Path.GetFileName(reader.Entry.Key).NotNull();
                        var folder =
                            Path.GetDirectoryName(reader.Entry.Key)
                            ?? throw new InvalidOperationException(
                                "Entry key must have a directory name."
                            );
                        var destdir = Path.Combine(SCRATCH_FILES_PATH, folder);
                        if (!Directory.Exists(destdir))
                        {
                            Directory.CreateDirectory(destdir);
                        }
                        var destinationFileName = Path.Combine(destdir, file);

                        using var fs = File.OpenWrite(destinationFileName);
                        await entryStream.CopyToAsync(fs);
                    }
                    finally
                    {
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
                        await entryStream.DisposeAsync();
#else
                        entryStream.Dispose();
#endif
                    }
                }
            }
        }
        VerifyFiles();
    }

    [Fact]
    public async ValueTask Rar_Reader_Audio_program_Async()
    {
        using (
            var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.Audio_program.rar"))
        )
        await using (
            var reader = await ReaderFactory.OpenAsyncReader(
                new AsyncOnlyStream(stream),
                ReaderOptions.ForExternalStream with
                {
                    LookForHeader = true,
                }
            )
        )
        {
            while (await reader.MoveToNextEntryAsync())
            {
                Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType);
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
        CompareFilesByPath(
            Path.Combine(SCRATCH_FILES_PATH, "test.dat"),
            Path.Combine(MISC_TEST_FILES_PATH, "test.dat")
        );
    }

    [Fact]
    public async ValueTask Rar_Jpg_Reader_Async()
    {
        using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg")))
        using (
            IReader baseReader = RarReader.OpenReader(
                stream,
                ReaderOptions.ForExternalStream with
                {
                    LookForHeader = true,
                }
            )
        )
        {
            IAsyncReader reader = (IAsyncReader)baseReader;
            while (await reader.MoveToNextEntryAsync())
            {
                Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType);
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public async ValueTask Rar_Solid_Reader_Async() =>
        await ReadAsync("Rar.solid.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar_Comment_Reader_Async() =>
        await ReadAsync("Rar.comment.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar5_Comment_Reader_Async() =>
        await ReadAsync("Rar5.comment.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar5_Solid_Reader_Async() =>
        await ReadAsync("Rar5.solid.rar", CompressionType.Rar);

    [Fact]
    public async ValueTask Rar_Solid_Skip_Reader_Async() =>
        await DoRar_Solid_Skip_Reader_Async("Rar.solid.rar");

    [Fact]
    public async ValueTask Rar5_Solid_Skip_Reader_Async() =>
        await DoRar_Solid_Skip_Reader_Async("Rar5.solid.rar");

    private async ValueTask DoRar_Solid_Skip_Reader_Async(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        await using var reader = await ReaderFactory.OpenAsyncReader(
            new AsyncOnlyStream(stream),
            ReaderOptions.ForExternalStream with
            {
                LookForHeader = true,
            }
        );
        while (await reader.MoveToNextEntryAsync())
        {
            if (reader.Entry.Key.NotNull().Contains("jpg"))
            {
                Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType);
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
    }

    [Fact]
    public async ValueTask Rar_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar.rar");

    [Fact]
    public async ValueTask Rar5_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar5.rar");

    private async ValueTask DoRar_Reader_Skip_Async(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        await using var reader = await ReaderFactory.OpenAsyncReader(
            new AsyncOnlyStream(stream),
            ReaderOptions.ForExternalStream with
            {
                LookForHeader = true,
            }
        );
        while (await reader.MoveToNextEntryAsync())
        {
            if (reader.Entry.Key.NotNull().Contains("jpg"))
            {
                Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType);
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
    }

    private async ValueTask ReadAsync(
        string testArchive,
        CompressionType expectedCompression,
        ReaderOptions? readerOptions = null
    )
    {
        testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
        using Stream stream = File.OpenRead(testArchive);
        await using var reader = await ReaderFactory.OpenAsyncReader(
            new AsyncOnlyStream(stream),
            readerOptions ?? ReaderOptions.ForExternalStream
        );
        while (await reader.MoveToNextEntryAsync())
        {
            if (!reader.Entry.IsDirectory)
            {
                Assert.Equal(expectedCompression, reader.Entry.CompressionType);
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }
}
