// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Logging;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.Windows;
using NUnit.Framework;
using NUnit.Framework.Legacy;

namespace Duplicati.UnitTest
{
    [TestFixture]
    public class TestHyperVParser
    {
        private const char PS = ';'; // Windows path separator
        private const char DS = '\\'; // Directory separator
        private sealed record MachineEntry(string ID, string Name, string[] Paths);

        private sealed class MockHyperVUtility : IHyperVUtility
        {
            public bool IsHyperVInstalled { get; set; } = true;
            public Guid HyperVWriterGuid { get; set; } = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");

            public bool IsVSSWriterSupported { get; set; } = true;

            public List<HyperVGuest> Guests { get; set; } = null;

            public void QueryHyperVGuestsInfo(WindowsSnapshotProvider provider, bool bIncludePaths = false)
            {
                // Mock implementation, no actual querying                
            }
        }

        private sealed class LogSink : ILogDestination
        {
            public List<LogEntry> Entries { get; } = [];

            public void WriteMessage(LogEntry entry)
            {
                Entries.Add(entry);
            }
        }

        private static readonly string HyperVPathTemplate = $"%HYPERV%{DS}{{0}}";
        private static string MakeHyperVSource(Guid id) => string.Format(HyperVPathTemplate, id);

        [Test]
        public void Returns_empty_change_set_when_HyperV_not_installed()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var paths = new[] { @"C:\data" };
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            var utility = new MockHyperVUtility { IsHyperVInstalled = false };

            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());

            var result = module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.That(result, Is.Empty, "No cmdline option should be rewritten.");
            Assert.That(paths, Is.EqualTo(new[] { @"C:\data" }), "Source paths must stay untouched.");
        }

        [Test]
        public void Removes_HyperV_writer_from_vss_exclude_list()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var hyperVGuid = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
            var paths = new[] { string.Format(HyperVPathTemplate, Guid.NewGuid()) };
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["vss-exclude-writers"] = $"{hyperVGuid};bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
                ["snapshot-policy"] = "required"
            };

            var utility = new MockHyperVUtility { HyperVWriterGuid = hyperVGuid };
            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());

            var result = module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.Multiple(() =>
            {
                Assert.That(result.ContainsKey("vss-exclude-writers"), Is.True,
                            "The change-set must contain the corrected option.");
                Assert.That(result["vss-exclude-writers"],
                            Does.Not.Contain(hyperVGuid.ToString()),
                            "The Hyper-V writer GUID must have been stripped.");
            });
        }

        [Test]
        public void Adds_required_snapshot_policy_when_missing()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var guestId = Guid.NewGuid();
            var paths = new[] { MakeHyperVSource(guestId) };
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            var utility = new MockHyperVUtility
            {
                Guests = new List<HyperVGuest>
                {
                    new("TestVM", guestId, [@"C:\VMs\TestVM\TestVM.vhdx"])
                }
            };

            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            var result = module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.Multiple(() =>
            {
                Assert.That(result.ContainsKey("snapshot-policy"), Is.True);
                Assert.That(result["snapshot-policy"], Is.EqualTo("required"),
                            "Snapshot policy must be enforced to 'required'.");
            });
        }

        [Test]
        public void Expands_guest_data_paths_into_source_list()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var guestId = Guid.NewGuid();
            var guest = new HyperVGuest("TestVM", guestId, [@"C:\HyperV\TestVM\TestVM.vhdx", @"C:\HyperV\TestVM\State.bin"]);

            var paths = new[] { MakeHyperVSource(guestId) };
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["snapshot-policy"] = "required"
            };

            var utility = new MockHyperVUtility
            {
                Guests = new List<HyperVGuest> { guest }
            };

            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.That(paths, Does.Contain(guest.DataPaths[0]));
            Assert.That(paths, Does.Contain(guest.DataPaths[1]));
        }

        [Test]
        public void Respects_exclude_filter_expressions()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var guestId = Guid.NewGuid();
            var data = @"C:\HyperV\TestVM\TestVM.vhdx";

            var guest = new HyperVGuest("TestVM", guestId, [data]);

            var paths = new[] { MakeHyperVSource(guestId) };
            var filter = $"-{data}";                       // exclude the only path
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["snapshot-policy"] = "required"
            };

            var utility = new MockHyperVUtility
            {
                Guests = new List<HyperVGuest> { guest }
            };

            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.That(paths, Does.Not.Contain(data),
                        "The excluded VHDX must not make it into the final path list.");
        }

        [Test]
        public void Respects_exclude_filter_expressions_hyperv()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var guestId = Guid.NewGuid();
            var data = @$"C:{DS}HyperV{DS}TestVM{DS}TestVM.vhdx";

            var guest = new HyperVGuest("TestVM", guestId, [data]);

            var paths = new[] { MakeHyperVSource(guestId) };
            var filter = $"-{MakeHyperVSource(guestId)}{DS}{data}";                       // exclude the only path
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["snapshot-policy"] = "required"
            };

            var utility = new MockHyperVUtility
            {
                Guests = new List<HyperVGuest> { guest }
            };

            using var log = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            var module = new Duplicati.Library.Modules.Builtin.HyperVOptions();
            module.RealParseSourcePaths(ref paths, ref filter, options, utility);

            Assert.That(paths, Does.Not.Contain(data),
                        "The excluded VHDX must not make it into the final path list.");
        }

        [Test]
        public void HyperV_all_marker_includes_every_machine()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            // arrange
            var g1 = new HyperVGuest("Alpha", Guid.NewGuid(),
                                     [@$"C:{DS}HyperV{DS}Alpha{DS}Alpha.vhdx"]);
            var g2 = new HyperVGuest("Beta", Guid.NewGuid(),
                                     [@$"D:{DS}VMs{DS}Beta{DS}Disk1.vhdx", @$"D:{DS}VMs{DS}Beta{DS}State.bin"]);

            var paths = new[] { "%HYPERV%" };              // <-- only the ALL-marker
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { g1, g2 } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            // assert – every data-path from every guest must be present
            var expected = g1.DataPaths.Concat(g2.DataPaths);
            CollectionAssert.IsSubsetOf(expected, paths);
        }

        [Test]
        public void All_marker_but_single_guest_excluded()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var gKeep = new HyperVGuest("KeepMe", Guid.NewGuid(),
                                        [@$"C:{DS}VMs{DS}KeepMe{DS}KeepMe.vhdx"]);
            var gDrop = new HyperVGuest("DropMe", Guid.NewGuid(),
                                        [@$"C:{DS}VMs{DS}DropMe{DS}DropMe.vhdx"]);

            var paths = new[] { "%HYPERV%" };
            var filter = $"-{MakeHyperVSource(gDrop.ID)}";
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { gKeep, gDrop } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            Assert.That(paths, Does.Contain(gKeep.DataPaths.First()), "kept VM missing");
            Assert.That(paths, Does.Not.Contain(gDrop.DataPaths.First()), "excluded VM leaked in");
        }

        [Test]
        public void All_marker_but_single_file_excluded()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var g = new HyperVGuest("VM", Guid.NewGuid(),
                                    [@$"C:{DS}VMs{DS}VM{DS}Disk.vhdx",
                                     @$"C:{DS}VMs{DS}VM{DS}State.bin"]);

            var cutFile = g.DataPaths[1];
            var paths = new[] { "%HYPERV%" };
            var filter = $"-{MakeHyperVSource(g.ID)}{DS}{cutFile}";
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { g } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            Assert.That(paths, Does.Contain(g.DataPaths[0]), "unaffected file missing");
            Assert.That(paths, Does.Not.Contain(cutFile), "excluded file leaked in");
        }

        [Test]
        public void Include_then_exclude_results_in_inclusion()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var g = new HyperVGuest("VM", Guid.NewGuid(),
                                    [@$"C:{DS}VMs{DS}VM{DS}disk.vhdx"]);

            var include = MakeHyperVSource(g.ID);
            var filter = $"{include}{PS}-{include}";
            var paths = new[] { "%HYPERV%" };
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { g } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            Assert.That(paths, Does.Contain(g.DataPaths.First()),
                        "first-seen INCLUDE should win over later EXCLUDE");
        }

        [Test]
        public void Exclude_then_include_results_in_exclusion()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var g = new HyperVGuest("VM", Guid.NewGuid(),
                                    [@$"C:{DS}VMs{DS}VM{DS}disk.vhdx"]);

            var include = MakeHyperVSource(g.ID);
            var filter = $"-{include}{PS}{include}";
            var paths = new[] { "%HYPERV%" };
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { g } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            Assert.That(paths, Does.Not.Contain(g.DataPaths.First()),
                        "first-seen EXCLUDE should win over later INCLUDE");
        }

        [Test]
        public void Direct_guest_source_path_includes_that_guest_only()
        {
            if (!OperatingSystem.IsWindows())
                return; // Hyper-V is only available on Windows

            var gWanted = new HyperVGuest("Wanted", Guid.NewGuid(),
                                          [@$"E:{DS}VMs{DS}Wanted{DS}disk.vhdx"]);
            var gOther = new HyperVGuest("Skip", Guid.NewGuid(),
                                          [@$"E:{DS}VMs{DS}Skip{DS}disk.vhdx"]);

            var paths = new[] { MakeHyperVSource(gWanted.ID) };  // ← only that guest
            var filter = string.Empty;
            var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            { ["snapshot-policy"] = "required" };

            var util = new MockHyperVUtility { Guests = new List<HyperVGuest> { gWanted, gOther } };

            using var _ = Duplicati.Library.Logging.Log.StartScope(new LogSink());
            new Duplicati.Library.Modules.Builtin.HyperVOptions()
                .RealParseSourcePaths(ref paths, ref filter, options, util);

            Assert.That(paths, Does.Contain(gWanted.DataPaths.First()), "wanted VM missing");
            Assert.That(paths, Does.Not.Contain(gOther.DataPaths.First()), "unexpected VM leaked in");
        }
    }
}