using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Aaru.Checksums;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Core;
using FluentAssertions;
using FluentAssertions.Execution;
using NUnit.Framework;
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;

namespace Aaru.Tests.Filesystems;

public abstract class ReadOnlyFilesystemTest : FilesystemTest
{
    protected ReadOnlyFilesystemTest() {}

    protected ReadOnlyFilesystemTest(string fileSystemType) : base(fileSystemType) {}

    [Test]
    public void Contents()
    {
        Environment.CurrentDirectory = DataFolder;

        using(new AssertionScope())
        {
            foreach(FileSystemTest test in Tests)
            {
                string testFile  = test.TestFile;
                var    found     = false;
                var    partition = new Partition();

                bool exists = File.Exists(testFile);
                exists.Should().BeTrue(Localization._0_not_found, testFile);

                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                // It arrives here...
                if(!exists) continue;

                IFilter inputFilter = PluginRegister.Singleton.GetFilter(testFile);

                inputFilter.Should().NotBeNull(Localization.Filter_0, testFile);

                var image = ImageFormat.Detect(inputFilter) as IMediaImage;

                image.Should().NotBeNull(Localization.Image_format_0, testFile);

                image.Open(inputFilter)
                     .Should()
                     .Be(ErrorNumber.NoError, string.Format(Localization.Cannot_open_image_for_0, testFile));

                List<string> idPlugins;

                if(Partitions)
                {
                    List<Partition> partitionsList = Core.Partitions.GetAll(image);

                    partitionsList.Should().NotBeEmpty(Localization.No_partitions_found_for_0, testFile);

                    // In reverse to skip boot partitions we're not interested in
                    for(int index = partitionsList.Count - 1; index >= 0; index--)
                    {
                        Core.Filesystems.Identify(image, out idPlugins, partitionsList[index], true);

                        if(idPlugins.Count == 0) continue;

                        if(!idPlugins.Contains(Plugin.Id.ToString())) continue;

                        found     = true;
                        partition = partitionsList[index];

                        break;
                    }
                }
                else
                {
                    partition = new Partition
                    {
                        Name   = "Whole device",
                        Length = image.Info.Sectors,
                        Size   = image.Info.Sectors * image.Info.SectorSize
                    };

                    Core.Filesystems.Identify(image, out idPlugins, partition, true);

                    idPlugins.Should().NotBeEmpty(Localization.No_filesystems_found_for_0, testFile);

                    found = idPlugins.Contains(Plugin.Id.ToString());
                }

                found.Should().BeTrue(Localization.Filesystem_not_identified_for_0, testFile);

                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                // It is not the case, it changes
                if(!found) continue;

                var fs = Activator.CreateInstance(Plugin.GetType()) as IReadOnlyFilesystem;

                fs.Should().NotBeNull(Localization.Could_not_instantiate_filesystem_for_0, testFile);

                test.Encoding ??= Encoding.ASCII;

                ErrorNumber ret = fs.Mount(image, partition, test.Encoding, null, test.Namespace);

                ret.Should().Be(ErrorNumber.NoError, string.Format(Localization.Unmountable_0, testFile));

                var serializerOptions = new JsonSerializerOptions
                {
                    Converters =
                    {
                        new JsonStringEnumConverter()
                    },
                    MaxDepth                    = 1536, // More than this an we get a StackOverflowException
                    WriteIndented               = true,
                    DefaultIgnoreCondition      = JsonIgnoreCondition.WhenWritingNull,
                    PropertyNameCaseInsensitive = true
                };

                if(test.ContentsJson != null)
                {
                    test.Contents =
                        JsonSerializer.Deserialize<Dictionary<string, FileData>>(test.ContentsJson, serializerOptions);
                }
                else if(File.Exists($"{testFile}.contents.json"))
                {
                    var sr = new FileStream($"{testFile}.contents.json", FileMode.Open);
                    test.Contents = JsonSerializer.Deserialize<Dictionary<string, FileData>>(sr, serializerOptions);
                }

                if(test.Contents is null) continue;

                var currentDepth = 0;

                TestDirectory(fs, "/", test.Contents, testFile, true, out List<NextLevel> currentLevel, currentDepth);

                while(currentLevel.Count > 0)
                {
                    currentDepth++;
                    List<NextLevel> nextLevels = [];

                    foreach(NextLevel subLevel in currentLevel)
                    {
                        TestDirectory(fs,
                                      subLevel.Path,
                                      subLevel.Children,
                                      testFile,
                                      true,
                                      out List<NextLevel> nextLevel,
                                      currentDepth);

                        nextLevels.AddRange(nextLevel);
                    }

                    currentLevel = nextLevels;
                }
            }
        }
    }

    [Test]
    [Ignore("Not a test, do not run")]
    public void Build()
    {
        Environment.CurrentDirectory = DataFolder;

        foreach(FileSystemTest test in Tests)
        {
            string testFile  = test.TestFile;
            var    found     = false;
            var    partition = new Partition();

            bool exists = File.Exists(testFile);

            // ReSharper disable once ConditionIsAlwaysTrueOrFalse
            // It arrives here...
            if(!exists) continue;

            IFilter inputFilter = PluginRegister.Singleton.GetFilter(testFile);

            if(ImageFormat.Detect(inputFilter) is not IMediaImage image) continue;

            ErrorNumber opened = image.Open(inputFilter);

            if(opened != ErrorNumber.NoError) continue;

            List<string> idPlugins;

            if(Partitions)
            {
                List<Partition> partitionsList = Core.Partitions.GetAll(image);

                // In reverse to skip boot partitions we're not interested in
                for(int index = partitionsList.Count - 1; index >= 0; index--)
                {
                    Core.Filesystems.Identify(image, out idPlugins, partitionsList[index], true);

                    if(idPlugins.Count == 0) continue;

                    if(!idPlugins.Contains(Plugin.Id.ToString())) continue;

                    found     = true;
                    partition = partitionsList[index];

                    break;
                }
            }
            else
            {
                partition = new Partition
                {
                    Name   = "Whole device",
                    Length = image.Info.Sectors,
                    Size   = image.Info.Sectors * image.Info.SectorSize
                };

                Core.Filesystems.Identify(image, out idPlugins, partition, true);

                found = idPlugins.Contains(Plugin.Id.ToString());
            }

            // ReSharper disable once ConditionIsAlwaysTrueOrFalse
            // It is not the case, it changes
            if(!found) continue;

            var fs = Activator.CreateInstance(Plugin.GetType()) as IReadOnlyFilesystem;

            test.Encoding ??= Encoding.ASCII;

            fs?.Mount(image, partition, test.Encoding, null, test.Namespace);

            Dictionary<string, FileData> contents = BuildDirectory(fs, "/", 0);

            var serializerOptions = new JsonSerializerOptions
            {
                Converters =
                {
                    new JsonStringEnumConverter()
                },
                MaxDepth                    = 1536,
                WriteIndented               = true,
                DefaultIgnoreCondition      = JsonIgnoreCondition.WhenWritingNull,
                PropertyNameCaseInsensitive = true
            };

            var sw = new FileStream($"{testFile}.contents.json", FileMode.Create);
            JsonSerializer.Serialize(sw, contents, serializerOptions);
            sw.Close();
        }
    }

    internal static Dictionary<string, FileData> BuildDirectory(IReadOnlyFilesystem fs, string path, int currentDepth)
    {
        currentDepth++;

        if(path == "/") path = "";

        Dictionary<string, FileData> children = new();
        ErrorNumber                  ret      = fs.OpenDir(path, out IDirNode node);

        if(ret != ErrorNumber.NoError) return children;

        while(fs.ReadDir(node, out string child) == ErrorNumber.NoError && child is not null)
        {
            var childPath = $"{path}/{child}";
            fs.Stat(childPath, out FileEntryInfo stat);

            var data = new FileData
            {
                Info = stat
            };

            if(stat.Attributes.HasFlag(FileAttributes.Directory))
            {
                // Cannot serialize to JSON too many depth levels 🤷‍♀️
                if(currentDepth < 384) data.Children = BuildDirectory(fs, childPath, currentDepth);
            }
            else if(stat.Attributes.HasFlag(FileAttributes.Symlink))
            {
                if(fs.ReadLink(childPath, out string link) == ErrorNumber.NoError) data.LinkTarget = link;
            }
            else
                data.Md5 = BuildFile(fs, childPath, stat.Length);

            if(fs.ListXAttr(childPath, out List<string> xattrs) == ErrorNumber.NoError && xattrs.Count > 0)
                data.XattrsWithMd5 = BuildFileXattrs(fs, childPath);

            children[child] = data;
        }

        fs.CloseDir(node);

        return children;
    }

    static string BuildFile(IReadOnlyFilesystem fs, string path, long length)
    {
        var buffer = new byte[length];

        ErrorNumber error = fs.OpenFile(path, out IFileNode fileNode);

        if(error != ErrorNumber.NoError) return Md5Context.Data(buffer, out _);

        fs.ReadFile(fileNode, length, buffer, out _);
        fs.CloseFile(fileNode);

        return Md5Context.Data(buffer, out _);
    }

    static Dictionary<string, string> BuildFileXattrs(IReadOnlyFilesystem fs, string path)
    {
        fs.ListXAttr(path, out List<string> contents);

        if(contents.Count == 0) return null;

        Dictionary<string, string> xattrs = new();

        foreach(string xattr in contents)
        {
            byte[]      buffer = [];
            ErrorNumber ret    = fs.GetXattr(path, xattr, ref buffer);

            string data = ret != ErrorNumber.NoError && ret != ErrorNumber.OutOfRange
                              ? Md5Context.Data([],     out _)
                              : Md5Context.Data(buffer, out _);

            xattrs[xattr] = data;
        }

        return xattrs;
    }

    internal static void TestDirectory(IReadOnlyFilesystem fs,       string path, Dictionary<string, FileData> children,
                                       string              testFile, bool   testXattr, out List<NextLevel> nextLevels,
                                       int                 currentDepth)
    {
        currentDepth++;
        nextLevels = [];
        ErrorNumber ret = fs.OpenDir(path, out IDirNode node);

        // Directory is not readable, probably filled the volume, just ignore it
        if(ret == ErrorNumber.InvalidArgument) return;

        ret.Should()
           .Be(ErrorNumber.NoError,
               string.Format(Localization.Unexpected_error_0_when_reading_directory_1_of_2, ret, path, testFile));

        if(ret != ErrorNumber.NoError) return;

        List<string> contents = [];

        while(fs.ReadDir(node, out string filename) == ErrorNumber.NoError && filename is not null)
            contents.Add(filename);

        fs.CloseDir(node);

        if(children.Count == 0 && contents.Count == 0) return;

        if(path == "/") path = "";

        List<string> expectedNotFound = [];

        foreach(KeyValuePair<string, FileData> child in children)
        {
            var childPath = $"{path}/{child.Key}";
            ret = fs.Stat(childPath, out FileEntryInfo stat);

            if(ret == ErrorNumber.NoSuchFile ||
               contents is null              ||
               ret == ErrorNumber.NoError && !contents.Contains(child.Key))
            {
                expectedNotFound.Add(child.Key);

                continue;
            }

            contents.Remove(child.Key);

            ret.Should()
               .Be(ErrorNumber.NoError,
                   string.Format(Localization.Unexpected_error_0_retrieving_stats_for_1_in_2,
                                 ret,
                                 childPath,
                                 testFile));

            if(child.Value.Info is not null)
            {
                if((stat.AccessTime - child.Value.Info.AccessTime)?.Hours is 1 or -1)
                    stat.AccessTime = child.Value.Info.AccessTime;

                if((stat.AccessTimeUtc - child.Value.Info.AccessTimeUtc)?.Hours is 1 or -1)
                    stat.AccessTimeUtc = child.Value.Info.AccessTimeUtc;

                if((stat.BackupTime - child.Value.Info.BackupTime)?.Hours is 1 or -1)
                    stat.BackupTime = child.Value.Info.BackupTime;

                if((stat.BackupTimeUtc - child.Value.Info.BackupTimeUtc)?.Hours is 1 or -1)
                    stat.BackupTimeUtc = child.Value.Info.BackupTimeUtc;

                if((stat.CreationTime - child.Value.Info.CreationTime)?.Hours is 1 or -1)
                    stat.CreationTime = child.Value.Info.CreationTime;

                if((stat.CreationTimeUtc - child.Value.Info.CreationTimeUtc)?.Hours is 1 or -1)
                    stat.CreationTimeUtc = child.Value.Info.CreationTimeUtc;

                if((stat.LastWriteTime - child.Value.Info.LastWriteTime)?.Hours is 1 or -1)
                    stat.LastWriteTime = child.Value.Info.LastWriteTime;

                if((stat.LastWriteTimeUtc - child.Value.Info.LastWriteTimeUtc)?.Hours is 1 or -1)
                    stat.LastWriteTimeUtc = child.Value.Info.LastWriteTimeUtc;

                if((stat.StatusChangeTime - child.Value.Info.StatusChangeTime)?.Hours is 1 or -1)
                    stat.StatusChangeTime = child.Value.Info.StatusChangeTime;

                if((stat.StatusChangeTimeUtc - child.Value.Info.StatusChangeTimeUtc)?.Hours is 1 or -1)
                    stat.StatusChangeTimeUtc = child.Value.Info.StatusChangeTimeUtc;
            }

            stat.Should()
                .BeEquivalentTo(child.Value.Info,
                                string.Format(Localization.Wrong_info_for_0_in_1, childPath, testFile));

            byte[] buffer = [];

            if(child.Value.Info.Attributes.HasFlag(FileAttributes.Directory))
            {
                ret = fs.OpenFile(childPath, out _);

                ret.Should()
                   .Be(ErrorNumber.IsDirectory,
                       string.Format(Localization.Got_wrong_data_for_directory_0_in_1, childPath, testFile));

                // Cannot serialize to JSON too many depth levels 🤷‍♀️
                if(currentDepth < 384)
                {
                    child.Value.Children.Should()
                         .NotBeNull(Localization.Contents_for_0_in_1_must_be_defined_in_unit_test_declaration,
                                    childPath,
                                    testFile);

                    if(child.Value.Children != null)
                    {
                        nextLevels.Add(new NextLevel(childPath, child.Value.Children));

                        //   TestDirectory(fs, childPath, child.Value.Children, testFile, testXattr);
                    }
                }
            }
            else if(child.Value.Info.Attributes.HasFlag(FileAttributes.Symlink))
            {
                ret = fs.ReadLink(childPath, out string link);

                ret.Should()
                   .Be(ErrorNumber.NoError,
                       string.Format(Localization.Got_wrong_data_for_symbolic_link_0_in_1, childPath, testFile));

                link.Should()
                    .Be(child.Value.LinkTarget,
                        string.Format(Localization.Invalid_target_for_symbolic_link_0_in_1, childPath, testFile));
            }
            else

                // This ensure the buffer does not hang for collection
                TestFile(fs, childPath, child.Value.Md5, child.Value.Info.Length, testFile);

            if(!testXattr) continue;

            ret = fs.ListXAttr(childPath, out List<string> xattrs);

            if(ret == ErrorNumber.NotSupported)
            {
                child.Value.XattrsWithMd5.Should()
                     .BeNull(Localization.Defined_extended_attributes_for_0_in_1_are_not_supported_by_filesystem,
                             childPath,
                             testFile);

                continue;
            }

            ret.Should()
               .Be(ErrorNumber.NoError,
                   string.Format(Localization.Unexpected_error_0_when_listing_extended_attributes_for_1_in_2,
                                 ret,
                                 childPath,
                                 testFile));

            if(xattrs.Count > 0)
            {
                child.Value.XattrsWithMd5.Should()
                     .NotBeNull(Localization.Extended_attributes_for_0_in_1_must_be_defined_in_unit_test_declaration,
                                childPath,
                                testFile);
            }

            if(xattrs.Count > 0 || child.Value.XattrsWithMd5?.Count > 0)
                TestFileXattrs(fs, childPath, child.Value.XattrsWithMd5, testFile);
        }

        expectedNotFound.Should()
                        .BeEmpty(Localization.Could_not_find_the_children_of_0_in_1_2,
                                 path,
                                 testFile,
                                 string.Join(" ", expectedNotFound));

        if(contents != null)
        {
            contents.Should()
                    .BeEmpty(Localization.Found_the_following_unexpected_children_of_0_in_1_2,
                             path,
                             testFile,
                             string.Join(" ", contents));
        }
    }

    static void TestFile(IReadOnlyFilesystem fs, string path, string md5, long length, string testFile)
    {
        var         buffer = new byte[length];
        ErrorNumber ret    = fs.OpenFile(path, out IFileNode fileNode);

        ret.Should()
           .Be(ErrorNumber.NoError,
               string.Format(Localization.Unexpected_error_0_when_reading_1_in_2, ret, path, testFile));

        ret = fs.ReadFile(fileNode, length, buffer, out long readBytes);

        ret.Should()
           .Be(ErrorNumber.NoError,
               string.Format(Localization.Unexpected_error_0_when_reading_1_in_2, ret, path, testFile));

        readBytes.Should()
                 .Be(length,
                     string.Format(Localization.Got_less_bytes_0_than_expected_1_when_reading_2_in_3,
                                   readBytes,
                                   length,
                                   path,
                                   testFile));

        fs.CloseFile(fileNode);

        string data = Md5Context.Data(buffer, out _);

        data.Should()
            .Be(md5, string.Format(Localization.Got_MD5_0_for_1_in_2_but_expected_3, data, path, testFile, md5));
    }

    static void TestFileXattrs(IReadOnlyFilesystem fs, string path, Dictionary<string, string> xattrs, string testFile)
    {
        // Nothing to test
        if(xattrs is null) return;

        fs.ListXAttr(path, out List<string> contents);

        if(xattrs.Count == 0 && contents.Count == 0) return;

        List<string> expectedNotFound = [];

        foreach(KeyValuePair<string, string> xattr in xattrs)
        {
            byte[]      buffer = [];
            ErrorNumber ret    = fs.GetXattr(path, xattr.Key, ref buffer);

            if(ret == ErrorNumber.NoSuchExtendedAttribute || !contents.Contains(xattr.Key))
            {
                expectedNotFound.Add(xattr.Key);

                continue;
            }

            contents.Remove(xattr.Key);

            // Partially read extended attribute... dunno why it happens with some Toast images
            if(ret != ErrorNumber.OutOfRange)
            {
                ret.Should()
                   .Be(ErrorNumber.NoError,
                       string.Format(Localization.Unexpected_error_0_retrieving_extended_attributes_for_1_in_2,
                                     ret,
                                     path,
                                     testFile));
            }

            string data = Md5Context.Data(buffer, out _);

            data.Should()
                .Be(xattr.Value,
                    string.Format(Localization.Got_MD5_0_for_1_of_2_in_3_but_expected_4,
                                  data,
                                  xattr.Key,
                                  path,
                                  testFile,
                                  xattr.Value));
        }

        expectedNotFound.Should()
                        .BeEmpty(Localization.Could_not_find_the_following_extended_attributes_of_0_in_1_2,
                                 path,
                                 testFile,
                                 string.Join(" ", expectedNotFound));

        contents.Should()
                .BeEmpty(Localization.Found_the_following_unexpected_extended_attributes_of_0_in_1_2,
                         path,
                         testFile,
                         string.Join(" ", contents));
    }

#region Nested type: NextLevel

    internal sealed record NextLevel(string Path, Dictionary<string, FileData> Children);

#endregion
}