using Mbproxy.Configuration;
using Mbproxy.Options;
using Xunit;

namespace Mbproxy.Tests.Configuration;

/// <summary>
/// Unit tests for <see cref="ReloadValidator.Validate"/>.
/// Each test covers one specific failure mode or the happy path.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ReloadValidatorTests
{
    // ── Helpers ───────────────────────────────────────────────────────────────────────────

    private static PlcOptions MakePlc(string name, int listenPort, string host = "127.0.0.1")
        => new() { Name = name, ListenPort = listenPort, Host = host, Port = 502 };

    private static MbproxyOptions MakeOptions(
        PlcOptions[] plcs,
        int adminPort = 8080,
        BcdTagListOptions? global = null)
        => new()
        {
            Plcs      = plcs,
            AdminPort = adminPort,
            BcdTags   = global ?? new BcdTagListOptions(),
        };

    // ── 1. Duplicate PLC name → fails ────────────────────────────────────────────────────

    [Fact]
    public void Validate_DuplicatePlcName_Fails()
    {
        var opts = MakeOptions([
            MakePlc("PLC-A", 5020),
            MakePlc("PLC-A", 5021),  // same name
        ]);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("PLC-A") && e.Contains("uplicate"));
    }

    // ── 2. Duplicate ListenPort → fails ──────────────────────────────────────────────────

    [Fact]
    public void Validate_DuplicateListenPort_Fails()
    {
        var opts = MakeOptions([
            MakePlc("PLC-A", 5020),
            MakePlc("PLC-B", 5020),  // same port
        ]);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("5020") && e.Contains("uplicate"));
    }

    // ── 3. AdminPort collides with a PLC's ListenPort → fails ────────────────────────────

    [Fact]
    public void Validate_AdminPortCollidesWith_PlcListenPort_Fails()
    {
        var opts = MakeOptions(
            plcs: [MakePlc("PLC-A", 5020)],
            adminPort: 5020);  // collides with PLC-A

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("AdminPort") && e.Contains("5020"));
    }

    // ── 4. Per-PLC BCD map build error → fails ────────────────────────────────────────────

    [Fact]
    public void Validate_PerPlc_BcdMapBuildError_Fails()
    {
        // A 32-bit tag at address 100 and a 16-bit tag at 101 collide on high register.
        var global = new BcdTagListOptions
        {
            Global =
            [
                new BcdTagOptions { Address = 100, Width = 32 },
                new BcdTagOptions { Address = 101, Width = 16 },  // overlaps 100's high register
            ],
        };
        var opts = MakeOptions([MakePlc("PLC-A", 5020)], global: global);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("PLC-A"));
    }

    // ── 5. Port out of range → fails ─────────────────────────────────────────────────────

    [Fact]
    public void Validate_PortOutOfRange_Fails()
    {
        // ListenPort 0 is below the valid [1, 65535] range.
        var opts = MakeOptions([MakePlc("PLC-A", 0)]);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("0") && e.Contains("range"));
    }

    // ── 5b. AdminPort out of range → fails ───────────────────────────────────────────────

    [Fact]
    public void Validate_AdminPortOutOfRange_Fails()
    {
        var opts = MakeOptions([MakePlc("PLC-A", 5020)], adminPort: 70000);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("70000") && e.Contains("range"));
    }

    // ── 6. Happy path → passes ───────────────────────────────────────────────────────────

    [Fact]
    public void Validate_HappyPath_Passes()
    {
        var global = new BcdTagListOptions
        {
            Global = [new BcdTagOptions { Address = 1072, Width = 16 }],
        };
        var opts = MakeOptions(
            plcs: [MakePlc("PLC-A", 5020), MakePlc("PLC-B", 5021)],
            adminPort: 8080,
            global: global);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.True(valid);
        Assert.Empty(errors);
    }

    // ── 7. Empty PLC name → fails ────────────────────────────────────────────────────────

    [Fact]
    public void Validate_EmptyPlcName_Fails()
    {
        var opts = MakeOptions([MakePlc("", 5020)]);

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("non-empty"));
    }

    // ── Cache.AllowLongTtl gate ─────────────────────────────────────────────────────────

    /// <summary>
    /// Per-tag CacheTtlMs > 60_000 without Cache.AllowLongTtl is rejected.
    /// </summary>
    [Fact]
    public void Validate_PerTagCacheTtl_Above60s_Without_AllowLongTtl_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs    = [MakePlc("PLC-A", 5020)],
            BcdTags = new BcdTagListOptions
            {
                Global = [ new BcdTagOptions { Address = 1024, Width = 16, CacheTtlMs = 120_000 } ],
            },
            Cache = new CacheOptions { AllowLongTtl = false },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("AllowLongTtl") && e.Contains("60_000"));
    }

    /// <summary>
    /// Same value passes when AllowLongTtl is true (operator opt-in).
    /// </summary>
    [Fact]
    public void Validate_PerTagCacheTtl_Above60s_With_AllowLongTtl_Passes()
    {
        var opts = new MbproxyOptions
        {
            Plcs    = [MakePlc("PLC-A", 5020)],
            BcdTags = new BcdTagListOptions
            {
                Global = [ new BcdTagOptions { Address = 1024, Width = 16, CacheTtlMs = 120_000 } ],
            },
            Cache = new CacheOptions { AllowLongTtl = true },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.True(valid);
        Assert.Empty(errors);
    }

    /// <summary>
    /// Per-PLC DefaultCacheTtlMs > 60_000 inherited by a tag with null CacheTtlMs is
    /// caught by the resolved-value check even if the per-PLC default check itself
    /// passes (it doesn't, but this validates the defensive resolved re-check).
    /// </summary>
    [Fact]
    public void Validate_ResolvedTtl_FromPerPlcDefault_AboveCap_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs = [
                new PlcOptions
                {
                    Name = "PLC-A", ListenPort = 5020, Host = "127.0.0.1", Port = 502,
                    DefaultCacheTtlMs = 90_000,
                },
            ],
            BcdTags = new BcdTagListOptions
            {
                // Tag with no explicit CacheTtlMs — inherits the per-PLC 90_000.
                Global = [ new BcdTagOptions { Address = 1024, Width = 16 } ],
            },
            Cache = new CacheOptions { AllowLongTtl = false },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("60_000"));
    }

    // ── ConnectionOptions validation ────────────────────────────────────────────────────

    [Fact]
    public void Validate_ZeroBackendConnectTimeoutMs_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs       = [MakePlc("PLC-A", 5020)],
            Connection = new ConnectionOptions { BackendConnectTimeoutMs = 0 },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("BackendConnectTimeoutMs"));
    }

    [Fact]
    public void Validate_NegativeGracefulShutdownTimeoutMs_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs       = [MakePlc("PLC-A", 5020)],
            Connection = new ConnectionOptions { GracefulShutdownTimeoutMs = -1 },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("GracefulShutdownTimeoutMs"));
    }

    // ── Keepalive section ─────────────────────────────────────────────────────

    [Fact]
    public void Validate_DefaultKeepalive_Passes()
    {
        // Default ConnectionOptions → default KeepaliveOptions (idle 30 s, request 3 s).
        var opts = MakeOptions([MakePlc("PLC-A", 5020)]);

        bool valid = ReloadValidator.Validate(opts, out _);

        Assert.True(valid);
    }

    [Fact]
    public void Validate_NonPositiveTcpProbeCount_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs       = [MakePlc("PLC-A", 5020)],
            Connection = new ConnectionOptions
            {
                Keepalive = new KeepaliveOptions { TcpProbeCount = 0 },
            },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("TcpProbeCount"));
    }

    [Fact]
    public void Validate_OutOfRangeHeartbeatProbeAddress_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs       = [MakePlc("PLC-A", 5020)],
            Connection = new ConnectionOptions
            {
                Keepalive = new KeepaliveOptions { BackendHeartbeatProbeAddress = 70000 },
            },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("BackendHeartbeatProbeAddress"));
    }

    [Fact]
    public void Validate_HeartbeatIdleNotAboveRequestTimeout_Fails()
    {
        // BackendHeartbeatIdleMs must sit ABOVE BackendRequestTimeoutMs, else a heartbeat
        // would be timed out as fast as it could be issued.
        var opts = new MbproxyOptions
        {
            Plcs       = [MakePlc("PLC-A", 5020)],
            Connection = new ConnectionOptions
            {
                BackendRequestTimeoutMs = 3000,
                Keepalive = new KeepaliveOptions { BackendHeartbeatIdleMs = 3000 },
            },
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("BackendHeartbeatIdleMs"));
    }

    // ── AdminPushIntervalMs ────────────────────────────────────────────────────

    [Fact]
    public void Validate_AdminPushIntervalMs_Zero_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs                = [MakePlc("PLC-A", 5020)],
            AdminPushIntervalMs = 0,
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("AdminPushIntervalMs"));
    }

    [Fact]
    public void Validate_AdminPushIntervalMs_Negative_Fails()
    {
        var opts = new MbproxyOptions
        {
            Plcs                = [MakePlc("PLC-A", 5020)],
            AdminPushIntervalMs = -5,
        };

        bool valid = ReloadValidator.Validate(opts, out var errors);

        Assert.False(valid);
        Assert.Contains(errors, e => e.Contains("AdminPushIntervalMs"));
    }
}
