using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar;
using SharpCompress.Factories;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;
using SharpCompress.Test.Mocks;
using SharpCompress.Writers.Tar;
using Xunit;

namespace SharpCompress.Test.Tar;

public class TarReaderAsyncTests : ReaderTests
{
    public TarReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;

    [Fact]
    public async ValueTask Tar_Reader_Async() => await ReadAsync("Tar.tar", CompressionType.None);

    [Fact]
    public async ValueTask Tar_Skip_Async()
    {
        using Stream stream = new ForwardOnlyStream(
            File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"))
        );
        await using var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream));
        var x = 0;
        while (await reader.MoveToNextEntryAsync())
        {
            if (!reader.Entry.IsDirectory)
            {
                x++;
                if (x % 2 == 0)
                {
                    await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
                }
            }
        }
    }

    [Fact]
    public async ValueTask Tar_Z_Reader_Async() =>
        await ReadAsync("Tar.tar.Z", CompressionType.Lzw);

    [Theory]
    [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, false)]
    [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, true)]
    [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, false)]
    [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, true)]
    public async ValueTask ReaderFactory_ExtensionHint_PreservesCompressedTarDetection_Async(
        string archiveName,
        string extensionHint,
        CompressionType compressionType,
        bool useForwardOnlyStream
    )
    {
        using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName));
        Stream source = useForwardOnlyStream ? new ForwardOnlyStream(file) : file;
        await using var stream = new AsyncOnlyStream(source);
        var options = ReaderOptions.ForExternalStream.WithExtensionHint(extensionHint);

        await using var reader = await ReaderFactory.OpenAsyncReader(stream, options);

        Assert.Equal(ArchiveType.Tar, reader.Type);
        var entryCount = 0;
        while (await reader.MoveToNextEntryAsync())
        {
            entryCount++;
            Assert.Equal(compressionType, reader.Entry.CompressionType);
            if (!reader.Entry.IsDirectory)
            {
                await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
            }
        }

        Assert.True(entryCount > 0);
        VerifyFiles();
    }

    [Fact]
    public async ValueTask Tar_Async_Assert() => await AssertArchiveAsync("Tar.tar");

    [Fact]
    public async ValueTask Tar_BZip2_Reader_Async() =>
        await ReadAsync("Tar.tar.bz2", CompressionType.BZip2);

    [Fact]
    public async ValueTask Tar_GZip_Reader_Async() =>
        await ReadAsync("Tar.tar.gz", CompressionType.GZip);

    [Fact]
    public async ValueTask Tar_ZStandard_Reader_Async() =>
        await ReadAsync("Tar.tar.zst", CompressionType.ZStandard);

    [Fact]
    public async ValueTask Tar_LZip_Reader_Async() =>
        await ReadAsync("Tar.tar.lz", CompressionType.LZip);

    [Fact]
    public async ValueTask Tar_Xz_Reader_Async() =>
        await ReadAsync("Tar.tar.xz", CompressionType.Xz);

    [Fact]
    public async ValueTask Tar_GZip_OldGnu_Reader_Async() =>
        await ReadAsync("Tar.oldgnu.tar.gz", CompressionType.GZip);

    [Fact]
    public async ValueTask Tar_BZip2_Entry_Stream_Async()
    {
        using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"));
        await using var reader = await TarReader.OpenAsyncReader(stream);
        while (await reader.MoveToNextEntryAsync())
        {
            if (!reader.Entry.IsDirectory)
            {
                Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
                using var entryStream = await reader.OpenEntryStreamAsync();
                var file = Path.GetFileName(reader.Entry.Key);
                var folder =
                    Path.GetDirectoryName(reader.Entry.Key) ?? throw new ArgumentNullException();
                var destdir = Path.Combine(SCRATCH_FILES_PATH, folder);
                if (!Directory.Exists(destdir))
                {
                    Directory.CreateDirectory(destdir);
                }
                var destinationFileName = Path.Combine(destdir, file.NotNull());

                using var fs = File.OpenWrite(destinationFileName);
                await entryStream.CopyToAsync(fs);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Tar_LongNamesWithLongNameExtension_Async()
    {
        var filePaths = new List<string>();

        using (
            Stream stream = File.OpenRead(
                Path.Combine(TEST_ARCHIVES_PATH, "Tar.LongPathsWithLongNameExtension.tar")
            )
        )
        using (var reader = TarReader.OpenReader(stream))
        {
            while (reader.MoveToNextEntry())
            {
                if (!reader.Entry.IsDirectory)
                {
                    filePaths.Add(reader.Entry.Key.NotNull("Entry Key is null"));
                }
            }
        }

        Assert.Equal(3, filePaths.Count);
        Assert.Contains("a.txt", filePaths);
        Assert.Contains(
            "wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php",
            filePaths
        );
        Assert.Contains(
            "wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Foo.php",
            filePaths
        );
    }

    [Fact]
    public async ValueTask Tar_BZip2_Skip_Entry_Stream_Async()
    {
        using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"));
        await using var reader = await TarReader.OpenAsyncReader(stream);
        var names = new List<string>();
        while (await reader.MoveToNextEntryAsync())
        {
            if (!reader.Entry.IsDirectory)
            {
                Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
                using var entryStream = await reader.OpenEntryStreamAsync();
                await entryStream.SkipEntryAsync();
                names.Add(reader.Entry.Key.NotNull());
            }
        }
        Assert.Equal(3, names.Count);
    }

    [Fact]
    public async ValueTask Tar_PaxLocalHeader_Reader_Async()
    {
        var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxLocalHeader.tar");

        using Stream stream = File.OpenRead(archivePath);
        await using var reader = await TarReader.OpenAsyncReader(stream);

        Assert.True(await reader.MoveToNextEntryAsync());
        var firstEntry = (TarEntry)reader.Entry;
        Assert.Equal("pax/overridden-name.txt", firstEntry.Key);
        Assert.Equal(10, firstEntry.Size);
        Assert.Equal(1234, firstEntry.UserID);
        Assert.Equal(2345, firstEntry.GroupId);
        Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);

        var expectedTime = DateTimeOffset.FromUnixTimeSeconds(1700000000).LocalDateTime;
        Assert.Equal(expectedTime, firstEntry.LastModifiedTime);

        using (var entryStream = await reader.OpenEntryStreamAsync())
        using (var memoryStream = new MemoryStream())
        {
            await entryStream.CopyToAsync(memoryStream);
            Assert.Equal(10, memoryStream.Length);
        }

        Assert.True(await reader.MoveToNextEntryAsync());
        var secondEntry = (TarEntry)reader.Entry;
        Assert.Equal("second.txt", secondEntry.Key);
        Assert.Equal(11, secondEntry.UserID);
        Assert.Equal(22, secondEntry.GroupId);
        Assert.Equal(Convert.ToInt64("644", 8), secondEntry.Mode);
        Assert.Equal(2, secondEntry.Size);

        Assert.False(await reader.MoveToNextEntryAsync());
    }

    [Fact]
    public async ValueTask Tar_PaxLocalHeader_Link_Reader_Async()
    {
        var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxLocalHeader.Link.tar");

        using Stream stream = File.OpenRead(archivePath);
        await using var reader = await TarReader.OpenAsyncReader(stream);

        Assert.True(await reader.MoveToNextEntryAsync());
        Assert.Equal("pax/link-entry", reader.Entry.Key);
        Assert.Equal("pax/target-entry", reader.Entry.LinkTarget);
        Assert.False(reader.Entry.IsDirectory);
        Assert.False(await reader.MoveToNextEntryAsync());
    }

    [Fact]
    public async ValueTask Tar_PaxGlobalHeader_Reader_Async()
    {
        var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.tar");

        using Stream stream = File.OpenRead(archivePath);
        await using var reader = await TarReader.OpenAsyncReader(stream);

        var globalTime = DateTimeOffset.FromUnixTimeSeconds(1700000100).LocalDateTime;
        var localOverrideTime = DateTimeOffset.FromUnixTimeSeconds(1700000200).LocalDateTime;

        Assert.True(await reader.MoveToNextEntryAsync());
        var firstEntry = (TarEntry)reader.Entry;
        Assert.Equal("global-one.txt", firstEntry.Key);
        Assert.Equal(4000, firstEntry.UserID);
        Assert.Equal(5000, firstEntry.GroupId);
        Assert.Equal(Convert.ToInt64("640", 8), firstEntry.Mode);
        Assert.Equal(globalTime, firstEntry.LastModifiedTime);

        Assert.True(await reader.MoveToNextEntryAsync());
        var secondEntry = (TarEntry)reader.Entry;
        Assert.Equal("global-local-override.txt", secondEntry.Key);
        Assert.Equal(4010, secondEntry.UserID);
        Assert.Equal(5010, secondEntry.GroupId);
        Assert.Equal(Convert.ToInt64("600", 8), secondEntry.Mode);
        Assert.Equal(localOverrideTime, secondEntry.LastModifiedTime);

        Assert.True(await reader.MoveToNextEntryAsync());
        var thirdEntry = (TarEntry)reader.Entry;
        Assert.Equal("global-three.txt", thirdEntry.Key);
        Assert.Equal(4000, thirdEntry.UserID);
        Assert.Equal(5000, thirdEntry.GroupId);
        Assert.Equal(Convert.ToInt64("640", 8), thirdEntry.Mode);
        Assert.Equal(globalTime, thirdEntry.LastModifiedTime);

        Assert.False(await reader.MoveToNextEntryAsync());
    }

    [Fact]
    public async ValueTask Tar_PaxGlobalHeader_Link_Reader_Async()
    {
        var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.PaxGlobalHeader.Link.tar");

        using Stream stream = File.OpenRead(archivePath);
        await using var reader = await TarReader.OpenAsyncReader(stream);

        Assert.True(await reader.MoveToNextEntryAsync());
        var firstEntry = (TarEntry)reader.Entry;
        Assert.Equal("global-link", firstEntry.Key);
        Assert.Equal("global-target", firstEntry.LinkTarget);
        Assert.Equal(4100, firstEntry.UserID);
        Assert.Equal(5100, firstEntry.GroupId);
        Assert.Equal(Convert.ToInt64("777", 8), firstEntry.Mode);

        Assert.True(await reader.MoveToNextEntryAsync());
        var secondEntry = (TarEntry)reader.Entry;
        Assert.Equal("local-link-override", secondEntry.Key);
        Assert.Equal("local-target", secondEntry.LinkTarget);
        Assert.Equal(4100, secondEntry.UserID);
        Assert.Equal(5100, secondEntry.GroupId);
        Assert.Equal(Convert.ToInt64("777", 8), secondEntry.Mode);

        Assert.False(await reader.MoveToNextEntryAsync());
    }

    [Fact]
    public async ValueTask Tar_WithSymlink_Reader_SurfacesLinkTargets_Async()
    {
        var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz");

        using Stream stream = File.OpenRead(archivePath);
        await using var reader = await TarReader.OpenAsyncReader(stream);

        var foundVulkanToolsLink = false;
        var foundVulkanSamplesLink = false;

        while (await reader.MoveToNextEntryAsync())
        {
            if (reader.Entry.Key == "MoltenVK-1.0.21/Demos/LunarG-VulkanSamples/Vulkan-Tools")
            {
                foundVulkanToolsLink = true;
                Assert.Equal("../../External/Vulkan-Tools", reader.Entry.LinkTarget);
            }

            if (reader.Entry.Key == "MoltenVK-1.0.21/Demos/LunarG-VulkanSamples/VulkanSamples")
            {
                foundVulkanSamplesLink = true;
                Assert.Equal("../../External/VulkanSamples", reader.Entry.LinkTarget);
            }
        }

        Assert.True(foundVulkanToolsLink);
        Assert.True(foundVulkanSamplesLink);
    }

    [Fact]
    public void Tar_Containing_Rar_Reader_Async()
    {
        var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsRar.tar");
        using Stream stream = File.OpenRead(archiveFullPath);
        using var reader = ReaderFactory.OpenReader(stream);
        Assert.True(reader.Type == ArchiveType.Tar);
    }

    [Fact]
    public async ValueTask Tar_With_TarGz_With_Flushed_EntryStream_Async()
    {
        var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsTarGz.tar");
        using Stream stream = File.OpenRead(archiveFullPath);
        await using var reader = await ReaderFactory.OpenAsyncReader(stream);
        Assert.True(await reader.MoveToNextEntryAsync());
        Assert.Equal("inner.tar.gz", reader.Entry.Key);

        await using var entryStream = await reader.OpenEntryStreamAsync();
        await using var flushingStream = new FlushOnDisposeStream(entryStream);

        // Extract inner.tar.gz
        await using var innerReader = await ReaderFactory.OpenAsyncReader(flushingStream);
        Assert.True(await innerReader.MoveToNextEntryAsync());
        Assert.Equal("test", innerReader.Entry.Key);
    }

    [Fact]
    public async ValueTask Tar_Broken_Stream_Async()
    {
        var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
        using Stream stream = File.OpenRead(archiveFullPath);
        await using var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream));
        var memoryStream = new MemoryStream();

        Assert.True(await reader.MoveToNextEntryAsync());
        Assert.True(await reader.MoveToNextEntryAsync());
        await reader.WriteEntryToAsync(memoryStream);
        stream.Close();
        await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
            await reader.MoveToNextEntryAsync()
        );
    }

    [Fact]
    public async ValueTask Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream_Async()
    {
        var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 };
        var tarWriterOptions = new TarWriterOptions(CompressionType.None, true)
        {
            ArchiveEncoding = archiveEncoding,
        };
        var testBytes = Encoding.UTF8.GetBytes("This is a test.");

        using var memoryStream = new MemoryStream();
        using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions))
        using (var testFileStream = new MemoryStream(testBytes))
        {
            await tarWriter.WriteAsync("file0.txt", testFileStream, null);
            testFileStream.Position = 0;
            await tarWriter.WriteAsync("file1.txt", testFileStream, null);
            tarWriter.WriteDirectory("folder0", null);
            testFileStream.Position = 0;
            await tarWriter.WriteAsync("folder0/file_in_folder0.txt", testFileStream, null);
        }

        memoryStream.Position = 0;

        var entryKeys = new List<string?>();
        var openEntryStreams = new List<Stream>();

        await using (
            var reader = await TarReader.OpenAsyncReader(new AsyncOnlyStream(memoryStream))
        )
        {
            while (await reader.MoveToNextEntryAsync())
            {
                entryKeys.Add(reader.Entry.Key);
                if (reader.Entry.IsDirectory)
                {
                    continue;
                }

                var entryStream = await reader.OpenEntryStreamAsync();
                openEntryStreams.Add(entryStream);

                using var testFileStream = new MemoryStream();
                await entryStream.CopyToAsync(testFileStream);
                Assert.Equal(testBytes.Length, testFileStream.Length);
            }
        }

        openEntryStreams.ForEach(stream => stream.Dispose());

        Assert.Equal(
            ["file0.txt", "file1.txt", "folder0/", "folder0/file_in_folder0.txt"],
            entryKeys
        );
    }

    [Fact]
    public async ValueTask Tar_Corrupted_Async()
    {
        var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "TarCorrupted.tar");
        using Stream stream = File.OpenRead(archiveFullPath);
        await using var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream));
        var memoryStream = new MemoryStream();

        Assert.True(await reader.MoveToNextEntryAsync());
        Assert.True(await reader.MoveToNextEntryAsync());
        await reader.WriteEntryToAsync(memoryStream);
        stream.Close();
        await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
            await reader.MoveToNextEntryAsync()
        );
    }
}
