using Microsoft.Extensions.Options;
using ZB.MOM.WW.Overview.Registry;

namespace ZB.MOM.WW.Overview.Tests;

/// <summary>
/// Registry validation. A malformed registry must fail the host at startup with a message naming
/// the offending key — the alternative is a dashboard that boots and quietly shows nothing, or one
/// that throws on its first poll where nobody is looking.
/// </summary>
public class OverviewOptionsValidatorTests
{
    private static readonly OverviewOptionsValidator Validator = new();

    private static OverviewOptions Valid(Action<OverviewOptions>? mutate = null)
    {
        var options = new OverviewOptions
        {
            PollIntervalSeconds = 10,
            TimeoutSeconds = 3,
            StaleAfterSeconds = 45,
            Applications =
            {
                new ApplicationEntry
                {
                    Name = "OtOpcUa",
                    ManagementLabel = "AdminUI",
                    Instances =
                    {
                        new InstanceEntry
                        {
                            Name = "central-1",
                            Group = "central",
                            BaseUrl = "http://otopcua-central-1:9000",
                            ManagementUrl = "http://otopcua-central-1:9000/",
                            HasActiveRole = true,
                        },
                    },
                },
            },
        };

        mutate?.Invoke(options);
        return options;
    }

    private static ValidateOptionsResult Run(OverviewOptions options) => Validator.Validate(null, options);

    private static void AssertFailsWith(OverviewOptions options, string expectedFragment)
    {
        var result = Run(options);

        Assert.True(result.Failed, "expected validation to fail");
        Assert.Contains(
            result.Failures,
            f => f.Contains(expectedFragment, StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void ValidRegistry_Succeeds()
    {
        // Positive control: without this, every "fails with X" assertion below could be passing
        // because the fixture itself is malformed for some unrelated reason.
        Assert.True(Run(Valid()).Succeeded);
    }

    [Fact]
    public void EmptyRegistry_IsRejected()
    {
        AssertFailsWith(Valid(o => o.Applications.Clear()), "Overview:Applications");
    }

    [Fact]
    public void ApplicationWithNoInstances_IsRejected()
    {
        AssertFailsWith(Valid(o => o.Applications[0].Instances.Clear()), "Instances");
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    public void ApplicationWithoutName_IsRejected(string name)
    {
        AssertFailsWith(Valid(o => o.Applications[0].Name = name), "Applications:0:Name");
    }

    [Fact]
    public void DuplicateApplicationNames_AreRejected()
    {
        AssertFailsWith(
            Valid(o => o.Applications.Add(new ApplicationEntry
            {
                Name = "OtOpcUa",
                Instances = { new InstanceEntry { Name = "other", BaseUrl = "http://other:9000" } },
            })),
            "duplicated");
    }

    [Fact]
    public void DuplicateInstanceNamesWithinAnApplication_AreRejected()
    {
        AssertFailsWith(
            Valid(o => o.Applications[0].Instances.Add(new InstanceEntry
            {
                Name = "central-1",
                BaseUrl = "http://otopcua-central-2:9000",
            })),
            "duplicated");
    }

    [Fact]
    public void SameInstanceNameInDifferentApplications_IsAllowed()
    {
        // "central-1" under OtOpcUa and "central-1" under ScadaBridge are different machines with
        // the same conventional name — uniqueness is per application, not global.
        var options = Valid(o => o.Applications.Add(new ApplicationEntry
        {
            Name = "ScadaBridge",
            Instances = { new InstanceEntry { Name = "central-1", BaseUrl = "http://sb-central-a:5000" } },
        }));

        Assert.True(Run(options).Succeeded);
    }

    [Theory]
    [InlineData("")]
    [InlineData("otopcua-central-1:9000")]   // no scheme
    [InlineData("/health/ready")]            // relative
    [InlineData("ftp://otopcua-central-1")]  // wrong scheme
    public void NonAbsoluteHttpBaseUrl_IsRejected(string baseUrl)
    {
        AssertFailsWith(Valid(o => o.Applications[0].Instances[0].BaseUrl = baseUrl), "BaseUrl");
    }

    [Fact]
    public void RelativeManagementUrl_IsRejected()
    {
        AssertFailsWith(Valid(o => o.Applications[0].Instances[0].ManagementUrl = "/admin"), "ManagementUrl");
    }

    [Fact]
    public void OmittedManagementUrl_IsAllowed()
    {
        Assert.True(Run(Valid(o => o.Applications[0].Instances[0].ManagementUrl = null)).Succeeded);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void NonPositiveGlobalIntervals_AreRejected(int value)
    {
        AssertFailsWith(Valid(o => o.PollIntervalSeconds = value), "PollIntervalSeconds");
        AssertFailsWith(Valid(o => o.TimeoutSeconds = value), "TimeoutSeconds");
        AssertFailsWith(Valid(o => o.StaleAfterSeconds = value), "StaleAfterSeconds");
    }

    [Fact]
    public void StaleWindowNotGreaterThanPollInterval_IsRejected()
    {
        AssertFailsWith(Valid(o => o.StaleAfterSeconds = o.PollIntervalSeconds), "StaleAfterSeconds");
        AssertFailsWith(Valid(o => o.StaleAfterSeconds = o.PollIntervalSeconds - 1), "StaleAfterSeconds");
    }

    [Fact]
    public void StaleWindowIsCheckedOnEffectiveValues_NotGlobals()
    {
        // The globals here are perfectly sane (10 / 45). Only the INSTANCE override inverts the
        // relationship, which a globals-only check would wave straight through.
        AssertFailsWith(
            Valid(o => o.Applications[0].Instances[0].PollIntervalSeconds = 120),
            "effective StaleAfterSeconds");
    }

    [Fact]
    public void ApplicationLevelOverrideCanSatisfyTheStaleRule()
    {
        // The mirror of the previous test: an override that RESTORES the relationship must pass, so
        // the rule is not just "any override fails".
        var options = Valid(o =>
        {
            o.Applications[0].Instances[0].PollIntervalSeconds = 120;
            o.Applications[0].Instances[0].StaleAfterSeconds = 300;
        });

        Assert.True(Run(options).Succeeded);
    }

    [Fact]
    public void AllFailuresAreAccumulated_NotJustTheFirst()
    {
        // OptionsValidatorBase accumulates; an operator fixing a registry one boot at a time is the
        // failure mode this avoids.
        var result = Run(Valid(o =>
        {
            o.Applications[0].Name = string.Empty;
            o.Applications[0].Instances[0].BaseUrl = "not-a-url";
            o.PollIntervalSeconds = 0;
        }));

        Assert.True(result.Failed);
        Assert.True(result.Failures.Count() >= 3, $"expected >= 3 failures, got {result.Failures.Count()}");
    }

    [Theory]
    [InlineData(null, null, null, 10, 3, 45)]      // all defaults
    [InlineData(20, null, null, 20, 3, 45)]        // application override
    [InlineData(20, 30, null, 30, 3, 45)]          // instance beats application
    [InlineData(null, 30, 90, 30, 90, 45)]         // independent fields resolve independently
    public void EffectiveTimings_ResolveInstanceOverApplicationOverGlobal(
        int? applicationPoll, int? instancePoll, int? instanceTimeout,
        int expectedPoll, int expectedTimeout, int expectedStale)
    {
        var options = Valid(o =>
        {
            o.Applications[0].PollIntervalSeconds = applicationPoll;
            o.Applications[0].Instances[0].PollIntervalSeconds = instancePoll;
            o.Applications[0].Instances[0].TimeoutSeconds = instanceTimeout;
        });

        var timings = EffectiveTimings.Resolve(options, options.Applications[0], options.Applications[0].Instances[0]);

        Assert.Equal(TimeSpan.FromSeconds(expectedPoll), timings.PollInterval);
        Assert.Equal(TimeSpan.FromSeconds(expectedTimeout), timings.Timeout);
        Assert.Equal(TimeSpan.FromSeconds(expectedStale), timings.StaleAfter);
    }
}
