using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA.Utilities;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using Xunit;

namespace SharpCompress.Test.Rar;

public class RarArchiveTests : ArchiveTests
{
    [Theory]
    [InlineData("Rar15.rar")]
    [InlineData("Rar2.rar")]
    [InlineData("Rar.rar")]
    [InlineData("Rar.Audio_program.rar")]
    [InlineData("Rar5.rar")]
    [InlineData("Rar5.solid.rar")]
    public void Rar_Archive_Recently_Changed_Unpackers_Sync(string filename)
    {
        var extractedEntries = 0;
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        using var archive = RarArchive.OpenArchive(
            stream,
            new ReaderOptions { LookForHeader = true }
        );

        foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
        {
            using var output = new MemoryStream();
            entry.WriteTo(output);
            extractedEntries++;
        }

        Assert.True(extractedEntries > 0);
    }

    [Fact]
    public void Rar_EncryptedFileAndHeader_Archive() =>
        ReadRarPassword("Rar.encrypted_filesAndHeader.rar", "test");

    [Fact]
    public void Rar_EncryptedFileAndHeader_NoPasswordExceptionTest() =>
        Assert.Throws<CryptographicException>(() =>
            ReadRarPassword("Rar.encrypted_filesAndHeader.rar", null)
        );

    [Fact]
    public void Rar5_EncryptedFileAndHeader_Archive() =>
        ReadRarPassword("Rar5.encrypted_filesAndHeader.rar", "test");

    [Fact]
    public void Rar5_EncryptedFileAndHeader_Archive_Err() =>
        Assert.Throws<CryptographicException>(() =>
            ReadRarPassword("Rar5.encrypted_filesAndHeader.rar", "failed")
        );

    [Fact]
    public void Rar5_EncryptedFileAndHeader_NoPasswordExceptionTest() =>
        Assert.Throws<CryptographicException>(() =>
            ReadRarPassword("Rar5.encrypted_filesAndHeader.rar", null)
        );

    [Fact]
    public void Rar_EncryptedFileOnly_Archive() =>
        ReadRarPassword("Rar.encrypted_filesOnly.rar", "test");

    [Fact]
    public void Rar_EncryptedFileOnly_Archive_Err() =>
        Assert.Throws<CryptographicException>(() =>
            ReadRarPassword("Rar5.encrypted_filesOnly.rar", "failed")
        );

    [Fact]
    public void Rar5_EncryptedFileOnly_Archive() =>
        ReadRarPassword("Rar5.encrypted_filesOnly.rar", "test");

    [Fact]
    public void Rar_Encrypted_Archive() => ReadRarPassword("Rar.Encrypted.rar", "test");

    [Fact]
    public void Rar5_Encrypted_Archive() =>
        ReadRarPassword("Rar5.encrypted_filesAndHeader.rar", "test");

    private void ReadRarPassword(string testArchive, string? password)
    {
        using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testArchive)))
        using (
            var archive = RarArchive.OpenArchive(
                stream,
                ReaderOptions.ForExternalStream with
                {
                    Password = password,
                    LeaveStreamOpen = true,
                }
            )
        )
        {
            foreach (var entry in archive.Entries)
            {
                if (!entry.IsDirectory)
                {
                    Assert.Equal(CompressionType.Rar, entry.CompressionType);
                    entry.WriteToDirectory(SCRATCH_FILES_PATH);
                }
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Rar_Multi_Archive_Encrypted() =>
        Assert.Throws<InvalidFormatException>(() =>
            ArchiveFileReadPassword("Rar.EncryptedParts.part01.rar", "test")
        );

    protected void ArchiveFileReadPassword(string archiveName, string password)
    {
        using (
            var archive = RarArchive.OpenArchive(
                Path.Combine(TEST_ARCHIVES_PATH, archiveName),
                ReaderOptions.ForFilePath with
                {
                    Password = password,
                    LeaveStreamOpen = true,
                }
            )
        )
        {
            foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
            {
                entry.WriteToDirectory(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Rar_None_ArchiveStreamRead() => ArchiveStreamRead("Rar.none.rar");

    [Fact]
    public void Rar5_None_ArchiveStreamRead() => ArchiveStreamRead("Rar5.none.rar");

    [Fact]
    public void Rar_ArchiveStreamRead() => ArchiveStreamRead("Rar.rar");

    [Fact]
    public void RarArchive_StreamCollection_Throws_On_NonSeekable_Stream()
    {
        using var nonSeekable = new ForwardOnlyStream(new MemoryStream());
        using var seekable = new MemoryStream();

        Assert.Throws<ArgumentException>(() => RarArchive.OpenArchive([nonSeekable, seekable]));
    }

    [Fact]
    public void RarArchive_Stream_Throws_On_Unreadable_Stream()
    {
        using var unreadable = new TestStream(new MemoryStream(), false, true, true);

        Assert.Throws<ArgumentException>(() => RarArchive.OpenArchive(unreadable));
    }

    [Fact]
    public void Rar5_ArchiveStreamRead() => ArchiveStreamRead("Rar5.rar");

    [Fact]
    public void Rar_test_invalid_exttime_ArchiveStreamRead() =>
        DoRar_test_invalid_exttime_ArchiveStreamRead("Rar.test_invalid_exttime.rar");

    private void DoRar_test_invalid_exttime_ArchiveStreamRead(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        using var archive = ArchiveFactory.OpenArchive(stream);
        foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
        {
            entry.WriteToDirectory(SCRATCH_FILES_PATH);
        }
    }

    [Fact]
    public void Rar_Jpg_ArchiveStreamRead()
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"));
        using (
            var archive = RarArchive.OpenArchive(
                stream,
                ReaderOptions.ForExternalStream with
                {
                    LookForHeader = true,
                }
            )
        )
        {
            foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
            {
                entry.WriteToDirectory(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Rar_IsSolidArchiveCheck() => DoRar_IsSolidArchiveCheck("Rar.rar");

    [Fact]
    public void Rar5_IsSolidArchiveCheck() => DoRar_IsSolidArchiveCheck("Rar5.rar");

    private void DoRar_IsSolidArchiveCheck(string filename)
    {
        using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)))
        {
            using var archive = RarArchive.OpenArchive(stream);
            Assert.False(archive.IsSolid);
            foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
            {
                entry.WriteToDirectory(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Rar_IsSolidEntryStreamCheck() => DoRar_IsSolidEntryStreamCheck("Rar.solid.rar");

    //Extract the 2nd file in a solid archive to check that the first file is skipped properly
    private void DoRar_IsSolidEntryStreamCheck(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        using var archive = RarArchive.OpenArchive(stream);
        Assert.True(archive.IsSolid);
        IArchiveEntry[] entries = archive.Entries.Where(a => !a.IsDirectory).ToArray();
        Assert.NotInRange(entries.Length, 0, 1);
        Assert.False(entries[0].IsSolid); //first item in a solid archive is not marked solid and is seekable
        var testEntry = entries[1];
        Assert.True(testEntry.IsSolid); //the target. The non seekable entry

        //process all entries in solid archive until the one we want to test
        foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
        {
            using (var crcStream = new CrcCheckStream((uint)entry.Crc)) //use the 7zip CRC stream for convenience (required a bug fix)
            {
                using var eStream = entry.OpenEntryStream(); //bug fix in RarStream to report the correct Position
                eStream.CopyTo(crcStream);
            } //throws if not valid
            if (entry == testEntry)
            {
                break;
            }
        }
    }

    [Fact]
    public void Rar_Solid_ArchiveStreamRead() => ArchiveStreamRead("Rar.solid.rar");

    [Fact]
    public void Rar5_Solid_ArchiveStreamRead() => ArchiveStreamRead("Rar5.solid.rar");

    [Fact]
    public void Rar_Solid_StreamRead_Extract_All() =>
        ArchiveStreamReadExtractAll("Rar.solid.rar", CompressionType.Rar);

    [Fact]
    public void Rar5_Solid_StreamRead_Extract_All() =>
        ArchiveStreamReadExtractAll("Rar5.solid.rar", CompressionType.Rar);

    [Fact]
    public void Rar_Multi_ArchiveStreamRead() =>
        DoRar_Multi_ArchiveStreamRead(
            [
                "Rar.multi.part01.rar",
                "Rar.multi.part02.rar",
                "Rar.multi.part03.rar",
                "Rar.multi.part04.rar",
                "Rar.multi.part05.rar",
                "Rar.multi.part06.rar",
            ],
            false
        );

    [Fact]
    public void Rar5_Multi_ArchiveStreamRead() =>
        DoRar_Multi_ArchiveStreamRead(
            [
                "Rar5.multi.part01.rar",
                "Rar5.multi.part02.rar",
                "Rar5.multi.part03.rar",
                "Rar5.multi.part04.rar",
                "Rar5.multi.part05.rar",
                "Rar5.multi.part06.rar",
            ],
            false
        );

    private void DoRar_Multi_ArchiveStreamRead(string[] archives, bool isSolid)
    {
        using var archive = RarArchive.OpenArchive(
            archives
                .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
                .Select(File.OpenRead)
                .ToArray()
        );
        Assert.Equal(archive.IsSolid, isSolid);
        foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
        {
            entry.WriteToDirectory(SCRATCH_FILES_PATH);
        }
    }

    [Fact]
    public void Rar5_MultiSolid_ArchiveStreamRead() =>
        DoRar_Multi_ArchiveStreamRead(
            [
                "Rar.multi.solid.part01.rar",
                "Rar.multi.solid.part02.rar",
                "Rar.multi.solid.part03.rar",
                "Rar.multi.solid.part04.rar",
                "Rar.multi.solid.part05.rar",
                "Rar.multi.solid.part06.rar",
            ],
            true
        );

    [Fact]
    public void RarNoneArchiveFileRead() => ArchiveFileRead("Rar.none.rar");

    [Fact]
    public void Rar5NoneArchiveFileRead() => ArchiveFileRead("Rar5.none.rar");

    [Fact]
    public void Rar_ArchiveFileRead() => ArchiveFileRead("Rar.rar");

    [Fact]
    public void Rar5_ArchiveFileRead() => ArchiveFileRead("Rar5.rar");

    [Fact]
    public void Rar_ArchiveFileRead_HasDirectories() =>
        DoRar_ArchiveFileRead_HasDirectories("Rar.rar");

    [Fact]
    public void Rar5_ArchiveFileRead_HasDirectories() =>
        DoRar_ArchiveFileRead_HasDirectories("Rar5.rar");

    private void DoRar_ArchiveFileRead_HasDirectories(string filename)
    {
        using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
        using var archive = RarArchive.OpenArchive(stream);
        Assert.False(archive.IsSolid);
        Assert.Contains(true, archive.Entries.Select(entry => entry.IsDirectory));
    }

    [Fact]
    public void Rar_Jpg_ArchiveFileRead()
    {
        using (
            var archive = RarArchive.OpenArchive(
                Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"),
                ReaderOptions.ForFilePath with
                {
                    LookForHeader = true,
                }
            )
        )
        {
            foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
            {
                entry.WriteToDirectory(SCRATCH_FILES_PATH);
            }
        }
        VerifyFiles();
    }

    [Fact]
    public void Rar_Solid_ArchiveFileRead() => ArchiveFileRead("Rar.solid.rar");

    [Fact]
    public void Rar5_Solid_ArchiveFileRead() => ArchiveFileRead("Rar5.solid.rar");

    [Fact]
    public void Rar2_Multi_ArchiveStreamRead() =>
        DoRar_Multi_ArchiveStreamRead(
            [
                "Rar2.multi.rar",
                "Rar2.multi.r00",
                "Rar2.multi.r01",
                "Rar2.multi.r02",
                "Rar2.multi.r03",
                "Rar2.multi.r04",
                "Rar2.multi.r05",
            ],
            false
        );

    [Fact]
    public void Rar2_Multi_ArchiveFileRead() => ArchiveFileRead("Rar2.multi.rar"); //r00, r01...

    [Fact]
    public void Rar2_ArchiveFileRead() => ArchiveFileRead("Rar2.rar");

    [Fact]
    public void Rar15_ArchiveFileRead()
    {
        UseExtensionInsteadOfNameToVerify = true;
        UseCaseInsensitiveToVerify = true;
        ArchiveFileRead("Rar15.rar");
    }

    [Fact]
    public void Rar15_ArchiveVersionTest()
    {
        var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Rar15.rar");

        using var archive = RarArchive.OpenArchive(testArchive);
        Assert.Equal(1, archive.MinVersion);
        Assert.Equal(1, archive.MaxVersion);
    }

    [Fact]
    public void Rar2_ArchiveVersionTest()
    {
        var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Rar2.rar");

        using var archive = RarArchive.OpenArchive(testArchive);
        Assert.Equal(2, archive.MinVersion);
        Assert.Equal(2, archive.MaxVersion);
    }

    [Fact]
    public void Rar4_ArchiveVersionTest()
    {
        var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Rar4.multi.part01.rar");

        using var archive = RarArchive.OpenArchive(testArchive);
        Assert.Equal(3, archive.MinVersion);
        Assert.Equal(4, archive.MaxVersion);
    }

    [Fact]
    public void Rar5_ArchiveVersionTest()
    {
        var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Rar5.solid.rar");

        using var archive = RarArchive.OpenArchive(testArchive);
        Assert.Equal(5, archive.MinVersion);
        Assert.Equal(6, archive.MaxVersion);
    }

    [Fact]
    public void Rar4_Multi_ArchiveFileRead() => ArchiveFileRead("Rar4.multi.part01.rar");

    [Fact]
    public void Rar4_ArchiveFileRead() => ArchiveFileRead("Rar4.rar");

    [Fact]
    public void Rar_GetPartsSplit() =>
        //uses first part to search for all parts and compares against this array
        ArchiveGetParts(
            new[]
            {
                "Rar4.split.001",
                "Rar4.split.002",
                "Rar4.split.003",
                "Rar4.split.004",
                "Rar4.split.005",
                "Rar4.split.006",
            }
        );

    [Fact]
    public void Rar_GetPartsOld() =>
        //uses first part to search for all parts and compares against this array
        ArchiveGetParts(
            new[]
            {
                "Rar2.multi.rar",
                "Rar2.multi.r00",
                "Rar2.multi.r01",
                "Rar2.multi.r02",
                "Rar2.multi.r03",
                "Rar2.multi.r04",
                "Rar2.multi.r05",
            }
        );

    [Fact]
    public void Rar_GetPartsNew() =>
        //uses first part to search for all parts and compares against this array
        ArchiveGetParts(
            new[]
            {
                "Rar4.multi.part01.rar",
                "Rar4.multi.part02.rar",
                "Rar4.multi.part03.rar",
                "Rar4.multi.part04.rar",
                "Rar4.multi.part05.rar",
                "Rar4.multi.part06.rar",
                "Rar4.multi.part07.rar",
            }
        );

    [Fact]
    public void Rar4_Multi_ArchiveStreamRead() =>
        DoRar_Multi_ArchiveStreamRead(
            [
                "Rar4.multi.part01.rar",
                "Rar4.multi.part02.rar",
                "Rar4.multi.part03.rar",
                "Rar4.multi.part04.rar",
                "Rar4.multi.part05.rar",
                "Rar4.multi.part06.rar",
                "Rar4.multi.part07.rar",
            ],
            false
        );

    //no extension to test the lib identifies the archive by content not ext
    [Fact]
    public void Rar4_Split_ArchiveStreamRead() =>
        ArchiveStreamMultiRead(
            null,
            [
                "Rar4.split.001",
                "Rar4.split.002",
                "Rar4.split.003",
                "Rar4.split.004",
                "Rar4.split.005",
                "Rar4.split.006",
            ]
        );

    //will detect and load other files
    [Fact]
    public void Rar4_Multi_ArchiveFirstFileRead() => ArchiveFileRead("Rar4.multi.part01.rar");

    //"Rar4.multi.part02.rar",
    //"Rar4.multi.part03.rar",
    //"Rar4.multi.part04.rar",
    //"Rar4.multi.part05.rar",
    //"Rar4.multi.part06.rar",
    //"Rar4.multi.part07.rar"
    //will detect and load other files
    [Fact]
    public void Rar4_Split_ArchiveFirstFileRead() => ArchiveFileRead("Rar4.split.001");

    //"Rar4.split.002",
    //"Rar4.split.003",
    //"Rar4.split.004",
    //"Rar4.split.005",
    //"Rar4.split.006"
    //will detect and load other files
    [Fact]
    public void Rar4_Split_ArchiveStreamFirstFileRead() =>
        ArchiveStreamMultiRead(
            null,
            [
                "Rar4.split.001",
                //"Rar4.split.002",
                //"Rar4.split.003",
                //"Rar4.split.004",
                //"Rar4.split.005",
                //"Rar4.split.006"
            ]
        );

    //open with ArchiveFactory.Open and stream
    [Fact]
    public void Rar4_Split_ArchiveOpen() =>
        ArchiveOpenStreamRead(
            null,
            "Rar4.split.001",
            "Rar4.split.002",
            "Rar4.split.003",
            "Rar4.split.004",
            "Rar4.split.005",
            "Rar4.split.006"
        );

    //open with ArchiveFactory.Open and stream
    [Fact]
    public void Rar4_Multi_ArchiveOpen() =>
        ArchiveOpenStreamRead(
            null,
            "Rar4.multi.part01.rar",
            "Rar4.multi.part02.rar",
            "Rar4.multi.part03.rar",
            "Rar4.multi.part04.rar",
            "Rar4.multi.part05.rar",
            "Rar4.multi.part06.rar",
            "Rar4.multi.part07.rar"
        );

    [Fact]
    public void Rar4_Multi_ArchiveOpenEntryVolumeIndexTest() =>
        ArchiveOpenEntryVolumeIndexTest(
            [
                [0, 1], //exe - Rar4.multi.part01.rar to Rar4.multi.part02.rar
                [1, 5], //jpg - Rar4.multi.part02.rar to Rar4.multi.part06.rar
                [5, 6], //txt - Rar4.multi.part06.rar to Rar4.multi.part07.rar
            ],
            null,
            "Rar4.multi.part01.rar",
            "Rar4.multi.part02.rar",
            "Rar4.multi.part03.rar",
            "Rar4.multi.part04.rar",
            "Rar4.multi.part05.rar",
            "Rar4.multi.part06.rar",
            "Rar4.multi.part07.rar"
        );

    [Fact]
    public void Rar_Multi_ArchiveFileRead() => ArchiveFileRead("Rar.multi.part01.rar");

    [Fact]
    public void Rar5_Multi_ArchiveFileRead() => ArchiveFileRead("Rar5.multi.part01.rar");

    [Fact]
    public void Rar_IsFirstVolume_True() => DoRar_IsFirstVolume_True("Rar.multi.part01.rar");

    [Fact]
    public void Rar5_IsFirstVolume_True() => DoRar_IsFirstVolume_True("Rar5.multi.part01.rar");

    private void DoRar_IsFirstVolume_True(string firstFilename)
    {
        using var archive = RarArchive.OpenArchive(Path.Combine(TEST_ARCHIVES_PATH, firstFilename));
        Assert.True(archive.IsMultipartVolume());
        Assert.True(archive.IsFirstVolume());
    }

    [Fact]
    public void Rar_IsFirstVolume_False() => DoRar_IsFirstVolume_False("Rar.multi.part03.rar");

    [Fact]
    public void Rar5_IsFirstVolume_False() => DoRar_IsFirstVolume_False("Rar5.multi.part03.rar");

    private void DoRar_IsFirstVolume_False(string notFirstFilename)
    {
        using var archive = RarArchive.OpenArchive(
            Path.Combine(TEST_ARCHIVES_PATH, notFirstFilename)
        );
        Assert.True(archive.IsMultipartVolume());
        Assert.False(archive.IsFirstVolume());
    }

    [Fact]
    public void Rar5_CRC_Blake2_Archive() => ArchiveFileRead("Rar5.crc_blake2.rar");

    [Fact]
    void Rar_Iterate_Archive() =>
        ArchiveFileSkip("Rar.rar", "Failure jpg exe Empty jpg\\test.jpg exe\\test.exe тест.txt");

    [Fact]
    public void Rar2_Iterate_Archive() =>
        ArchiveFileSkip("Rar2.rar", "Failure Empty тест.txt jpg\\test.jpg exe\\test.exe jpg exe");

    [Fact]
    public void Rar4_Iterate_Archive() =>
        ArchiveFileSkip("Rar4.rar", "Failure Empty jpg exe тест.txt jpg\\test.jpg exe\\test.exe");

    [Fact]
    public void Rar5_Iterate_Archive() =>
        ArchiveFileSkip("Rar5.rar", "Failure jpg exe Empty тест.txt jpg\\test.jpg exe\\test.exe");

    [Fact]
    public void Rar_Encrypted_Iterate_Archive() =>
        ArchiveFileSkip(
            "Rar.encrypted_filesOnly.rar",
            "Failure jpg exe Empty тест.txt jpg\\test.jpg exe\\test.exe"
        );

    [Fact]
    public void Rar5_Encrypted_Iterate_Archive() =>
        ArchiveFileSkip(
            "Rar5.encrypted_filesOnly.rar",
            "Failure jpg exe Empty тест.txt jpg\\test.jpg exe\\test.exe"
        );

    [Fact]
    public void Rar_TestEncryptedDetection()
    {
        using var passwordProtectedFilesArchive = RarArchive.OpenArchive(
            Path.Combine(TEST_ARCHIVES_PATH, "Rar.encrypted_filesOnly.rar")
        );
        Assert.True(passwordProtectedFilesArchive.IsEncrypted);
    }

    /// <summary>
    /// Test for issue: InvalidOperationException when extracting RAR files.
    /// This test verifies the fix for the validation logic that was changed from
    /// (_position != Length) to (_position &lt; Length).
    /// The old logic would throw an exception when position exceeded expected length,
    /// but the new logic only throws when decompression ends prematurely (position &lt; expected).
    /// </summary>
    [Fact]
    public void Rar_StreamValidation_OnlyThrowsOnPrematureEnd()
    {
        // Test normal extraction - should NOT throw InvalidOperationException
        // even if actual decompressed size differs from header
        var testFiles = new[] { "Rar.rar", "Rar5.rar", "Rar4.rar", "Rar2.rar" };

        foreach (var testFile in testFiles)
        {
            using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
            using var archive = RarArchive.OpenArchive(stream);

            // Extract all entries and read them completely
            foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
            {
                using var entryStream = entry.OpenEntryStream();
                using var ms = new MemoryStream();

                // This should complete without throwing InvalidOperationException
                // The fix ensures we only throw when position &lt; expected length, not when position >= expected
                entryStream.CopyTo(ms);

                // Verify we read some data
                Assert.True(
                    ms.Length > 0,
                    $"Failed to extract data from {entry.Key} in {testFile}"
                );
            }
        }
    }

    /// <summary>
    /// Negative test case: Verifies that InvalidOperationException IS thrown when
    /// a RAR stream ends prematurely (position &lt; expected length).
    /// This tests the validation condition (_position &lt; Length) works correctly.
    /// </summary>
    [Fact]
    public void Rar_StreamValidation_ThrowsOnTruncatedStream()
    {
        // This test verifies the exception is thrown when decompression ends prematurely
        // by using a truncated stream that stops reading after a small number of bytes
        var testFile = "Rar.rar";
        using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

        // Wrap the file stream with a truncated stream that will stop reading early
        // This simulates a corrupted or truncated RAR file
        using var truncatedStream = new TruncatedStream(fileStream, 1000);

        // Opening the archive should work, but extracting should throw
        // when we try to read beyond the truncated data
        var exception = Assert.Throws<ArchiveOperationException>(() =>
        {
            using var archive = RarArchive.OpenArchive(truncatedStream);
            foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
            {
                using var entryStream = entry.OpenEntryStream();
                using var ms = new MemoryStream();
                // This should throw InvalidOperationException when it can't read all expected bytes
                entryStream.CopyTo(ms);
            }
        });

        // Verify the exception message matches our expectation
        Assert.Contains("unpacked file size does not match header", exception.Message);
    }

    /// <summary>
    /// Tests for Issue #1050 - RAR extraction with WriteToDirectory creates folders
    /// but places all files at the top level instead of in their subdirectories.
    /// </summary>
    [Fact]
    public void Rar_Issue1050_WriteToDirectory_ExtractsToSubdirectories()
    {
        var testFile = "Rar.issue1050.rar";
        using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
        using var archive = RarArchive.OpenArchive(fileStream);

        // Extract using archive.WriteToDirectory without explicit options
        archive.WriteToDirectory(SCRATCH_FILES_PATH);

        // Verify files are in their subdirectories, not at the root
        Assert.True(
            File.Exists(Path.Combine(SCRATCH_FILES_PATH, "PhysicsBraid", "263825.tr11dtp")),
            "File should be in PhysicsBraid subdirectory"
        );
        Assert.True(
            File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Animations", "15441.tr11anim")),
            "File should be in Animations subdirectory"
        );
        Assert.True(
            File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766728.tr11dtp")),
            "File should be in Braid subdirectory"
        );
        Assert.True(
            File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")),
            "File should be in Braid subdirectory"
        );
        Assert.True(
            File.Exists(Path.Combine(SCRATCH_FILES_PATH, "HeadBraid", "321353.tr11modeldata")),
            "File should be in HeadBraid subdirectory"
        );

        // Verify the exact file size of 766832.tr11dtp matches the archive entry size
        var fileInfo = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp"));
        Assert.Equal(4867620, fileInfo.Length); // Expected: 4,867,620 bytes
    }

    /// <summary>
    /// Test case for malformed RAR archives that previously caused infinite loops.
    /// This test verifies that attempting to read entries from a potentially malformed
    /// 512-byte RAR archive throws an InvalidOperationException instead of looping infinitely.
    /// See: https://github.com/adamhathcock/sharpcompress/issues/1176
    /// </summary>
    [Fact]
    public void Rar_MalformedArchive_NoInfiniteLoop()
    {
        var testFile = "Rar.malformed_512byte.rar";
        var readerOptions = ReaderOptions.ForExternalStream.WithLookForHeader(true);

        // This should throw InvalidOperationException, not hang in an infinite loop
        var exception = Assert.Throws<ArchiveOperationException>(() =>
        {
            using var fileStream = File.Open(
                Path.Combine(TEST_ARCHIVES_PATH, testFile),
                FileMode.Open
            );
            using var archive = RarArchive.OpenArchive(fileStream, readerOptions);

            // Attempting to enumerate entries should throw an exception
            // instead of looping infinitely
            foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
            {
                // This line should not be reached due to the exception
            }
        });

        // Verify that the exception is related to seeking beyond available data
        Assert.Contains("Cannot seek to position", exception.Message);
    }
}
