using System.Text;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class LegacyIniDatabaseOptionsLoaderTests
{
    private const string IntegrationPathEnvironmentVariable =
        "MBN_STOCK_LEGACY_INI_INTEGRATION_PATH";

    [Fact]
    public void Load_Utf8LegacyFile_MapsBothProvidersAndKeepsLastValue()
    {
        using var file = TemporaryLegacyIni.Create(
            Encoding.UTF8.GetBytes(
                """
                [Oracle]
                //ConnectionName=ignored.invalid:1/IGNORED
                ConnectionName=oracle.dev.invalid:1522/DEVORA
                ID=old-user
                ID=oracle-user
                Pass=oracle=password

                [Maria]
                ConnectionName=maria.dev.invalid:3307/dev_stock
                ID=maria-user
                Pass=maria=password
                """));
        var loader = new LegacyIniDatabaseOptionsLoader(_ => null);

        var options = loader.Load(file.Path);

        Assert.True(options.IsConfigured(DataSourceKind.Oracle));
        Assert.True(options.IsConfigured(DataSourceKind.MariaDb));
        Assert.Equal("oracle.dev.invalid", options.Oracle.Host);
        Assert.Equal(1522, options.Oracle.Port);
        Assert.Equal("DEVORA", options.Oracle.Sid);
        Assert.Null(options.Oracle.ServiceName);
        Assert.Equal("oracle-user", options.Oracle.UserName);
        Assert.Equal("oracle=password", options.Oracle.Password);
        Assert.Equal("maria.dev.invalid", options.MariaDb.Host);
        Assert.Equal(3307, options.MariaDb.Port);
        Assert.Equal("dev_stock", options.MariaDb.Database);
        Assert.Equal("maria-user", options.MariaDb.UserName);
        Assert.Equal("maria=password", options.MariaDb.Password);
        Assert.Equal(MariaDbTlsMode.Disabled, options.MariaDb.TlsMode);
    }

    [Fact]
    public void Load_Cp949LegacyFile_PreservesKoreanValues()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        var cp949 = Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
        using var file = TemporaryLegacyIni.Create(cp949.GetBytes(
            """
            [Oracle]
            ConnectionName=oracle.dev.invalid:1521/개발
            ID=오라클사용자
            Pass=암호
            [Maria]
            ConnectionName=maria.dev.invalid:3306/주식
            ID=마리아사용자
            Pass=암호2
            """));

        var options = new LegacyIniDatabaseOptionsLoader(_ => null).Load(file.Path);

        Assert.Equal("개발", options.Oracle.Sid);
        Assert.Equal("오라클사용자", options.Oracle.UserName);
        Assert.Equal("주식", options.MariaDb.Database);
        Assert.Equal("마리아사용자", options.MariaDb.UserName);
    }

    [Fact]
    public void Load_EnvironmentStillOverridesLegacyValues()
    {
        using var file = TemporaryLegacyIni.CreateValidUtf8();
        var environment = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["MBN_STOCK_ORACLE_HOST"] = "override-oracle.invalid",
            ["MBN_STOCK_MARIADB_TLS_MODE"] = "Required"
        };
        var loader = new LegacyIniDatabaseOptionsLoader(
            name => environment.TryGetValue(name, out var value) ? value : null);

        var options = loader.Load(file.Path);

        Assert.Equal("override-oracle.invalid", options.Oracle.Host);
        Assert.Equal(MariaDbTlsMode.Required, options.MariaDb.TlsMode);
    }

    [Fact]
    public void Load_InvalidFile_DoesNotExposeFileValues()
    {
        const string secret = "sensitive-password-sentinel";
        using var file = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes(
            $"[Oracle]\nConnectionName=invalid\nID=user\nPass={secret}"));

        var exception = Assert.Throws<DatabaseConfigurationException>(
            () => new LegacyIniDatabaseOptionsLoader(_ => null).Load(file.Path));

        Assert.DoesNotContain(secret, exception.ToString(), StringComparison.Ordinal);
        Assert.DoesNotContain(file.Path, exception.ToString(), StringComparison.Ordinal);
    }

    [Fact]
    public void Runtime_PrefersExecutableResIni_AndFallsBackWhenAbsent()
    {
        using var directory = TemporaryDirectory.Create();
        var res = Path.Combine(directory.Path, "Res");
        Directory.CreateDirectory(res);
        File.WriteAllBytes(
            Path.Combine(res, "MmoneyCoder.ini"),
            TemporaryLegacyIni.ValidUtf8Bytes());

        var runtime = DatabaseRuntime.CreateLegacyExecutableDefault(
            directory.Path,
            Path.Combine(directory.Path, "missing.json"));

        Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);

        File.Delete(Path.Combine(res, "MmoneyCoder.ini"));
        var fallback = DatabaseRuntime.CreateLegacyExecutableDefault(
            directory.Path,
            Path.Combine(directory.Path, "missing.json"),
            Path.Combine(directory.Path, "missing-legacy.ini"));
        Assert.False(fallback.Options.IsConfigured(DataSourceKind.Oracle));
        Assert.False(fallback.Options.IsConfigured(DataSourceKind.MariaDb));
    }

    [Fact]
    public void Runtime_UsesLocalLegacyOverrideWhenPackageDoesNotContainCredentials()
    {
        using var directory = TemporaryDirectory.Create();
        using var overrideFile = TemporaryLegacyIni.CreateValidUtf8();

        var runtime = DatabaseRuntime.CreateLegacyExecutableDefault(
            directory.Path,
            Path.Combine(directory.Path, "missing.json"),
            overrideFile.Path);

        Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);
        Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
    }

    [Fact]
    public void Runtime_UserSelectedResCanExplicitlyPrecedeExecutableCredentials()
    {
        using var directory = TemporaryDirectory.Create();
        var executableRes = Path.Combine(directory.Path, "Res");
        Directory.CreateDirectory(executableRes);
        File.WriteAllBytes(
            Path.Combine(executableRes, "MmoneyCoder.ini"),
            TemporaryLegacyIni.ValidUtf8Bytes());
        using var selected = TemporaryLegacyIni.Create(Encoding.UTF8.GetBytes(
            """
            [Oracle]
            ConnectionName=selected.oracle.invalid:1521/DEVORA
            ID=oracle-user
            Pass=oracle-password
            [Maria]
            ConnectionName=selected.maria.invalid:3306/dev_stock
            ID=maria-user
            Pass=maria-password
            """));

        var runtime = DatabaseRuntime.CreateLegacyExecutableDefault(
            directory.Path,
            Path.Combine(directory.Path, "missing.json"),
            selected.Path,
            preferLegacyOverride: true);

        Assert.Equal("selected.oracle.invalid", runtime.Options.Oracle.Host);
        Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host);
    }

    [Fact]
    public void Runtime_DevelopmentLiveUsesOnlyExactLocalAppDataIni()
    {
        using var localApplicationData = TemporaryDirectory.Create();
        var resourceDirectory = Path.Combine(
            localApplicationData.Path,
            "MBN_STOCK_WEBVIEW",
            "Res");
        Directory.CreateDirectory(resourceDirectory);
        File.WriteAllBytes(
            Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
            TemporaryLegacyIni.ValidUtf8Bytes());

        const string environmentName = "MBN_STOCK_ORACLE_HOST";
        var originalEnvironment = Environment.GetEnvironmentVariable(environmentName);
        DatabaseRuntime runtime;
        try
        {
            Environment.SetEnvironmentVariable(
                environmentName,
                "stale-environment.invalid");
            runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
                localApplicationData.Path);
        }
        finally
        {
            Environment.SetEnvironmentVariable(
                environmentName,
                originalEnvironment);
        }

        Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);
        Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
    }

    [Fact]
    public void Runtime_DevelopmentLiveFailsClosedWhenBothProtectedProfilesAreMissing()
    {
        using var localApplicationData = TemporaryDirectory.Create();
        Directory.CreateDirectory(Path.Combine(
            localApplicationData.Path,
            "MBN_STOCK_WEBVIEW",
            "Config"));

        Assert.Throws<DatabaseConfigurationException>(
            () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
                localApplicationData.Path));
    }

    [Fact]
    public void Runtime_DevelopmentLiveFallsBackToProtectedLocalAppDataJson()
    {
        using var localApplicationData = TemporaryDirectory.Create();
        var configurationDirectory = Path.Combine(
            localApplicationData.Path,
            "MBN_STOCK_WEBVIEW",
            "Config");
        Directory.CreateDirectory(configurationDirectory);
        File.WriteAllText(
            Path.Combine(configurationDirectory, "database.local.json"),
            """
            {
              "oracle": {
                "host": "json.oracle.invalid",
                "port": 1521,
                "sid": "DEVORA",
                "serviceName": null,
                "userName": "oracle-user",
                "password": "oracle-password"
              },
              "mariaDb": {
                "host": "json.maria.invalid",
                "port": 3306,
                "database": "dev_stock",
                "userName": "maria-user",
                "password": "maria-password",
                "tlsMode": "Required"
              }
            }
            """);

        var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
            localApplicationData.Path);

        Assert.Equal("json.oracle.invalid", runtime.Options.Oracle.Host);
        Assert.Equal("json.maria.invalid", runtime.Options.MariaDb.Host);
        Assert.Equal(MariaDbTlsMode.Required, runtime.Options.MariaDb.TlsMode);
    }

    [Fact]
    public void Runtime_DevelopmentLiveRejectsNonOrdinaryIniTarget()
    {
        using var localApplicationData = TemporaryDirectory.Create();
        var iniDirectory = Path.Combine(
            localApplicationData.Path,
            "MBN_STOCK_WEBVIEW",
            "Res",
            "MmoneyCoder.ini");
        Directory.CreateDirectory(iniDirectory);

        Assert.Throws<DatabaseConfigurationException>(
            () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
                localApplicationData.Path));
    }

    [Fact]
    public void Runtime_DevelopmentLiveRejectsNonOrdinaryJsonTarget()
    {
        using var localApplicationData = TemporaryDirectory.Create();
        var jsonDirectory = Path.Combine(
            localApplicationData.Path,
            "MBN_STOCK_WEBVIEW",
            "Config",
            "database.local.json");
        Directory.CreateDirectory(jsonDirectory);

        Assert.Throws<DatabaseConfigurationException>(
            () => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
                localApplicationData.Path));
    }

    [Fact]
    public void Load_OptionalRealLegacyIni_IsConfiguredWithoutPrintingValues()
    {
        var path = Environment.GetEnvironmentVariable(
            IntegrationPathEnvironmentVariable);
        if (string.IsNullOrWhiteSpace(path))
        {
            return;
        }

        var options = new LegacyIniDatabaseOptionsLoader().Load(path);

        Assert.True(options.IsConfigured(DataSourceKind.Oracle));
        Assert.True(options.IsConfigured(DataSourceKind.MariaDb));
    }

    private sealed class TemporaryLegacyIni : IDisposable
    {
        private TemporaryLegacyIni(string directory, string path)
        {
            Directory = directory;
            Path = path;
        }

        public string Directory { get; }

        public string Path { get; }

        public static TemporaryLegacyIni CreateValidUtf8() =>
            Create(ValidUtf8Bytes());

        public static byte[] ValidUtf8Bytes() => Encoding.UTF8.GetBytes(
            """
            [Oracle]
            ConnectionName=oracle.dev.invalid:1521/DEVORA
            ID=oracle-user
            Pass=oracle-password
            [Maria]
            ConnectionName=maria.dev.invalid:3306/dev_stock
            ID=maria-user
            Pass=maria-password
            """);

        public static TemporaryLegacyIni Create(byte[] bytes)
        {
            var directory = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                Guid.NewGuid().ToString("N"));
            System.IO.Directory.CreateDirectory(directory);
            var path = System.IO.Path.Combine(directory, "MmoneyCoder.ini");
            File.WriteAllBytes(path, bytes);
            return new TemporaryLegacyIni(directory, path);
        }

        public void Dispose() =>
            System.IO.Directory.Delete(Directory, recursive: true);
    }

    private sealed class TemporaryDirectory : IDisposable
    {
        private TemporaryDirectory(string path) => Path = path;

        public string Path { get; }

        public static TemporaryDirectory Create()
        {
            var path = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                Guid.NewGuid().ToString("N"));
            System.IO.Directory.CreateDirectory(path);
            return new TemporaryDirectory(path);
        }

        public void Dispose() =>
            System.IO.Directory.Delete(Path, recursive: true);
    }
}
