namespace DodoSSH.Client.Ssh.Tests;

/// <summary>
/// Remote paths, permission bits and byte counts — the three things the file browser renders.
/// </summary>
/// <remarks>
/// Not in the SSH collection, so this class needs no container. Every case here is one where the obvious
/// implementation is wrong on Windows, at a filesystem root, or on a number that has just crossed a
/// threshold — which is to say, one that a listing of somebody's home directory would not reveal.
/// </remarks>
public sealed class RemotePathTests
{
    [Theory]
    [InlineData("/var/log", "syslog", "/var/log/syslog")]
    [InlineData("/", "etc", "/etc")]
    [InlineData("/home/dodo/", "notes", "/home/dodo/notes")]
    public void Combine_JoinsWithExactlyOneSeparator(string directory, string name, string expected)
    {
        // Deliberately not Path.Combine, which on Windows would yield "/var/log\syslog" — a path the remote
        // cannot resolve, failing as "no such file" somewhere the backslash is invisible.
        SftpPath.Combine(directory, name).ShouldBe(expected);
    }

    [Theory]
    [InlineData("/var/log/syslog", "/var/log")]
    [InlineData("/var/log", "/var")]
    [InlineData("/var", "/")]
    [InlineData("/", "/")]
    [InlineData("/var/log/", "/var")]
    public void Parent_StopsAtTheRoot(string path, string expected)
    {
        // The root is its own parent rather than null, which is what lets the breadcrumb's "up" be a plain
        // navigation with nothing above it to special-case.
        SftpPath.Parent(path).ShouldBe(expected);
    }

    [Theory]
    [InlineData("/var/log/syslog", "syslog")]
    [InlineData("/var/log/", "log")]
    [InlineData("/", "/")]
    public void Name_IsTheLastSegment(string path, string expected)
    {
        SftpPath.Name(path).ShouldBe(expected);
    }

    [Fact]
    public void Trail_NamesEverySegmentWithThePathThatReachesIt()
    {
        SftpPath.Trail("/var/log/nginx").ShouldBe(
            [("var", "/var"), ("log", "/var/log"), ("nginx", "/var/log/nginx")]);
    }

    [Fact]
    public void Trail_IsEmptyAtTheRoot()
    {
        // The root has no segment to name. The breadcrumb draws it as a leading separator, so a trail with a
        // phantom empty crumb in it would render as a button with no label.
        SftpPath.Trail("/").ShouldBeEmpty();
    }

    [Fact]
    public void PosixMode_RendersTheKindCharacterAndThreeTriples()
    {
        PosixMode.Format(
            SftpEntryKind.Directory,
            ownerRead: true, ownerWrite: true, ownerExecute: true,
            groupRead: true, groupWrite: false, groupExecute: true,
            othersRead: true, othersWrite: false, othersExecute: true)
            .ShouldBe("drwxr-xr-x");

        PosixMode.Format(
            SftpEntryKind.File,
            ownerRead: true, ownerWrite: true, ownerExecute: false,
            groupRead: true, groupWrite: false, groupExecute: false,
            othersRead: false, othersWrite: false, othersExecute: false)
            .ShouldBe("-rw-r-----");

        PosixMode.Format(
            SftpEntryKind.SymbolicLink,
            ownerRead: true, ownerWrite: true, ownerExecute: true,
            groupRead: true, groupWrite: true, groupExecute: true,
            othersRead: true, othersWrite: true, othersExecute: true)
            .ShouldBe("lrwxrwxrwx");
    }

    [Theory]
    [InlineData("-rwxr-xr-x", true)]
    [InlineData("-rw-r-xr--", true)]
    [InlineData("-rw-r--r-x", true)]
    [InlineData("-rw-r--r--", false)]
    [InlineData("----------", false)]
    public void PosixMode_ReadsAnExecuteBitInAnyTriple(string mode, bool expected)
    {
        // Any of the three, not the owner's: the account browsing is not necessarily the owner, and a file
        // that only the group may run is still a file somebody runs.
        PosixMode.HasAnyExecuteBit(mode).ShouldBe(expected);
    }

    [Theory]
    [InlineData("-rw-rw-rw-", true)]
    [InlineData("-rw-rw-r--", false)]
    [InlineData("-rw-r--r--", false)]
    [InlineData("--------w-", true)]
    public void PosixMode_ReadsTheOthersWriteBit(string mode, bool expected)
    {
        // The others triple only. A group-writable file is writable by a named set of people, which is what
        // groups are for; this asks about the column that names nobody.
        PosixMode.IsWorldWritable(mode).ShouldBe(expected);
    }

    [Theory]
    [InlineData("")]
    [InlineData("rwxrwxrwx")]
    [InlineData("?")]
    public void PosixMode_AnswersFalseForAModeItDidNotWrite(string mode)
    {
        // These two decide a row's colour and nothing else. A listing is not worth failing over a string of
        // the wrong length, and an index off the end of one is how that would happen.
        PosixMode.HasAnyExecuteBit(mode).ShouldBeFalse();
        PosixMode.IsWorldWritable(mode).ShouldBeFalse();
    }

    [Theory]
    [InlineData(SftpEntryKind.File, "-rwxr-xr-x", true)]
    [InlineData(SftpEntryKind.File, "-rw-r--r--", false)]
    [InlineData(SftpEntryKind.Directory, "drwxr-xr-x", false)]
    [InlineData(SftpEntryKind.SymbolicLink, "lrwxrwxrwx", false)]
    public void AnEntry_IsExecutableOnlyWhenItIsAFile(SftpEntryKind kind, string mode, bool expected)
    {
        // The x on a directory means "may be searched", which is true of very nearly every directory a host
        // has. A file browser that marked them all would be marking nothing.
        Entry(kind, mode).IsExecutable.ShouldBe(expected);
    }

    [Theory]
    [InlineData(SftpEntryKind.File, "-rw-rw-rw-", true)]
    [InlineData(SftpEntryKind.File, "-rw-r--r--", false)]
    [InlineData(SftpEntryKind.SymbolicLink, "lrwxrwxrwx", false)]
    [InlineData(SftpEntryKind.Directory, "drwxrwxrwx", false)]
    public void AnEntry_IsWorldWritableOnlyWhenItIsAFile(SftpEntryKind kind, string mode, bool expected)
    {
        // The two exclusions are the point. Every symbolic link is lrwxrwxrwx and its mode governs nothing —
        // what may be written is the target, whose mode this listing never fetched. And a world-writable
        // directory is /tmp, made safe by a sticky bit PosixMode does not render: warning there would be
        // warning about the half of the mode that is on screen.
        Entry(kind, mode).IsWorldWritable.ShouldBe(expected);
    }

    [Theory]
    [InlineData(0, "0 B")]
    [InlineData(1023, "1023 B")]
    [InlineData(1024, "1.0 KB")]
    [InlineData(10 * 1024, "10 KB")]
    [InlineData(1536 * 1024, "1.5 MB")]
    [InlineData(5L * 1024 * 1024 * 1024, "5.0 GB")]
    public void ByteSize_KeepsOneDecimalOnlyWhileItMeansSomething(long bytes, string expected)
    {
        // The boundary at ten is the whole rule: "9.4 MB" and "512 MB" carry the same information, and
        // "512.3 MB" claims a precision the figure does not have by the time it is that large.
        ByteSize.Format(bytes).ShouldBe(expected);
    }

    /// <summary>An entry that is nothing but a kind and a mode, which is all these two questions read.</summary>
    private static SftpEntry Entry(SftpEntryKind kind, string mode) =>
        new("thing", "/tmp/thing", kind, 0, DateTimeOffset.UnixEpoch, mode);
}
