using ClaudeDo.Worker.Findings;

namespace ClaudeDo.Worker.Tests.Findings;

public sealed class FindingsStoreTests : IDisposable
{
    private readonly string _root;

    public FindingsStoreTests()
    {
        _root = Path.Combine(Path.GetTempPath(), "cdo-findings-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(_root);
    }

    public void Dispose()
    {
        try { Directory.Delete(_root, recursive: true); } catch { /* best effort */ }
    }

    private static FindingInput Input(string slug, string title = "Something is not what it looks like")
        => new(slug, title, "Body line one.\n\nBody line two.", "src/ClaudeDo.Worker", "task-1", "abc1234");

    [Fact]
    public async Task SaveAsync_WritesFindingFileWithFrontmatter()
    {
        var store = new FindingsStore();

        var outcome = await store.SaveAsync(_root, Input("conpty-arg-quoting"), CancellationToken.None, tracked: true);

        Assert.True(outcome.Created);
        Assert.Equal("conpty-arg-quoting", outcome.Slug);

        var text = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "traps", "conpty-arg-quoting.md"));
        Assert.StartsWith("---\n", text.Replace("\r\n", "\n"));
        Assert.Contains("slug: conpty-arg-quoting", text);
        Assert.Contains("scope: src/ClaudeDo.Worker", text);
        Assert.Contains("source-task: task-1", text);
        Assert.Contains("verified-against: abc1234", text);
        Assert.Contains("# Something is not what it looks like", text);
        Assert.Contains("Body line one.", text);
    }

    [Fact]
    public async Task SaveAsync_SameSlugOverwritesInsteadOfCreatingASecondFile()
    {
        var store = new FindingsStore();
        await store.SaveAsync(_root, Input("dup", "First title"), CancellationToken.None, tracked: true);

        var outcome = await store.SaveAsync(_root, Input("dup", "Second title"), CancellationToken.None, tracked: true);

        Assert.False(outcome.Created);
        Assert.Equal(1, outcome.TotalFindings);
        var files = Directory.GetFiles(Path.Combine(_root, ".claudedo", "traps"));
        Assert.Single(files);
        var text = await File.ReadAllTextAsync(files[0]);
        Assert.Contains("# Second title", text);
        Assert.DoesNotContain("# First title", text);
    }

    [Fact]
    public async Task SaveAsync_RebuildsIndexFromDiskSortedBySlug()
    {
        var store = new FindingsStore();
        await store.SaveAsync(_root, Input("zulu", "Zulu trap"), CancellationToken.None, tracked: true);
        await store.SaveAsync(_root, Input("alpha", "Alpha trap"), CancellationToken.None, tracked: true);

        var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md"));
        var lines = index.Replace("\r\n", "\n").Split('\n');

        var alphaAt = Array.FindIndex(lines, l => l.Contains("alpha"));
        var zuluAt = Array.FindIndex(lines, l => l.Contains("zulu"));
        Assert.True(alphaAt >= 0 && zuluAt >= 0);
        Assert.True(alphaAt < zuluAt);
        Assert.Contains("- [alpha](traps/alpha.md) — Alpha trap", index);
    }

    [Fact]
    public async Task SaveAsync_IndexDropsFindingsDeletedOnDisk()
    {
        var store = new FindingsStore();
        await store.SaveAsync(_root, Input("gone", "Gone trap"), CancellationToken.None, tracked: true);
        await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None, tracked: true);

        File.Delete(Path.Combine(_root, ".claudedo", "traps", "gone.md"));
        await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None, tracked: true);

        var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md"));
        Assert.DoesNotContain("gone", index);
        Assert.Contains("stays", index);
    }

    [Theory]
    [InlineData("../escape")]
    [InlineData("has space")]
    [InlineData("UPPER")]
    [InlineData("trailing-")]
    [InlineData("")]
    public async Task SaveAsync_RejectsInvalidSlug(string slug)
    {
        var store = new FindingsStore();

        await Assert.ThrowsAsync<ArgumentException>(
            () => store.SaveAsync(_root, Input(slug), CancellationToken.None, tracked: true));
    }

    [Fact]
    public async Task SaveAsync_FlagsNearCapacityAtWarnThreshold()
    {
        var store = new FindingsStore();
        SaveFindingOutcome outcome = null!;
        for (var i = 0; i < FindingsStore.WarnThreshold; i++)
            outcome = await store.SaveAsync(_root, Input($"trap-{i:000}"), CancellationToken.None, tracked: true);

        Assert.Equal(FindingsStore.WarnThreshold, outcome.TotalFindings);
        Assert.True(outcome.NearCapacity);
    }

    [Fact]
    public async Task SaveAsync_DoesNotFlagNearCapacityBelowThreshold()
    {
        var store = new FindingsStore();

        var outcome = await store.SaveAsync(_root, Input("only-one"), CancellationToken.None, tracked: true);

        Assert.False(outcome.NearCapacity);
    }
}
