using Mbproxy.Options;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Options;

/// <summary>
/// Verifies that <see cref="MbproxyOptions"/> binds correctly from
/// <see cref="IConfiguration"/> and that schema-level validation fires.
/// </summary>
[Trait("Category", "Unit")]
public sealed class MbproxyOptionsBindingTests
{
    // -------------------------------------------------------------------------
    // Helper: build MbproxyOptions directly from an in-memory configuration.
    // We configure the DI container with IConfiguration so BindConfiguration works.
    // -------------------------------------------------------------------------
    private static MbproxyOptions BindOptions(Dictionary<string, string?> values)
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(values)
            .Build();

        var services = new ServiceCollection();
        // Register IConfiguration so BindConfiguration("Mbproxy") can resolve it.
        services.AddSingleton<IConfiguration>(config);
        services
            .AddOptions<MbproxyOptions>()
            .BindConfiguration("Mbproxy");

        var provider = services.BuildServiceProvider();
        return provider.GetRequiredService<IOptionsMonitor<MbproxyOptions>>().CurrentValue;
    }

    // -------------------------------------------------------------------------
    // Test 1 — global BCD tags bind correctly
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_BindsGlobalBcdTags_From_appsettings()
    {
        var options = BindOptions(new Dictionary<string, string?>
        {
            ["Mbproxy:BcdTags:Global:0:Address"] = "1072",
            ["Mbproxy:BcdTags:Global:0:Width"] = "16",
            ["Mbproxy:BcdTags:Global:1:Address"] = "1080",
            ["Mbproxy:BcdTags:Global:1:Width"] = "32",
        });

        options.BcdTags.Global.Count.ShouldBe(2);
        options.BcdTags.Global[0].Address.ShouldBe((ushort)1072);
        options.BcdTags.Global[0].Width.ShouldBe((byte)16);
        options.BcdTags.Global[1].Address.ShouldBe((ushort)1080);
        options.BcdTags.Global[1].Width.ShouldBe((byte)32);
    }

    // -------------------------------------------------------------------------
    // Test 2 — per-PLC Add and Remove override lists bind correctly
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_BindsPerPlcAddAndRemove()
    {
        var options = BindOptions(new Dictionary<string, string?>
        {
            ["Mbproxy:Plcs:0:Name"] = "Line1-Mixer",
            ["Mbproxy:Plcs:0:ListenPort"] = "5020",
            ["Mbproxy:Plcs:0:Host"] = "10.0.1.1",
            ["Mbproxy:Plcs:0:BcdTags:Add:0:Address"] = "1200",
            ["Mbproxy:Plcs:0:BcdTags:Add:0:Width"] = "32",
            ["Mbproxy:Plcs:0:BcdTags:Remove:0"] = "1080",
        });

        options.Plcs.Count.ShouldBe(1);
        var plc = options.Plcs[0];
        plc.Name.ShouldBe("Line1-Mixer");
        plc.ListenPort.ShouldBe(5020);
        plc.Host.ShouldBe("10.0.1.1");
        plc.BcdTags.ShouldNotBeNull();
        plc.BcdTags!.Add.Count.ShouldBe(1);
        plc.BcdTags.Add[0].Address.ShouldBe((ushort)1200);
        plc.BcdTags.Add[0].Width.ShouldBe((byte)32);
        plc.BcdTags.Remove.Count.ShouldBe(1);
        plc.BcdTags.Remove[0].ShouldBe((ushort)1080);
    }

    // -------------------------------------------------------------------------
    // Test 3 — defaults apply when the "Mbproxy" section is absent
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_DefaultsAreApplied_WhenSectionMissing()
    {
        var options = BindOptions(new Dictionary<string, string?>());

        options.AdminPort.ShouldBe(8080);
        options.Connection.BackendConnectTimeoutMs.ShouldBe(3000);
        options.Connection.BackendRequestTimeoutMs.ShouldBe(3000);
        options.Resilience.BackendConnect.MaxAttempts.ShouldBe(3);
        options.Resilience.BackendConnect.BackoffMs.ShouldBe([100, 500, 2000]);
        options.Resilience.ListenerRecovery.SteadyStateMs.ShouldBe(30000);
        options.Resilience.ListenerRecovery.InitialBackoffMs.ShouldBe([1000, 2000, 5000, 15000, 30000]);
        options.Plcs.ShouldBeEmpty();
        options.BcdTags.Global.ShouldBeEmpty();

        // Keepalive defaults — enabled, with the documented timer values.
        options.Connection.Keepalive.Enabled.ShouldBeTrue();
        options.Connection.Keepalive.TcpIdleTimeMs.ShouldBe(30000);
        options.Connection.Keepalive.TcpProbeIntervalMs.ShouldBe(5000);
        options.Connection.Keepalive.TcpProbeCount.ShouldBe(4);
        options.Connection.Keepalive.BackendHeartbeatIdleMs.ShouldBe(30000);
        options.Connection.Keepalive.BackendHeartbeatProbeAddress.ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // Test 5 — the Connection:Keepalive block binds from configuration
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_BindsKeepaliveBlock()
    {
        var options = BindOptions(new Dictionary<string, string?>
        {
            ["Mbproxy:Connection:Keepalive:Enabled"] = "false",
            ["Mbproxy:Connection:Keepalive:TcpIdleTimeMs"] = "45000",
            ["Mbproxy:Connection:Keepalive:TcpProbeIntervalMs"] = "7000",
            ["Mbproxy:Connection:Keepalive:TcpProbeCount"] = "6",
            ["Mbproxy:Connection:Keepalive:BackendHeartbeatIdleMs"] = "20000",
            ["Mbproxy:Connection:Keepalive:BackendHeartbeatProbeAddress"] = "1024",
        });

        var ka = options.Connection.Keepalive;
        ka.Enabled.ShouldBeFalse();
        ka.TcpIdleTimeMs.ShouldBe(45000);
        ka.TcpProbeIntervalMs.ShouldBe(7000);
        ka.TcpProbeCount.ShouldBe(6);
        ka.BackendHeartbeatIdleMs.ShouldBe(20000);
        ka.BackendHeartbeatProbeAddress.ShouldBe(1024);
    }

    // -------------------------------------------------------------------------
    // Test 4 — validator rejects Width != 16 && != 32 (schema-level only)
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_RejectsInvalidWidth()
    {
        // Build options with an invalid Width=8.
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Mbproxy:BcdTags:Global:0:Address"] = "1072",
                ["Mbproxy:BcdTags:Global:0:Width"] = "8",   // invalid: not 16 or 32
            })
            .Build();

        // Get<T> creates a new instance and populates it — works with init-only properties.
        var options = config.GetSection("Mbproxy").Get<MbproxyOptions>() ?? new MbproxyOptions();

        // Call the validator directly to check schema-level rejection.
        var validator = new MbproxyOptionsValidator();
        var result = validator.Validate(null, options);

        result.Failed.ShouldBeTrue("Width=8 should fail schema validation");
        result.Failures.ShouldNotBeEmpty();
    }

    // -------------------------------------------------------------------------
    // Test 6 — every shipped install template (Windows + Linux) loads as JSONC,
    //          binds to MbproxyOptions, and passes schema validation. This catches
    //          a malformed template at build time and keeps the two platform
    //          variants in lockstep.
    // -------------------------------------------------------------------------
    [Theory]
    [InlineData("mbproxy.config.template.json")]
    [InlineData("mbproxy.linux.config.template.json")]
    public void MbproxyOptionsBinding_ShippedInstallTemplate_BindsAndValidates(string templateFileName)
    {
        var templatePath = ResolveInstallFile(templateFileName);

        // The templates are JSONC; the .NET JSON config provider skips // and /* */
        // comments and allows trailing commas, so AddJsonFile loads them directly.
        var config = new ConfigurationBuilder()
            .AddJsonFile(templatePath, optional: false)
            .Build();

        var options = config.GetSection("Mbproxy").Get<MbproxyOptions>() ?? new MbproxyOptions();

        var result = new MbproxyOptionsValidator().Validate(null, options);
        result.Succeeded.ShouldBeTrue(
            $"{templateFileName} must pass schema validation — failures: " +
            string.Join("; ", result.Failures ?? []));
    }

    // -------------------------------------------------------------------------
    // Test 7 — AdminPushIntervalMs (SignalR dashboard push cadence)
    // -------------------------------------------------------------------------
    [Fact]
    public void MbproxyOptionsBinding_AdminPushIntervalMs_DefaultsTo1000()
    {
        var options = BindOptions(new Dictionary<string, string?>());

        options.AdminPushIntervalMs.ShouldBe(1000);
    }

    [Fact]
    public void MbproxyOptionsBinding_AdminPushIntervalMs_BindsConfiguredValue()
    {
        var options = BindOptions(new Dictionary<string, string?>
        {
            ["Mbproxy:AdminPushIntervalMs"] = "250",
        });

        options.AdminPushIntervalMs.ShouldBe(250);
    }

    /// <summary>
    /// Resolves an <c>install/</c> file by walking up from the test assembly directory.
    /// Works from both the Windows dev box and the Linux test box.
    /// </summary>
    private static string ResolveInstallFile(string fileName)
    {
        for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
        {
            var candidate = Path.Combine(dir.FullName, "install", fileName);
            if (File.Exists(candidate))
                return candidate;
        }
        throw new FileNotFoundException(
            $"Could not locate install/{fileName} above {AppContext.BaseDirectory}");
    }
}
