using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Config;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
///
/// Die tägliche Sicherung, seit sie der Instanz gehört (#528, Stufe 1).
///
/// Schalter, Uhrzeit, Zielordner und Aufbewahrung standen in den
/// Anwendungseinstellungen und galten damit für jede Instanz. Zwei Folgen: Wer die
/// Sicherung für eine Instanz einschaltete, schaltete sie für alle ein — und alle
/// Archive lagen in einem Ordner, in dem die Aufbewahrung nach dem Präfix
/// backup_<Instanzname>_ aufräumt. Dass ein Präfix keine Grenze ist, zeigt
/// .
///
public sealed class BackupSchedulerTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(), "clawd-tests", "backup-" + Guid.NewGuid().ToString("N")[..8]);
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); }
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
}
/// Ein Instanzverzeichnis mit genug Inhalt, dass sich eine Sicherung lohnt.
private string NeueInstanz(string name)
{
var dir = Path.Combine(_root, "Instance-" + name);
Directory.CreateDirectory(dir);
File.WriteAllText(Path.Combine(dir, "InstanceSettings.json"), $$"""{"instanceName":"{{name}}"}""");
return dir;
}
private static InstanceConfig Config(string name, string? backupDirectory = null, int keep = 14)
=> new()
{
InstanceName = name,
WorkingDirectory = "",
AutoBackupEnabled = true,
AutoBackupTime = "03:00",
BackupDirectory = backupDirectory ?? "",
BackupKeepCount = keep
};
///
/// Eine Uhr, die auf einer Zeit steht — der Zeitpunkt entscheidet, ob
/// TickAsync etwas tut.
///
private sealed class FesteUhr(DateTime jetzt) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => new(jetzt, TimeSpan.Zero);
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}
private static BackupScheduler Scheduler(string dir, InstanceConfig config, DateTime jetzt)
=> new(dir, config, NullLogger.Instance, new FesteUhr(jetzt));
private static DateTime Nachts(int tag) => new(2026, 9, tag, 4, 0, 0, DateTimeKind.Utc);
// ─── Der Zielordner ───
[Fact]
public async Task Ohne_Angabe_landet_die_Sicherung_im_Instanzverzeichnis()
{
var dir = NeueInstanz("Allein");
await Scheduler(dir, Config("Allein"), Nachts(1)).TickAsync();
var ziel = Path.Combine(dir, "Backups");
Directory.Exists(ziel).ShouldBeTrue();
Directory.GetFiles(ziel, "*.zip").Length.ShouldBe(1);
}
[Fact]
public async Task Zwei_Instanzen_sichern_ohne_Angabe_getrennt()
{
var a = NeueInstanz("Eins");
var b = NeueInstanz("Zwei");
await Scheduler(a, Config("Eins"), Nachts(1)).TickAsync();
await Scheduler(b, Config("Zwei"), Nachts(1)).TickAsync();
Directory.GetFiles(Path.Combine(a, "Backups"), "*.zip").Length.ShouldBe(1);
Directory.GetFiles(Path.Combine(b, "Backups"), "*.zip").Length.ShouldBe(1);
}
// ─── Der Schalter ───
[Fact]
public async Task Ausgeschaltet_passiert_nichts()
{
var dir = NeueInstanz("Ruhig");
var config = Config("Ruhig");
config.AutoBackupEnabled = false;
await Scheduler(dir, config, Nachts(1)).TickAsync();
Directory.Exists(Path.Combine(dir, "Backups")).ShouldBeFalse();
}
[Fact]
public async Task Vor_der_eingestellten_Uhrzeit_passiert_nichts()
{
var dir = NeueInstanz("Frueh");
await Scheduler(dir, Config("Frueh"), new DateTime(2026, 9, 1, 2, 0, 0, DateTimeKind.Utc))
.TickAsync();
Directory.Exists(Path.Combine(dir, "Backups")).ShouldBeFalse();
}
[Fact]
public async Task Zweimal_am_selben_Tag_sichert_nur_einmal()
{
var dir = NeueInstanz("Einmal");
var scheduler = Scheduler(dir, Config("Einmal"), Nachts(1));
await scheduler.TickAsync();
await scheduler.TickAsync();
Directory.GetFiles(Path.Combine(dir, "Backups"), "*.zip").Length.ShouldBe(1);
}
// ─── Die Aufbewahrung ───
[Fact]
public async Task Die_Aufbewahrung_raeumt_die_eigenen_Archive_auf()
{
var dir = NeueInstanz("Alt");
var ziel = Path.Combine(dir, "Backups");
Directory.CreateDirectory(ziel);
// Drei Archive aus der Vergangenheit, die aelter sind als das gleich entstehende.
foreach (var tag in new[] { 1, 2, 3 })
{
var datei = Path.Combine(ziel, $"backup_Alt_2026-08-0{tag}_0300.zip");
File.WriteAllText(datei, "alt");
File.SetLastWriteTime(datei, new DateTime(2026, 8, tag, 3, 0, 0));
}
await Scheduler(dir, Config("Alt", keep: 2), Nachts(1)).TickAsync();
Directory.GetFiles(ziel, "*.zip").Length.ShouldBe(2);
}
[Fact]
public async Task Aufbewahrung_null_behaelt_alles()
{
var dir = NeueInstanz("Sammler");
var ziel = Path.Combine(dir, "Backups");
Directory.CreateDirectory(ziel);
File.WriteAllText(Path.Combine(ziel, "backup_Sammler_2026-08-01_0300.zip"), "alt");
await Scheduler(dir, Config("Sammler", keep: 0), Nachts(1)).TickAsync();
Directory.GetFiles(ziel, "*.zip").Length.ShouldBe(2);
}
///
/// Warum der Zielordner der Instanz gehören muss.
///
/// Die Aufbewahrung sucht nach dem Präfix backup_<Instanzname>_,
/// und ein Präfix ist keine Grenze: backup_Test_2_….zip — das Archiv der
/// Instanz Test_2 — fängt mit backup_Test_ an. Im gemeinsamen Ordner
/// zählt Test es also zu den eigenen und wirft es weg. Getrennte Ordner
/// nehmen der Namensfrage ihre Wirkung: Der Test hier stellt den alten Zustand
/// eigens wieder her, indem er beiden Instanzen denselben Ordner einträgt.
///
[Fact]
public async Task Im_gemeinsamen_Ordner_greift_die_Aufbewahrung_ueber_die_Instanz_hinaus()
{
var gemeinsam = Path.Combine(_root, "Backups");
Directory.CreateDirectory(gemeinsam);
var fremd = Path.Combine(gemeinsam, "backup_Test_2_2026-08-01_0300.zip");
File.WriteAllText(fremd, "gehoert Test_2");
File.SetLastWriteTime(fremd, new DateTime(2026, 8, 1, 3, 0, 0));
var dir = NeueInstanz("Test");
await Scheduler(dir, Config("Test", gemeinsam, keep: 1), Nachts(1)).TickAsync();
File.Exists(fremd).ShouldBeFalse(
"Der alte Zustand ist damit belegt — im gemeinsamen Ordner räumt eine "
+ "Instanz über ihre eigenen Archive hinaus auf.");
// Und die Gegenprobe: mit getrennten Ordnern kommen sie sich nicht in die Quere.
var eigen = NeueInstanz("Test2");
var eigenesArchiv = Path.Combine(eigen, "Backups", "backup_Test2_2026-08-01_0300.zip");
Directory.CreateDirectory(Path.GetDirectoryName(eigenesArchiv)!);
File.WriteAllText(eigenesArchiv, "gehoert Test2");
var dir2 = NeueInstanz("Test3");
await Scheduler(dir2, Config("Test3", keep: 1), Nachts(2)).TickAsync();
File.Exists(eigenesArchiv).ShouldBeTrue();
}
}