using ClaudeDo.Worker.Usage.TokenTracker;

namespace ClaudeDo.Worker.Tests.Usage.TokenTracker;

public sealed class TokenTrackerAggregatorTests
{
    private static readonly DateOnly RangeStart = new(2026, 8, 20);
    private static readonly DateOnly RangeEnd = new(2026, 8, 21);

    private static IReadOnlyList<TokenTrackerSession> Sessions() =>
        TokenTrackerExportParser.Parse(
            TokenTrackerFixtures.SessionsJson,
            new DateTime(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc))!.Sessions;

    private static HashSet<string> ClaudeDoHashes() =>
    [
        SessionHash.ForClaudeSession(TokenTrackerFixtures.ClaudeDoSessionIdA)!,
        SessionHash.ForClaudeSession(TokenTrackerFixtures.ClaudeDoSessionIdB)!,
    ];

    [Fact]
    public void ByModel_ExcludesSessionsOutsideTheRange()
    {
        var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, ClaudeDoHashes());

        Assert.All(rows, r => Assert.InRange(r.Date, RangeStart, RangeEnd));
        Assert.DoesNotContain(rows, r => r.CostUsd == 99.0);
    }

    [Fact]
    public void ByModel_SplitsClaudeDoFromOther()
    {
        var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, ClaudeDoHashes());

        var opus = rows.Single(r => r.Model == "claude-opus-5" && r.Scope == "claudedo");
        Assert.Equal(110, opus.InputTokens);
        Assert.Equal(440, opus.OutputTokens);
        Assert.Equal(990, opus.CacheReadTokens);
        Assert.Equal(55, opus.CacheCreationTokens);
        Assert.Equal(2, opus.Sessions);
        Assert.Equal(3.5, opus.CostUsd, 6);

        var sonnet = rows.Single(r => r.Model == "claude-sonnet-5");
        Assert.Equal("other", sonnet.Scope);
        Assert.Equal(0.25, sonnet.CostUsd, 6);
    }

    [Fact]
    public void ByModel_UnknownHash_CountsAsOther()
    {
        var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, new HashSet<string>());

        Assert.All(rows, r => Assert.Equal("other", r.Scope));
    }

    [Fact]
    public void ByModel_EmptyInput_ReturnsEmpty()
    {
        Assert.Empty(TokenTrackerAggregator.ByModel([], RangeStart, RangeEnd, new HashSet<string>()));
    }

    [Fact]
    public void ForSessions_SumsCostAndRetriesOfMatchingRows()
    {
        var extras = TokenTrackerAggregator.ForSessions(
            Sessions(),
            [TokenTrackerFixtures.ClaudeDoSessionIdA, TokenTrackerFixtures.ClaudeDoSessionIdB]);

        Assert.NotNull(extras);
        Assert.Equal(3.5, extras!.CostUsd, 6);
        Assert.Equal(3, extras.Retries);
        Assert.True(extras.Productive);   // A is productive
        Assert.False(extras.OneShot);     // B is not one-shot
    }

    [Fact]
    public void ForSessions_SingleOneShotSession_ReportsOneShot()
    {
        var extras = TokenTrackerAggregator.ForSessions(
            Sessions(), [TokenTrackerFixtures.ClaudeDoSessionIdA]);

        Assert.True(extras!.OneShot);
        Assert.Equal(2.5, extras.CostUsd, 6);
    }

    [Fact]
    public void ForSessions_NoMatch_ReturnsNull()
    {
        Assert.Null(TokenTrackerAggregator.ForSessions(Sessions(), ["11111111-2222-3333-4444-555555555555"]));
    }

    [Fact]
    public void ForSessions_BlankIds_ReturnsNull()
    {
        Assert.Null(TokenTrackerAggregator.ForSessions(Sessions(), [null, "", "  "]));
    }

    [Fact]
    public void Totals_SumsTokensAndCostInRange()
    {
        var totals = TokenTrackerAggregator.Totals(Sessions(), RangeStart, RangeEnd);

        Assert.Equal(1615, totals.TotalTokens);   // 1450 + 145 + 20
        Assert.Equal(3.75, totals.CostUsd, 6);
    }
}
