using Shouldly;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;

/// <summary>
///     v3: <see cref="EquipmentTagRefResolver{TDef}"/> is now a thin wrapper over the driver's
///     authored <c>RawPath → TDef</c> lookup. The pre-v3 two-arg ctor (byName + a JSON-blob parse
///     fallback with a transient cache) is retired — a reference is always a RawPath, and a miss is a
///     miss (no parse attempt). These tests pin the single-func RawPath-lookup contract.
/// </summary>
public class EquipmentTagRefResolverTests
{
    private sealed record Def(string Id);

    private static EquipmentTagRefResolver<Def> Make(Dictionary<string, Def> byRawPath)
        => new(r => byRawPath.TryGetValue(r, out var d) ? d : null);

    [Fact]
    public void RawPath_resolves_via_lookup()
    {
        var r = Make(new() { ["Area/Modbus/Dev/Temp"] = new Def("Temp") });
        r.TryResolve("Area/Modbus/Dev/Temp", out var def).ShouldBeTrue();
        def!.Id.ShouldBe("Temp");
    }

    [Fact]
    public void Unknown_rawPath_returns_false()
    {
        var r = Make(new() { ["Area/Modbus/Dev/Temp"] = new Def("Temp") });
        r.TryResolve("Area/Modbus/Dev/Missing", out _).ShouldBeFalse();
    }

    [Fact]
    public void Lookup_is_consulted_each_call()
    {
        // v3 holds no transient parse cache — the resolver reads the driver's live table by closure,
        // so every TryResolve consults the underlying lookup (a table rebuild is immediately visible).
        var calls = 0;
        var table = new Dictionary<string, Def>();
        var r = new EquipmentTagRefResolver<Def>(reference =>
        {
            calls++;
            return table.TryGetValue(reference, out var d) ? d : null;
        });

        r.TryResolve("Area/Dev/Temp", out _).ShouldBeFalse();
        table["Area/Dev/Temp"] = new Def("Temp");
        r.TryResolve("Area/Dev/Temp", out var def).ShouldBeTrue();
        def!.Id.ShouldBe("Temp");
        calls.ShouldBe(2);
    }

    [Fact]
    public void Clear_is_a_noop_and_lookup_still_works()
    {
        // Clear() is retained for call-site compatibility only — there is no transient cache to drop.
        var r = Make(new() { ["ref"] = new Def("ref") });
        r.TryResolve("ref", out _).ShouldBeTrue();
        r.Clear();
        r.TryResolve("ref", out var def).ShouldBeTrue();
        def!.Id.ShouldBe("ref");
    }

    /// <summary>
    ///     Core.Abstractions-009: <see cref="EquipmentTagRefResolver{TDef}.TryResolve"/> carries
    ///     <c>[MaybeNullWhen(false)]</c> on its <c>out</c> parameter so NRT-enabled callers do
    ///     not need a null-forgiving operator when the return value is <see langword="false"/>.
    ///     This test pins the false-path behaviour: <c>def</c> must not be used when the method
    ///     returns <see langword="false"/> — the contract is that it is undefined (currently null).
    /// </summary>
    [Fact]
    public void TryResolve_false_path_sets_def_to_default()
    {
        var r = Make(new());
        var returned = r.TryResolve("no-such-ref", out var def);
        returned.ShouldBeFalse();
        // The [MaybeNullWhen(false)] attribute means callers must treat def as potentially null
        // on the false path — the value here is the implementation-defined default (null for
        // a reference type), which satisfies that contract.
        def.ShouldBeNull();
    }
}
