using ClawdDotNet.Core.Scheduling;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Scheduling;

/// <summary>
/// Cron-Parsing und Terminberechnung (deckt Teile von B6/B7 ab). Der bisher ungetestete
/// Basis-Scheduler bekommt hier sein Netz: gültige Ausdrücke rechnen richtig, ungültige
/// werfen eine saubere <see cref="FormatException"/> statt einer rohen Parser-Exception
/// oder einer Endlosschleife.
/// </summary>
public sealed class CronExpressionTests
{
    private static DateTime Local(int y, int mo, int d, int h, int mi)
        => new(y, mo, d, h, mi, 0, DateTimeKind.Local);

    // ─── R1: korrekte nächste Zeitpunkte ───

    [Fact]
    public void Alle_30_Minuten()
    {
        var cron = CronExpression.Parse("*/30 * * * *");
        cron.GetNextOccurrence(Local(2026, 7, 31, 10, 5)).ShouldBe(Local(2026, 7, 31, 10, 30));
        cron.GetNextOccurrence(Local(2026, 7, 31, 10, 45)).ShouldBe(Local(2026, 7, 31, 11, 0));
    }

    [Fact]
    public void Werktags_um_drei_Uhr_frueh()
    {
        var cron = CronExpression.Parse("0 3 * * 1-5"); // Mo–Fr
        // 2026-08-01 ist ein Samstag → nächster Lauf Montag, 2026-08-03 03:00
        cron.GetNextOccurrence(Local(2026, 8, 1, 4, 0)).ShouldBe(Local(2026, 8, 3, 3, 0));
    }

    [Fact]
    public void Listen_von_Minuten()
    {
        var cron = CronExpression.Parse("15,45 * * * *");
        cron.GetNextOccurrence(Local(2026, 7, 31, 10, 20)).ShouldBe(Local(2026, 7, 31, 10, 45));
        cron.GetNextOccurrence(Local(2026, 7, 31, 10, 50)).ShouldBe(Local(2026, 7, 31, 11, 15));
    }

    [Fact]
    public void Schaltjahr_29_Februar()
    {
        var cron = CronExpression.Parse("0 0 29 2 *"); // nur am 29.02.
        // 2028 ist das nächste Schaltjahr
        cron.GetNextOccurrence(Local(2026, 3, 1, 0, 0)).ShouldBe(Local(2028, 2, 29, 0, 0));
    }

    // ─── R2: Invariante ───

    [Theory]
    [InlineData("*/5 * * * *")]
    [InlineData("0 3 * * 1-5")]
    [InlineData("15,45 9-17 * * *")]
    [InlineData("0 0 1 * *")]
    public void Der_naechste_Termin_liegt_echt_in_der_Zukunft_und_passt(string expr)
    {
        var cron = CronExpression.Parse(expr);
        var now = Local(2026, 7, 31, 10, 23);

        var next = cron.GetNextOccurrence(now);

        next.ShouldNotBeNull();
        next!.Value.ShouldBeGreaterThan(now);
        cron.Matches(next.Value).ShouldBeTrue();
    }

    // ─── R3: ungültige Ausdrücke ───

    [Theory]
    [InlineData("* * *")]           // zu wenige Felder
    [InlineData("* * * * * *")]     // zu viele Felder
    [InlineData("99 * * * *")]      // Minute außerhalb 0-59
    [InlineData("* 25 * * *")]      // Stunde außerhalb 0-23
    [InlineData("a b c d e")]       // keine Zahlen
    [InlineData("*/0 * * * *")]     // Schritt 0 (wäre Endlosschleife)
    [InlineData("5-2 * * * *")]     // rückwärtiger Bereich
    [InlineData("* * * * ")]        // ein leeres Feld nach Trim → 4 Felder
    public void Ungueltige_Ausdruecke_werfen_eine_saubere_FormatException(string expr)
        => Should.Throw<FormatException>(() => CronExpression.Parse(expr));

    [Fact]
    public void Ein_gueltiger_Ausdruck_mit_Schritt_bleibt_endlich()
    {
        // Regressionswächter: */0 hätte früher eine Endlosschleife erzeugt.
        Should.CompleteIn(() => CronExpression.Parse("*/15 * * * *"), TimeSpan.FromSeconds(2));
    }
}
