using FluentAssertions;
using DragonISO.App.Services;

namespace DragonISO.App.Tests.Services;

/// <summary>
/// Token-expansion + sanitization tests for <see cref="OutputNameTemplate"/>.
/// We don't touch <see cref="OutputNameTemplate.Get"/> / <see cref="OutputNameTemplate.Set"/>
/// here because those round-trip through %LOCALAPPDATA% file IO; the file-IO
/// path is exercised at integration test time. The token expander is pure and
/// easy to cover.
/// </summary>
public class OutputNameTemplateTests
{
    private static readonly Guid TestId = new("11223344-5566-7788-99aa-bbccddeeff00");

    [Fact]
    public void Render_DefaultTemplate_RendersSpeakerDisplayName()
    {
        var name = OutputNameTemplate.Render(OutputNameTemplate.DefaultTemplate, TestId, "Jane");
        // Default is "{name}" since 0.9.0-rc19 â€” produces the speaker name
        // directly so downstream switchers see human-readable identifiers.
        // Previously was "Dragon-ISO_{guid}"; see DefaultTemplate's xmldoc.
        name.Should().Be("Jane");
    }

    [Fact]
    public void Render_DefaultTemplate_EmptyName_FallsBackToGuidPrefix()
    {
        // The "{name}" default would render to an empty string for a
        // participant with no display name yet (Teams sometimes delivers
        // DisplayName a tick after the join event). The empty-name
        // fallback substitutes Dragon-ISO_{guid} so the NDI sender is
        // always uniquely identifiable. Without this, the engine would
        // throw on an empty sender name.
        var name = OutputNameTemplate.Render(OutputNameTemplate.DefaultTemplate, TestId, "");
        name.Should().Be("Dragon-ISO_11223344");
    }

    [Fact]
    public void Render_DefaultTemplate_WhitespaceName_FallsBackToGuidPrefix()
    {
        // Mirror of the empty-name case â€” whitespace-only display names
        // sanitize down to empty and should trigger the same fallback.
        var name = OutputNameTemplate.Render(OutputNameTemplate.DefaultTemplate, TestId, "   ");
        name.Should().Be("Dragon-ISO_11223344");
    }

    [Fact]
    public void Render_NameToken_UsesSanitizedDisplayName()
    {
        var name = OutputNameTemplate.Render("Dragon-ISO_{name}", TestId, "Jane Doe");
        name.Should().Be("Dragon-ISO_Jane_Doe", because: "spaces become underscores in the sanitizer");
    }

    [Fact]
    public void Render_NameToken_StripsSpecialCharacters()
    {
        // NDI accepts more chars than we allow, but we keep it conservative
        // (alphanumeric + underscore + hyphen + period). Anything else is dropped
        // or converted to underscore (whitespace).
        var name = OutputNameTemplate.Render("{name}", TestId, "Jane (PM) â€” Lead!");
        // Expect: "Jane_PM__Lead" â€” parens dropped, em-dash dropped, exclamation dropped.
        // Whitespace runs collapse into adjacent underscores.
        name.Should().NotContain("(");
        name.Should().NotContain(")");
        name.Should().NotContain("â€”");
        name.Should().NotContain("!");
        name.Should().Contain("Jane");
        name.Should().Contain("Lead");
    }

    [Fact]
    public void Render_GuidToken_IsUppercaseFirst8()
    {
        var name = OutputNameTemplate.Render("{guid}", TestId, "Jane");
        name.Should().Be("11223344");
    }

    [Fact]
    public void Render_MachineToken_UsesEnvironmentMachineName()
    {
        var name = OutputNameTemplate.Render("{machine}", TestId, "Jane");
        // Sanitization may transform spaces in machine names, so just assert non-empty
        // and that it contains the machine name's alphanumeric-ish chars.
        name.Should().NotBeNullOrEmpty();
        // MachineName itself is sanitized in render â€” equality check would be brittle.
    }

    [Fact]
    public void Render_TimestampToken_HasExpectedShape()
    {
        var name = OutputNameTemplate.Render("session_{timestamp}", TestId, "Jane");
        // yyyyMMdd_HHmmss is 15 chars + underscore separator = 16.
        // Combined with "session_" prefix â†’ length should be at least 23.
        name.Should().StartWith("session_");
        name.Length.Should().BeGreaterThan("session_".Length + 14);
    }

    [Fact]
    public void Render_MultipleTokens_AllExpand()
    {
        var name = OutputNameTemplate.Render("{name}_{guid}_{machine}", TestId, "Jane");
        name.Should().StartWith("Jane_11223344_");
        name.Should().NotContain("{");
        name.Should().NotContain("}");
    }

    [Fact]
    public void Render_TemplateWithNoTokens_PassesThrough()
    {
        var name = OutputNameTemplate.Render("STATIC_NAME", TestId, "Jane");
        name.Should().Be("STATIC_NAME");
    }

    [Fact]
    public void Render_EmptyDisplayName_DegradesToEmptyToken()
    {
        var name = OutputNameTemplate.Render("PFX_{name}", TestId, "");
        name.Should().Be("PFX_");
    }

    [Theory]
    [InlineData("Jane123")]
    [InlineData("Jane-Doe")]
    [InlineData("Jane.PM")]
    public void Render_AllowedCharactersPreserved(string displayName)
    {
        var name = OutputNameTemplate.Render("{name}", TestId, displayName);
        name.Should().Be(displayName, because: "alphanumeric, underscore, hyphen, period are all valid NDI chars");
    }
}
