using ZB.MOM.WW.ScadaBridge.CentralUI.Services;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;

/// <summary>
/// Inbound-API key re-arch (C3): unit tests for <see cref="ApiMethodKeyScopeReconciler"/>, which
/// inverts the API-method form's "Approved API Keys" selection into per-key method-scope edits.
/// Covers approve, revoke (preserving other scopes), the empty-last-scope guard, and the no-op case.
/// </summary>
public sealed class ApiMethodKeyScopeReconcilerTests
{
    private static IReadOnlyDictionary<string, IReadOnlyList<string>> Current(
        params (string KeyId, string[] Methods)[] entries) =>
        entries.ToDictionary(
            e => e.KeyId,
            e => (IReadOnlyList<string>)e.Methods.ToList(),
            StringComparer.Ordinal);

    private static IReadOnlyDictionary<string, string> Names(params (string KeyId, string Name)[] entries) =>
        entries.ToDictionary(e => e.KeyId, e => e.Name, StringComparer.Ordinal);

    [Fact]
    public void Approve_AddsMethodToKey_PreservingExistingScopes()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string> { "k1" },
            initialKeyIds: new HashSet<string>(),
            currentMethodsByKey: Current(("k1", new[] { "GetStatus" })),
            keyNamesById: Names(("k1", "Key One")));

        Assert.Empty(result.EmptyScopeKeyNames);
        var update = Assert.Single(result.Updates);
        Assert.Equal("k1", update.KeyId);
        Assert.Equal(new[] { "GetStatus", "PlaceOrder" }, update.NewMethods);
    }

    [Fact]
    public void Approve_KeyWithNoExistingScopes_GetsJustThisMethod()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string> { "k1" },
            initialKeyIds: new HashSet<string>(),
            currentMethodsByKey: Current(("k1", Array.Empty<string>())),
            keyNamesById: Names(("k1", "Key One")));

        var update = Assert.Single(result.Updates);
        Assert.Equal(new[] { "PlaceOrder" }, update.NewMethods);
    }

    [Fact]
    public void Revoke_RemovesMethod_LeavingOtherScopesIntact()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string>(),
            initialKeyIds: new HashSet<string> { "k1" },
            currentMethodsByKey: Current(("k1", new[] { "PlaceOrder", "GetStatus" })),
            keyNamesById: Names(("k1", "Key One")));

        Assert.Empty(result.EmptyScopeKeyNames);
        var update = Assert.Single(result.Updates);
        Assert.Equal("k1", update.KeyId);
        Assert.Equal(new[] { "GetStatus" }, update.NewMethods);
    }

    [Fact]
    public void Revoke_LastScope_ReportedAsEmptyConflict_AndNotInUpdates()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string>(),
            initialKeyIds: new HashSet<string> { "k1" },
            currentMethodsByKey: Current(("k1", new[] { "PlaceOrder" })),
            keyNamesById: Names(("k1", "Key One")));

        Assert.Empty(result.Updates);
        var emptyName = Assert.Single(result.EmptyScopeKeyNames);
        Assert.Equal("Key One", emptyName);
    }

    [Fact]
    public void Mixed_ApproveOneRevokeAnother_ProducesBothUpdates()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string> { "k2" },          // approve k2
            initialKeyIds: new HashSet<string> { "k1" },           // revoke k1
            currentMethodsByKey: Current(
                ("k1", new[] { "PlaceOrder", "GetStatus" }),
                ("k2", new[] { "Ping" })),
            keyNamesById: Names(("k1", "Key One"), ("k2", "Key Two")));

        Assert.Empty(result.EmptyScopeKeyNames);
        Assert.Equal(2, result.Updates.Count);

        var k1 = result.Updates.Single(u => u.KeyId == "k1");
        Assert.Equal(new[] { "GetStatus" }, k1.NewMethods);

        var k2 = result.Updates.Single(u => u.KeyId == "k2");
        Assert.Equal(new[] { "Ping", "PlaceOrder" }, k2.NewMethods);
    }

    [Fact]
    public void NoChange_ProducesNoUpdates()
    {
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string> { "k1" },
            initialKeyIds: new HashSet<string> { "k1" },
            currentMethodsByKey: Current(("k1", new[] { "PlaceOrder" })),
            keyNamesById: Names(("k1", "Key One")));

        Assert.Empty(result.Updates);
        Assert.Empty(result.EmptyScopeKeyNames);
    }

    /// <summary>
    /// Concurrent-edit guard: if selected == initial (no diff), the reconciler must produce
    /// ZERO updates even when the live store shows different scopes on that key. The reconciler
    /// only acts on keys that appear in the diff (added or removed relative to <c>initialKeyIds</c>)
    /// — it must never touch keys that are not in the diff, regardless of what their current
    /// live scopes look like.
    /// </summary>
    [Fact]
    public void NoDiff_ProducesNoUpdates_EvenWhenLiveScopesDiffer()
    {
        // k1 was approved at load time and is still approved — no diff.
        // However, a concurrent edit changed k1's live scopes to include an extra method.
        var result = ApiMethodKeyScopeReconciler.Reconcile(
            methodName: "PlaceOrder",
            selectedKeyIds: new HashSet<string> { "k1" },
            initialKeyIds: new HashSet<string> { "k1" },
            currentMethodsByKey: Current(("k1", new[] { "PlaceOrder", "OtherMethod" })),
            keyNamesById: Names(("k1", "Key One")));

        // k1 is not in the diff → reconciler must not touch it.
        Assert.Empty(result.Updates);
        Assert.Empty(result.EmptyScopeKeyNames);
    }
}
