using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Staging;

/// <summary>
/// Der Freigabe-/Ablehnungs-Fluss (A2). Kernpunkte: der eingefrorene Aufruf wird bei
/// Freigabe direkt ausgeführt, der Agent wird über einen Folge-Task (A1) geweckt, und jede
/// Entscheidung landet als Approval-Record im Audit-Log (A3).
/// </summary>
public sealed class StagingServiceTests : IDisposable
{
    private readonly string _directory;
    private readonly SqliteStorage _storage;
    private readonly SqliteStagingRepository _staging;
    private readonly SqliteTaskRepository _tasks;
    private readonly TaskboardService _board;
    private readonly InMemoryAuditRepository _audit = new();
    private readonly FakeExecutor _executor = new();
    private readonly StagingService _service;

    public StagingServiceTests()
    {
        _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
        _storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
        _staging = new SqliteStagingRepository(_storage);
        _tasks = new SqliteTaskRepository(_storage);
        _board = new TaskboardService(_tasks, Path.Combine(_directory, "SharedWorkspace", "tasks"));
        _service = new StagingService(_staging, _executor, _board, NullLoggerFactory.Instance, _audit);
    }

    public void Dispose()
    {
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
        try { Directory.Delete(_directory, recursive: true); }
        catch { /* Aufräumen ist Nebensache */ }
    }

    private Task<long> Stage(string args = "{\"action\":\"send\",\"to\":\"x@y.de\"}")
        => _staging.AppendAsync(new StagedCall
        {
            RunId = "run-1",
            AgentId = "agent-a",
            Tool = "Mail",
            Action = "send",
            ArgumentsJson = args,
            Proposal = "Mail.send"
        }, default);

    [Fact]
    public async Task Freigabe_fuehrt_genau_den_eingefrorenen_Aufruf_aus()
    {
        var id = await Stage("{\"action\":\"send\",\"to\":\"chef@firma.de\"}");

        var result = await _service.ApproveAsync(id, "richard", default);

        result.Kind.ShouldBe(StagingResultKind.Executed);
        _executor.Calls.Count.ShouldBe(1);
        _executor.Calls[0].Tool.ShouldBe("Mail");
        _executor.Calls[0].Args.ShouldContain("chef@firma.de", Case.Sensitive);
        (await _staging.GetAsync(id, default))!.Status.ShouldBe(StagingStatus.Executed);
    }

    [Fact]
    public async Task Freigabe_weckt_den_Agenten_ueber_einen_Folge_Task()
    {
        var id = await Stage();

        await _service.ApproveAsync(id, "richard", default);

        var tasks = await _tasks.ListAsync(new TaskQuery(), default);
        tasks.Count.ShouldBe(1);
        tasks[0].Assignee.ShouldBe("@agent-a", "der vorschlagende Agent wird mit seinem Kontext geweckt");
        tasks[0].Status.ShouldBe(TaskItemStatus.Todo);
        tasks[0].Body.ShouldContain("freigegeben");
    }

    [Fact]
    public async Task Jede_Freigabe_wird_als_Approval_Record_protokolliert()
    {
        var id = await Stage();

        await _service.ApproveAsync(id, "richard", default);

        var entry = _audit.Entries.Single();
        entry.Source.ShouldBe("approval");
        entry.Tool.ShouldBe("Mail");
        entry.Summary.ShouldContain("Freigegeben von richard");
    }

    [Fact]
    public async Task Ablehnung_fuehrt_nichts_aus_und_weckt_den_Agenten_mit_dem_Grund()
    {
        var id = await Stage();

        var result = await _service.RejectAsync(id, "richard", "zu riskant", default);

        result.Kind.ShouldBe(StagingResultKind.Rejected);
        _executor.Calls.ShouldBeEmpty("eine Ablehnung führt nichts aus");
        (await _staging.GetAsync(id, default))!.Status.ShouldBe(StagingStatus.Rejected);

        var tasks = await _tasks.ListAsync(new TaskQuery(), default);
        tasks[0].Body.ShouldContain("abgelehnt");
        tasks[0].Body.ShouldContain("zu riskant");
        _audit.Entries.Single().Summary.ShouldContain("Abgelehnt von richard");
    }

    [Fact]
    public async Task Ein_zweiter_Freigabeklick_laeuft_ins_Leere_und_fuehrt_nicht_erneut_aus()
    {
        var id = await Stage();

        var first = await _service.ApproveAsync(id, "a", default);
        var second = await _service.ApproveAsync(id, "b", default);

        first.Kind.ShouldBe(StagingResultKind.Executed);
        second.Kind.ShouldBe(StagingResultKind.AlreadyDecided);
        _executor.Calls.Count.ShouldBe(1, "genau einmal ausgeführt");
    }

    [Fact]
    public async Task Ein_unbekannter_Vorschlag_wird_gemeldet()
        => (await _service.ApproveAsync(999, "x", default)).Kind.ShouldBe(StagingResultKind.NotFound);

    [Fact]
    public async Task Scheitert_die_Ausfuehrung_wird_der_Vorschlag_als_fehlgeschlagen_markiert()
    {
        _executor.Throw = true;
        var id = await Stage();

        var result = await _service.ApproveAsync(id, "richard", default);

        result.Kind.ShouldBe(StagingResultKind.Failed);
        (await _staging.GetAsync(id, default))!.Status.ShouldBe(StagingStatus.Failed);
        // Der Agent wird trotzdem geweckt (mit der Fehlermeldung).
        (await _tasks.ListAsync(new TaskQuery(), default)).Count.ShouldBe(1);
    }

    private sealed class FakeExecutor : IFrozenCallExecutor
    {
        public List<(string Agent, string Tool, string Args)> Calls { get; } = [];
        public bool Throw { get; set; }

        public Task<string> ExecuteApprovedCallAsync(
            string agentId, string tool, string argumentsJson, string runId, CancellationToken ct)
        {
            if (Throw) throw new InvalidOperationException("Tool kaputt");
            Calls.Add((agentId, tool, argumentsJson));
            return Task.FromResult("ausgeführt");
        }
    }
}
