using System.Net;
using System.Text;
using System.Text.Json;
using ClaudeDo.Worker.Online;

namespace ClaudeDo.Worker.Tests.Online;

/// <summary>
/// Tests for <see cref="OnlineInboxApiClient"/> using a stubbed <see cref="HttpMessageHandler"/>.
/// </summary>
public sealed class OnlineInboxApiClientTests
{
    private sealed class StubHandler : HttpMessageHandler
    {
        public List<HttpRequestMessage> Requests { get; } = new();
        public HttpStatusCode ResponseStatus { get; set; } = HttpStatusCode.OK;
        public string ResponseBody { 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"),
            };
            return Task.FromResult(resp);
        }
    }

    private static (OnlineInboxApiClient Client, StubHandler Handler) Build(string? token = "test-token")
    {
        var handler = new StubHandler();
        // Base address carries a path segment (/api) — requests must nest under it,
        // so the client uses relative paths without a leading slash.
        var http = new HttpClient(handler) { BaseAddress = new Uri("https://inbox.example.com/api/") };
        var auth = new StaticTokenAuthProvider(token);
        return (new OnlineInboxApiClient(http, auth), handler);
    }

    // ---- PutListsAsync ----

    [Fact]
    public async Task PutListsAsync_UsesCorrectVerbAndPath()
    {
        var (client, handler) = Build();
        await client.PutListsAsync([new RemoteList("id1", "List 1")]);

        Assert.Single(handler.Requests);
        Assert.Equal(HttpMethod.Put, handler.Requests[0].Method);
        Assert.Equal("/api/lists", handler.Requests[0].RequestUri!.AbsolutePath);
    }

    [Fact]
    public async Task PutListsAsync_AttachesBearerToken()
    {
        var (client, handler) = Build("my-bearer");
        await client.PutListsAsync([]);

        Assert.Equal("Bearer", handler.Requests[0].Headers.Authorization!.Scheme);
        Assert.Equal("my-bearer", handler.Requests[0].Headers.Authorization!.Parameter);
    }

    // ---- GetUnimportedTasksAsync ----

    [Fact]
    public async Task GetUnimportedTasksAsync_UsesGetWithQueryParam()
    {
        var (client, handler) = Build();
        handler.ResponseBody = "[]";
        await client.GetUnimportedTasksAsync();

        Assert.Equal(HttpMethod.Get, handler.Requests[0].Method);
        Assert.Contains("imported=false", handler.Requests[0].RequestUri!.Query);
    }

    [Fact]
    public async Task GetUnimportedTasksAsync_DeserializesResponse()
    {
        var (client, handler) = Build();
        var tasks = new[]
        {
            new { id = "t1", listId = "l1", title = "Title", description = (string?)null, createdAt = DateTimeOffset.UtcNow },
        };
        handler.ResponseBody = JsonSerializer.Serialize(tasks);

        var result = await client.GetUnimportedTasksAsync();

        Assert.Single(result);
        Assert.Equal("t1", result[0].Id);
        Assert.Equal("l1", result[0].ListId);
        Assert.Equal("Title", result[0].Title);
    }

    // ---- MarkImportedAsync ----

    [Fact]
    public async Task MarkImportedAsync_UsesPostAndCorrectPath()
    {
        var (client, handler) = Build();
        await client.MarkImportedAsync("task-id-123");

        Assert.Equal(HttpMethod.Post, handler.Requests[0].Method);
        Assert.Equal("/api/tasks/task-id-123/imported", handler.Requests[0].RequestUri!.AbsolutePath);
    }

    // ---- PutMirrorAsync ----

    [Fact]
    public async Task PutMirrorAsync_UsesPutAndCorrectPath()
    {
        var (client, handler) = Build();
        await client.PutMirrorAsync([new MirrorTask("id1", "l1", "T", null)]);

        Assert.Equal(HttpMethod.Put, handler.Requests[0].Method);
        Assert.Equal("/api/tasks/mirror", handler.Requests[0].RequestUri!.AbsolutePath);
    }

    // ---- 401 handling ----

    [Fact]
    public async Task NonSuccessResponse_Throws_OnlineInboxException()
    {
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.Unauthorized;
        handler.ResponseBody = "Unauthorized";

        var ex = await Assert.ThrowsAsync<OnlineInboxException>(
            () => client.PutListsAsync([]));

        Assert.Equal(401, ex.StatusCode);
    }

    [Fact]
    public async Task Unauthorized_RetriesOnceWithForcedRefresh_ThenThrowsMissingRole()
    {
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.Unauthorized;
        handler.ResponseBody = "Unauthorized";

        var ex = await Assert.ThrowsAsync<OnlineInboxException>(
            () => client.PutListsAsync([]));

        Assert.Equal(401, ex.StatusCode);
        Assert.Equal(OnlineInboxApiClient.MissingRoleMessage, ex.Message);
        // Original attempt + one forced-refresh retry.
        Assert.Equal(2, handler.Requests.Count);
    }

    [Fact]
    public async Task Unauthorized_ThenSuccessOnRetry_Succeeds()
    {
        var handler = new SequenceHandler(HttpStatusCode.Unauthorized, HttpStatusCode.OK);
        var http = new HttpClient(handler) { BaseAddress = new Uri("https://inbox.example.com/api/") };
        var client = new OnlineInboxApiClient(http, new StaticTokenAuthProvider("test-token"));

        await client.PutListsAsync([]);

        Assert.Equal(2, handler.Requests.Count);
    }

    private sealed class SequenceHandler : HttpMessageHandler
    {
        private readonly Queue<HttpStatusCode> _statuses;
        public List<HttpRequestMessage> Requests { get; } = new();

        public SequenceHandler(params HttpStatusCode[] statuses) => _statuses = new(statuses);

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
        {
            Requests.Add(request);
            var status = _statuses.Count > 0 ? _statuses.Dequeue() : HttpStatusCode.OK;
            return Task.FromResult(new HttpResponseMessage(status)
            {
                Content = new StringContent("[]", Encoding.UTF8, "application/json"),
            });
        }
    }

    [Fact]
    public async Task ServerError_Throws_OnlineInboxException_WithStatusCode()
    {
        var (client, handler) = Build();
        handler.ResponseStatus = HttpStatusCode.InternalServerError;
        handler.ResponseBody = "error";

        var ex = await Assert.ThrowsAsync<OnlineInboxException>(
            () => client.GetUnimportedTasksAsync());

        Assert.Equal(500, ex.StatusCode);
    }

    // ---- No token ----

    [Fact]
    public async Task NoToken_SendsRequestWithoutAuthHeader()
    {
        var (client, handler) = Build(token: null);
        await client.PutListsAsync([]);

        Assert.Null(handler.Requests[0].Headers.Authorization);
    }

    // ---- URL validation ----

    [Fact]
    public void ValidateBaseUrl_AcceptsHttps()
    {
        OnlineInboxApiClient.ValidateBaseUrl("https://example.com");
    }

    [Fact]
    public void ValidateBaseUrl_AcceptsLoopback()
    {
        OnlineInboxApiClient.ValidateBaseUrl("http://127.0.0.1:5000");
        OnlineInboxApiClient.ValidateBaseUrl("http://localhost:5000");
    }

    [Fact]
    public void ValidateBaseUrl_Rejects_HttpNonLoopback()
    {
        Assert.Throws<InvalidOperationException>(
            () => OnlineInboxApiClient.ValidateBaseUrl("http://example.com"));
    }

    [Fact]
    public void ValidateBaseUrl_Rejects_Empty()
    {
        Assert.Throws<InvalidOperationException>(
            () => OnlineInboxApiClient.ValidateBaseUrl(""));
    }
}
