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

namespace ClawdDotNet.Core.Tests.Engine;

/// <summary>
/// Regressionstests für Bug B2: _chatContexts[agentId] ist eine geteilte
/// List&lt;ChatMessage&gt;. Nur der Lookup läuft unter Lock — alle Add-Aufrufe im
/// Schleifenkörper sind ungeschützt.
///
/// Derselbe Agent kann gleichzeitig von mehreren Seiten angestoßen werden:
/// Telegram-/Mail-ToolJob, WebView-Nachricht des Benutzers, send_message eines
/// anderen Agenten. Ergebnis: korrupte Liste, verschränkte Tool-Sequenzen und
/// dadurch HTTP-400-Fehler.
/// </summary>
public sealed class AgentEngineConcurrencyTests
{
    // ═══════════════════════════════════════════════════════════
    //  E1 — Zwei parallele Chats auf demselben Agenten
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [Repeat(15)]
    public async Task Zwei_parallele_Chats_hinterlassen_einen_gueltigen_Kontext(int iteration)
    {
        _ = iteration;

        var fixture = new EngineFixture()
            .WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(15)));
        var agent = fixture.AddAgent("agent-a", "TestTool");

        // Jeder Lauf: ein Tool-Schritt, dann eine Textantwort — unabhängig davon,
        // wie die beiden Konversationen ineinander verschränkt werden.
        fixture.Client.RespondsContextually();

        await Task.WhenAll(
            fixture.Engine.ChatAsync(agent, "Anfrage A", "test-instance", default),
            fixture.Engine.ChatAsync(agent, "Anfrage B", "test-instance", default));

        var context = fixture.Engine.GetChatContext(agent.AgentId);
        ContextInvariants.AssertValid(context);
    }

    // ═══════════════════════════════════════════════════════════
    //  E2 — Stresstest
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Viele_parallele_Chats_korrumpieren_den_Kontext_nicht()
    {
        var fixture = new EngineFixture()
            .WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(2)));
        var agent = fixture.AddAgent("agent-stress", "TestTool");

        fixture.Client.RespondsContextually();

        var tasks = Enumerable.Range(0, 30)
            .Select(i => fixture.Engine.ChatAsync(agent, $"Anfrage {i}", "test-instance", default));

        var results = await Task.WhenAll(tasks);

        results.ShouldAllBe(r => r.Status == AgentRunStatus.Completed);

        var context = fixture.Engine.GetChatContext(agent.AgentId);
        ContextInvariants.AssertValid(context);

        // Jede der 30 Anfragen muss genau einmal im Kontext stehen — nichts verloren,
        // nichts doppelt durch verlorene Schreibzugriffe auf die Liste.
        var userMessages = context.Count(m => m.Role == "user");
        userMessages.ShouldBe(30);
    }

    // ═══════════════════════════════════════════════════════════
    //  E3 — AbortChat
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task AbortChat_bricht_alle_laufenden_Chats_des_Agenten_ab()
    {
        var fixture = new EngineFixture()
            .WithTool(FakeTool.Slow(TimeSpan.FromSeconds(10)));
        var agent = fixture.AddAgent("agent-abort", "TestTool");

        fixture.Client.AlwaysRespondsWithToolCall();

        var first = fixture.Engine.ChatAsync(agent, "Erste", "test-instance", default);
        var second = fixture.Engine.ChatAsync(agent, "Zweite", "test-instance", default);

        // Warten, bis beide tatsächlich angelaufen sind.
        await WaitUntilAsync(() => fixture.Engine.IsRunning(agent.AgentId));
        await Task.Delay(50);

        var sw = System.Diagnostics.Stopwatch.StartNew();
        fixture.Engine.AbortChat(agent.AgentId);

        var results = await Task.WhenAll(first, second);
        sw.Stop();

        results.ShouldAllBe(r => r.Status == AgentRunStatus.Cancelled);

        // Entscheidend: _runningChats[agentId] hielt bisher nur EINE CancellationTokenSource —
        // der zweite Lauf überschrieb den ersten. AbortChat brach dann nur einen ab, der
        // andere lief bis ins Run-Timeout. Ohne diese Zeitprüfung wäre der Test grün,
        // obwohl der Abbruch gar nicht gegriffen hat.
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5),
            "AbortChat muss beide Läufe sofort beenden, nicht erst über das Timeout");
    }

    // ═══════════════════════════════════════════════════════════
    //  E4 — Gegenprobe: verschiedene Agenten dürfen parallel laufen
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Verschiedene_Agenten_laufen_wirklich_parallel()
    {
        // Wichtige Gegenprobe: Der Fix für B2 darf nicht versehentlich alle Agenten
        // gegeneinander sperren.
        var toolDelay = TimeSpan.FromMilliseconds(200);
        var fixture = new EngineFixture().WithTool(FakeTool.Slow(toolDelay));

        var a = fixture.AddAgent("agent-1", "TestTool");
        var b = fixture.AddAgent("agent-2", "TestTool");
        var c = fixture.AddAgent("agent-3", "TestTool");

        fixture.Client.AlwaysRespondsWithText("Antwort");

        var sw = System.Diagnostics.Stopwatch.StartNew();
        await Task.WhenAll(
            fixture.Engine.ChatAsync(a, "x", "test-instance", default),
            fixture.Engine.ChatAsync(b, "x", "test-instance", default),
            fixture.Engine.ChatAsync(c, "x", "test-instance", default));
        sw.Stop();

        // Seriell wären es mindestens 3 × 200 ms. Parallel deutlich weniger.
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromMilliseconds(450),
            "Agenten dürfen sich nicht gegenseitig blockieren");
    }

    private static async Task WaitUntilAsync(Func<bool> condition, int timeoutMs = 2_000)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        while (!condition() && sw.ElapsedMilliseconds < timeoutMs)
            await Task.Delay(10);
    }
}
