using ClawdDotNet.Core.Memory;
using ClawdDotNet.Core.Storage;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Memory;

/// <summary>
/// K1 aus der Bestandsaufnahme: Geplante Agenten begannen bei jedem Cron-Lauf bei null.
///
/// Getestet wird gegen eine echte SQLite-Datei, nicht gegen eine Attrappe — nur so
/// werden Schema, Indizes, Sperrverhalten und die Behandlung gleichzeitiger Zugriffe
/// tatsächlich geprüft.
/// </summary>
public sealed class MemoryRepositoryTests : IDisposable
{
    private readonly string _directory;
    private readonly SqliteStorage _storage;
    private readonly SqliteMemoryRepository _repository;

    private const string AgentA = "agent-a";
    private const string AgentB = "agent-b";

    public MemoryRepositoryTests()
    {
        _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
        _storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
        _repository = new SqliteMemoryRepository(_storage);
    }

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

    private Task<MemoryEntry> Remember(
        string subject, string content, string owner = AgentA,
        MemoryScope scope = MemoryScope.Agent, string? key = null,
        int importance = 3, string? category = null, params string[] tags)
        => _repository.RememberAsync(new MemoryEntry
        {
            Scope = scope,
            OwnerId = owner,
            Subject = subject,
            Content = content,
            Key = key,
            Importance = importance,
            Category = category ?? MemoryCategory.Observation,
            Tags = tags,
            CreatedBy = owner
        }, default);

    private Task<IReadOnlyList<MemoryEntry>> Recall(
        string? search = null, string owner = AgentA, MemoryScope scope = MemoryScope.Agent,
        string? subject = null, int limit = 20, int minImportance = 1, params string[] tags)
        => _repository.RecallAsync(new MemoryQuery
        {
            Scope = scope,
            OwnerId = owner,
            Search = search,
            Subject = subject,
            Tags = tags,
            Limit = limit,
            MinImportance = minImportance
        }, default);

    // ═══════════════════════════════════════════════════════════
    //  Grundfunktionen
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Eine_Erinnerung_ueberdauert_und_wird_wiedergefunden()
    {
        await Remember("NVDA", "Quartalszahlen am 20.11. erwartet");

        var found = await Recall("Quartalszahlen");

        found.Count.ShouldBe(1);
        found[0].Subject.ShouldBe("NVDA");
        found[0].Content.ShouldContain("20.11.");
    }

    [Fact]
    public async Task Die_Suche_greift_auf_Betreff_Inhalt_und_Schlagworte()
    {
        await Remember("NVDA", "Kursziel angehoben", tags: ["analyse", "quartal"]);

        (await Recall("NVDA")).Count.ShouldBe(1);
        (await Recall("Kursziel")).Count.ShouldBe(1);
        (await Recall("analyse")).Count.ShouldBe(1);
        (await Recall("voellig anderes")).ShouldBeEmpty();
    }

    [Fact]
    public async Task Die_Suche_beachtet_Gross_und_Kleinschreibung_nicht()
    {
        await Remember("NVDA", "Kursziel angehoben");

        (await Recall("kursZIEL")).Count.ShouldBe(1);
    }

    [Fact]
    public async Task Nach_Betreff_laesst_sich_gezielt_filtern()
    {
        await Remember("NVDA", "Erste Notiz");
        await Remember("TSLA", "Zweite Notiz");

        var found = await Recall(subject: "NVDA");

        found.Count.ShouldBe(1);
        found[0].Content.ShouldBe("Erste Notiz");
    }

    // ═══════════════════════════════════════════════════════════
    //  Schlüssel: aktualisieren statt zuwachsen
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Ein_Schluessel_aktualisiert_statt_eine_zweite_Erinnerung_anzulegen()
    {
        // Sonst wüchse das Gedächtnis bei einem alle 30 Minuten laufenden Agenten
        // binnen eines Tages um 48 Einträge zur selben Sache.
        await Remember("NVDA", "Kursziel 120", key: "kursziel_nvda");
        await Remember("NVDA", "Kursziel 145", key: "kursziel_nvda");

        var found = await Recall(subject: "NVDA");

        found.Count.ShouldBe(1);
        found[0].Content.ShouldBe("Kursziel 145");
    }

    [Fact]
    public async Task Beim_Aktualisieren_bleibt_das_Anlagedatum_erhalten()
    {
        var first = await Remember("NVDA", "Erste Fassung", key: "k");
        await Task.Delay(10);
        var second = await Remember("NVDA", "Zweite Fassung", key: "k");

        second.Id.ShouldBe(first.Id);
        second.CreatedAt.ShouldBe(first.CreatedAt);
        second.UpdatedAt.ShouldBeGreaterThan(first.CreatedAt);
    }

    [Fact]
    public async Task Ohne_Schluessel_entstehen_mehrere_Eintraege()
    {
        await Remember("NVDA", "Beobachtung eins");
        await Remember("NVDA", "Beobachtung zwei");

        (await Recall(subject: "NVDA")).Count.ShouldBe(2);
    }

    [Fact]
    public async Task Derselbe_Schluessel_bei_verschiedenen_Agenten_kollidiert_nicht()
    {
        await Remember("NVDA", "Sicht von A", owner: AgentA, key: "einschaetzung");
        await Remember("NVDA", "Sicht von B", owner: AgentB, key: "einschaetzung");

        (await Recall(owner: AgentA))[0].Content.ShouldBe("Sicht von A");
        (await Recall(owner: AgentB))[0].Content.ShouldBe("Sicht von B");
    }

    // ═══════════════════════════════════════════════════════════
    //  Bereiche
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Private_Erinnerungen_sind_fuer_andere_Agenten_unsichtbar()
    {
        await Remember("Geheim", "Nur für A", owner: AgentA);

        (await Recall(owner: AgentB)).ShouldBeEmpty();
    }

    [Fact]
    public async Task Geteilte_Erinnerungen_sehen_alle_Agenten()
    {
        await Remember("Team", "Gemeinsame Erkenntnis", owner: AgentA, scope: MemoryScope.Shared);

        var fromB = await Recall(owner: AgentB, scope: MemoryScope.Shared);

        fromB.Count.ShouldBe(1);
        fromB[0].CreatedBy.ShouldBe(AgentA, "auch im geteilten Bereich bleibt die Herkunft sichtbar");
    }

    [Fact]
    public async Task Geteilte_und_private_Bereiche_sind_getrennt()
    {
        await Remember("X", "privat", owner: AgentA);
        await Remember("X", "geteilt", owner: AgentA, scope: MemoryScope.Shared);

        (await Recall(owner: AgentA)).Count.ShouldBe(1);
        (await Recall(owner: AgentA, scope: MemoryScope.Shared)).Count.ShouldBe(1);
    }

    // ═══════════════════════════════════════════════════════════
    //  Reihenfolge und Kappung
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Wichtiges_kommt_zuerst()
    {
        // Entscheidend, weil der Abruf begrenzt wird — das Wichtigste muss überleben.
        await Remember("A", "nebensächlich", importance: 1);
        await Remember("B", "zentral", importance: 5);
        await Remember("C", "mittel", importance: 3);

        var found = await Recall();

        found[0].Content.ShouldBe("zentral");
        found[^1].Content.ShouldBe("nebensächlich");
    }

    [Fact]
    public async Task Bei_gleicher_Wichtigkeit_kommt_das_Aktuellste_zuerst()
    {
        await Remember("A", "aelter", importance: 3);
        await Task.Delay(10);
        await Remember("B", "neuer", importance: 3);

        (await Recall())[0].Content.ShouldBe("neuer");
    }

    [Fact]
    public async Task Unwichtiges_laesst_sich_ausblenden()
    {
        await Remember("A", "nebensächlich", importance: 1);
        await Remember("B", "zentral", importance: 5);

        var found = await Recall(minImportance: 4);

        found.Count.ShouldBe(1);
        found[0].Content.ShouldBe("zentral");
    }

    [Fact]
    public async Task Die_Trefferzahl_ist_begrenzt()
    {
        for (var i = 0; i < 50; i++)
            await Remember("Massen", $"Notiz {i}");

        (await Recall(limit: 5)).Count.ShouldBe(5);
    }

    [Fact]
    public async Task Die_Wichtigkeit_bleibt_im_gueltigen_Bereich()
    {
        var tooHigh = await Remember("A", "x", importance: 99);
        var tooLow = await Remember("B", "y", importance: -5);

        tooHigh.Importance.ShouldBe(5);
        tooLow.Importance.ShouldBe(1);
    }

    // ═══════════════════════════════════════════════════════════
    //  Schlagworte
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Nach_Schlagworten_laesst_sich_filtern()
    {
        await Remember("A", "eins", tags: ["dringend", "kunde"]);
        await Remember("B", "zwei", tags: ["kunde"]);
        await Remember("C", "drei");

        (await Recall(tags: "kunde")).Count.ShouldBe(2);
        (await Recall(tags: "dringend")).Count.ShouldBe(1);
    }

    [Fact]
    public async Task Ein_Schlagwort_trifft_kein_laengeres_mit_gleichem_Anfang()
    {
        // "news" darf nicht "newsletter" treffen.
        await Remember("A", "eins", tags: ["newsletter"]);

        (await Recall(tags: "news")).ShouldBeEmpty();
        (await Recall(tags: "newsletter")).Count.ShouldBe(1);
    }

    [Fact]
    public async Task Schlagworte_ueberstehen_den_Rundlauf()
    {
        await Remember("A", "eins", tags: ["Alpha", "Beta"]);

        var found = await Recall();

        found[0].Tags.ShouldBe(["alpha", "beta"]);
    }

    // ═══════════════════════════════════════════════════════════
    //  Löschen und Überblick
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Eine_Erinnerung_laesst_sich_loeschen()
    {
        var entry = await Remember("A", "vergänglich");

        (await _repository.ForgetAsync(entry.Id, default)).ShouldBeTrue();
        (await Recall()).ShouldBeEmpty();
    }

    [Fact]
    public async Task Das_Loeschen_einer_unbekannten_Id_meldet_das()
    {
        (await _repository.ForgetAsync(99_999, default)).ShouldBeFalse();
    }

    [Fact]
    public async Task Alle_Erinnerungen_zu_einem_Betreff_lassen_sich_loeschen()
    {
        await Remember("NVDA", "eins");
        await Remember("NVDA", "zwei");
        await Remember("TSLA", "drei");

        var removed = await _repository.ForgetBySubjectAsync(MemoryScope.Agent, AgentA, "NVDA", default);

        removed.ShouldBe(2);
        (await Recall()).Count.ShouldBe(1);
    }

    [Fact]
    public async Task Der_Ueberblick_zeigt_Betreffs_mit_Anzahl()
    {
        await Remember("NVDA", "eins");
        await Remember("NVDA", "zwei");
        await Remember("TSLA", "drei");

        var subjects = await _repository.ListSubjectsAsync(MemoryScope.Agent, AgentA, 10, default);

        subjects.Count.ShouldBe(2);
        subjects[0].ShouldBe(("NVDA", 2));
    }

    // ═══════════════════════════════════════════════════════════
    //  Nebenläufigkeit — der eigentliche Grund für WAL und busy_timeout
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Viele_Agenten_koennen_gleichzeitig_schreiben()
    {
        // Ohne WAL und Sperr-Wartezeit quittiert SQLite das mit "database is locked".
        var tasks = Enumerable.Range(0, 60)
            .Select(i => Remember($"Betreff {i % 5}", $"Notiz {i}", owner: $"agent-{i % 6}"));

        await Task.WhenAll(tasks);

        var total = 0;
        for (var i = 0; i < 6; i++)
            total += await _repository.CountAsync(MemoryScope.Agent, $"agent-{i}", default);

        total.ShouldBe(60);
    }

    [Fact]
    public async Task Gleichzeitiges_Schreiben_und_Lesen_stoert_sich_nicht()
    {
        await Remember("Basis", "vorhanden");

        var writes = Enumerable.Range(0, 20).Select(i => Remember("Neu", $"Notiz {i}"));
        var reads = Enumerable.Range(0, 20).Select(_ => Recall());

        await Task.WhenAll(writes.Cast<Task>().Concat(reads));

        (await _repository.CountAsync(MemoryScope.Agent, AgentA, default)).ShouldBe(21);
    }

    // ═══════════════════════════════════════════════════════════
    //  Sonderfälle
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Ein_Prozentzeichen_im_Suchtext_wirkt_nicht_als_Platzhalter()
    {
        await Remember("A", "Erster Eintrag");
        await Remember("B", "Zweiter Eintrag");

        // Als LIKE-Muster würde "%" alles treffen.
        (await Recall("%")).ShouldBeEmpty();
    }

    [Fact]
    public async Task Umlaute_und_Emoji_ueberstehen_die_Ablage()
    {
        await Remember("Größe", "Prüfung mit Umlauten und 🦀");

        var found = await Recall("Prüfung");

        found[0].Content.ShouldBe("Prüfung mit Umlauten und 🦀");
        found[0].Subject.ShouldBe("Größe");
    }

    [Fact]
    public async Task Eine_unbekannte_Kategorie_faellt_auf_other_zurueck()
    {
        var entry = await Remember("A", "x", category: "phantasie");

        entry.Category.ShouldBe(MemoryCategory.Other);
    }

    [Fact]
    public async Task Erinnerungen_ueberdauern_das_Schliessen_der_Datenbank()
    {
        // Der eigentliche Zweck: Ein Neustart der Anwendung darf nichts kosten.
        await Remember("Dauerhaft", "muss einen Neustart überleben");

        var wiederEroeffnet = new SqliteMemoryRepository(
            new SqliteStorage(Path.Combine(_directory, "state.db")));

        var found = await wiederEroeffnet.RecallAsync(
            new MemoryQuery { Scope = MemoryScope.Agent, OwnerId = AgentA }, default);

        found.Count.ShouldBe(1);
        found[0].Content.ShouldBe("muss einen Neustart überleben");
    }
}
