using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
/// archreview 06/S-11 — verifies the net-new <see cref="ServerHistorianOptionsValidator"/> (built on
/// the shared <c>ZB.MOM.WW.Configuration</c> <c>OptionsValidatorBase</c>/<c>ValidationBuilder</c>).
/// The fail tier is deliberately narrow: only <b>provably-crashing</b> configs fail — an enabled
/// historian (or an enabled <c>AlarmHistorian</c>, which sources its gateway connection from the
/// <c>ServerHistorian</c> section) with an empty / non-absolute / non-http(s) <c>Endpoint</c>, i.e.
/// exactly the configs that would otherwise throw <see cref="System.UriFormatException"/> deep in the
/// gateway client factory at startup. Empty <c>ApiKey</c> / non-positive <c>MaxTieClusterOverfetch</c>
/// stay operator warnings in <see cref="ServerHistorianOptions.Validate"/> (they degrade, not crash).
/// </summary>
public sealed class ServerHistorianOptionsValidatorTests
{
    private static ServerHistorianOptionsValidator Sut(bool alarmHistorianEnabled = false)
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AlarmHistorian:Enabled"] = alarmHistorianEnabled ? "true" : "false",
            })
            .Build();
        return new ServerHistorianOptionsValidator(configuration);
    }

    /// <summary>Enabled historian with an empty endpoint fails (would crash-loop in the factory).</summary>
    [Fact]
    public void Enabled_with_empty_endpoint_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "" });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
    }

    /// <summary>Enabled with a relative (non-absolute) URI fails.</summary>
    [Fact]
    public void Enabled_with_relative_uri_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "host:5222" });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
    }

    /// <summary>Enabled with a non-http(s) scheme (ftp) fails.</summary>
    [Fact]
    public void Enabled_with_bad_scheme_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "ftp://host:5222" });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
    }

    /// <summary>
    /// Enabled with a valid absolute https endpoint <b>and a key</b> succeeds. The key is not
    /// incidental: this case previously omitted it and still asserted success, which pinned a config
    /// that actually crash-loops the host — see <see cref="Consumed_with_empty_api_key_fails"/>.
    /// </summary>
    [Fact]
    public void Enabled_with_valid_https_endpoint_succeeds()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "https://host:5222", ApiKey = "histgw_id_secret",
        });

        result.Succeeded.ShouldBeTrue();
    }

    /// <summary>A disabled section may legitimately be empty — no failure.</summary>
    [Fact]
    public void Disabled_with_empty_endpoint_succeeds()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" });

        result.Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// The residual corner: <c>ServerHistorian:Enabled=false</c> but <c>AlarmHistorian:Enabled=true</c>
    /// (which sources its connection from the ServerHistorian section) with an empty endpoint fails —
    /// and the message names BOTH sections so the operator understands why a disabled section is required.
    /// </summary>
    [Fact]
    public void Disabled_but_alarm_historian_enabled_with_empty_endpoint_fails_naming_both_sections()
    {
        var result = Sut(alarmHistorianEnabled: true)
            .Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f =>
            f.Contains("ServerHistorian:Endpoint") && f.Contains("AlarmHistorian:Enabled"));
    }

    /// <summary>Alarm-only mode with a valid endpoint and a key succeeds.</summary>
    [Fact]
    public void Disabled_but_alarm_historian_enabled_with_valid_endpoint_succeeds()
    {
        var result = Sut(alarmHistorianEnabled: true)
            .Validate(null, new ServerHistorianOptions
            {
                Enabled = false, Endpoint = "https://host:5222", ApiKey = "histgw_id_secret",
            });

        result.Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// An empty <c>ApiKey</c> on a consumed section fails, because it <b>crashes</b> — it does not
    /// degrade.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         Found by the LocalDb Phase 2 live gate. The rig booted with
    ///         <c>AlarmHistorian:Enabled=true</c> and a valid endpoint but no key, and every driver
    ///         node crash-looped on
    ///         <c>ArgumentException: The gateway API key must not be empty (Parameter 'ApiKey')</c>
    ///         thrown by <c>HistorianGatewayClientOptions.Validate()</c> — reached through
    ///         <c>GatewayHistorian.CreateAlarmWriter</c> during Akka startup.
    ///     </para>
    ///     <para>
    ///         That is the *same* factory path, and the same crash-loop-under-a-restart-policy
    ///         failure mode, this validator was built to convert into a named
    ///         <c>OptionsValidationException</c>. The previous "empty ApiKey degrades, the gateway
    ///         just rejects calls" premise was simply wrong: the client validates its own options
    ///         at construction, so the process never reaches the point of making a call.
    ///     </para>
    /// </remarks>
    [Fact]
    public void Consumed_with_empty_api_key_fails()
    {
        var result = Sut(alarmHistorianEnabled: true)
            .Validate(null, new ServerHistorianOptions
            {
                Enabled = false, Endpoint = "https://host:5222", ApiKey = "",
            });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f =>
            f.Contains("ServerHistorian:ApiKey") && f.Contains("AlarmHistorian:Enabled"));
    }

    /// <summary>The read path has the identical crash, so it is gated identically.</summary>
    [Fact]
    public void Enabled_with_empty_api_key_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "https://host:5222", ApiKey = "",
        });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f => f.Contains("ServerHistorian:ApiKey"));
    }

    /// <summary>
    /// The key must never be echoed. An endpoint is not a secret and is quoted to make the error
    /// actionable; a key is, so the failure names only the setting.
    /// </summary>
    [Fact]
    public void Api_key_failure_never_echoes_the_key()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "https://host:5222", ApiKey = "   ",
        });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldNotContain(f => f.Contains("   ", StringComparison.Ordinal));
    }

    /// <summary>A section nobody consumes may be keyless — no failure.</summary>
    [Fact]
    public void Disabled_with_empty_api_key_succeeds()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = false, ApiKey = "" });

        result.Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// <c>UseTls=true</c> against an <c>http://</c> endpoint fails — the third member of the
    /// crash-at-construction family, and the easiest of the three for an operator to trip.
    /// </summary>
    /// <remarks>
    ///     Also found by the Phase 2 live gate, immediately after the <c>ApiKey</c> fix: the rig points
    ///     at a deliberately unresolvable <c>http://</c> endpoint, and <c>UseTls</c> defaults to
    ///     <see langword="true"/>, so every node crash-looped a second time on
    ///     <c>ArgumentException: UseTls requires an https gateway endpoint</c>. Switching an endpoint
    ///     from https to http without clearing <c>UseTls</c> is an ordinary migration slip, and it takes
    ///     the host down rather than degrading it.
    /// </remarks>
    [Fact]
    public void Consumed_with_use_tls_against_http_endpoint_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "http://host:5222", ApiKey = "histgw_id_secret", UseTls = true,
        });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f =>
            f.Contains("ServerHistorian:UseTls") && f.Contains("http://host:5222"));
    }

    /// <summary>An http endpoint with TLS explicitly off is the valid h2c shape — no failure.</summary>
    [Fact]
    public void Consumed_with_http_endpoint_and_tls_off_succeeds()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "http://host:5222", ApiKey = "histgw_id_secret", UseTls = false,
        });

        result.Succeeded.ShouldBeTrue();
    }

    /// <summary>
    /// The mirror slip — <c>UseTls=false</c> against an <c>https://</c> endpoint — also fails, so the
    /// scheme and the flag are pinned to agree in both directions rather than in one.
    /// </summary>
    [Fact]
    public void Consumed_with_tls_off_against_https_endpoint_fails()
    {
        var result = Sut().Validate(null, new ServerHistorianOptions
        {
            Enabled = true, Endpoint = "https://host:5222", ApiKey = "histgw_id_secret", UseTls = false,
        });

        result.Failed.ShouldBeTrue();
        result.Failures.ShouldContain(f => f.Contains("ServerHistorian:UseTls"));
    }

    /// <summary>
    /// Wiring guard (the "register-AND-consume" trap): resolving <c>IOptions&lt;ServerHistorianOptions&gt;.Value</c>
    /// after <see cref="ServiceCollectionExtensions.AddValidatedOptions{TOptions,TValidator}"/> throws
    /// <see cref="OptionsValidationException"/> for an enabled+empty section — proving the validator is
    /// attached to the options pipeline (not merely defined). <c>ValidateOnStart</c> reaching host start
    /// is already proven by the LDAP precedent.
    /// </summary>
    [Fact]
    public void AddValidatedOptions_wiring_throws_on_enabled_empty_endpoint()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ServerHistorian:Enabled"] = "true",
                ["ServerHistorian:Endpoint"] = "",
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(configuration);
        services.AddValidatedOptions<ServerHistorianOptions, ServerHistorianOptionsValidator>(
            configuration, ServerHistorianOptions.SectionName);
        using var provider = services.BuildServiceProvider();

        var ex = Should.Throw<OptionsValidationException>(
            () => _ = provider.GetRequiredService<IOptions<ServerHistorianOptions>>().Value);
        ex.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
    }
}
