using System.Text.Json;
using ClawdDotNet.Core.Api;
using ClawdDotNet.Core.Api.Models;

namespace ClawdDotNet.Core.Tests.Infrastructure;

/// <summary>
/// Ersetzt den OpenRouterClient in Tests. Liefert eine vorprogrammierte Antwortfolge
/// und schreibt jeden empfangenen Request mit.
///
/// Wichtig: Requests werden tief kopiert. Die Engine reicht dieselbe List&lt;ChatMessage&gt;
/// weiter und verändert sie danach — ohne Kopie würden Tests den Endzustand prüfen
/// statt dessen, was tatsächlich gesendet wurde.
/// </summary>
internal sealed class FakeChatClient : IChatCompletionClient
{
    private readonly Queue<Func<ChatRequest, ChatResponse>> _responses = new();
    private Func<ChatRequest, ChatResponse>? _fallback;
    private readonly Lock _gate = new();

    /// <summary>Alle empfangenen Requests, als tiefe Kopien.</summary>
    public List<ChatRequest> ReceivedRequests { get; } = new();

    public int CallCount => ReceivedRequests.Count;

    // ─── Programmierung der Antworten ───

    public FakeChatClient RespondsWithText(string text, Usage? usage = null)
    {
        _responses.Enqueue(_ => TextResponse(text, usage));
        return this;
    }

    public FakeChatClient RespondsWithToolCall(string toolName, string argumentsJson = "{}", string? id = null)
        => RespondsWithToolCalls((toolName, argumentsJson, id));

    public FakeChatClient RespondsWithToolCalls(params (string Tool, string Args, string? Id)[] calls)
    {
        _responses.Enqueue(_ => ToolCallResponse(calls));
        return this;
    }

    private static ChatResponse ToolCallResponse((string Tool, string Args, string? Id)[] calls)
    {
        // Ids müssen pro Antwort eindeutig sein, sonst kollidieren parallele Läufe.
        var toolCalls = calls.Select(c => new ToolCall
        {
            Id = c.Id ?? "call_" + Guid.NewGuid().ToString("N")[..12],
            Type = "function",
            Function = new ToolCallFunction { Name = c.Tool, Arguments = c.Args }
        }).ToList();

        return new ChatResponse
        {
            Id = "resp",
            Choices = [new Choice { Index = 0, Message = new ChatMessage { Role = "assistant", ToolCalls = toolCalls } }],
            Usage = new Usage { PromptTokens = 100, CompletionTokens = 20, TotalTokens = 120 }
        };
    }

    /// <summary>Antwortet mit einer bestimmten Prompt-Token-Zahl — für Compaction-Schwellen.</summary>
    public FakeChatClient RespondsWithTokens(int promptTokens, string text = "fertig")
    {
        _responses.Enqueue(_ => TextResponse(text, new Usage
        {
            PromptTokens = promptTokens,
            CompletionTokens = 10,
            TotalTokens = promptTokens + 10
        }));
        return this;
    }

    public FakeChatClient Throws(Exception ex)
    {
        _responses.Enqueue(_ => throw ex);
        return this;
    }

    /// <summary>Antwort für alle Aufrufe, die über die programmierte Folge hinausgehen.</summary>
    public FakeChatClient AlwaysRespondsWithText(string text)
    {
        _fallback = _ => TextResponse(text);
        return this;
    }

    public FakeChatClient AlwaysRespondsWithToolCall(string toolName = "TestTool")
    {
        _fallback = _ => ToolCallResponse([(toolName, "{}", null)]);
        return this;
    }

    /// <summary>
    /// Antwortet passend zum jeweiligen Gesprächsstand: nach einer Tool-Antwort
    /// kommt Text, sonst ein Tool-Aufruf. Damit verhält sich der Fake auch dann
    /// sinnvoll, wenn mehrere Konversationen ineinander verschränkt laufen.
    /// </summary>
    public FakeChatClient RespondsContextually(string toolName = "TestTool")
    {
        _fallback = req => req.Messages.LastOrDefault()?.Role == "tool"
            ? TextResponse("Fertig")
            : ToolCallResponse([(toolName, "{}", null)]);
        return this;
    }

    // ─── IChatCompletionClient ───

    public Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        // Erst kopieren, dann auswerten: Die Engine mutiert die Original-Liste
        // weiter, und bei Nebenläufigkeitstests soll der Fake daran nicht scheitern.
        var snapshot = DeepClone(request);
        lock (_gate)
            ReceivedRequests.Add(snapshot);

        Func<ChatRequest, ChatResponse>? responder;
        lock (_gate)
            responder = _responses.Count > 0 ? _responses.Dequeue() : _fallback;

        if (responder is null)
            throw new InvalidOperationException(
                $"FakeChatClient: unerwarteter Aufruf Nr. {ReceivedRequests.Count} — " +
                "keine Antwort mehr programmiert.");

        return Task.FromResult(responder(snapshot));
    }

    // ─── Helfer ───

    private static ChatResponse TextResponse(string text, Usage? usage = null) => new()
    {
        Id = "resp",
        Choices = [new Choice { Index = 0, Message = new ChatMessage { Role = "assistant", Content = text }, FinishReason = "stop" }],
        Usage = usage ?? new Usage { PromptTokens = 100, CompletionTokens = 20, TotalTokens = 120 }
    };

    private static ChatRequest DeepClone(ChatRequest request) => new()
    {
        Model = request.Model,
        Stream = request.Stream,
        Temperature = request.Temperature,
        MaxTokens = request.MaxTokens,
        ToolChoice = request.ToolChoice,
        Tools = request.Tools?.ToList(),
        Messages = request.Messages.Select(CloneMessage).ToList()
    };

    private static ChatMessage CloneMessage(ChatMessage m) => new()
    {
        Role = m.Role,
        Content = m.Content,
        ToolCallId = m.ToolCallId,
        // Muss mitkopiert werden, sonst prüfen Caching-Tests am Wire-Format vorbei.
        CacheBreakpoint = m.CacheBreakpoint,
        ToolCalls = m.ToolCalls?.Select(tc => new ToolCall
        {
            Id = tc.Id,
            Type = tc.Type,
            Function = new ToolCallFunction { Name =tc.Function.Name, Arguments = tc.Function.Arguments }
        }).ToList()
    };
}
