using ClawdDotNet.Tools.FileRW;
using Shouldly;

namespace ClawdDotNet.Tools.Tests.FileRW;

/// <summary>
/// S6 aus der Bestandsaufnahme: Die Pfadprüfung verglich nur Zeichenketten-Präfixe.
/// Ohne abschließenden Verzeichnistrenner erlaubte ein Root "…\Workspace" damit auch
/// "…\Workspace-Backup\…".
///
/// Das Dateisystem wird hier bewusst NICHT abstrahiert — die Tests sollen die echte
/// Pfadsemantik der jeweiligen Plattform prüfen (.., UNC, Alternate Data Streams,
/// abschließende Punkte, unter Linux zusätzlich Groß-/Kleinschreibung und
/// Verknüpfungen). Eine Abstraktion würde genau die Fehlerklasse verstecken, um die es
/// geht.
///
/// Die Verzeichnisgrenze selbst prüft <c>PathBoundaryTests</c> in Core.Tests; hier geht
/// es um das, was <see cref="WorkspacePath.Resolve"/> darüber hinaus abweist.
/// </summary>
public sealed class WorkspacePathTests : IDisposable
{
    private readonly string _root;

    public WorkspacePathTests()
    {
        _root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
        Directory.CreateDirectory(_root);
    }

    public void Dispose()
    {
        try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
        catch { /* Aufräumen ist Nebensache */ }
    }

    private string Resolve(string? relative) => WorkspacePath.Resolve(_root, relative, "personal");

    // ═══════════════════════════════════════════════════════════
    //  Ausbruchsversuche
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData("../../../Windows/System32/drivers/etc/hosts")]
    [InlineData(@"..\..\evil.txt")]
    [InlineData("unterordner/../../../ausserhalb.txt")]
    [InlineData("./../../evil.txt")]
    [InlineData("..")]
    public void Relative_Ausbrueche_werden_abgelehnt(string path)
    {
        Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
    }

    /// <summary>
    /// Die Windows-Formen müssen auch unter Linux abgewiesen werden — dort greift
    /// aber eine andere Regel: <c>Path.IsPathRooted(@"C:\temp\x")</c> ist unter Linux
    /// <c>false</c>, weil das schlicht ein Dateiname mit Doppelpunkt ist. Gefangen
    /// werden die Fälle dann von der Doppelpunkt- bzw. der <c>\\</c>-Prüfung.
    /// Das Ergebnis ist auf beiden Plattformen dasselbe, der Weg dorthin nicht.
    /// </summary>
    [Theory]
    [InlineData(@"C:\Windows\System32\config\SAM")]
    [InlineData(@"\\server\share\evil.txt")]
    [InlineData("//server/share/evil.txt")]
    [InlineData(@"C:\temp\datei.txt")]
    public void Absolute_Pfade_und_UNC_Freigaben_werden_abgelehnt(string path)
    {
        Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
    }

    /// <summary>
    /// Die Linux-Entsprechungen. Unter Windows sind das gewöhnliche relative Pfade,
    /// die entweder ins Leere zeigen oder von der <c>..</c>-Regel gefangen werden —
    /// deshalb laufen sie dort mit, statt übersprungen zu werden.
    /// </summary>
    [Theory]
    [InlineData("/etc/passwd")]
    [InlineData("/root/.ssh/id_rsa")]
    [InlineData("../../../../etc/shadow")]
    public void Unix_Systempfade_werden_abgelehnt(string path)
    {
        Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
    }

    [Theory]
    [InlineData("datei.txt:versteckt")]
    [InlineData("datei.txt:$DATA")]
    public void Alternate_Data_Streams_werden_abgelehnt(string path)
    {
        // Ein ADS umgeht sonst die Endungsprüfung: "x.txt:evil.exe".
        Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
    }

    /// <summary>
    /// Der eigentliche Kern von S6: ein Nachbarverzeichnis mit gleichem Präfix.
    /// </summary>
    [Fact]
    public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_gilt_als_ausserhalb()
    {
        var backup = _root + "-Backup";

        WorkspacePath.IsInside(Path.Combine(backup, "geheim.txt"), _root).ShouldBeFalse();
    }

    [Fact]
    public void Auch_bei_Rootangabe_mit_Trenner_bleibt_das_Nachbarverzeichnis_aussen()
    {
        var rootWithSeparator = _root + Path.DirectorySeparatorChar;

        WorkspacePath.IsInside(_root + "-Backup" + Path.DirectorySeparatorChar + "x.txt", rootWithSeparator)
            .ShouldBeFalse();
    }

    // ═══════════════════════════════════════════════════════════
    //  Gegenproben — normale Nutzung muss funktionieren
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData("bericht.md")]
    [InlineData("unterordner/bericht.md")]
    [InlineData("a/b/c/tief.json")]
    [InlineData("./bericht.md")]
    [InlineData("unterordner/../bericht.md")]
    public void Pfade_innerhalb_des_Workspace_werden_aufgeloest(string path)
    {
        var resolved = Resolve(path);

        WorkspacePath.IsInside(resolved, _root).ShouldBeTrue();
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(".")]
    public void Leere_Angaben_ergeben_das_Wurzelverzeichnis(string? path)
    {
        var resolved = Resolve(path);

        resolved.TrimEnd(Path.DirectorySeparatorChar)
            .ShouldBe(Path.GetFullPath(_root).TrimEnd(Path.DirectorySeparatorChar));
    }

    [Fact]
    public void Das_Wurzelverzeichnis_selbst_gilt_als_innerhalb()
    {
        WorkspacePath.IsInside(_root, _root).ShouldBeTrue();
    }

    [Fact]
    public void Ein_Unterverzeichnis_mit_aehnlichem_Namen_bleibt_innerhalb()
    {
        // Gegenprobe zur Präfix-Regel: Innerhalb des Roots ist alles erlaubt.
        var inner = Path.Combine(_root, "Workspace-Backup", "x.txt");

        WorkspacePath.IsInside(inner, _root).ShouldBeTrue();
    }
}
