using System.Text;

namespace DodoSSH.Client.Domain.Tests;

/// <summary>
/// A tag: one name, and the reasons it is only that.
/// </summary>
/// <remarks>
/// <para>
/// There is very little behaviour here, which is the design — a tag exists to have an identity, so that
/// renaming it is one write instead of twenty. What these pin is that the envelope round-trips, that a
/// nameless tag cannot be stored, and that renaming the same tag on two machines is reported rather than
/// silently resolved.
/// </para>
/// <para>
/// The absent test worth naming: nothing here refuses a second tag called the same thing. Two people
/// creating "staging" offline is how it happens, and refusing the loser would mean discarding the tags a
/// colleague had already put on their hosts. See <see cref="TagSecret.TryValidate"/>.
/// </para>
/// </remarks>
public sealed class TagSecretTests
{
    [Fact]
    public void ATag_RoundTrips()
    {
        var tag = new TagSecret { Label = "pci" };

        TagSecretCodec.TryDecode(TagSecretCodec.Encode(tag), out var document).ShouldBeTrue();

        document.ShouldNotBeNull();
        document.Tag.ShouldBe(tag);
        document.SchemaVersion.ShouldBe(TagSecretCodec.CurrentSchemaVersion);
        document.IsReadOnly.ShouldBeFalse();
    }

    [Fact]
    public void ATag_EncodesAtTheBaseVersionWithNothingBesideItsName()
    {
        // Pinned as a literal, so the first field this type ever grows has to be appended and has to be
        // omitted when null — the two properties that keep every tag already in every vault re-encoding to
        // the bytes it was stored with, rather than looking like a change on the first sync after an upgrade.
        Encoding.UTF8.GetString(TagSecretCodec.Encode(new TagSecret { Label = "pci" }))
            .ShouldBe("""{"schemaVersion":1,"label":"pci"}""");
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    public void ATagWithNoName_IsRefused(string label)
    {
        new TagSecret { Label = label }.TryValidate(out var reason).ShouldBeFalse();

        reason.ShouldNotBeNull();
    }

    [Fact]
    public void ATagWrittenByANewerClient_IsReadableButNotWritableHere()
    {
        var payload = Encoding.UTF8.GetBytes(
            """
            {"schemaVersion":99,"label":"pci","colour":"a field this build has never heard of"}
            """);

        TagSecretCodec.TryDecode(payload, out var document).ShouldBeTrue();

        document.ShouldNotBeNull();
        document.Tag.Label.ShouldBe("pci");
        document.IsReadOnly.ShouldBeTrue();
    }

    [Fact]
    public void AnUnnamedPayload_FailsToDecodeRatherThanProducingABlankTag()
    {
        // Failing closed, as every codec in this folder does: a nameless tag is an empty chip beside a host,
        // filtering a set nobody can name.
        var payload = Encoding.UTF8.GetBytes("""{"schemaVersion":1}""");

        TagSecretCodec.TryDecode(payload, out var document).ShouldBeFalse();

        document.ShouldBeNull();
    }

    [Fact]
    public void TwoDifferentRenames_AreReportedWithBothNames()
    {
        var ancestor = new TagSecret { Label = "pci" };

        var result = TagSecretMerge.Merge(
            ancestor,
            ancestor with { Label = "pci-dss" },
            ancestor with { Label = "in-scope" });

        result.Merged.Label.ShouldBe("in-scope");

        var conflict = result.Conflicts.ShouldHaveSingleItem();

        conflict.Field.ShouldBe(nameof(TagSecret.Label));
        conflict.Kept.ShouldBe("in-scope");
        conflict.Discarded.ShouldBe("pci-dss");
    }

    /// <remarks>
    /// The case that would collide if membership were held on the tag instead of on each host: two people
    /// putting one tag on two different machines at the same time. It cannot reach the merge at all, because
    /// neither of those actions writes to this item.
    /// </remarks>
    [Fact]
    public void TaggingHosts_DoesNotTouchTheTag()
    {
        var ancestor = new TagSecret { Label = "pci" };

        var result = TagSecretMerge.Merge(ancestor, ancestor, ancestor);

        result.HasConflicts.ShouldBeFalse();
        result.Merged.ShouldBe(ancestor);
    }
}
