using System.Net;
using System.Net.Http.Headers;
using System.Text;
using ClaudeDo.Worker.Usage;

namespace ClaudeDo.Worker.Tests.Usage;

/// <summary>
/// Tests for <see cref="ClaudeOAuthUsageClient"/> using a stubbed <see cref="HttpMessageHandler"/>
/// and a temp credentials file. No real network call and no real credentials file are touched.
/// </summary>
public sealed class ClaudeOAuthUsageClientTests : IDisposable
{
    // Real fixture from the Anthropic OAuth usage endpoint, trimmed to the relevant fields.
    private const string RealFixture = """
        {
          "five_hour": { "utilization": 30.0, "resets_at": "2026-08-05T12:59:59.62334+02:00", "limit_dollars": null },
          "seven_day": { "utilization": 57.0, "resets_at": "2026-08-06T10:59:59.62336+02:00" },
          "seven_day_opus": null, "seven_day_sonnet": null, "seven_day_oauth_apps": null,
          "extra_usage": { "is_enabled": false },
          "limits": [
            { "kind": "session",       "group": "session", "percent": 30, "severity": "normal", "resets_at": "2026-08-05T12:59:59.62334+02:00",  "scope": null, "is_active": false },
            { "kind": "weekly_all",    "group": "weekly",  "percent": 57, "severity": "normal", "resets_at": "2026-08-06T10:59:59.62336+02:00",  "scope": null, "is_active": true },
            { "kind": "weekly_scoped", "group": "weekly",  "percent": 14, "severity": "normal", "resets_at": "2026-08-06T10:59:59.623573+02:00", "scope": { "model": { "id": null, "display_name": "Fable" }, "surface": null }, "is_active": false }
          ],
          "spend": { "percent": 0, "enabled": false },
          "member_dashboard_available": false
        }
        """;

    private readonly string _credentialsPath = Path.Combine(Path.GetTempPath(), $"claudedo-usage-test-{Guid.NewGuid():N}.json");

    public void Dispose()
    {
        if (File.Exists(_credentialsPath))
            File.Delete(_credentialsPath);
    }

    private void WriteCredentials(string accessToken = "test-access-token") =>
        File.WriteAllText(_credentialsPath, $$"""{ "claudeAiOauth": { "accessToken": "{{accessToken}}" } }""");

    private sealed class StubHandler : HttpMessageHandler
    {
        public List<HttpRequestMessage> Requests { get; } = new();
        public HttpStatusCode ResponseStatus { get; set; } = HttpStatusCode.OK;
        public string ResponseBody { get; set; } = "{}";
        public RetryConditionHeaderValue? RetryAfter { get; set; }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
        {
            Requests.Add(request);
            var resp = new HttpResponseMessage(ResponseStatus)
            {
                Content = new StringContent(ResponseBody, Encoding.UTF8, "application/json"),
            };
            if (RetryAfter is not null) resp.Headers.RetryAfter = RetryAfter;
            return Task.FromResult(resp);
        }
    }

    private (ClaudeOAuthUsageClient Client, StubHandler Handler) Build()
    {
        var handler = new StubHandler();
        var http = new HttpClient(handler);
        return (new ClaudeOAuthUsageClient(http, _credentialsPath), handler);
    }

    [Fact]
    public async Task GetUsageAsync_ParsesRealFixture()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseBody = RealFixture;

        var snapshot = await client.GetUsageAsync();

        Assert.NotNull(snapshot.FiveHour);
        Assert.Equal(30, snapshot.FiveHour!.Utilization);
        Assert.NotNull(snapshot.SevenDay);
        Assert.Equal(57, snapshot.SevenDay!.Utilization);
        Assert.Equal(3, snapshot.Limits.Count);

        var scoped = snapshot.Limits[2];
        Assert.Equal("weekly_scoped", scoped.Kind);
        Assert.Equal("Fable", scoped.ScopeModelDisplayName);
    }

    [Fact]
    public async Task GetUsageAsync_SendsBearerTokenAndBetaHeader()
    {
        WriteCredentials("my-secret-token");
        var (client, handler) = Build();
        handler.ResponseBody = RealFixture;

        await client.GetUsageAsync();

        var request = handler.Requests[0];
        Assert.Equal("Bearer", request.Headers.Authorization!.Scheme);
        Assert.Equal("my-secret-token", request.Headers.Authorization!.Parameter);
        Assert.Contains("oauth-2025-04-20", request.Headers.GetValues("anthropic-beta"));
        Assert.Equal("https://api.anthropic.com/api/oauth/usage", request.RequestUri!.ToString());
    }

    [Fact]
    public async Task GetUsageAsync_MissingFiveHour_ReturnsNull()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseBody = """{ "seven_day": { "utilization": 12.0 } }""";

        var snapshot = await client.GetUsageAsync();

        Assert.Null(snapshot.FiveHour);
        Assert.NotNull(snapshot.SevenDay);
        Assert.Empty(snapshot.Limits);
    }

    [Fact]
    public async Task GetUsageAsync_NullFiveHour_ReturnsNull()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseBody = """{ "five_hour": null, "seven_day": null }""";

        var snapshot = await client.GetUsageAsync();

        Assert.Null(snapshot.FiveHour);
        Assert.Null(snapshot.SevenDay);
    }

    [Fact]
    public async Task GetUsageAsync_MissingLimits_ReturnsEmptyList()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseBody = "{}";

        var snapshot = await client.GetUsageAsync();

        Assert.Empty(snapshot.Limits);
    }

    [Fact]
    public async Task GetUsageAsync_GarbageBody_ThrowsWithoutLeakingToken()
    {
        WriteCredentials("super-secret-token");
        var (client, handler) = Build();
        handler.ResponseBody = "not json at all {{{";

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetUsageAsync());

        Assert.DoesNotContain("super-secret-token", ex.Message);
    }

    [Fact]
    public async Task GetUsageAsync_Http401_ThrowsWithoutLeakingToken()
    {
        WriteCredentials("super-secret-token");
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.Unauthorized;
        handler.ResponseBody = "unauthorized";

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetUsageAsync());

        Assert.DoesNotContain("super-secret-token", ex.Message);
    }

    [Fact]
    public async Task GetUsageAsync_MissingCredentialsFile_Throws()
    {
        var (client, _) = Build(); // credentials file was never written

        await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetUsageAsync());
    }

    [Fact]
    public async Task GetUsageAsync_NoAccessTokenInCredentials_Throws()
    {
        File.WriteAllText(_credentialsPath, """{ "claudeAiOauth": {} }""");
        var (client, _) = Build();

        await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetUsageAsync());
    }

    [Fact]
    public async Task GetUsageAsync_429_ThrowsRateLimitedWithRetryAfterDelta()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.TooManyRequests;
        handler.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(45));

        var ex = await Assert.ThrowsAsync<UsageRateLimitedException>(() => client.GetUsageAsync());

        Assert.Equal(TimeSpan.FromSeconds(45), ex.RetryAfter);
    }

    [Fact]
    public async Task GetUsageAsync_429_WithoutRetryAfter_HasNullRetryAfter()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.TooManyRequests;

        var ex = await Assert.ThrowsAsync<UsageRateLimitedException>(() => client.GetUsageAsync());

        Assert.Null(ex.RetryAfter);
        Assert.Contains("429", ex.Message);
    }

    [Fact]
    public async Task GetUsageAsync_429_WithPastRetryAfterDate_IgnoresIt()
    {
        WriteCredentials();
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.TooManyRequests;
        handler.RetryAfter = new RetryConditionHeaderValue(DateTimeOffset.UtcNow.AddMinutes(-5));

        var ex = await Assert.ThrowsAsync<UsageRateLimitedException>(() => client.GetUsageAsync());

        Assert.Null(ex.RetryAfter);
    }
}
