using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared;

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

/// <summary>
///     #504 — <see cref="DisplayText.Abbreviate"/>, the null/short-safe replacement for the
///     <c>@value[..N]</c> range-operator slices the Admin UI used to abbreviate hashes and thumbprints.
/// </summary>
/// <remarks>
///     The bug being pinned: a range operator over an unvalidated DB string throws
///     <see cref="System.ArgumentOutOfRangeException"/> out of <c>BuildRenderTree</c>, which is unhandled
///     in Blazor and takes the WHOLE page to an HTTP 500 — not just the offending row. Nothing enforces a
///     minimum length at the schema, entity, or render layer, so the short-input cases below are the ones
///     that matter; the happy path is almost incidental.
/// </remarks>
[Trait("Category", "Unit")]
public sealed class DisplayTextTests
{
    /// <summary>A value longer than the budget is truncated and marked with an ellipsis — the normal
    /// case for a 64-char SHA-256.</summary>
    [Fact]
    public void A_long_value_is_truncated_and_marked()
    {
        DisplayText.Abbreviate(new string('a', 64), 12).ShouldBe(new string('a', 12) + "…");
    }

    /// <summary>The regression: a value SHORTER than the budget must render, not throw. This is exactly
    /// what a hand-inserted / API-authored <c>SourceHash</c> like <c>h1</c> hits.</summary>
    [Theory]
    [InlineData("h1", 12)]
    [InlineData("h-abs-hr200", 12)] // 11 chars — one short of the old slice, the original crash
    [InlineData("abc", 16)]
    [InlineData("x", 64)]
    public void A_value_shorter_than_the_budget_renders_verbatim(string value, int maxLength)
    {
        DisplayText.Abbreviate(value, maxLength).ShouldBe(value);
    }

    /// <summary>No ellipsis when nothing was dropped — the display must never imply text that is not
    /// there. (The old markup appended "…" unconditionally, outside the slice.)</summary>
    [Fact]
    public void A_short_value_gets_no_ellipsis()
    {
        DisplayText.Abbreviate("h1", 12).ShouldNotContain("…");
    }

    /// <summary>A value exactly at the budget is the boundary — it fits, so it is neither truncated nor
    /// marked.</summary>
    [Fact]
    public void A_value_exactly_at_the_budget_is_untouched()
    {
        DisplayText.Abbreviate("123456789012", 12).ShouldBe("123456789012");
    }

    /// <summary>Null / blank render as the Admin UI's em-dash placeholder rather than throwing or
    /// producing a stray ellipsis.</summary>
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void A_null_or_blank_value_renders_the_placeholder(string? value)
    {
        DisplayText.Abbreviate(value, 12).ShouldBe(DisplayText.Empty);
    }

    /// <summary>A nonsensical budget is clamped rather than allowed to throw — a display bug must not
    /// become a page crash, which is the whole point of this helper.</summary>
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(int.MinValue)]
    public void A_non_positive_budget_is_clamped_not_thrown(int maxLength)
    {
        DisplayText.Abbreviate("abcdef", maxLength).ShouldBe("a…");
    }

    /// <summary>Whatever the inputs, it never throws — the property that keeps a bad row from killing a
    /// page. Includes the shapes that broke the old code.</summary>
    [Fact]
    public void It_never_throws_for_any_combination()
    {
        string?[] values = [null, "", " ", "h1", "h-abs-hr200", new string('f', 64), "…"];
        int[] budgets = [int.MinValue, -1, 0, 1, 12, 16, 64, int.MaxValue];

        foreach (var value in values)
        {
            foreach (var budget in budgets)
            {
                Should.NotThrow(() => DisplayText.Abbreviate(value, budget));
            }
        }
    }
}
