using System.Text;
using System.Text.Json;
using ClawdDotNet.Core.Storage;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Storage;

/// <summary>
/// Alle Konfigurations- und Zustandsdateien wurden mit File.WriteAllText geschrieben.
/// Das kürzt die Zieldatei zuerst auf null und füllt sie dann — bricht der Vorgang
/// dazwischen ab, ist der alte Inhalt weg und der neue unvollständig.
///
/// Das ist im Betrieb bereits eingetreten: In einer Instanz lag eine
/// TokenUsage.json.corrupt_…
/// </summary>
public sealed class AtomicFileTests : IDisposable
{
    private readonly string _directory;

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

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

    private string Path_(string name) => Path.Combine(_directory, name);

    // ═══════════════════════════════════════════════════════════
    //  Grundverhalten
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public void Inhalt_wird_geschrieben()
    {
        var path = Path_("datei.json");

        AtomicFile.WriteAllText(path, """{"a":1}""");

        File.ReadAllText(path).ShouldBe("""{"a":1}""");
    }

    [Fact]
    public void Eine_bestehende_Datei_wird_ersetzt()
    {
        var path = Path_("datei.json");
        File.WriteAllText(path, "alter Inhalt der deutlich laenger ist");

        AtomicFile.WriteAllText(path, "neu");

        File.ReadAllText(path).ShouldBe("neu");
    }

    [Fact]
    public void Fehlende_Verzeichnisse_werden_angelegt()
    {
        var path = Path_(Path.Combine("a", "b", "c", "datei.json"));

        AtomicFile.WriteAllText(path, "inhalt");

        File.Exists(path).ShouldBeTrue();
    }

    [Fact]
    public void Es_bleiben_keine_Nebendateien_zurueck()
    {
        var path = Path_("datei.json");

        AtomicFile.WriteAllText(path, "eins");
        AtomicFile.WriteAllText(path, "zwei");

        Directory.GetFiles(_directory).ShouldHaveSingleItem();
    }

    [Fact]
    public void Umlaute_und_Emoji_werden_als_UTF8_ohne_BOM_geschrieben()
    {
        var path = Path_("datei.txt");
        const string content = "Grüße aus München 🦀";

        AtomicFile.WriteAllText(path, content);

        var bytes = File.ReadAllBytes(path);
        bytes[0].ShouldNotBe((byte)0xEF, "kein BOM am Dateianfang");
        new UTF8Encoding(false).GetString(bytes).ShouldBe(content);
    }

    [Fact]
    public async Task Die_asynchrone_Fassung_verhaelt_sich_gleich()
    {
        var path = Path_("datei.json");

        await AtomicFile.WriteAllTextAsync(path, "inhalt");

        File.ReadAllText(path).ShouldBe("inhalt");
        Directory.GetFiles(_directory).ShouldHaveSingleItem();
    }

    // ═══════════════════════════════════════════════════════════
    //  Die eigentliche Zusage: nie ein halber Inhalt
    // ═══════════════════════════════════════════════════════════

    /// <summary>
    /// Während geschrieben wird, wird parallel gelesen. Beim alten Verfahren erwischt
    /// man dabei zwangsläufig abgeschnittene Zwischenstände — hier darf jeder Lesevorgang
    /// nur ein vollständiges Dokument sehen.
    /// </summary>
    [Fact]
    public async Task Waehrend_des_Schreibens_wird_nie_ein_halber_Inhalt_sichtbar()
    {
        var path = Path_("gross.json");
        var klein = JsonSerializer.Serialize(Enumerable.Range(0, 50).Select(i => $"eintrag-{i}"));
        var gross = JsonSerializer.Serialize(Enumerable.Range(0, 20_000).Select(i => $"eintrag-{i}"));

        AtomicFile.WriteAllText(path, klein);

        // Harte Zeitgrenze, damit ein Fehler im Schreiber den Test nicht hängen lässt.
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

        var schreiber = Task.Run(async () =>
        {
            try
            {
                for (var i = 0; i < 40; i++)
                {
                    AtomicFile.WriteAllText(path, i % 2 == 0 ? gross : klein);
                    await Task.Delay(1);
                }
            }
            finally
            {
                await cts.CancelAsync();
            }
        });

        var fehler = 0;
        var gelesen = 0;

        var leser = Task.Run(async () =>
        {
            while (!cts.IsCancellationRequested)
            {
                try
                {
                    // AtomicFile.ReadAllText, damit der Leser den Schreiber nicht
                    // blockiert — genau dafür gibt es den Helfer.
                    var inhalt = AtomicFile.ReadAllText(path);
                    gelesen++;

                    // Muss immer ein vollständiges JSON-Array sein.
                    JsonSerializer.Deserialize<string[]>(inhalt);
                }
                catch (JsonException)
                {
                    Interlocked.Increment(ref fehler);
                }
                catch (IOException)
                {
                    // Kurzzeitig nicht zu öffnen ist kein Datenfehler.
                }
                await Task.Delay(5);
            }
        });

        await Task.WhenAll(schreiber, leser);

        gelesen.ShouldBeGreaterThan(5, "der Test muss tatsächlich gelesen haben");
        fehler.ShouldBe(0, "es darf nie ein unvollständiges Dokument sichtbar sein");
    }

    /// <summary>
    /// Der nicht offensichtliche Teil: Ein Leser, der die Datei geöffnet hält, würde
    /// das Ersetzen blockieren — es sei denn, er gibt das Löschen frei. Genau das tut
    /// <see cref="AtomicFile.ReadAllText"/>.
    ///
    /// Gemessen auf Windows: File.Move scheitert in jedem Fall an einem offenen Leser,
    /// File.Replace kommt mit einem freigebenden Leser zurecht. Deshalb Replace.
    /// </summary>
    [Fact]
    public void Ein_offener_Leser_blockiert_das_Schreiben_nicht()
    {
        var path = Path_("gehalten.json");
        AtomicFile.WriteAllText(path, """{"stand":"alt"}""");

        using var stream = new FileStream(path, FileMode.Open, FileAccess.Read,
            FileShare.ReadWrite | FileShare.Delete);

        Should.NotThrow(() => AtomicFile.WriteAllText(path, """{"stand":"neu"}"""));

        AtomicFile.ReadAllText(path).ShouldBe("""{"stand":"neu"}""");
    }

    [Fact]
    public void Parallele_Schreibvorgaenge_hinterlassen_eine_gueltige_Datei()
    {
        var path = Path_("parallel.json");

        Parallel.For(0, 40, i =>
            AtomicFile.WriteAllText(path, JsonSerializer.Serialize(new { lauf = i })));

        // Welcher Lauf gewinnt, ist offen — die Datei muss aber lesbar sein.
        Should.NotThrow(() => JsonDocument.Parse(File.ReadAllText(path)));
        Directory.GetFiles(_directory).ShouldHaveSingleItem();
    }
}
