using System.Net;
using ClawdDotNet.Core.Deploymentcenter;
using ClawdDotNet.Core.Deploymentcenter.Watchdog;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Deploymentcenter;

public sealed class WatchdogClientTests
{
    private static WatchdogClient Client(HttpMessageHandler handler, string instance = "inst-1") =>
        WatchdogClient.Create("https://dc.example", "tok", "clawddotnet", instance,
            "ClawdDotNet", "Windows / .NET 10", "0.1.0", new HttpClient(handler));

    private static InstanceHealth Health(
        string status = WatchdogStatus.Ok,
        string? message = null,
        Dictionary<string, double>? metrics = null,
        Dictionary<string, HealthCheck>? checks = null)
        => new(status, message, metrics ?? [], checks ?? []);

    [Fact]
    public async Task Heartbeat_geht_an_den_v1_Pfad_mit_Bearer_Token()
    {
        var handler = new CapturingHandler();

        await Client(handler).SendHeartbeatAsync(Health(message: "Betrieb normal."), 60, default);

        handler.LastRequest!.RequestUri!.ToString().ShouldBe("https://dc.example/api/watchdog/v1/ping");
        handler.LastRequest.Method.ShouldBe(HttpMethod.Post);
        handler.LastRequest.Headers.Authorization!.ToString().ShouldBe("Bearer tok");

        handler.LastBody.ShouldNotBeNull();
        handler.LastBody.ShouldContain("\"source\":\"clawddotnet\"");
        handler.LastBody.ShouldContain("\"instance\":\"inst-1\"");
        handler.LastBody.ShouldContain("\"type\":\"heartbeat\"");
        handler.LastBody.ShouldContain("\"interval\":60");
    }

    /// <summary>
    /// Der Server legt die Version in <c>watchdog_monitors.app_version</c> ab. Bei
    /// mehreren Instanzen ist das der Unterschied zwischen „läuft" und „läuft noch auf
    /// der alten Fassung".
    /// </summary>
    [Fact]
    public async Task Heartbeat_traegt_die_Anwendungsversion()
    {
        var handler = new CapturingHandler();

        await Client(handler).SendHeartbeatAsync(Health(), 60, default);

        handler.LastBody!.ShouldContain("\"version\":\"0.1.0\"");
    }

    /// <summary>
    /// Der Monitor wird serverseitig über source + instance geführt. Zwei Instanzen
    /// derselben Anwendung müssen deshalb unterschiedliche instance-Werte melden —
    /// sonst überschreiben sie sich gegenseitig, und der Ausfall einer bliebe unsichtbar.
    /// </summary>
    [Fact]
    public async Task Jede_Instanz_meldet_unter_eigenem_Instanznamen()
    {
        var first = new CapturingHandler();
        var second = new CapturingHandler();

        await Client(first, "inst-a").SendHeartbeatAsync(Health(), 60, default);
        await Client(second, "inst-b").SendHeartbeatAsync(Health(), 60, default);

        first.LastBody!.ShouldContain("\"instance\":\"inst-a\"");
        second.LastBody!.ShouldContain("\"instance\":\"inst-b\"");
    }

    [Fact]
    public async Task Checks_und_Metriken_gehen_mit()
    {
        var handler = new CapturingHandler();

        await Client(handler).SendHeartbeatAsync(
            Health(
                metrics: new Dictionary<string, double> { ["agentCount"] = 3 },
                checks: new Dictionary<string, HealthCheck>
                {
                    ["scheduler"] = new(false, "Steht.")
                }),
            60, default);

        handler.LastBody!.ShouldContain("\"agentCount\":3");
        handler.LastBody.ShouldContain("\"scheduler\":{\"ok\":false,\"message\":\"Steht.\"}");
    }

    /// <summary>Leere Angaben bleiben weg, statt als leeres Objekt zu erscheinen.</summary>
    [Fact]
    public async Task Ohne_Checks_und_Metriken_bleiben_die_Felder_weg()
    {
        var handler = new CapturingHandler();

        await Client(handler).SendHeartbeatAsync(Health(), 60, default);

        handler.LastBody!.ShouldNotContain("\"checks\"");
        handler.LastBody.ShouldNotContain("\"metrics\"");
    }

    [Fact]
    public async Task Antwort_liefert_Zustand_und_fehlgeschlagene_Pruefungen()
    {
        var handler = new CapturingHandler(HttpStatusCode.OK,
            """{"status":"success","monitor":{"state":"warning","failing_checks":["scheduler"]}}""");

        var result = await Client(handler).SendHeartbeatAsync(Health(), 60, default);

        result.State.ShouldBe("warning");
        result.FailingChecks.ShouldHaveSingleItem().ShouldBe("scheduler");
    }

    [Fact]
    public async Task Ereignis_geht_an_den_Event_Endpunkt()
    {
        var handler = new CapturingHandler();

        await Client(handler).SendEventAsync(
            WatchdogEventKind.StoppedGraceful, "info", "Instanz beendet.", null, default);

        handler.LastRequest!.RequestUri!.ToString().ShouldBe("https://dc.example/api/watchdog/v1/event");
        handler.LastBody!.ShouldContain("\"kind\":\"stopped_graceful\"");
    }

    [Fact]
    public async Task Abgelehntes_Token_wirft_mit_Code()
    {
        var handler = new CapturingHandler(HttpStatusCode.Unauthorized,
            """{"status":"error","error":{"code":"unauthorized","message":"Kein gueltiges Token."}}""");

        var ex = await Should.ThrowAsync<DeploymentcenterException>(
            () => Client(handler).SendHeartbeatAsync(Health(), 60, default));

        ex.Code.ShouldBe("unauthorized");
    }
}
