using ClawdDotNet.Core.Api.Models;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Tests.Infrastructure;
using FsCheck;
using FsCheck.Xunit;

namespace ClawdDotNet.Core.Tests.Engine;

/// <summary>
/// Property-Based Tests: Statt einzelner Beispiele werden hunderte zufällige,
/// aber gültige Konversationen erzeugt und geprüft, dass die Compaction die
/// API-Invarianten in JEDEM Fall erhält.
///
/// Genau diese Grenzfälle — wo die Tail-Grenze zufällig mitten in einen
/// Tool-Zyklus fällt — schreibt von Hand niemand auf. Bug B1 fällt in diese Klasse.
/// </summary>
public sealed class ContextCompactorPropertyTests
{
    private const string Model = "test/model";

    private static readonly LoopGuardConfig AlwaysCompact = new()
    {
        MaxContextTokens = 1_000,
        CompactionThreshold = 0.5
    };

    [Property(MaxTest = 300, Arbitrary = [typeof(ConversationArbitrary)])]
    public Property Compaction_erhaelt_immer_eine_gueltige_Nachrichtenfolge(ConversationShape shape)
    {
        var messages = shape.ToMessages();

        // Vorbedingung: Der Generator erzeugt nur gültige Ausgangsfolgen.
        if (!ContextInvariants.IsValid(messages))
            return false.ToProperty().Label("Generator hat eine ungültige Folge erzeugt");

        var client = new FakeChatClient().AlwaysRespondsWithText("- Zusammenfassung des Verlaufs.");
        var compactor = new ContextCompactor(client, TestLogging.Factory);

        compactor.CompactIfNeededAsync(messages, 50_000, AlwaysCompact, Model, default)
            .GetAwaiter().GetResult();

        var valid = ContextInvariants.IsValid(messages);

        return valid.ToProperty()
            .Label($"Nach Compaction ungültig. Ausgangsform: {shape}\n{ContextInvariants.Describe(messages)}");
    }

    [Property(MaxTest = 300, Arbitrary = [typeof(ConversationArbitrary)])]
    public Property Compaction_behaelt_den_SystemPrompt_genau_einmal(ConversationShape shape)
    {
        var messages = shape.ToMessages();
        var hadSystem = messages.Any(m => m.Role == "system");

        var client = new FakeChatClient().AlwaysRespondsWithText("- Zusammenfassung.");
        var compactor = new ContextCompactor(client, TestLogging.Factory);

        compactor.CompactIfNeededAsync(messages, 50_000, AlwaysCompact, Model, default)
            .GetAwaiter().GetResult();

        var systemCount = messages.Count(m => m.Role == "system");
        var expected = hadSystem ? 1 : 0;

        return (systemCount == expected).ToProperty()
            .Label($"system-Nachrichten: erwartet {expected}, gefunden {systemCount}");
    }
}

// ═══════════════════════════════════════════════════════════════
//  Generator
// ═══════════════════════════════════════════════════════════════

/// <summary>
/// Beschreibt die Form einer Konversation. FsCheck erzeugt davon zufällige
/// Varianten und schrumpft sie im Fehlerfall auf das minimale Gegenbeispiel.
/// </summary>
public sealed record ConversationShape(bool HasSystemPrompt, IReadOnlyList<TurnShape> Turns)
{
    public List<ChatMessage> ToMessages()
    {
        var c = Conversation.Start(HasSystemPrompt ? "Du bist ein Testagent." : null);

        foreach (var turn in Turns)
        {
            c.User($"Anfrage mit {turn.ToolCalls} Tool-Aufrufen");

            if (turn.ToolCalls > 0)
                c.ToolCycle(turn.ToolCalls, turn.ResultLength);

            if (turn.EndsWithText)
                c.Assistant("Abschließende Antwort des Agenten.");
        }

        return c.Build();
    }

    public override string ToString()
        => $"system={HasSystemPrompt}, turns=[{string.Join(", ", Turns)}]";
}

public sealed record TurnShape(int ToolCalls, int ResultLength, bool EndsWithText)
{
    public override string ToString() => $"{ToolCalls}tc/{ResultLength}b/{(EndsWithText ? "text" : "offen")}";
}

public static class ConversationArbitrary
{
    public static Arbitrary<ConversationShape> Conversations()
    {
        var turnGen =
            from toolCalls in Gen.Choose(0, 4)
            from resultLength in Gen.Elements(20, 200, 2_500, 9_000)
            from endsWithText in Gen.Elements(true, false)
            select new TurnShape(toolCalls, resultLength, endsWithText);

        var gen =
            from hasSystem in Gen.Elements(true, false)
            from turnCount in Gen.Choose(1, 12)
            from turns in Gen.ListOf(turnCount, turnGen)
            select new ConversationShape(hasSystem, turns.ToList());

        return Arb.From(gen, Shrink);
    }

    /// <summary>Im Fehlerfall auf das kleinste Gegenbeispiel reduzieren.</summary>
    private static IEnumerable<ConversationShape> Shrink(ConversationShape shape)
    {
        // Weniger Turns
        for (var i = 0; i < shape.Turns.Count; i++)
        {
            var reduced = shape.Turns.Where((_, idx) => idx != i).ToList();
            if (reduced.Count > 0)
                yield return shape with { Turns = reduced };
        }

        // Weniger Tool-Calls pro Turn
        for (var i = 0; i < shape.Turns.Count; i++)
        {
            var turn = shape.Turns[i];
            if (turn.ToolCalls <= 0) continue;

            var reduced = shape.Turns.ToList();
            reduced[i] = turn with { ToolCalls = turn.ToolCalls - 1 };
            yield return shape with { Turns = reduced };
        }

        if (shape.HasSystemPrompt)
            yield return shape with { HasSystemPrompt = false };
    }
}
