using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Server.Configuration;

namespace ZB.MOM.WW.MxGateway.Tests.Configuration;

public sealed class GatewayOptionsValidatorTests
{
    // Constructs the minimal valid GatewayOptions by relying on each sub-option's
    // design-default values; those defaults are validated separately in GatewayOptionsTests.
    private static GatewayOptions ValidOptions() => new();

    // Returns enabled LDAP options that pass all checks except Port.
    // The class defaults already satisfy the blank-field checks; we only
    // override Enabled (must be true to exercise the port check) and Port.
    private static LdapOptions LdapOptionsWithPort(int port) => new()
    {
        Enabled = true,
        Port = port,
    };

    private static GatewayOptions CloneWithLdap(GatewayOptions source, LdapOptions ldap)
        => new()
        {
            Authentication = source.Authentication,
            Ldap = ldap,
            Worker = source.Worker,
            Sessions = source.Sessions,
            Events = source.Events,
            Dashboard = source.Dashboard,
            Protocol = source.Protocol,
            Alarms = source.Alarms,
            Tls = source.Tls,
        };

    private static GatewayOptions CloneWithTls(GatewayOptions source, TlsOptions tls)
        => new()
        {
            Authentication = source.Authentication,
            Ldap = source.Ldap,
            Worker = source.Worker,
            Sessions = source.Sessions,
            Events = source.Events,
            Dashboard = source.Dashboard,
            Protocol = source.Protocol,
            Alarms = source.Alarms,
            Tls = tls,
        };

    [Fact]
    public void Validate_Succeeds_WithDefaultTlsOptions()
    {
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, ValidOptions());
        Assert.True(result.Succeeded);
    }

    [Fact]
    public void Validate_Fails_WhenTlsValidityYearsOutOfRange()
    {
        GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 0 });
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
    }

    [Fact]
    public void Validate_Fails_WhenTlsValidityYearsTooLarge()
    {
        GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 101 });
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
    }

    [Fact]
    public void Validate_Fails_WhenAdditionalDnsNameBlank()
    {
        GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { AdditionalDnsNames = [" "] });
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames"));
    }

    [Fact]
    public void Validate_Fails_WhenSelfSignedCertPathBlank()
    {
        GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { SelfSignedCertPath = "  " });
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank."));
    }

    [Fact]
    public void Validate_Fails_WhenLdapPortIsZero()
    {
        GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(0));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(
            result.Failures!,
            f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)"));
    }

    [Fact]
    public void Validate_Fails_WhenLdapPortExceedsMaximum()
    {
        GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(70000));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(
            result.Failures!,
            f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)"));
    }

    [Fact]
    public void Validate_Succeeds_WhenLdapEnabledWithValidPort()
    {
        GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(389));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    // -------------------------------------------------------------------------
    // AlarmFallbackOptions validation
    // -------------------------------------------------------------------------

    private static AlarmsOptions EnabledAlarmsWithFallback(AlarmFallbackOptions fallback) => new()
    {
        Enabled = true,
        DefaultArea = "Galaxy",
        Fallback = fallback,
    };

    private static GatewayOptions CloneWithAlarms(GatewayOptions source, AlarmsOptions alarms)
        => new()
        {
            Authentication = source.Authentication,
            Ldap = source.Ldap,
            Worker = source.Worker,
            Sessions = source.Sessions,
            Events = source.Events,
            Dashboard = source.Dashboard,
            Protocol = source.Protocol,
            Alarms = alarms,
            Tls = source.Tls,
        };

    [Fact]
    public void Validate_Succeeds_WhenAlarmsDisabled_FallbackNotValidated()
    {
        // Even an invalid Mode is acceptable when Enabled = false.
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            new AlarmsOptions
            {
                Enabled = false,
                Fallback = new AlarmFallbackOptions { Mode = "InvalidMode" },
            });
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    [Fact]
    public void Validate_Succeeds_WhenAlarmsEnabled_DefaultAutoConfig()
    {
        // Default AlarmFallbackOptions (Mode="Auto") must pass validation when alarms are enabled.
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions()));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    [Theory]
    [InlineData("Auto")]
    [InlineData("ForceAlarmManager")]
    [InlineData("ForceSubtag")]
    [InlineData("auto")]
    [InlineData("FORCESUBTAG")]
    public void Validate_Succeeds_WhenAlarmsEnabled_RecognisedMode(string mode)
    {
        AlarmsOptions alarms = EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = mode });
        GatewayOptions options = CloneWithAlarms(ValidOptions(), alarms);
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    [Fact]
    public void Validate_Fails_WhenAlarmsEnabled_InvalidMode()
    {
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "InvalidMode" }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Alarms:Fallback") && f.Contains("Mode"));
    }

    [Fact]
    public void Validate_Fails_WhenForceSubtag_NoGalaxyRepository_NoIncludes()
    {
        // ForceSubtag without galaxy repository and without IncludeAttributes must fail.
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions
            {
                Mode = "ForceSubtag",
                Discovery = new AlarmDiscoveryOptions
                {
                    UseGalaxyRepository = false,
                    IncludeAttributes = [],
                },
            }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(
            result.Failures!,
            f => f.Contains("ForceSubtag") && f.Contains("Discovery"));
    }

    [Fact]
    public void Validate_Succeeds_WhenForceSubtag_NoGalaxyRepository_WithIncludes()
    {
        // ForceSubtag without galaxy repository is allowed when IncludeAttributes is non-empty.
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions
            {
                Mode = "ForceSubtag",
                Discovery = new AlarmDiscoveryOptions
                {
                    UseGalaxyRepository = false,
                    IncludeAttributes = ["attr1"],
                },
            }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    [Fact]
    public void Validate_Succeeds_WhenForceSubtag_WithGalaxyRepository()
    {
        // ForceSubtag + UseGalaxyRepository=true (default) must pass even without IncludeAttributes.
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions
            {
                Mode = "ForceSubtag",
                Discovery = new AlarmDiscoveryOptions { UseGalaxyRepository = true },
            }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Succeeded);
    }

    [Theory]
    [InlineData(0, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
    [InlineData(-1, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
    public void Validate_Fails_WhenConsecutiveFailureThresholdBelowOne(int value, string keyPart)
    {
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions { ConsecutiveFailureThreshold = value }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains(keyPart));
    }

    [Theory]
    [InlineData(0, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
    [InlineData(-5, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
    public void Validate_Fails_WhenFailbackProbeIntervalSecondsBelowOne(int value, string keyPart)
    {
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackProbeIntervalSeconds = value }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains(keyPart));
    }

    [Theory]
    [InlineData(0, nameof(AlarmFallbackOptions.FailbackStableProbes))]
    [InlineData(-1, nameof(AlarmFallbackOptions.FailbackStableProbes))]
    public void Validate_Fails_WhenFailbackStableProbesBelowOne(int value, string keyPart)
    {
        GatewayOptions options = CloneWithAlarms(
            ValidOptions(),
            EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackStableProbes = value }));
        ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
        Assert.True(result.Failed);
        Assert.Contains(result.Failures!, f => f.Contains(keyPart));
    }
}
