using System.Text;

namespace DodoSSH.Client.Domain.Tests;

/// <summary>
/// A saved command: what it stores, and what it refuses to change about it.
/// </summary>
/// <remarks>
/// Two things carry weight. That the text survives a round trip untouched, because a shell is not forgiving
/// about whitespace it did not expect. And that <see cref="SnippetSecret.RunsOnInsert"/> only ever becomes
/// true because somebody set it — never because a decode defaulted it, and never because a merge picked a
/// side.
/// </remarks>
public sealed class SnippetSecretTests
{
    [Fact]
    public void ASnippet_RoundTrips()
    {
        var snippet = new SnippetSecret
        {
            Label = "restart the api",
            Command = "sudo systemctl restart dodossh-api",
            Notes = "check the on-call rota first",
            RunsOnInsert = true,
        };

        SnippetSecretCodec.TryDecode(SnippetSecretCodec.Encode(snippet), out var document)
            .ShouldBeTrue();

        document.ShouldNotBeNull();
        document.Snippet.ShouldBe(snippet);
        document.SchemaVersion.ShouldBe(SnippetSecretCodec.CurrentSchemaVersion);
        document.IsReadOnly.ShouldBeFalse();
    }

    /// <remarks>
    /// The property the whole type is built around. A heredoc's terminator has to arrive on a line of its
    /// own with nothing after it; trim the trailing newline and the shell waits for one that never comes,
    /// which looks to the user like the snippet hanging the terminal.
    /// </remarks>
    [Theory]
    [InlineData("  indented\n")]
    [InlineData("cat <<'EOF'\nline one\nEOF\n")]
    [InlineData("first\r\nsecond\r\n")]
    [InlineData("trailing space   ")]
    [InlineData("")]
    public void TheCommandSurvivesAsItWasWritten(string command)
    {
        var snippet = new SnippetSecret { Label = "verbatim", Command = command };

        SnippetSecretCodec.TryDecode(SnippetSecretCodec.Encode(snippet), out var document)
            .ShouldBeTrue();

        document.ShouldNotBeNull().Snippet.Command.ShouldBe(command);
    }

    [Fact]
    public void ARunFlagThatIsMissingFromThePayload_ReadsAsOff()
    {
        // The safe direction, and the one a truncated or hand-written payload has to fall in: a snippet
        // whose flag could not be read must not be one that runs on its own.
        var payload = Encoding.UTF8.GetBytes(
            """
            {"schemaVersion":1,"label":"restart","command":"sudo reboot"}
            """);

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

        document.ShouldNotBeNull().Snippet.RunsOnInsert.ShouldBeFalse();
    }

    [Theory]
    [InlineData("", "echo hi")]
    [InlineData("   ", "echo hi")]
    [InlineData("named", "")]
    public void AnUnusableSnippet_IsRefused(string label, string command)
    {
        new SnippetSecret { Label = label, Command = command }
            .TryValidate(out var reason)
            .ShouldBeFalse();

        reason.ShouldNotBeNull();
    }

    /// <remarks>
    /// Whitespace is a legitimate command — a bare space at a prompt, an indented continuation line — so the
    /// blank check on the text is deliberately <c>IsNullOrEmpty</c> rather than the whitespace-aware one used
    /// on the label.
    /// </remarks>
    [Fact]
    public void ACommandThatIsNothingButWhitespace_IsAllowed()
    {
        new SnippetSecret { Label = "a space", Command = " " }.TryValidate(out _).ShouldBeTrue();
    }

    // ---- The merge ----

    [Fact]
    public void AnUncontestedEdit_IsTakenFromWhicheverSideMadeIt()
    {
        var ancestor = Snippet();
        var local = ancestor with { Notes = "now with a note" };

        var result = SnippetSecretMerge.Merge(ancestor, local, ancestor);

        result.HasConflicts.ShouldBeFalse();
        result.Merged.Notes.ShouldBe("now with a note");
    }

    /// <remarks>
    /// <para>
    /// The property that matters about this field, stated over every combination there is: a merge never
    /// produces a snippet that runs on its own unless one of the two sides asked for one. It holds for a
    /// structural reason rather than a defensive one — a three-way clash needs both sides to differ from the
    /// ancestor and from each other, which two values cannot do — and that is exactly why it is worth a test.
    /// The reasoning is easy to lose, and the field is the whole safety story.
    /// </para>
    /// <para>
    /// An earlier draft of the merge special-cased a clash here to resolve to <see langword="false"/>. This
    /// test is what showed the branch was unreachable.
    /// </para>
    /// </remarks>
    [Theory]
    [InlineData(false, false, false, false)]
    [InlineData(false, true, false, true)]
    [InlineData(false, false, true, true)]
    [InlineData(false, true, true, true)]
    [InlineData(true, false, true, false)]
    [InlineData(true, true, false, false)]
    [InlineData(true, true, true, true)]
    public void TheRunFlag_OnlyEverBecomesTrueBecauseASideAskedForIt(
        bool ancestor,
        bool local,
        bool remote,
        bool expected)
    {
        var start = Snippet() with { RunsOnInsert = ancestor };

        var result = SnippetSecretMerge.Merge(
            start,
            start with { RunsOnInsert = local },
            start with { RunsOnInsert = remote });

        result.Merged.RunsOnInsert.ShouldBe(expected);

        result.Conflicts.ShouldNotContain(
            conflict => conflict.Field == nameof(SnippetSecret.RunsOnInsert),
            "a two-valued field cannot produce a three-way conflict");
    }

    [Fact]
    public void TwoDifferentEditsToTheCommand_AreReportedWithBothTexts()
    {
        // Nothing is redacted here, unlike a credential: a snippet is text somebody wrote on purpose, and a
        // notice that hid the discarded version would leave them unable to tell which one survived.
        var ancestor = Snippet();

        var result = SnippetSecretMerge.Merge(
            ancestor,
            ancestor with { Command = "systemctl restart api" },
            ancestor with { Command = "systemctl reload api" });

        var conflict = result.Conflicts.ShouldHaveSingleItem();

        conflict.Field.ShouldBe(nameof(SnippetSecret.Command));
        conflict.Kept.ShouldBe("systemctl reload api");
        conflict.Discarded.ShouldBe("systemctl restart api");
    }

    private static SnippetSecret Snippet() => new()
    {
        Label = "restart the api",
        Command = "sudo systemctl restart dodossh-api",
    };
}
