// Copyright (c) 2026 Duplicati Inc. All rights reserved.

using System;
using System.IO;
using System.Threading.Tasks;
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using Assert = NUnit.Framework.Legacy.ClassicAssert;

#nullable enable

namespace Duplicati.UnitTest.DiskImage.Fat32;

/// <summary>
/// Unit tests for the <see cref="Fat32Filesystem.Fat32ZeroStream"/> class.
/// </summary>
[TestFixture]
[Category("DiskImageUnit")]
public class Fat32ZeroStreamTests
{
    [Test]
    public void Test_Fat32ZeroStream_Read_ReturnsZeros()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        var buffer = new byte[1024];
        int bytesRead = stream.Read(buffer, 0, buffer.Length);

        Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");

        // Verify all bytes are zero
        for (int i = 0; i < buffer.Length; i++)
        {
            Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
        }
    }

    [Test]
    public async Task Test_Fat32ZeroStream_ReadAsync_ReturnsZeros()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        var buffer = new byte[1024];
        int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);

        Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");

        // Verify all bytes are zero
        for (int i = 0; i < buffer.Length; i++)
        {
            Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
        }
    }

    [Test]
    public void Test_Fat32ZeroStream_Length_MatchesBlockSize()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.AreEqual(blockSize, stream.Length, "Stream length should match block size");
    }

    [Test]
    public void Test_Fat32ZeroStream_Seek_Works()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        // Seek to beginning
        var position = stream.Seek(0, SeekOrigin.Begin);
        Assert.AreEqual(0, position, "Should be at position 0");

        // Seek to middle
        position = stream.Seek(blockSize / 2, SeekOrigin.Begin);
        Assert.AreEqual(blockSize / 2, position, "Should be at middle position");

        // Seek relative to current
        position = stream.Seek(1024, SeekOrigin.Current);
        Assert.AreEqual(blockSize / 2 + 1024, position, "Should be at relative position");

        // Seek from end
        position = stream.Seek(-1024, SeekOrigin.End);
        Assert.AreEqual(blockSize - 1024, position, "Should be at position from end");
    }

    [Test]
    public void Test_Fat32ZeroStream_Write_ThrowsNotSupported()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        var buffer = new byte[1024];
        Assert.Throws<NotSupportedException>(() => stream.Write(buffer, 0, buffer.Length));
    }

    [Test]
    public void Test_Fat32ZeroStream_MultipleInstances_ShareBuffer()
    {
        const int blockSize = 1024 * 1024; // 1MB

        // Create two streams with the same block size
        using var stream1 = new Fat32Filesystem.Fat32ZeroStream(blockSize);
        using var stream2 = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        // Both should read zeros
        var buffer1 = new byte[1024];
        var buffer2 = new byte[1024];

        stream1.ReadExactly(buffer1);
        stream2.ReadExactly(buffer2);

        // Verify both have zeros
        for (int i = 0; i < buffer1.Length; i++)
        {
            Assert.AreEqual(0, buffer1[i], $"Buffer1 byte at position {i} should be zero");
            Assert.AreEqual(0, buffer2[i], $"Buffer2 byte at position {i} should be zero");
        }
    }

    [Test]
    public void Test_Fat32ZeroStream_ReadPartial_ReturnsCorrectCount()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        // Seek to near the end
        stream.Seek(blockSize - 100, SeekOrigin.Begin);

        var buffer = new byte[1024];
        int bytesRead = stream.Read(buffer, 0, buffer.Length);

        Assert.AreEqual(100, bytesRead, "Should only read remaining bytes");
    }

    [Test]
    public void Test_Fat32ZeroStream_ReadPastEnd_ReturnsZero()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        // Seek to end
        stream.Seek(0, SeekOrigin.End);

        var buffer = new byte[1024];
        int bytesRead = stream.Read(buffer, 0, buffer.Length);

        Assert.AreEqual(0, bytesRead, "Should return 0 when reading past end");
    }

    [Test]
    public void Test_Fat32ZeroStream_CanRead_CanWrite_CanSeek()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.IsTrue(stream.CanRead, "CanRead should be true");
        Assert.IsFalse(stream.CanWrite, "CanWrite should be false");
        Assert.IsTrue(stream.CanSeek, "CanSeek should be true");
    }

    [Test]
    public void Test_Fat32ZeroStream_InvalidBlockSize_Throws()
    {
        Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(0));
        Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(-1));
    }

    [Test]
    public void Test_Fat32ZeroStream_SetLength_ThrowsNotSupported()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.Throws<NotSupportedException>(() => stream.SetLength(1024));
    }

    [Test]
    public void Test_Fat32ZeroStream_Flush_DoesNotThrow()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.DoesNotThrow(() => stream.Flush());
    }

    [Test]
    public void Test_Fat32ZeroStream_Position_Property()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.AreEqual(0, stream.Position, "Initial position should be 0");

        stream.Position = 1024;
        Assert.AreEqual(1024, stream.Position, "Position should be settable");
    }

    [Test]
    public void Test_Fat32ZeroStream_Position_InvalidValue_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = -1);
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = blockSize + 1);
    }

    [Test]
    public void Test_Fat32ZeroStream_Seek_InvalidOrigin_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.Throws<ArgumentException>(() => stream.Seek(0, (SeekOrigin)999));
    }

    [Test]
    public void Test_Fat32ZeroStream_Seek_OutOfRange_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin));
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(blockSize + 1, SeekOrigin.Begin));
    }

    [Test]
    public void Test_Fat32ZeroStream_Read_NullBuffer_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

#pragma warning disable CS8600, CS8625
        Assert.Throws<ArgumentNullException>(() => stream.ReadExactly(null, 0, 1024));
#pragma warning restore CS8600, CS8625
    }

    [Test]
    public void Test_Fat32ZeroStream_Read_InvalidOffset_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        var buffer = new byte[1024];
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.ReadExactly(buffer, -1, 1024));
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.ReadExactly(buffer, buffer.Length + 1, 1024));
    }

    [Test]
    public void Test_Fat32ZeroStream_Read_InvalidCount_Throws()
    {
        const int blockSize = 1024 * 1024; // 1MB
        using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);

        var buffer = new byte[1024];
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.ReadExactly(buffer, 0, -1));
        Assert.Throws<ArgumentOutOfRangeException>(() => stream.ReadExactly(buffer, 512, 1024)); // Exceeds buffer length
    }
}
