using ClawdDotNet.Core.Api.Models;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Engine;

/// <summary>
/// Regressionstests für Bug B1: Die Compaction behält blind die letzten N Nachrichten.
/// Liegt diese Grenze mitten in einer Tool-Sequenz, entsteht eine tool-Antwort ohne
/// zugehörigen assistant-tool_call — die API lehnt das mit HTTP 400 ab.
/// </summary>
public sealed class ContextCompactorTests
{
    private const string Model = "test/model";

    /// <summary>Schwelle so gesetzt, dass Compaction sicher ausgelöst wird.</summary>
    private static LoopGuardConfig AlwaysCompact => new()
    {
        MaxContextTokens = 1_000,
        CompactionThreshold = 0.5   // Schwelle = 500 Tokens
    };

    private static LoopGuardConfig NeverCompact => new()
    {
        MaxContextTokens = 10_000_000,
        CompactionThreshold = 0.9
    };

    private static (ContextCompactor Compactor, FakeChatClient Client) CreateCompactor(
        string summary = "- Der Agent hat Daten geprüft.\n- Ergebnis war unauffällig.")
    {
        var client = new FakeChatClient().AlwaysRespondsWithText(summary);
        return (new ContextCompactor(client, TestLogging.Factory), client);
    }

    // ═══════════════════════════════════════════════════════════
    //  C1/C2 — Die eigentliche Bug-Reproduktion
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(4)]
    [InlineData(5)]
    [InlineData(6)]
    [InlineData(7)]
    [InlineData(8)]
    public async Task Compaction_erhaelt_gueltige_Sequenz_bei_beliebiger_Toolzyklus_Laenge(int toolCallsPerCycle)
    {
        // Je nach Anzahl der Tool-Calls fällt die "letzte 6 Nachrichten"-Grenze
        // an eine andere Stelle im Zyklus. Mindestens eine Variante trifft mitten hinein.
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start()
            .Repeat(times: 4, toolCallsPerCycle: toolCallsPerCycle, resultLength: 200)
            .Build();

        ContextInvariants.AssertValid(messages);   // Vorbedingung: Ausgangslage ist gültig

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        ContextInvariants.AssertValid(messages);
    }

    [Fact]
    public async Task Compaction_trennt_Toolantwort_nie_von_ihrem_Aufruf()
    {
        // Gezielt konstruiert: Der Tail von 6 Nachrichten beginnt genau bei einer tool-Antwort.
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start()
            .User("Erste Anfrage")
            .ToolCycle(count: 3, resultLength: 300)
            .Assistant("Zwischenergebnis")
            .User("Zweite Anfrage")
            .ToolCycle(count: 3, resultLength: 300)
            .Build();

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        ContextInvariants.AssertValid(messages);

        // Keine verwaiste tool-Antwort
        for (var i = 0; i < messages.Count; i++)
        {
            if (messages[i].Role != "tool") continue;

            var hasParent = messages
                .Take(i)
                .Any(m => m.ToolCalls?.Any(tc => tc.Id == messages[i].ToolCallId) == true);

            hasParent.ShouldBeTrue(
                $"tool-Antwort an Position {i} (id={messages[i].ToolCallId}) hat keinen Aufruf mehr.\n" +
                ContextInvariants.Describe(messages));
        }
    }

    // ═══════════════════════════════════════════════════════════
    //  C3/C4 — System-Prompt
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Compaction_behaelt_den_SystemPrompt_an_Position_0()
    {
        const string systemPrompt = "Du bist ein sehr spezifischer Testagent.";
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start(systemPrompt).Repeat(6).Build();

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        messages[0].Role.ShouldBe("system");
        messages[0].Content.ShouldBe(systemPrompt);
        messages.Count(m => m.Role == "system").ShouldBe(1);
    }

    [Fact]
    public async Task Compaction_erfindet_keinen_SystemPrompt_wenn_keiner_da_war()
    {
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start(systemPrompt: null).Repeat(6).Build();

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        messages.ShouldNotContain(m => m.Role == "system");
    }

    // ═══════════════════════════════════════════════════════════
    //  C5/C6 — Fehlerfälle der Zusammenfassung
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Compaction_laesst_Nachrichten_unveraendert_wenn_die_Zusammenfassung_fehlschlaegt()
    {
        var client = new FakeChatClient().Throws(new HttpRequestException("API nicht erreichbar"));
        var compactor = new ContextCompactor(client, TestLogging.Factory);
        var messages = Conversation.Start().Repeat(6).Build();
        var before = messages.Count;

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        ContextInvariants.AssertValid(messages);
        messages.Count.ShouldBe(before, "bei fehlgeschlagener Zusammenfassung darf nichts verloren gehen");
    }

    [Fact]
    public async Task Compaction_laesst_Nachrichten_unveraendert_wenn_die_Zusammenfassung_leer_ist()
    {
        var client = new FakeChatClient().AlwaysRespondsWithText("   ");
        var compactor = new ContextCompactor(client, TestLogging.Factory);
        var messages = Conversation.Start().Repeat(6).Build();
        var before = messages.Count;

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        ContextInvariants.AssertValid(messages);
        messages.Count.ShouldBe(before);
    }

    // ═══════════════════════════════════════════════════════════
    //  C7/C8 — Tool-Result-Pruning
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public async Task Pruning_kuerzt_lange_Toolergebnisse_ausserhalb_des_geschuetzten_Endes()
    {
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start()
            .Repeat(times: 5, toolCallsPerCycle: 1, resultLength: 8_000)
            .Build();

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        ContextInvariants.AssertValid(messages);
        messages.Where(m => m.Role == "tool")
            .ShouldAllBe(m => m.Content!.Length <= 8_000,
                "kein Tool-Ergebnis darf nach dem Pruning gewachsen sein");
    }

    [Fact]
    public async Task Pruning_laesst_kurze_Toolergebnisse_unveraendert()
    {
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start()
            .Repeat(times: 5, toolCallsPerCycle: 1, resultLength: 100)
            .Build();

        await compactor.CompactIfNeededAsync(messages, 5_000, AlwaysCompact, Model, default);

        foreach (var content in messages.Where(m => m.Role == "tool").Select(m => m.Content))
            (content ?? "").ShouldNotContain("gekürzt", Case.Insensitive);
    }

    // ═══════════════════════════════════════════════════════════
    //  C9/C10 — Schätzung und Schwelle
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public void EstimateTokens_waechst_monoton_mit_dem_Inhalt()
    {
        var klein = Conversation.Start().Repeat(1).Build();
        var mittel = Conversation.Start().Repeat(5).Build();
        var gross = Conversation.Start().Repeat(20).Build();

        var a = ContextCompactor.EstimateTokens(klein);
        var b = ContextCompactor.EstimateTokens(mittel);
        var c = ContextCompactor.EstimateTokens(gross);

        a.ShouldBeLessThan(b);
        b.ShouldBeLessThan(c);
    }

    [Fact]
    public async Task Unterhalb_der_Schwelle_wird_kein_ApiCall_ausgeloest()
    {
        // Wichtig fürs Budget: Compaction darf nicht unnötig ein Modell anwerfen.
        var (compactor, client) = CreateCompactor();
        var messages = Conversation.Start().Repeat(2).Build();

        var compacted = await compactor.CompactIfNeededAsync(messages, 10, NeverCompact, Model, default);

        compacted.ShouldBeFalse();
        client.CallCount.ShouldBe(0);
    }

    [Fact]
    public async Task Compaction_reduziert_die_Nachrichtenzahl_deutlich()
    {
        var (compactor, _) = CreateCompactor();
        var messages = Conversation.Start().Repeat(15).Build();
        var before = messages.Count;

        var compacted = await compactor.CompactIfNeededAsync(messages, 50_000, AlwaysCompact, Model, default);

        compacted.ShouldBeTrue();
        messages.Count.ShouldBeLessThan(before);
        ContextInvariants.AssertValid(messages);
    }
}
