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

namespace Mbproxy.Tests.Configuration;

/// <summary>
/// Unit tests for <see cref="ReloadPlan.Compute"/>.
/// All tests verify the pure function logic — no side effects, no DI, no sockets.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ReloadPlanTests
{
    // ── Helpers ───────────────────────────────────────────────────────────────────────────

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

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

    private static BcdTagListOptions GlobalWith(params (ushort addr, byte width)[] tags)
        => new()
        {
            Global = tags.Select(t => new BcdTagOptions { Address = t.addr, Width = t.width }).ToList(),
        };

    // ── 1. Add one PLC ───────────────────────────────────────────────────────────────────

    [Fact]
    public void Compute_AddOnePlc_OnlyToAddPopulated()
    {
        var current = MakeOptions([MakePlc("A", 5020)]);
        var next    = MakeOptions([MakePlc("A", 5020), MakePlc("B", 5021)]);

        var plan = ReloadPlan.Compute(current, next);

        Assert.Single(plan.ToAdd);
        Assert.Equal("B", plan.ToAdd[0].Name);
        Assert.Empty(plan.ToRemove);
        Assert.Empty(plan.ToRestart);
        Assert.Empty(plan.ToReseat);
    }

    // ── 2. Remove one PLC ────────────────────────────────────────────────────────────────

    [Fact]
    public void Compute_RemoveOnePlc_OnlyToRemovePopulated()
    {
        var current = MakeOptions([MakePlc("A", 5020), MakePlc("B", 5021)]);
        var next    = MakeOptions([MakePlc("A", 5020)]);

        var plan = ReloadPlan.Compute(current, next);

        Assert.Empty(plan.ToAdd);
        Assert.Single(plan.ToRemove);
        Assert.Equal("B", plan.ToRemove[0]);
        Assert.Empty(plan.ToRestart);
        Assert.Empty(plan.ToReseat);
    }

    // ── 3. Changed ListenPort → goes to ToRestart, NOT ToReseat ──────────────────────────

    [Fact]
    public void Compute_ChangePort_GoesToToRestart_NotToReseat()
    {
        var current = MakeOptions([MakePlc("A", 5020)]);
        var next    = MakeOptions([MakePlc("A", 5022)]);  // ListenPort changed

        var plan = ReloadPlan.Compute(current, next);

        Assert.Empty(plan.ToAdd);
        Assert.Empty(plan.ToRemove);
        Assert.Single(plan.ToRestart);
        Assert.Equal("A", plan.ToRestart[0].Name);
        Assert.Equal(5022, plan.ToRestart[0].New.ListenPort);
        Assert.Empty(plan.ToReseat);
    }

    // ── 3b. Changed Host → goes to ToRestart ─────────────────────────────────────────────

    [Fact]
    public void Compute_ChangeHost_GoesToToRestart()
    {
        var current = MakeOptions([MakePlc("A", 5020, host: "10.0.0.1")]);
        var next    = MakeOptions([MakePlc("A", 5020, host: "10.0.0.2")]);

        var plan = ReloadPlan.Compute(current, next);

        Assert.Single(plan.ToRestart);
        Assert.Empty(plan.ToReseat);
    }

    // ── 4. Changed per-PLC tag override → goes to ToReseat ───────────────────────────────

    [Fact]
    public void Compute_ChangePerPlcTagOverride_GoesToToReseat()
    {
        var global = GlobalWith((1072, 16));

        // Current: PLC-A has no overrides.
        var current = MakeOptions([MakePlc("A", 5020)], global: global);

        // Next: PLC-A adds address 1080.
        var plcWithOverride = new PlcOptions
        {
            Name       = "A",
            ListenPort = 5020,
            Host       = "127.0.0.1",
            Port       = 502,
            BcdTags    = new PlcBcdOverrides
            {
                Add = [new BcdTagOptions { Address = 1080, Width = 16 }],
            },
        };
        var next = new MbproxyOptions
        {
            Plcs    = [plcWithOverride],
            BcdTags = global,
        };

        var plan = ReloadPlan.Compute(current, next);

        Assert.Empty(plan.ToAdd);
        Assert.Empty(plan.ToRemove);
        Assert.Empty(plan.ToRestart);
        Assert.Single(plan.ToReseat);
        Assert.Equal("A", plan.ToReseat[0].Name);
    }

    // ── 5. Changed global tag list → all PLCs reseat, no restart ─────────────────────────

    [Fact]
    public void Compute_ChangeGlobalTagList_AllPlcsReseat_NoRestart()
    {
        var globalBefore = GlobalWith((1072, 16));
        var globalAfter  = GlobalWith((1072, 16), (1080, 32));  // new 32-bit tag added

        var current = MakeOptions([MakePlc("A", 5020), MakePlc("B", 5021)], global: globalBefore);
        var next    = MakeOptions([MakePlc("A", 5020), MakePlc("B", 5021)], global: globalAfter);

        var plan = ReloadPlan.Compute(current, next);

        Assert.Empty(plan.ToAdd);
        Assert.Empty(plan.ToRemove);
        Assert.Empty(plan.ToRestart);
        // Both PLCs should be reseated because the global tag list changed.
        Assert.Equal(2, plan.ToReseat.Count);
        Assert.Contains(plan.ToReseat, r => r.Name == "A");
        Assert.Contains(plan.ToReseat, r => r.Name == "B");
    }

    // ── 6. No changes → all empty ────────────────────────────────────────────────────────

    [Fact]
    public void Compute_NoChanges_AllSectionsEmpty()
    {
        var global = GlobalWith((1072, 16));
        var opts   = MakeOptions([MakePlc("A", 5020)], global: global);

        var plan = ReloadPlan.Compute(opts, opts);

        Assert.Empty(plan.ToAdd);
        Assert.Empty(plan.ToRemove);
        Assert.Empty(plan.ToRestart);
        Assert.Empty(plan.ToReseat);
    }

    // ── 7. Connection options propagated ─────────────────────────────────────────────────

    [Fact]
    public void Compute_ConnectionOptions_AreFromNextSnapshot()
    {
        var current = new MbproxyOptions
        {
            Plcs = [MakePlc("A", 5020)],
            Connection = new ConnectionOptions { BackendConnectTimeoutMs = 1000 },
        };
        var next = new MbproxyOptions
        {
            Plcs = [MakePlc("A", 5020)],
            Connection = new ConnectionOptions { BackendConnectTimeoutMs = 9999 },
        };

        var plan = ReloadPlan.Compute(current, next);

        Assert.Equal(9999, plan.Connection.BackendConnectTimeoutMs);
    }
}
