using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Registration;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class K3dRegistrationProbeTests
{
    private const string TypeLibraryPath = "C:\\vendor-secret\\typelib-x64.dll";
    private const string EnginePath = "C:\\vendor-secret\\engine-x64.dll";
    private const string EventHandlerPath = "C:\\vendor-secret\\events-x64.dll";

    [Fact]
    public void Probe_WithReciprocalAmd64MachineRegistration_IsReady()
    {
        var binaries = ReadyBinaries();
        var probe = CreateProbe(ReadySnapshot(), binaries);

        var report = probe.Probe();

        Assert.True(report.IsReady);
        Assert.Equal(K3dRegistrationIssue.None, report.Issues);
        Assert.True(report.IsEngineClassReady);
        Assert.True(report.IsEventHandlerClassReady);
        Assert.Equal(
            new[] { TypeLibraryPath, EnginePath, EventHandlerPath },
            binaries.InspectedPaths);
    }

    [Fact]
    public void Probe_WhenEitherDirectionOfReciprocalMappingDiffers_FailsClosed()
    {
        var snapshot = ReadySnapshot() with
        {
            Engine = ReadySnapshot().Engine with
            {
                ClassProgId = "Unexpected.Engine.1",
                ProgIdClassId = K3dComConstants.KaEventHandlerClassId
            },
            EventHandler = ReadySnapshot().EventHandler with
            {
                ProgIdClassId = K3dComConstants.KaEngineClassId
            }
        };
        var probe = CreateProbe(snapshot, ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineProgIdMismatch));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineProgIdClassIdMismatch));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerProgIdClassIdMismatch));
        Assert.False(report.IsEngineClassReady);
        Assert.False(report.IsEventHandlerClassReady);
        AssertSafe(report);
    }

    [Fact]
    public void Probe_WhenEitherClassServerIsNotAmd64_FailsClosed()
    {
        var binaries = ReadyBinaries();
        binaries.Set(EnginePath, exists: true, isAmd64: false);
        binaries.Set(EventHandlerPath, exists: true, isAmd64: false);
        var probe = CreateProbe(ReadySnapshot(), binaries);

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineServerIsNotAmd64));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerServerIsNotAmd64));
        Assert.False(report.IsEngineClassReady);
        Assert.False(report.IsEventHandlerClassReady);
        AssertSafe(report);
    }

    [Fact]
    public void Probe_WhenCurrentUserOverrideExists_FailsClosedWithoutDetails()
    {
        var probe = CreateProbe(
            ReadySnapshot() with { CurrentUserOverridePresent = true },
            ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.CurrentUserOverridePresent));
        Assert.False(report.IsEngineClassReady);
        Assert.False(report.IsEventHandlerClassReady);
        Assert.Contains("재정의", report.Message, StringComparison.Ordinal);
        AssertSafe(report);
    }

    [Fact]
    public void CurrentUserOverrideBoundary_CoversBothClassesProgIdsAndTypeLibrary()
    {
        Assert.Equal(
            new[]
            {
                $"CLSID\\{K3dComConstants.KaEngineClassId}",
                $"CLSID\\{K3dComConstants.KaEventHandlerClassId}",
                K3dComConstants.KaEngineProgId,
                K3dComConstants.KaEventHandlerProgId,
                $"TypeLib\\{K3dComConstants.TypeLibraryId}"
            },
            WindowsK3dRegistrySnapshotSource.CurrentUserOverridePaths);
    }

    [Fact]
    public void Probe_WhenRegistrySnapshotCannotBeRead_ReturnsSafeFailure()
    {
        var probe = new K3dRegistrationProbe(
            new ThrowingSnapshotSource(),
            ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.Equal(K3dRegistrationIssue.RegistryReadFailed, report.Issues);
        AssertSafe(report);
    }

    [Fact]
    public void Probe_WhenProcessOrTypeLibraryIsNotX64_FailsClosed()
    {
        var snapshot = ReadySnapshot() with
        {
            Is64BitProcess = false,
            Win64TypeLibraryPath = null
        };
        var probe = CreateProbe(snapshot, ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.ProcessIsNot64Bit));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.TypeLibraryMissing));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.Win64BinaryMissing));
        AssertSafe(report);
    }

    [Fact]
    public void Probe_WhenClassThreadingAndTypeLibraryDiffer_FailsClosed()
    {
        var snapshot = ReadySnapshot() with
        {
            Engine = ReadySnapshot().Engine with
            {
                ThreadingModel = "Both",
                TypeLibraryId = K3dComConstants.KaEventHandlerClassId
            },
            EventHandler = ReadySnapshot().EventHandler with
            {
                ThreadingModel = "Free",
                TypeLibraryId = K3dComConstants.KaEngineClassId
            }
        };
        var probe = CreateProbe(snapshot, ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineThreadingModelMismatch));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineTypeLibraryMismatch));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerThreadingModelMismatch));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerTypeLibraryMismatch));
        Assert.False(report.IsEngineClassReady);
        Assert.False(report.IsEventHandlerClassReady);
        AssertSafe(report);
    }

    [Fact]
    public void Probe_WhenEitherClassRegistrationIsMissing_FailsClosed()
    {
        var snapshot = ReadySnapshot() with
        {
            Engine = ReadySnapshot().Engine with { ClassKeyPresent = false },
            EventHandler = ReadySnapshot().EventHandler with { ClassKeyPresent = false }
        };
        var probe = CreateProbe(snapshot, ReadyBinaries());

        var report = probe.Probe();

        Assert.False(report.IsReady);
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EngineClassMissing));
        Assert.True(report.Issues.HasFlag(K3dRegistrationIssue.EventHandlerClassMissing));
        Assert.False(report.IsEngineClassReady);
        Assert.False(report.IsEventHandlerClassReady);
        AssertSafe(report);
    }

    private static K3dRegistrationProbe CreateProbe(
        K3dRegistrySnapshot snapshot,
        FakeBinaryInspector binaries) =>
        new(new FakeSnapshotSource(snapshot), binaries);

    private static K3dRegistrySnapshot ReadySnapshot() => new(
        Is64BitProcess: true,
        Win64TypeLibraryPath: TypeLibraryPath,
        Engine: new K3dClassRegistrationSnapshot(
            ClassKeyPresent: true,
            ClassProgId: K3dComConstants.KaEngineProgId,
            ProgIdClassId: K3dComConstants.KaEngineClassId,
            InprocServerPath: EnginePath,
            ThreadingModel: K3dComConstants.ApartmentThreadingModel,
            TypeLibraryId: K3dComConstants.TypeLibraryId),
        EventHandler: new K3dClassRegistrationSnapshot(
            ClassKeyPresent: true,
            ClassProgId: K3dComConstants.KaEventHandlerProgId,
            ProgIdClassId: K3dComConstants.KaEventHandlerClassId,
            InprocServerPath: EventHandlerPath,
            ThreadingModel: K3dComConstants.ApartmentThreadingModel,
            TypeLibraryId: K3dComConstants.TypeLibraryId),
        CurrentUserOverridePresent: false);

    private static FakeBinaryInspector ReadyBinaries() => new FakeBinaryInspector()
        .Set(TypeLibraryPath, exists: true, isAmd64: true)
        .Set(EnginePath, exists: true, isAmd64: true)
        .Set(EventHandlerPath, exists: true, isAmd64: true);

    private static void AssertSafe(K3dRegistrationReport report)
    {
        Assert.DoesNotContain(TypeLibraryPath, report.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(EnginePath, report.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(EventHandlerPath, report.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("HKEY", report.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("CLSID\\", report.Message, StringComparison.OrdinalIgnoreCase);
    }

    private sealed class FakeSnapshotSource : IK3dRegistrySnapshotSource
    {
        private readonly K3dRegistrySnapshot _snapshot;

        public FakeSnapshotSource(K3dRegistrySnapshot snapshot)
        {
            _snapshot = snapshot;
        }

        public K3dRegistrySnapshot Capture() => _snapshot;
    }

    private sealed class ThrowingSnapshotSource : IK3dRegistrySnapshotSource
    {
        public K3dRegistrySnapshot Capture() =>
            throw new UnauthorizedAccessException("HKEY secret registry detail");
    }

    private sealed class FakeBinaryInspector : IPeBinaryInspector
    {
        private readonly Dictionary<string, PeBinaryInspection> _inspections =
            new(StringComparer.OrdinalIgnoreCase);
        private readonly List<string> _inspectedPaths = [];

        public IReadOnlyList<string> InspectedPaths => _inspectedPaths;

        public FakeBinaryInspector Set(string path, bool exists, bool isAmd64)
        {
            _inspections[path] = new PeBinaryInspection(exists, isAmd64);
            return this;
        }

        public PeBinaryInspection Inspect(string? path)
        {
            if (path is null)
            {
                return new PeBinaryInspection(false, false);
            }

            _inspectedPaths.Add(path);
            return _inspections.TryGetValue(path, out var inspection)
                ? inspection
                : new PeBinaryInspection(false, false);
        }
    }
}
