using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using Shouldly;

namespace ClawdDotNet.Core.Tests.Tasks;

/// <summary>
/// Die Wahrheitsaufteilung in Aktion: Der Dienst hält Markdown-Datei (Definition) und
/// DB (Ausführungszustand) im Gleichschritt. Getestet gegen echtes Dateisystem und echte
/// SQLite — beide Seiten sind der Punkt.
/// </summary>
public sealed class TaskboardServiceTests : IDisposable
{
    private readonly string _directory;
    private readonly string _tasksDir;
    private readonly SqliteStorage _storage;
    private readonly SqliteTaskRepository _repo;
    private readonly TaskboardService _board;

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

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

    private static TaskItem Def(string title = "Aufgabe", string assignee = "@new")
        => new() { Title = title, Assignee = assignee, Body = "Tu etwas." };

    [Fact]
    public async Task Anlegen_erzeugt_Datei_und_DB_Zeile_in_einem_Zug()
    {
        var created = await _board.CreateAsync(Def("NVDA recherchieren"), default);

        created.Id.ShouldNotBeNullOrWhiteSpace();
        created.Id.ShouldStartWith("t-");

        // DB-Seite
        (await _repo.GetAsync(created.Id, default)).ShouldNotBeNull();

        // Datei-Seite: existiert und lässt sich zur selben Definition zurücklesen
        var path = Path.Combine(_tasksDir, created.FileName);
        File.Exists(path).ShouldBeTrue();

        TaskFrontmatter.TryParse(File.ReadAllText(path), out var fromFile, out _).ShouldBeTrue();
        fromFile.Id.ShouldBe(created.Id);
        fromFile.Title.ShouldBe("NVDA recherchieren");
        fromFile.Assignee.ShouldBe("@new");
    }

    [Fact]
    public async Task Der_Dateiname_ist_beschreibend()
    {
        var created = await _board.CreateAsync(Def("NVDA Earnings recherchieren"), default);
        created.FileName.ShouldStartWith("nvda-earnings-recherchieren-");
        created.FileName.ShouldEndWith(".md");
    }

    [Fact]
    public async Task Aendern_schreibt_Datei_und_DB_fort()
    {
        var created = await _board.CreateAsync(Def("Alt"), default);

        var updated = await _board.UpdateAsync(created.Id,
            c => c with { Title = "Neu", Status = TaskItemStatus.Done }, default);

        updated!.Title.ShouldBe("Neu");
        updated.Status.ShouldBe(TaskItemStatus.Done);

        // Beide Seiten spiegeln die Änderung
        (await _repo.GetAsync(created.Id, default))!.Status.ShouldBe(TaskItemStatus.Done);
        TaskFrontmatter.TryParse(
            File.ReadAllText(Path.Combine(_tasksDir, created.FileName)), out var fromFile, out _);
        fromFile.Title.ShouldBe("Neu");
        fromFile.Status.ShouldBe(TaskItemStatus.Done);
    }

    [Fact]
    public async Task Ein_Kommentar_landet_im_Rumpf()
    {
        var created = await _board.CreateAsync(Def(), default);

        await _board.AddCommentAsync(created.Id, "agent-a", "Erstes Zwischenergebnis.", default);
        await _board.AddCommentAsync(created.Id, "agent-b", "Bitte nachbessern.", default);

        var body = (await _repo.GetAsync(created.Id, default))!.Body;
        body.ShouldContain("Erstes Zwischenergebnis.");
        body.ShouldContain("agent-b");
        body.ShouldContain("Bitte nachbessern.");
    }

    [Fact]
    public async Task Import_spiegelt_vorhandene_Dateien_in_die_DB()
    {
        Directory.CreateDirectory(_tasksDir);
        File.WriteAllText(Path.Combine(_tasksDir, "eins.md"),
            "---\nid: t-eins\ntitle: Eins\nassignee: \"@new\"\n---\nMach eins.");
        File.WriteAllText(Path.Combine(_tasksDir, "zwei.md"),
            "---\nid: t-zwei\ntitle: Zwei\nassignee: \"@new\"\n---\nMach zwei.");

        var imported = await _board.ImportAllAsync(default);

        imported.ShouldBe(2);
        (await _repo.GetAsync("t-eins", default))!.Title.ShouldBe("Eins");
        (await _repo.GetAsync("t-zwei", default))!.Title.ShouldBe("Zwei");
    }

    [Fact]
    public async Task Eine_von_Hand_angelegte_Datei_ohne_Id_bekommt_eine_und_behaelt_sie()
    {
        Directory.CreateDirectory(_tasksDir);
        var path = Path.Combine(_tasksDir, "handarbeit.md");
        File.WriteAllText(path, "---\ntitle: Von Hand\nassignee: \"@human\"\n---\nManuell angelegt.");

        var imported = await _board.ImportFileAsync(path, default);

        imported.ShouldNotBeNull();
        imported!.Id.ShouldNotBeNullOrWhiteSpace();

        // Die Id wurde in die Datei zurückgeschrieben — überlebt den nächsten Start.
        TaskFrontmatter.TryParse(File.ReadAllText(path), out var fromFile, out _);
        fromFile.Id.ShouldBe(imported.Id);
    }

    [Fact]
    public async Task Eine_verschwundene_Datei_wird_archiviert_statt_geloescht()
    {
        var created = await _board.CreateAsync(Def("Vergänglich"), default);
        File.Delete(Path.Combine(_tasksDir, created.FileName));

        await _board.ImportAllAsync(default);

        // Die Historie bleibt: die Zeile ist archiviert, nicht weg.
        var after = await _repo.GetAsync(created.Id, default);
        after.ShouldNotBeNull();
        after!.Status.ShouldBe(TaskItemStatus.Archived);
    }

    [Fact]
    public async Task Ein_Re_Import_setzt_einen_laufenden_Status_nicht_zurueck()
    {
        // Die Kernregel der Wahrheitsaufteilung über den Dienst geprüft.
        var created = await _board.CreateAsync(Def(), default);
        await _repo.TryClaimAsync(created.Id, "2026-07-31T07:00:00.0000000Z", "tok",
            DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), default);
        await _repo.CompleteClaimAsync(created.Id, "tok", TaskItemStatus.Done, DateTime.UtcNow, default);

        // Die Datei sagt weiterhin "todo" (so wurde sie angelegt) — ein Import darf das
        // nicht über den erreichten Done-Status stülpen.
        await _board.ImportAllAsync(default);

        (await _repo.GetAsync(created.Id, default))!.Status.ShouldBe(TaskItemStatus.Done);
    }
}
