namespace DodoSSH.Client.Session.Tests;

/// <summary>
/// Where the profile goes.
/// </summary>
/// <remarks>
/// A short suite for something that looks trivial and is not. The cache is a SQLite file written by one
/// process, and the design assumes each machine has its own — the outbox holds changes only this machine
/// has made. Two machines sharing one file through a cloud sync client corrupts it, so a roaming or
/// synced directory is a correctness problem rather than a matter of taste.
/// </remarks>
public sealed class ClientPathsTests
{
    [Fact]
    public void TheCacheLivesInsideTheProfileDirectory()
    {
        var paths = new ClientPaths(Path.Combine("C:", "somewhere", "DodoSSH"));

        Path.GetDirectoryName(paths.CacheFile).ShouldBe(paths.DataDirectory);
        Path.GetFileName(paths.CacheFile).ShouldBe("cache.db");
    }

    [Fact]
    public void TheDefaultDirectoryIsAbsoluteAndNamed()
    {
        var paths = ClientPaths.Default;

        Path.IsPathFullyQualified(paths.DataDirectory).ShouldBeTrue(paths.DataDirectory);

        // OrdinalIgnoreCase, and the casing is the point rather than an oversight. ClientPaths spells the
        // folder DodoSSH on Windows and dodossh on Unix on purpose — one follows the platform's title-cased
        // convention, the other the lower-case dotfile one. The Ordinal "odoSSH" this used to look for was
        // clever enough to survive either spelling of the leading D and still only ever matched Windows,
        // which went unnoticed for exactly as long as nothing ran the suite anywhere else.
        paths.DataDirectory.Contains("dodossh", StringComparison.OrdinalIgnoreCase)
            .ShouldBeTrue($"'{paths.DataDirectory}' should be identifiable as ours");
    }

    [Fact]
    public void ResolvingTheDefault_CreatesNothing()
    {
        // Read during startup diagnostics and by tests. A side effect here would mean merely asking where
        // the cache would go creates a directory.
        var paths = new ClientPaths(
            Path.Combine(Path.GetTempPath(), $"dodossh-paths-{Guid.CreateVersion7():N}"));

        Directory.Exists(paths.DataDirectory).ShouldBeFalse();

        paths.EnsureCreated();

        try
        {
            Directory.Exists(paths.DataDirectory).ShouldBeTrue();

            // Idempotent, because startup runs it every launch.
            paths.EnsureCreated();
        }
        finally
        {
            Directory.Delete(paths.DataDirectory, recursive: true);
        }
    }

    /// <summary>
    /// The nightly channel keeps its own profile, and everything else keeps the existing one.
    /// </summary>
    /// <remarks>
    /// Both halves are load-bearing and they fail in opposite directions. A nightly sharing the release
    /// build's directory would migrate the cache schema of a database the release build then opens — the
    /// two are installed at once by design, so that is an ordinary Tuesday. And the release channel's
    /// directory moving even slightly would orphan every existing install's cache, outbox and device key:
    /// the application would start, find nothing, and ask for a server. See ADR 0013 decision 9.
    /// </remarks>
    [Theory]
    [InlineData("release")]
    [InlineData("")]
    [InlineData(null)]
    [InlineData("something nobody wrote")]
    public void EveryChannelButTheNightlyKeepsTheExistingProfile(string? channel)
    {
        var resolved = ClientPaths.ForChannel(channel).DataDirectory;

        // string.Equals with an explicit comparison rather than ShouldBe, here and below: these are paths
        // built from the same constants, so the comparison that means anything is the exact one.
        string.Equals(resolved, ClientPaths.Default.DataDirectory, StringComparison.Ordinal)
            .ShouldBeTrue($"'{channel}' resolved to '{resolved}' rather than to the default profile");
    }

    [Fact]
    public void TheNightlyProfileIsASiblingOfTheReleaseOne()
    {
        var release = ClientPaths.Default.DataDirectory;
        var nightly = ClientPaths.ForChannel("nightly").DataDirectory;

        string.Equals(nightly, release, StringComparison.Ordinal)
            .ShouldBeFalse($"the nightly and the release build would share '{release}'");

        // A sibling rather than a child, so neither install's uninstaller or reset can reach the other's.
        string.Equals(
                Path.GetDirectoryName(nightly),
                Path.GetDirectoryName(release),
                StringComparison.Ordinal)
            .ShouldBeTrue($"'{nightly}' should sit beside '{release}'");

        // And still identifiably ours, on either platform's spelling — the same property the default is
        // checked for above, for the same reason.
        nightly.Contains("dodossh", StringComparison.OrdinalIgnoreCase)
            .ShouldBeTrue($"'{nightly}' should be identifiable as ours");

        // The files follow the directory. A nightly writing the release build's cache or device key is the
        // whole failure this separation exists to prevent, so it is asserted rather than inferred.
        var separated = ClientPaths.ForChannel("nightly");

        string.Equals(separated.CacheFile, ClientPaths.Default.CacheFile, StringComparison.Ordinal)
            .ShouldBeFalse("the two builds would write one cache.db");

        string.Equals(separated.DeviceKeyFile, ClientPaths.Default.DeviceKeyFile, StringComparison.Ordinal)
            .ShouldBeFalse("the two builds would share one device key");
    }

    [Fact]
    public void OnWindowsItIsTheLocalProfileAndNotTheRoamingOne()
    {
        // %APPDATA% roams in a domain environment, which would sync one machine's SQLite cache to another
        // and corrupt it. %LOCALAPPDATA% does not.
        if (!OperatingSystem.IsWindows())
        {
            Assert.Skip("Windows-only path convention.");
        }

        var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

        ClientPaths.Default.DataDirectory.ShouldStartWith(local);

        // Guard against the two happening to be equal on some configuration, which would make the
        // assertion above meaningless.
        if (!string.Equals(local, roaming, StringComparison.OrdinalIgnoreCase))
        {
            ClientPaths.Default.DataDirectory.ShouldNotStartWith(roaming);
        }
    }
}
