using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Storage;

/// <summary>
/// Die Einschließungsprüfung verglich bis zur Linux-Portierung mit
/// <c>OrdinalIgnoreCase</c> — die Windows-Annahme, dass Groß- und Kleinschreibung
/// zusammenfallen. Unter Linux sind <c>Workspace</c> und <c>workspace</c> zwei
/// Verzeichnisse, und eine symbolische Verknüpfung kann aus dem Wurzelverzeichnis
/// hinauszeigen, ohne dass die reine Pfadrechnung das bemerkt.
///
/// Das Dateisystem wird hier bewusst nicht abstrahiert: Genau die Semantik, um die es
/// geht, würde eine Abstraktion verstecken.
/// </summary>
public sealed class PathBoundaryTests : IDisposable
{
    private readonly string _base;
    private readonly string _root;

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

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

    // ═══════════════════════════════════════════════════════════
    //  Verzeichnisgrenzen (auf beiden Plattformen gleich)
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_liegt_ausserhalb()
    {
        PathBoundary.IsInside(Path.Combine(_root + "-Backup", "geheim.txt"), _root)
            .ShouldBeFalse();
    }

    [Fact]
    public void Das_Wurzelverzeichnis_selbst_liegt_innerhalb()
    {
        PathBoundary.IsInside(_root, _root).ShouldBeTrue();
        PathBoundary.IsInside(_root + Path.DirectorySeparatorChar, _root).ShouldBeTrue();
    }

    [Fact]
    public void Ein_Unterverzeichnis_liegt_innerhalb()
    {
        PathBoundary.IsInside(Path.Combine(_root, "a", "b", "c.txt"), _root).ShouldBeTrue();
    }

    [Fact]
    public void Punkt_Punkt_fuehrt_hinaus()
    {
        PathBoundary.IsInside(Path.Combine(_root, "..", "evil.txt"), _root).ShouldBeFalse();
    }

    // ═══════════════════════════════════════════════════════════
    //  Groß-/Kleinschreibung — hier gehen die Plattformen auseinander
    // ═══════════════════════════════════════════════════════════

    [LinuxFact]
    public void Unter_Linux_ist_ein_Verzeichnis_mit_anderer_Schreibweise_ein_anderes()
    {
        // "workspace" neben "Workspace": zwei echte, getrennte Verzeichnisse.
        var lower = Path.Combine(_base, "workspace");
        Directory.CreateDirectory(lower);

        PathBoundary.IsInside(Path.Combine(lower, "geheim.txt"), _root).ShouldBeFalse();
    }

    [WindowsFact]
    public void Unter_Windows_meint_eine_andere_Schreibweise_dasselbe_Verzeichnis()
    {
        var lower = Path.Combine(_base, "workspace");

        PathBoundary.IsInside(Path.Combine(lower, "datei.txt"), _root).ShouldBeTrue();
    }

    [Fact]
    public void Der_Schluessel_fuer_Sperren_folgt_der_Plattform()
    {
        var upper = Path.Combine(_base, "Config.json");
        var lower = Path.Combine(_base, "config.json");

        var gleich = PathBoundary.CanonicalKey(upper) == PathBoundary.CanonicalKey(lower);

        // Unter Windows dieselbe Datei — dieselbe Sperre. Unter Linux zwei Dateien,
        // die sich keine teilen dürfen.
        gleich.ShouldBe(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
    }

    // ═══════════════════════════════════════════════════════════
    //  Symbolische Verknüpfungen
    // ═══════════════════════════════════════════════════════════

    [LinuxFact]
    public void Eine_Verknuepfung_aus_dem_Wurzelverzeichnis_heraus_wird_erkannt()
    {
        // Der Fall, den die reine Pfadrechnung nicht sieht: "Workspace/raus" zeigt
        // nach draußen, "Workspace/raus/beute.txt" liegt lexikalisch aber innerhalb.
        var draussen = Path.Combine(_base, "Draussen");
        Directory.CreateDirectory(draussen);
        Directory.CreateSymbolicLink(Path.Combine(_root, "raus"), draussen);

        PathBoundary.IsInside(Path.Combine(_root, "raus", "beute.txt"), _root)
            .ShouldBeFalse();
    }

    [LinuxFact]
    public void Eine_Verknuepfung_innerhalb_des_Wurzelverzeichnisses_bleibt_erlaubt()
    {
        // Gegenprobe: Wer innerhalb bleibt, wird nicht ausgesperrt.
        var ziel = Path.Combine(_root, "echte-daten");
        Directory.CreateDirectory(ziel);
        Directory.CreateSymbolicLink(Path.Combine(_root, "abkuerzung"), ziel);

        PathBoundary.IsInside(Path.Combine(_root, "abkuerzung", "bericht.md"), _root)
            .ShouldBeTrue();
    }

    [LinuxFact]
    public void Ein_Wurzelverzeichnis_hinter_einer_Verknuepfung_sperrt_sich_nicht_selbst_aus()
    {
        // Liegt der Workspace selbst hinter einer Verknüpfung (üblich bei /var → /private/var
        // oder gemounteten Datenverzeichnissen), muss die Auflösung auf beiden Seiten
        // greifen — sonst passt nach dem Auflösen nichts mehr zusammen.
        var echt = Path.Combine(_base, "EchterOrt");
        Directory.CreateDirectory(echt);
        var ueber = Path.Combine(_base, "Verknuepft");
        Directory.CreateSymbolicLink(ueber, echt);

        PathBoundary.IsInside(Path.Combine(ueber, "datei.txt"), ueber).ShouldBeTrue();
        PathBoundary.IsInside(Path.Combine(echt, "datei.txt"), ueber).ShouldBeTrue();
    }

    [Fact]
    public void Nicht_existierende_Pfade_bleiben_pruefbar()
    {
        // Beim Schreiben gibt es die Datei noch nicht. Was nicht existiert, kann keine
        // Verknüpfung sein — die Auflösung darf daran nicht scheitern.
        PathBoundary.IsInside(Path.Combine(_root, "gibt", "es", "noch", "nicht.txt"), _root)
            .ShouldBeTrue();
    }
}
