using System.Security.Cryptography;
using Autofac;
using Moq;
using NUnit.Framework;

namespace e_suite.DependencyInjection.UnitTests;

[TestFixture]
public class ModuleDiscoveryUnitTests
{
    [Test]
    public void LoadOrCreateModuleCache_UsesExistingCache_WhenHashMatches()
    {
        // Arrange
        var fileSystem = new Mock<IFileSystem>();
        var assemblyLoader = new Mock<IAssemblyLoader>();
        var identity = new Mock<IAssemblyIdentity>();

        var basePath = AppContext.BaseDirectory;
        var cacheDirectory = Path.Combine(basePath, ".cache");
        var cachePath = Path.Combine(cacheDirectory, "module-cache.json");

        // The hash we expect the discovery code to compute
        var fakeBytes = new byte[] { 1, 2, 3, 4 };
        var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes));

        // Existing cache with the SAME hash
        var existingCacheJson = $$"""
                                  {
                                      "AssemblyHash": "{{expectedHash}}",
                                      "Modules": [ "TestModule.dll" ]
                                  }
                                  """;

        // Mock filesystem behaviour
        fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory));
        fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true);
        fileSystem.Setup(fs => fs.FileExists(Path.Combine(basePath, "TestModule.dll"))).Returns(true);
        fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson);


        // Mock assembly identity → returns an assembly whose hash we control
        var assembly = typeof(ModuleDiscoveryUnitTests).Assembly;
        identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly);

        // Mock hashing stream
        // The bytes here don't matter — the test only cares that the hash matches
        fileSystem.Setup(fs => fs.OpenRead(assembly.Location))
            .Returns(new MemoryStream(fakeBytes));

        // Mock assembly loader for the module listed in the cache
        assemblyLoader.Setup(l => l.LoadFrom(Path.Combine(basePath, "TestModule.dll")))
            .Returns(assembly);

        var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object);

        // Act
        discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder());

        // Assert: cache should NOT be rewritten
        fileSystem.Verify(fs => fs.WriteAllText(It.IsAny<string>(), It.IsAny<string>()), Times.Never);

        // Assert: existing cache was read
        fileSystem.Verify(fs => fs.ReadAllText(cachePath), Times.Once);
    }

    [Test]
    public void LoadOrCreateModuleCache_RegeneratesCache_WhenHashDoesNotMatch()
    {
        // Arrange
        var fileSystem = new Mock<IFileSystem>();
        var assemblyLoader = new Mock<IAssemblyLoader>();
        var identity = new Mock<IAssemblyIdentity>();

        var basePath = AppContext.BaseDirectory;
        var cacheDirectory = Path.Combine(basePath, ".cache");
        var cachePath = Path.Combine(cacheDirectory, "module-cache.json");

        var existingCacheJson = """
                                {
                                    "AssemblyHash": "OLDHASH",
                                    "Modules": [ "OldModule.dll" ]
                                }
                                """;

        fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory));
        fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true);
        fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson);

        string? writtenJson = null;
        fileSystem.Setup(fs => fs.WriteAllText(cachePath, It.IsAny<string>()))
            .Callback<string, string>((_, json) => writtenJson = json);

        var assembly = typeof(TestRegistration).Assembly;
        identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly);

        var fakeStream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
        fileSystem.Setup(fs => fs.OpenRead(assembly.Location)).Returns(fakeStream);

        var newModulePath = Path.Combine(basePath, "NewModule.dll");

        // Regeneration scan
        fileSystem.Setup(fs => fs.GetFiles(basePath, "*.dll"))
            .Returns(new[] { newModulePath });

        // After regeneration, RegisterAllModuleIocRegistrations will check FileExists on fullPath
        fileSystem.Setup(fs => fs.FileExists(newModulePath)).Returns(true);

        assemblyLoader.Setup(l => l.LoadFrom(newModulePath))
            .Returns(typeof(TestRegistration).Assembly);

        var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object);

        // Act
        discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder());

        // Assert
        fileSystem.Verify(fs => fs.WriteAllText(cachePath, It.IsAny<string>()), Times.Once);
        fileSystem.Verify(fs => fs.GetFiles(basePath, "*.dll"), Times.Once);

        Assert.That(writtenJson, Is.Not.Null);
        Assert.That(writtenJson, Does.Contain("NewModule.dll"));
    }

    [Test]
    public void RegisterAllModuleIocRegistrations_Throws_WhenCachedModuleMissing()
    {
        // Arrange
        var fileSystem = new Mock<IFileSystem>();
        var assemblyLoader = new Mock<IAssemblyLoader>();
        var identity = new Mock<IAssemblyIdentity>();

        var basePath = AppContext.BaseDirectory;
        var cacheDirectory = Path.Combine(basePath, ".cache");
        var cachePath = Path.Combine(cacheDirectory, "module-cache.json");

        // Fake bytes for hashing
        var fakeBytes = new byte[] { 1, 2, 3, 4 };
        var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes));

        // Cache contains a module that does NOT exist
        var existingCacheJson = $$"""
                                  {
                                      "AssemblyHash": "{{expectedHash}}",
                                      "Modules": [ "MissingModule.dll" ]
                                  }
                                  """;

        // Mock filesystem
        fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory));
        fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true);
        fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson);

        // Hashing stream
        var assembly = typeof(ModuleDiscoveryUnitTests).Assembly;
        identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly);
        fileSystem.Setup(fs => fs.OpenRead(assembly.Location))
            .Returns(new MemoryStream(fakeBytes));

        // Critical: Missing module file → FileExists returns false
        var missingModulePath = Path.Combine(basePath, "MissingModule.dll");
        fileSystem.Setup(fs => fs.FileExists(missingModulePath)).Returns(false);

        var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object);

        // Act + Assert
        var ex = Assert.Throws<FileNotFoundException>(() =>
            discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder()));

        Assert.That(ex!.Message, Does.Contain("MissingModule.dll"));
    }

    [Test]
    public void RegenerateModuleCache_IgnoresAssemblyLoadFailures()
    {
        // Arrange
        var fileSystem = new Mock<IFileSystem>();
        var assemblyLoader = new Mock<IAssemblyLoader>();
        var identity = new Mock<IAssemblyIdentity>();

        var basePath = AppContext.BaseDirectory;
        var cacheDirectory = Path.Combine(basePath, ".cache");
        var cachePath = Path.Combine(cacheDirectory, "module-cache.json");

        // Existing cache with WRONG hash → forces regeneration
        var existingCacheJson = """
                                {
                                    "AssemblyHash": "OLDHASH",
                                    "Modules": [ "OldModule.dll" ]
                                }
                                """;

        fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory));
        fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true);
        fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson);

        // Capture regenerated JSON
        string? writtenJson = null;
        fileSystem.Setup(fs => fs.WriteAllText(cachePath, It.IsAny<string>()))
            .Callback<string, string>((_, json) => writtenJson = json);

        // Hashing setup
        var assembly = typeof(TestRegistration).Assembly;
        identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly);

        var fakeStream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
        fileSystem.Setup(fs => fs.OpenRead(assembly.Location)).Returns(fakeStream);

        // Regeneration scan finds a DLL
        var badDllPath = Path.Combine(basePath, "BadModule.dll");
        fileSystem.Setup(fs => fs.GetFiles(basePath, "*.dll"))
            .Returns(new[] { badDllPath });

        // Critical: assembly load fails
        assemblyLoader.Setup(l => l.LoadFrom(badDllPath))
            .Throws(new BadImageFormatException("Not a .NET assembly"));

        // After regeneration, RegisterAllModuleIocRegistrations will check FileExists
        // but since no modules are added, this path is never hit.
        // Still safe to mock it as false.
        fileSystem.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(false);

        var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object);

        // Act
        discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder());

        // Assert: regeneration happened
        fileSystem.Verify(fs => fs.GetFiles(basePath, "*.dll"), Times.Once);

        // Assert: cache was rewritten
        fileSystem.Verify(fs => fs.WriteAllText(cachePath, It.IsAny<string>()), Times.Once);

        // Assert: regenerated cache contains NO modules
        Assert.That(writtenJson, Is.Not.Null);
        Assert.That(writtenJson, Does.Not.Contain("BadModule.dll"));
    }

    [Test]
    public void RegisterAllModuleIocRegistrations_InvokesRegistrationTypes()
    {
        // Arrange
        TestRegistration.WasCalled = false;

        var fileSystem = new Mock<IFileSystem>();
        var assemblyLoader = new Mock<IAssemblyLoader>();
        var identity = new Mock<IAssemblyIdentity>();

        var basePath = AppContext.BaseDirectory;
        var cacheDirectory = Path.Combine(basePath, ".cache");
        var cachePath = Path.Combine(cacheDirectory, "module-cache.json");

        // Fake hash bytes
        var fakeBytes = new byte[] { 1, 2, 3, 4 };
        var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes));

        // Cache contains our test module
        var existingCacheJson = $$"""
                                  {
                                      "AssemblyHash": "{{expectedHash}}",
                                      "Modules": [ "TestModule.dll" ]
                                  }
                                  """;

        // Filesystem mocks
        fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory));
        fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true);
        fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson);

        // Hashing stream
        var assembly = typeof(TestRegistration).Assembly;
        identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly);
        fileSystem.Setup(fs => fs.OpenRead(assembly.Location))
            .Returns(new MemoryStream(fakeBytes));

        // Module file exists
        var modulePath = Path.Combine(basePath, "TestModule.dll");
        fileSystem.Setup(fs => fs.FileExists(modulePath)).Returns(true);

        // Assembly loader returns assembly containing TestRegistration
        assemblyLoader.Setup(l => l.LoadFrom(modulePath))
            .Returns(typeof(TestRegistration).Assembly);

        var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object);

        var builder = new ContainerBuilder();

        // Act
        discovery.RegisterAllModuleIocRegistrations(builder);

        // Assert
        Assert.That(TestRegistration.WasCalled, Is.True);
    }
}