using System.IO.Compression;
using System.Text.Json;
using ClawdDotNet.Core.Backup;
using ClawdDotNet.Core.Memory;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Backup;

/// <summary>
/// Ein ungeprüftes Wiederherstellen ist kein Backup, sondern eine Vermutung.
/// Deshalb liegt der Schwerpunkt hier auf dem vollständigen Rundlauf.
///
/// Läuft in <see cref="SecretKeyCollection"/>, weil das Umschreiben der Geheimnisse
/// beim Sichern denselben statischen Schlüssel benutzt wie die Geheimnistests.
/// </summary>
[Collection(SecretKeyCollection.Name)]
public sealed class BackupServiceTests : IDisposable
{
    private readonly string _root;
    private readonly string _instanceDir;
    private readonly BackupService _service = new();

    private const string ApiKeyPlain = "sk-or-v1-streng-geheim-12345";
    private const string MailPasswordPlain = "mail-passwort-geheim";

    public BackupServiceTests()
    {
        _root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
        _instanceDir = Path.Combine(_root, "Instance-Test");
        Directory.CreateDirectory(_instanceDir);
    }

    public void Dispose()
    {
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
        try { Directory.Delete(_root, recursive: true); }
        catch { /* Aufräumen ist Nebensache */ }
    }

    // ─── Aufbau einer realistischen Instanz ───

    private async Task BuildInstanceAsync()
    {
        // Instanzkonfiguration — Zugangsdaten liegen DPAPI-geschützt wie im Betrieb.
        var instanceSettings = $$"""
            {
              "instanceId": "test-01",
              "instanceName": "Testinstanz",
              "openRouterApiKey": "{{SecretProtector.Protect(ApiKeyPlain)}}",
              "webServerPort": 8080,
              "einUnbekanntesFeld": "muss erhalten bleiben"
            }
            """;
        AtomicFile.WriteAllText(Path.Combine(_instanceDir, "InstanceSettings.json"), instanceSettings);

        // Agent
        var agentDir = Path.Combine(_instanceDir, "Agents", "Agent-Analyst");
        Directory.CreateDirectory(Path.Combine(agentDir, "Workspace"));
        Directory.CreateDirectory(Path.Combine(agentDir, "Logs"));

        var agentSettings = $$"""
            {
              "agentId": "analyst",
              "displayName": "Analyst",
              "model": "anthropic/claude-sonnet-4-5",
              "tools": {
                "Mail": {
                  "smtpHost": "smtp.example.com",
                  "password": "{{SecretProtector.Protect(MailPasswordPlain)}}"
                }
              }
            }
            """;
        AtomicFile.WriteAllText(Path.Combine(agentDir, "AgentSettings.json"), agentSettings);
        AtomicFile.WriteAllText(Path.Combine(agentDir, "Identity.md"), "# Identity\nDer Analyst.");
        AtomicFile.WriteAllText(Path.Combine(agentDir, "Soul.md"), "# Soul\nGründlich und knapp.");
        AtomicFile.WriteAllText(Path.Combine(agentDir, "ChatHistory.json"), """[{"role":"user"}]""");
        AtomicFile.WriteAllText(Path.Combine(agentDir, "Workspace", "bericht.md"), "# Bericht\nInhalt.");
        AtomicFile.WriteAllText(Path.Combine(agentDir, "Logs", "lauf.log"), "viele Zeilen Protokoll");

        // Datenbank mit einer Erinnerung — der wertvollste Teil.
        var storage = new SqliteStorage(Path.Combine(_instanceDir, "state.db"));
        var memory = new SqliteMemoryRepository(storage);
        await memory.RememberAsync(new MemoryEntry
        {
            Scope = MemoryScope.Agent,
            OwnerId = "analyst",
            Subject = "NVDA",
            Content = "Muss die Sicherung überstehen",
            Key = "kernaussage",
            CreatedBy = "analyst"
        }, default);

        // Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
    }

    private string ZipPath => Path.Combine(_root, "sicherung.zip");
    private string RestoreDir => Path.Combine(_root, "wiederhergestellt");

    // ═══════════════════════════════════════════════════════════
    //  Der Rundlauf
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Der_vollstaendige_Rundlauf_stellt_alles_wieder_her()
    {
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });

        var result = await _service.RestoreAsync(ZipPath, RestoreDir,
            new RestoreOptions { Passphrase = "geheim" });

        result.Written.ShouldNotBeEmpty();

        // Persönlichkeit
        File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Identity.md"))
            .ShouldContain("Der Analyst");
        File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Soul.md"))
            .ShouldContain("Gründlich");

        // Arbeitsstand
        File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Workspace", "bericht.md"))
            .ShouldContain("Inhalt");

        // Datenbank samt Gedächtnis
        File.Exists(Path.Combine(RestoreDir, "state.db")).ShouldBeTrue();

        var memory = new SqliteMemoryRepository(new SqliteStorage(Path.Combine(RestoreDir, "state.db")));
        var recalled = await memory.RecallAsync(
            new MemoryQuery { Scope = MemoryScope.Agent, OwnerId = "analyst" }, default);

        recalled.ShouldHaveSingleItem();
        recalled[0].Content.ShouldBe("Muss die Sicherung überstehen");
    }

    [Fact]
    public async Task Zugangsdaten_sind_nach_dem_Wiederherstellen_wieder_nutzbar()
    {
        // Der eigentliche Zweck der Passphrase: Ein Backup wird gebraucht, wenn der
        // Rechner defekt ist — DPAPI-geschützte Werte wären dann unlesbar.
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });
        await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "geheim" });

        var settings = JsonDocument.Parse(
            File.ReadAllText(Path.Combine(RestoreDir, "InstanceSettings.json")));

        var stored = settings.RootElement.GetProperty("openRouterApiKey").GetString();

        SecretProtector.IsProtected(stored).ShouldBeTrue("wieder mit DPAPI geschützt");
        SecretProtector.Unprotect(stored).ShouldBe(ApiKeyPlain);
    }

    [Fact]
    public async Task Auch_verschachtelte_Zugangsdaten_in_Tool_Konfigurationen_kommen_zurueck()
    {
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });
        await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "geheim" });

        var agent = JsonDocument.Parse(File.ReadAllText(
            Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "AgentSettings.json")));

        var password = agent.RootElement
            .GetProperty("tools").GetProperty("Mail").GetProperty("password").GetString();

        SecretProtector.Unprotect(password).ShouldBe(MailPasswordPlain);
    }

    [Fact]
    public async Task Unbekannte_Felder_ueberstehen_den_Rundlauf()
    {
        // Eine Sicherung darf nichts wegwerfen, nur weil eine Programmfassung ein
        // Feld nicht kennt.
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });
        await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "geheim" });

        File.ReadAllText(Path.Combine(RestoreDir, "InstanceSettings.json"))
            .ShouldContain("einUnbekanntesFeld");
    }

    // ═══════════════════════════════════════════════════════════
    //  Zugangsdaten weglassen
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Ohne_Zugangsdaten_enthaelt_das_Archiv_keine_Geheimnisse()
    {
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Exclude });

        // Das gesamte Archiv im Klartext durchsuchen.
        var inhalt = ReadWholeArchive(ZipPath);

        inhalt.ShouldNotContain(ApiKeyPlain);
        inhalt.ShouldNotContain(MailPasswordPlain);
    }

    [Fact]
    public async Task Eine_Sicherung_mit_Passphrase_zeigt_die_Geheimnisse_nicht_im_Klartext()
    {
        await BuildInstanceAsync();

        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });

        var inhalt = ReadWholeArchive(ZipPath);

        inhalt.ShouldNotContain(ApiKeyPlain);
        inhalt.ShouldNotContain(MailPasswordPlain);
        inhalt.ShouldContain("pbe:v1:");
    }

    // ═══════════════════════════════════════════════════════════
    //  Manifest und Fehlerfälle
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Das_Manifest_beschreibt_die_Sicherung()
    {
        await BuildInstanceAsync();

        var result = await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });

        var manifest = await _service.InspectAsync(ZipPath);

        manifest.InstanceId.ShouldBe("test-01");
        manifest.InstanceName.ShouldBe("Testinstanz");
        manifest.HasSecrets.ShouldBeTrue();
        manifest.SecretCount.ShouldBe(2, "API-Schlüssel und Mail-Passwort");
        manifest.Files.ShouldNotBeEmpty();
        result.SizeBytes.ShouldBeGreaterThan(0);
    }

    [Fact]
    public async Task Ohne_Passphrase_wird_nicht_wiederhergestellt()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "geheim" });

        var ex = await Should.ThrowAsync<BackupException>(
            () => _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions()));

        ex.Message.ShouldContain("Passphrase");
    }

    [Fact]
    public async Task Eine_falsche_Passphrase_wird_erkannt()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath,
            new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "richtig" });

        await Should.ThrowAsync<SecretProtectionException>(
            () => _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "falsch" }));
    }

    [Fact]
    public async Task Ein_veraendertes_Archiv_faellt_auf()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        // Eine Datei im Archiv nachträglich verändern.
        using (var archive = ZipFile.Open(ZipPath, ZipArchiveMode.Update))
        {
            var entry = archive.Entries.First(e => e.FullName.EndsWith("Identity.md"));
            using var stream = entry.Open();
            stream.SetLength(0);
            using var writer = new StreamWriter(stream);
            writer.Write("manipuliert");
        }

        var ex = await Should.ThrowAsync<BackupException>(
            () => _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions()));

        ex.Message.ShouldContain("Prüfsumme");
    }

    [Fact]
    public async Task Ein_Archiv_ohne_Manifest_wird_abgelehnt()
    {
        var fremd = Path.Combine(_root, "fremd.zip");
        var quelle = Path.Combine(_root, "quelle");
        Directory.CreateDirectory(quelle);
        File.WriteAllText(Path.Combine(quelle, "irgendwas.txt"), "Inhalt");
        ZipFile.CreateFromDirectory(quelle, fremd);

        var ex = await Should.ThrowAsync<BackupException>(() => _service.InspectAsync(fremd));

        ex.Message.ShouldContain("Manifest");
    }

    // ═══════════════════════════════════════════════════════════
    //  Vorschau und Überschreiben
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Die_Vorschau_schreibt_nichts()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        var result = await _service.RestoreAsync(ZipPath, RestoreDir,
            new RestoreOptions { DryRun = true });

        result.Written.ShouldBeEmpty();
        Directory.Exists(RestoreDir).ShouldBeFalse();
    }

    [Fact]
    public async Task Vorhandene_Dateien_werden_ohne_Zustimmung_nicht_ueberschrieben()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        Directory.CreateDirectory(RestoreDir);
        var vorhanden = Path.Combine(RestoreDir, "InstanceSettings.json");
        AtomicFile.WriteAllText(vorhanden, """{"wichtig":"nicht verlieren"}""");

        var result = await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions());

        result.HasConflicts.ShouldBeTrue();
        result.Skipped.ShouldContain("InstanceSettings.json");
        File.ReadAllText(vorhanden).ShouldContain("nicht verlieren");
    }

    [Fact]
    public async Task Mit_Zustimmung_wird_ueberschrieben()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        Directory.CreateDirectory(RestoreDir);
        var vorhanden = Path.Combine(RestoreDir, "InstanceSettings.json");
        AtomicFile.WriteAllText(vorhanden, """{"alt":true}""");

        await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Overwrite = true });

        File.ReadAllText(vorhanden).ShouldContain("Testinstanz");
    }

    // ═══════════════════════════════════════════════════════════
    //  Umfang
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Protokolle_bleiben_standardmaessig_aussen_vor()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        var manifest = await _service.InspectAsync(ZipPath);

        manifest.Files.ShouldNotContain(f => f.Path.Contains("Logs/"));
    }

    [Fact]
    public async Task Protokolle_lassen_sich_einschliessen()
    {
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions { IncludeLogs = true });

        var manifest = await _service.InspectAsync(ZipPath);

        manifest.Files.ShouldContain(f => f.Path.Contains("Logs/"));
    }

    [Fact]
    public async Task Die_WAL_Begleitdateien_landen_nicht_im_Archiv()
    {
        // VACUUM INTO arbeitet sie bereits ein — mitzusichern wäre irreführend.
        await BuildInstanceAsync();
        await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());

        var manifest = await _service.InspectAsync(ZipPath);

        manifest.Files.ShouldNotContain(f => f.Path.EndsWith("-wal") || f.Path.EndsWith("-shm"));
        manifest.Files.ShouldContain(f => f.Path == "state.db");
    }

    [Fact]
    public async Task Ein_fehlendes_Instanzverzeichnis_wird_gemeldet()
    {
        var ex = await Should.ThrowAsync<BackupException>(
            () => _service.CreateAsync(Path.Combine(_root, "gibtsnicht"), ZipPath, new BackupOptions()));

        ex.Message.ShouldContain("nicht gefunden");
    }

    [Fact]
    public async Task Passphrase_Modus_ohne_Passphrase_wird_abgelehnt()
    {
        await BuildInstanceAsync();

        await Should.ThrowAsync<BackupException>(() => _service.CreateAsync(
            _instanceDir, ZipPath, new BackupOptions { Secrets = SecretMode.Passphrase }));
    }

    // ─── Helfer ───

    private static string ReadWholeArchive(string zipPath)
    {
        using var archive = ZipFile.OpenRead(zipPath);
        var sb = new System.Text.StringBuilder();

        foreach (var entry in archive.Entries)
        {
            using var stream = entry.Open();
            using var buffer = new MemoryStream();
            stream.CopyTo(buffer);
            sb.Append(System.Text.Encoding.UTF8.GetString(buffer.ToArray()));
        }

        return sb.ToString();
    }
}
