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

namespace ClawdDotNet.Core.Tests.Tasks;

/// <summary>
/// Der Scanner-Kern — der in der Roadmap für Opus 5/Fable markierte heikle Teil. Getestet
/// gegen echte SQLite (das atomare Claiming ist der Punkt) mit einer Attrappe für die
/// Ausführung, damit keine Engine nötig ist. Die drei Invarianten stehen im Mittelpunkt.
/// </summary>
public sealed class TaskScannerTests : IDisposable
{
    private readonly string _directory;
    private readonly SqliteStorage _storage;
    private readonly SqliteTaskRepository _repo;
    private readonly FakeTimeProvider _clock = new(new DateTimeOffset(2026, 7, 31, 12, 0, 0, TimeSpan.Zero));

    public TaskScannerTests()
    {
        _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
        _storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
        _repo = new SqliteTaskRepository(_storage);
    }

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

    private TaskScanner Scanner(ITaskDispatcher dispatcher, IMarketCalendar? market = null)
        => new(_repo, dispatcher, NullLoggerFactory.Instance, market, _clock);

    /// <summary>Eine sofort fällige Aufgabe (kein Termin, nie gelaufen).</summary>
    private Task<TaskItem> SeedDue(
        string id, string assignee = "@new",
        TaskItemStatus status = TaskItemStatus.Todo,
        bool requireApproval = false, IReadOnlyList<string>? blockedBy = null,
        bool onlyWhenMarketOpen = false)
        => _repo.UpsertAsync(new TaskItem
        {
            Id = id,
            Title = id,
            Assignee = assignee,
            Status = status,
            RequireApproval = requireApproval,
            BlockedBy = blockedBy ?? [],
            OnlyWhenMarketOpen = onlyWhenMarketOpen
        }, default);

    // ─── Grundfluss ───

    [Fact]
    public async Task Eine_faellige_Aufgabe_wird_ausgefuehrt_und_abgeschlossen()
    {
        await SeedDue("t-1");
        var dispatcher = new FakeDispatcher();

        var dispatched = await Scanner(dispatcher).ScanOnceAsync(default);

        dispatched.ShouldBe(1);
        dispatcher.Dispatched.ShouldBe(["t-1"]);
        (await _repo.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.Done);
    }

    [Fact]
    public async Task Mit_require_approval_landet_das_Ergebnis_im_Review()
    {
        await SeedDue("t-1", requireApproval: true);

        await Scanner(new FakeDispatcher()).ScanOnceAsync(default);

        (await _repo.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.InReview);
    }

    [Fact]
    public async Task Ein_gescheiterter_Lauf_geht_zurueck_auf_todo_und_wird_nicht_wiederholt()
    {
        await SeedDue("t-1");
        var dispatcher = new FakeDispatcher(succeed: false);
        var scanner = Scanner(dispatcher);

        await scanner.ScanOnceAsync(default);
        (await _repo.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.Todo);

        // Zweiter Takt: der Marker ist gesetzt, derselbe Termin läuft nicht erneut.
        await scanner.ScanOnceAsync(default);
        dispatcher.Dispatched.Count.ShouldBe(1, "kein Retry-Sturm für denselben Termin");
    }

    // ─── Invariante 2: kein Dispatch bei offenem Blocker / an Menschen ───

    [Fact]
    public async Task Eine_blockierte_Aufgabe_wird_nicht_angestossen()
    {
        await SeedDue("t-1", status: TaskItemStatus.Blocked, blockedBy: ["t-0"]);
        var dispatcher = new FakeDispatcher();

        (await Scanner(dispatcher).ScanOnceAsync(default)).ShouldBe(0);
        dispatcher.Dispatched.ShouldBeEmpty();
    }

    [Fact]
    public async Task Eine_Aufgabe_fuer_einen_Menschen_wird_nicht_angestossen()
    {
        await SeedDue("t-1", assignee: "@human");
        var dispatcher = new FakeDispatcher();

        (await Scanner(dispatcher).ScanOnceAsync(default)).ShouldBe(0);
        dispatcher.Dispatched.ShouldBeEmpty();
        (await _repo.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.Todo, "sie wartet unverändert");
    }

    // ─── Invariante 3: doppelter Takt = ein Lauf ───

    [Fact]
    public async Task Zwei_gleichzeitige_Takte_stossen_eine_Aufgabe_nur_einmal_an()
    {
        for (var round = 0; round < 20; round++)
        {
            var id = $"t-{round}";
            await SeedDue(id);
            var dispatcher = new FakeDispatcher();
            var scanner = Scanner(dispatcher);

            await Task.WhenAll(
                scanner.ScanOnceAsync(default),
                scanner.ScanOnceAsync(default));

            dispatcher.Dispatched.Count(x => x == id).ShouldBe(1, $"Runde {round}");
        }
    }

    // ─── Wiederkehrende Tasks (ersetzt den Alt-Scheduler) ───

    [Fact]
    public async Task Ein_wiederkehrender_Task_bleibt_nach_dem_Feuern_auf_todo()
    {
        _clock.SetUtcNow(new DateTimeOffset(2027, 1, 1, 9, 0, 0, TimeSpan.Zero));
        await _repo.UpsertAsync(new TaskItem
        {
            Id = "t-rec", Title = "täglich", Assignee = "@new", Status = TaskItemStatus.Todo,
            When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "0 9 * * *", TimeZone = "UTC" }
        }, default);

        await Scanner(new FakeDispatcher()).ScanOnceAsync(default);

        (await _repo.GetAsync("t-rec", default))!.Status.ShouldBe(
            TaskItemStatus.Todo, "sonst liefe ein Cron-Task nur ein einziges Mal");
    }

    [Fact]
    public async Task Ein_wiederkehrender_Task_feuert_an_jedem_Termin_erneut()
    {
        await _repo.UpsertAsync(new TaskItem
        {
            Id = "t-rec", Title = "täglich", Assignee = "@new", Status = TaskItemStatus.Todo,
            When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "0 9 * * *", TimeZone = "UTC" }
        }, default);
        var dispatcher = new FakeDispatcher();
        var scanner = Scanner(dispatcher);

        _clock.SetUtcNow(new DateTimeOffset(2027, 1, 1, 9, 0, 0, TimeSpan.Zero));
        await scanner.ScanOnceAsync(default);

        _clock.SetUtcNow(new DateTimeOffset(2027, 1, 2, 9, 0, 0, TimeSpan.Zero));
        await scanner.ScanOnceAsync(default);

        dispatcher.Dispatched.Count(x => x == "t-rec").ShouldBe(2, "zwei Termine, zwei Läufe");
    }

    [Fact]
    public async Task Ein_backlog_Task_wird_nicht_ausgefuehrt()
    {
        await SeedDue("t-1", status: TaskItemStatus.Backlog);
        var dispatcher = new FakeDispatcher();

        (await Scanner(dispatcher).ScanOnceAsync(default)).ShouldBe(0);
        dispatcher.Dispatched.ShouldBeEmpty();
    }

    // ─── C1-Haken: Marktkalender ───

    [Fact]
    public async Task Bei_geschlossenem_Markt_bleibt_ein_marktabhaengiger_Termin_liegen()
    {
        await SeedDue("t-1", onlyWhenMarketOpen: true);
        var dispatcher = new FakeDispatcher();

        var scanner = Scanner(dispatcher, new ClosedMarket());
        (await scanner.ScanOnceAsync(default)).ShouldBe(0);

        dispatcher.Dispatched.ShouldBeEmpty();
        (await _repo.GetAsync("t-1", default))!.Status.ShouldBe(TaskItemStatus.Todo);
    }

    // ─── Auto-Dispatch: Blocker fertig → Wartende frei ───

    [Fact]
    public async Task Wird_der_letzte_Blocker_fertig_gibt_das_die_wartende_Aufgabe_frei()
    {
        await SeedDue("t-blocker");
        await SeedDue("t-wartend", status: TaskItemStatus.Blocked, blockedBy: ["t-blocker"]);

        await Scanner(new FakeDispatcher()).ScanOnceAsync(default);

        (await _repo.GetAsync("t-blocker", default))!.Status.ShouldBe(TaskItemStatus.Done);
        (await _repo.GetAsync("t-wartend", default))!.Status.ShouldBe(
            TaskItemStatus.Todo, "der Blocker ist erledigt, also ist sie jetzt bereit");
    }

    [Fact]
    public async Task Solange_ein_Blocker_offen_ist_bleibt_die_wartende_Aufgabe_blockiert()
    {
        await SeedDue("t-b1");                       // wird in diesem Takt fertig
        await SeedDue("t-b2", assignee: "@human");   // ein Mensch — bleibt offen
        await SeedDue("t-wartend", status: TaskItemStatus.Blocked, blockedBy: ["t-b1", "t-b2"]);

        await Scanner(new FakeDispatcher()).ScanOnceAsync(default);

        (await _repo.GetAsync("t-wartend", default))!.Status.ShouldBe(
            TaskItemStatus.Blocked, "nicht alle Blocker sind erledigt");
    }

    // ─── Attrappen ───

    private sealed class FakeDispatcher(bool succeed = true) : ITaskDispatcher
    {
        private readonly Lock _lock = new();
        public List<string> Dispatched { get; } = [];

        public Task<bool> DispatchAsync(TaskItem task, CancellationToken ct)
        {
            lock (_lock)
                Dispatched.Add(task.Id);
            return Task.FromResult(succeed);
        }
    }

    private sealed class ClosedMarket : IMarketCalendar
    {
        public bool IsOpen(DateTime nowUtc) => false;
    }
}
