using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Usage;
using ClaudeDo.Worker.Usage.Interfaces;

namespace ClaudeDo.Worker.Tests.Usage;

public sealed class UsageSnapshotBuilderTests : IDisposable
{
    private readonly DbFixture _db = new();

    public void Dispose() => _db.Dispose();

    private sealed class FakeGate : IUsageGate
    {
        private readonly UsageGateDecision _decision;
        public FakeGate(UsageGateDecision decision) => _decision = decision;
        public Task<UsageGateDecision> EvaluateAsync(CancellationToken ct = default) => Task.FromResult(_decision);
    }

    private async Task SetThresholdsAsync(int fiveHourPct, int sevenDayPct)
    {
        using var ctx = _db.CreateContext();
        var repo = new AppSettingsRepository(ctx);
        var settings = await repo.GetAsync();
        settings.UsageGateFiveHourPct = fiveHourPct;
        settings.UsageGateSevenDayPct = sevenDayPct;
        await repo.UpdateAsync(settings);
    }

    private UsageSnapshotBuilder CreateBuilder(UsageState state, UsageGateDecision decision, WorkerConfig? cfg = null) =>
        new(state, new FakeGate(decision), _db.CreateFactory(), cfg ?? new WorkerConfig());

    [Fact]
    public async Task Maps_buckets_limits_thresholds_and_gate_state()
    {
        await SetThresholdsAsync(70, 85);

        var fiveHourReset = DateTimeOffset.UtcNow.AddHours(2);
        var sevenDayReset = DateTimeOffset.UtcNow.AddDays(3);
        var limit = new UsageLimitRow("session", "default", 42.5, "warn", fiveHourReset, "sonnet", true);
        var snapshot = new UsageSnapshot(
            new UsageBucket(55.5, fiveHourReset),
            new UsageBucket(88.8, sevenDayReset),
            new[] { limit },
            DateTime.UtcNow);

        var state = new UsageState();
        state.ReportSuccess(snapshot);

        var builder = CreateBuilder(state, new UsageGateDecision(true, "5h-Limit 90% >= 70%"));

        var dto = await builder.BuildAsync();

        Assert.Equal(55.5, dto.FiveHourPercent);
        Assert.Equal(fiveHourReset, dto.FiveHourResetsAt);
        Assert.Equal(88.8, dto.SevenDayPercent);
        Assert.Equal(sevenDayReset, dto.SevenDayResetsAt);
        Assert.Equal(70, dto.FiveHourThresholdPct);
        Assert.Equal(85, dto.SevenDayThresholdPct);
        Assert.True(dto.IsGateBlocked);
        Assert.Equal("5h-Limit 90% >= 70%", dto.GateReason);
        Assert.Equal(snapshot.FetchedAtUtc, dto.FetchedAtUtc);
        Assert.False(dto.IsStale);
        Assert.Null(dto.LastError);

        var mappedLimit = Assert.Single(dto.Limits);
        Assert.Equal(limit.Kind, mappedLimit.Kind);
        Assert.Equal(limit.Group, mappedLimit.Group);
        Assert.Equal(limit.Percent, mappedLimit.Percent);
        Assert.Equal(limit.Severity, mappedLimit.Severity);
        Assert.Equal(limit.ResetsAt, mappedLimit.ResetsAt);
        Assert.Equal(limit.ScopeModelDisplayName, mappedLimit.ScopeModelDisplayName);
        Assert.Equal(limit.IsActive, mappedLimit.IsActive);
    }

    [Fact]
    public async Task No_snapshot_yields_null_percents_stale_and_not_blocked()
    {
        await SetThresholdsAsync(80, 90);
        var state = new UsageState();
        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.Null(dto.FiveHourPercent);
        Assert.Null(dto.SevenDayPercent);
        Assert.Empty(dto.Limits);
        Assert.Null(dto.FetchedAtUtc);
        Assert.True(dto.IsStale);
        Assert.False(dto.IsGateBlocked);
    }

    [Fact]
    public async Task Snapshot_older_than_4x_poll_interval_is_stale()
    {
        await SetThresholdsAsync(80, 90);
        var cfg = new WorkerConfig { UsagePollIntervalActiveSeconds = 60, UsagePollIntervalIdleSeconds = 60 };
        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), null, Array.Empty<UsageLimitRow>(),
            DateTime.UtcNow.AddSeconds(-4 * 60)));

        var builder = CreateBuilder(state, new UsageGateDecision(false, null), cfg);

        var dto = await builder.BuildAsync();

        Assert.True(dto.IsStale);
    }

    [Fact]
    public async Task Fresh_snapshot_is_not_stale()
    {
        await SetThresholdsAsync(80, 90);
        var cfg = new WorkerConfig { UsagePollIntervalActiveSeconds = 60, UsagePollIntervalIdleSeconds = 60 };
        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), null, Array.Empty<UsageLimitRow>(),
            DateTime.UtcNow));

        var builder = CreateBuilder(state, new UsageGateDecision(false, null), cfg);

        var dto = await builder.BuildAsync();

        Assert.False(dto.IsStale);
    }

    [Fact]
    public async Task Failed_last_poll_marks_stale_even_with_fresh_snapshot()
    {
        await SetThresholdsAsync(80, 90);
        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), null, Array.Empty<UsageLimitRow>(), DateTime.UtcNow));
        state.ReportFailure("boom", DateTime.UtcNow);

        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.True(dto.IsStale);
        Assert.Equal("boom", dto.LastError);
    }

    private async Task SetThrottleAsync(int maxParallel, int softPct, int hardPct)
    {
        using var ctx = _db.CreateContext();
        var repo = new AppSettingsRepository(ctx);
        var settings = await repo.GetAsync();
        settings.MaxParallelExecutions = maxParallel;
        settings.UsageThrottleFiveHourSoftPct = softPct;
        settings.UsageThrottleFiveHourHardPct = hardPct;
        settings.UsageThrottleSevenDaySoftPct = softPct;
        settings.UsageThrottleSevenDayHardPct = hardPct;
        await repo.UpdateAsync(settings);
    }

    private async Task SetPerBucketThrottleAsync(
        int maxParallel, int fiveSoft, int fiveHard, int sevenSoft, int sevenHard)
    {
        using var ctx = _db.CreateContext();
        var repo = new AppSettingsRepository(ctx);
        var settings = await repo.GetAsync();
        settings.MaxParallelExecutions = maxParallel;
        settings.UsageThrottleFiveHourSoftPct = fiveSoft;
        settings.UsageThrottleFiveHourHardPct = fiveHard;
        settings.UsageThrottleSevenDaySoftPct = sevenSoft;
        settings.UsageThrottleSevenDayHardPct = sevenHard;
        await repo.UpdateAsync(settings);
    }

    [Fact]
    public async Task Per_bucket_throttle_stages_are_reported_for_the_gauges()
    {
        await SetThresholdsAsync(80, 90);
        await SetPerBucketThrottleAsync(maxParallel: 3, fiveSoft: 45, fiveHard: 60, sevenSoft: 70, sevenHard: 85);

        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), new UsageBucket(10, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));

        var dto = await CreateBuilder(state, new UsageGateDecision(false, null)).BuildAsync();

        Assert.Equal(45, dto.ThrottleFiveHourSoftPct);
        Assert.Equal(60, dto.ThrottleFiveHourHardPct);
        Assert.Equal(70, dto.ThrottleSevenDaySoftPct);
        Assert.Equal(85, dto.ThrottleSevenDayHardPct);
    }

    [Fact]
    public async Task Per_bucket_stages_apply_independently_to_effective_slots()
    {
        await SetThresholdsAsync(80, 90);
        // Both buckets sit at 60%: past the 5h soft stage (45) but below every 7d stage (70/85).
        await SetPerBucketThrottleAsync(maxParallel: 3, fiveSoft: 45, fiveHard: 90, sevenSoft: 70, sevenHard: 85);

        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(60, null), new UsageBucket(60, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));

        var dto = await CreateBuilder(state, new UsageGateDecision(false, null)).BuildAsync();

        Assert.Equal(2, dto.EffectiveSlots);
    }

    [Fact]
    public async Task Throttled_slots_and_decisive_bucket_reported()
    {
        await SetThresholdsAsync(80, 90);
        await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);

        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(70, null), new UsageBucket(20, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));

        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.Equal(3, dto.ConfiguredSlots);
        Assert.Equal(1, dto.EffectiveSlots);
        Assert.Equal("five_hour", dto.ThrottleBucket);
    }

    [Fact]
    public async Task SevenDay_decisive_bucket_reported_when_higher()
    {
        await SetThresholdsAsync(80, 90);
        await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);

        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), new UsageBucket(70, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));

        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.Equal("seven_day", dto.ThrottleBucket);
    }

    [Fact]
    public async Task Not_throttled_reports_null_bucket_and_equal_slots()
    {
        await SetThresholdsAsync(80, 90);
        await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);

        var state = new UsageState();
        state.ReportSuccess(new UsageSnapshot(
            new UsageBucket(10, null), new UsageBucket(20, null), Array.Empty<UsageLimitRow>(), DateTime.UtcNow));

        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.Equal(3, dto.ConfiguredSlots);
        Assert.Equal(3, dto.EffectiveSlots);
        Assert.Null(dto.ThrottleBucket);
    }

    [Fact]
    public async Task No_snapshot_falls_back_to_configured_slots_without_throttling()
    {
        await SetThresholdsAsync(80, 90);
        await SetThrottleAsync(maxParallel: 3, softPct: 50, hardPct: 65);

        var state = new UsageState();
        var builder = CreateBuilder(state, new UsageGateDecision(false, null));

        var dto = await builder.BuildAsync();

        Assert.Equal(3, dto.ConfiguredSlots);
        Assert.Equal(3, dto.EffectiveSlots);
        Assert.Null(dto.ThrottleBucket);
    }
}
