using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;

/// <summary>
///     Parity guards for the two remaining per-driver dispatch maps (<c>deferment.md</c> G-5, G-6).
///     Both were already testable structures — unlike the two Razor <c>@switch</c> blocks that needed
///     extracting first — so nothing but the tests was missing.
/// </summary>
[Trait("Category", "Unit")]
public sealed class DriverDispatchMapParityTests
{
    /// <summary>
    ///     Driver types with a typed tag editor but no CSV map, so import/export silently degrades them to
    ///     the raw <c>TagConfigJson</c> fallback. Empty — every editor-backed driver now has typed columns.
    ///     <para>Deliberately expressed as "has a typed editor ⇒ has typed CSV columns" rather than
    ///     "every driver type", because a driver with no typed editor has nothing to project into columns.
    ///     Tying the two maps together is what makes this catch the next omission.</para>
    /// </summary>
    [Fact]
    public void Every_driver_with_a_typed_tag_editor_has_typed_csv_columns()
    {
        var missing = DriverTypeNames.All
            .Where(t => TagConfigEditorMap.Resolve(t) is not null && CsvColumnMap.Resolve(t) is null)
            .OrderBy(t => t, StringComparer.Ordinal)
            .ToList();

        missing.ShouldBeEmpty(
            $"these driver types have a typed tag editor but no CsvColumnMap entry, so CSV import/export "
            + $"drops them to the raw TagConfigJson fallback: {string.Join(", ", missing)}");
    }

    /// <summary>A CSV map for a driver type that no longer exists is a stale entry.</summary>
    [Fact]
    public void Every_csv_map_targets_a_declared_driver_type()
    {
        var declared = DriverTypeNames.All.ToHashSet(StringComparer.OrdinalIgnoreCase);
        var stale = CsvColumnMap.All
            .Select(m => m.DriverType)
            .Where(t => !declared.Contains(t))
            .OrderBy(t => t, StringComparer.Ordinal)
            .ToList();

        stale.ShouldBeEmpty($"CsvColumnMap has entries for undeclared driver types: {string.Join(", ", stale)}");
    }

    /// <summary>
    ///     Every driver type either produces a TYPED blob from browse-commit, or is on the documented
    ///     non-browsable list. The generic <c>{"address": …}</c> fallback opens EMPTY in a typed editor and
    ///     blanks the field on save — the MTConnect branch was added for exactly that, and Sql had the same
    ///     bug unnoticed (<c>deferment.md</c> G-6) because the fallback's comment claimed "browsable drivers
    ///     are all handled above", which stopped being true the moment <c>SqlDriverBrowser</c> registered.
    ///     <para><b>Asserted in both directions on purpose.</b> A one-way check would let a newly-browsable
    ///     driver be quietly added to the exclusion list instead of getting a branch. Requiring the two sets
    ///     to match exactly means a driver gaining or losing a browser forces this list to be revisited.</para>
    /// </summary>
    [Fact]
    public void Browse_commit_falls_back_to_the_generic_address_key_for_exactly_the_non_browsable_drivers()
    {
        // Address fields as a browser supplies them; a driver whose browser states none must still produce
        // something its typed editor can read from the leaf name alone.
        var addressFields = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["schema"] = "dbo",
            ["table"] = "Readings",
            ["columnName"] = "Speed",
        };

        var fellBack = DriverTypeNames.All
            .Where(t => RawBrowseCommitMapper.BuildTagConfig(t, "Speed", addressFields) == """{"address":"Speed"}""")
            .OrderBy(t => t, StringComparer.Ordinal)
            .ToList();

        fellBack.ShouldBe(
            NonBrowsableDriverTypes.OrderBy(t => t, StringComparer.Ordinal).ToList(),
            $"the set of driver types committing to the generic \"address\" key drifted. Extra entries are "
            + $"browsable drivers whose typed editor will open EMPTY and blank the field on save; missing "
            + $"entries gained a typed branch and should leave this list. Got: {string.Join(", ", fellBack)}");
    }

    /// <summary>
    ///     Driver types with no browser, for which the generic <c>address</c> key is the correct commit.
    ///     <list type="bullet">
    ///       <item><b>Modbus</b> — register addresses are numeric coordinates, not a browsable namespace.</item>
    ///       <item><b>Calculation</b> — a pseudo-driver over other tags' RawPaths; there is no device.</item>
    ///     </list>
    ///     Adding a type here is a claim that it cannot be browsed. If it can, give it a branch instead.
    /// </summary>
    private static readonly string[] NonBrowsableDriverTypes =
    [
        DriverTypeNames.Modbus,
        DriverTypeNames.Calculation,
    ];
}
