using System.Text.Json;
using ClawdDotNet.Core.Api.Models;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Engine;

public sealed class PromptCacheTests
{
    // ═══════════════════════════════════════════════════════════
    //  Aktivierung
    // ═══════════════════════════════════════════════════════════

    [Theory]
    [InlineData("auto", "anthropic/claude-sonnet-4-5", true)]
    [InlineData("auto", "anthropic/claude-haiku-4.5", true)]
    [InlineData("auto", "openai/gpt-4o", false)]
    [InlineData("auto", "google/gemini-2.5-flash", false)]
    [InlineData("on", "openai/gpt-4o", true)]
    [InlineData("off", "anthropic/claude-sonnet-4-5", false)]
    [InlineData("OFF", "anthropic/claude-sonnet-4-5", false)]
    public void Aktivierung_richtet_sich_nach_Einstellung_und_Modell(string setting, string model, bool expected)
    {
        PromptCache.IsEnabledFor(setting, model).ShouldBe(expected);
    }

    // ═══════════════════════════════════════════════════════════
    //  Platzierung der Breakpoints
    // ═══════════════════════════════════════════════════════════

    [Fact]
    public void Der_SystemPrompt_bekommt_einen_Breakpoint()
    {
        var messages = Conversation.Start("System").User("Frage").Build();

        PromptCache.ApplyBreakpoints(messages);

        messages[0].Role.ShouldBe("system");
        messages[0].CacheBreakpoint.ShouldBeTrue();
    }

    [Fact]
    public void Die_letzte_Nachricht_mit_Inhalt_bekommt_einen_rollierenden_Breakpoint()
    {
        var messages = Conversation.Start().User("A").Assistant("B").User("C").Build();

        PromptCache.ApplyBreakpoints(messages);

        messages[^1].CacheBreakpoint.ShouldBeTrue();
        messages[^1].Content.ShouldBe("C");
    }

    [Fact]
    public void Eine_AssistantNachricht_ohne_Inhalt_traegt_keinen_Breakpoint()
    {
        // assistant mit tool_calls hat keinen Textinhalt und kann keinen Block tragen.
        var messages = Conversation.Start().User("A").Build();
        messages.Add(ChatMessage.AssistantWithToolCalls([
            new ToolCall { Id = "c1", Function = new ToolCallFunction { Name = "T", Arguments = "{}" } }
        ]));

        PromptCache.ApplyBreakpoints(messages);

        messages[^1].CacheBreakpoint.ShouldBeFalse();
        messages.Count(m => m.CacheBreakpoint).ShouldBeGreaterThan(0, "der System-Breakpoint muss bleiben");
    }

    [Fact]
    public void Es_werden_hoechstens_zwei_Breakpoints_gesetzt()
    {
        // Anbieter erlauben nur eine begrenzte Zahl — sie dürfen sich nicht ansammeln.
        var messages = Conversation.Start().Repeat(10).Build();

        PromptCache.ApplyBreakpoints(messages);

        messages.Count(m => m.CacheBreakpoint).ShouldBeLessThanOrEqualTo(2);
    }

    [Fact]
    public void Wiederholtes_Anwenden_sammelt_keine_Breakpoints_an()
    {
        var messages = Conversation.Start().Repeat(3).Build();

        PromptCache.ApplyBreakpoints(messages);
        messages.Add(ChatMessage.User("Noch eine Frage"));
        PromptCache.ApplyBreakpoints(messages);
        messages.Add(ChatMessage.User("Und noch eine"));
        PromptCache.ApplyBreakpoints(messages);

        messages.Count(m => m.CacheBreakpoint).ShouldBeLessThanOrEqualTo(2);
        messages[^1].CacheBreakpoint.ShouldBeTrue("der Breakpoint muss mitwandern");
    }

    [Fact]
    public void Eine_leere_Liste_fuehrt_nicht_zu_einem_Fehler()
    {
        var messages = new List<ChatMessage>();

        Should.NotThrow(() => PromptCache.ApplyBreakpoints(messages));
    }

    // ═══════════════════════════════════════════════════════════
    //  Präfix-Stabilität — der entscheidende Test
    // ═══════════════════════════════════════════════════════════

    /// <summary>
    /// Der gecachte Prompt-Abschnitt muss zwischen zwei Schritten zeichengenau identisch
    /// sein. Ein einziger Zeitstempel im System-Prompt würde den Cache bei jedem Schritt
    /// verwerfen — die Kosten blieben unverändert, ohne dass es irgendwo auffiele.
    /// Genau davor schützt dieser Test.
    /// </summary>
    [Fact]
    public async Task Der_Praefix_bleibt_ueber_alle_Schritte_zeichengleich()
    {
        var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok"));
        var agent = fixture.AddAgent("agent-cache", "TestTool");
        agent.Model = "anthropic/claude-sonnet-4-5";
        agent.PromptCaching = "on";

        // Acht Tool-Schritte, dann eine Textantwort.
        for (var i = 0; i < 8; i++)
            fixture.Client.RespondsWithToolCall("TestTool");
        fixture.Client.RespondsWithText("Fertig");

        await fixture.Engine.ChatAsync(agent, "Los", "test-instance", default);

        fixture.Client.ReceivedRequests.Count.ShouldBe(9);

        // Die system-Nachricht ist der stabile Präfix — sie muss in jedem Request
        // byte-identisch serialisiert werden.
        var systemPayloads = fixture.Client.ReceivedRequests
            .Select(r => JsonSerializer.Serialize(r.Messages[0]))
            .Distinct()
            .ToList();

        systemPayloads.Count.ShouldBe(1,
            "der System-Prompt muss über alle Schritte hinweg identisch serialisiert werden:\n" +
            string.Join("\n", systemPayloads));

        systemPayloads[0].ShouldContain("cache_control");
    }

    [Fact]
    public async Task Ohne_Caching_enthaelt_der_Request_kein_cache_control()
    {
        var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok"));
        var agent = fixture.AddAgent("agent-nocache", "TestTool");
        agent.Model = "openai/gpt-4o";
        agent.PromptCaching = "auto";      // bei diesem Modell also aus

        fixture.Client.RespondsWithText("Fertig");

        await fixture.Engine.ChatAsync(agent, "Los", "test-instance", default);

        var json = JsonSerializer.Serialize(fixture.Client.ReceivedRequests[0].Messages);
        json.ShouldNotContain("cache_control");
    }
}
