const builtin = @import("builtin");
const native_endian = builtin.cpu.arch.endian();

const std = @import("std");
const http = std.http;
const mem = std.mem;
const net = std.Io.net;
const Io = std.Io;
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectError = std.testing.expectError;

test "content length reader state update" {
    var in = Io.Reader.fixed("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nHello!\r\nHTTP/1.1 200 OK\r\n\r\n");
    var reader: http.Reader = .{
        .in = &in,
        .interface = undefined,
        .state = .ready,
        .max_head_len = 1024,
    };

    _ = try reader.receiveHead();
    var body: [6]u8 = undefined;
    _ = try reader.bodyReader(&.{}, .none, body.len).readSliceAll(&body);
    try expectEqual(.ready, reader.state);
    _ = try reader.receiveHead();

    in.seek = 0;
    _ = try reader.receiveHead();
    try reader.bodyReader(&.{}, .none, body.len).discardAll(body.len);
    try expectEqual(.ready, reader.state);
    _ = try reader.receiveHead();
}

test "trailers" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;
    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [1024]u8 = undefined;
            var send_buffer: [1024]u8 = undefined;
            var remaining: usize = 1;
            while (remaining != 0) : (remaining -= 1) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                var connection_br = stream.reader(io, &recv_buffer);
                var connection_bw = stream.writer(io, &send_buffer);
                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);

                try expectEqual(.ready, server.reader.state);
                var request = try server.receiveHead();
                try serve(&request);
                try expectEqual(.ready, server.reader.state);
            }
        }

        fn serve(request: *http.Server.Request) !void {
            try expectEqualStrings(request.head.target, "/trailer");

            var response = try request.respondStreaming(&.{}, .{});
            try response.writer.writeAll("Hello, ");
            try response.flush();
            try response.writer.writeAll("World!\n");
            try response.flush();
            try response.endChunked(.{
                .trailers = &.{
                    .{ .name = "X-Checksum", .value = "aaaa" },
                },
            });
        }
    });
    defer test_server.destroy();

    const gpa = std.testing.allocator;

    var client: http.Client = .{ .allocator = gpa, .io = io };
    defer client.deinit();

    const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{
        test_server.port(),
    });
    defer gpa.free(location);
    const uri = try std.Uri.parse(location);

    {
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&.{});

        {
            var it = response.head.iterateHeaders();
            const header = it.next().?;
            try expectEqualStrings("transfer-encoding", header.name);
            try expectEqualStrings("chunked", header.value);
            try expectEqual(null, it.next());
        }

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);

        {
            var it = response.iterateTrailers();
            const header = it.next().?;
            try expectEqualStrings("X-Checksum", header.name);
            try expectEqualStrings("aaaa", header.value);
            try expectEqual(null, it.next());
        }
    }

    // connection has been kept alive
    try expect(client.connection_pool.free_len == 1);
}

test "HTTP server handles a chunked transfer coding request" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;
    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [8192]u8 = undefined;
            var send_buffer: [500]u8 = undefined;
            var stream = try net_server.accept(io);
            defer stream.close(io);

            var connection_br = stream.reader(io, &recv_buffer);
            var connection_bw = stream.writer(io, &send_buffer);
            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
            var request = try server.receiveHead();

            try expect(request.head.transfer_encoding == .chunked);

            var buf: [128]u8 = undefined;
            var br = try request.readerExpectContinue(&.{});
            const n = try br.readSliceShort(&buf);
            try expectEqualStrings("ABCD", buf[0..n]);

            try request.respond("message from server!\n", .{
                .extra_headers = &.{
                    .{ .name = "content-type", .value = "text/plain" },
                },
                .keep_alive = false,
            });
        }
    });
    defer test_server.destroy();

    const request_bytes =
        "POST / HTTP/1.1\r\n" ++
        "Content-Type: text/plain\r\n" ++
        "Transfer-Encoding: chunked\r\n" ++
        "\r\n" ++
        "1\r\n" ++
        "A\r\n" ++
        "1\r\n" ++
        "B\r\n" ++
        "2\r\n" ++
        "CD\r\n" ++
        "0\r\n" ++
        "\r\n";

    const host_name: net.HostName = try .init("127.0.0.1");
    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
    defer stream.close(io);
    var stream_writer = stream.writer(io, &.{});
    try stream_writer.interface.writeAll(request_bytes);

    const gpa = std.testing.allocator;
    const expected_response =
        "HTTP/1.1 200 OK\r\n" ++
        "connection: close\r\n" ++
        "content-length: 21\r\n" ++
        "content-type: text/plain\r\n" ++
        "\r\n" ++
        "message from server!\n";
    var stream_reader = stream.reader(io, &.{});
    const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1));
    defer gpa.free(response);
    try expectEqualStrings(expected_response, response);
}

test "echo content server" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;
    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [1024]u8 = undefined;
            var send_buffer: [100]u8 = undefined;

            accept: while (!test_server.shutting_down) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                var connection_br = stream.reader(io, &recv_buffer);
                var connection_bw = stream.writer(io, &send_buffer);
                var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);

                while (http_server.reader.state == .ready) {
                    var request = http_server.receiveHead() catch |err| switch (err) {
                        error.HttpConnectionClosing => continue :accept,
                        else => |e| return e,
                    };
                    if (mem.eql(u8, request.head.target, "/end")) {
                        return request.respond("", .{ .keep_alive = false });
                    }
                    if (request.head.expect) |expect_header_value| {
                        if (mem.eql(u8, expect_header_value, "garbage")) {
                            try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{}));
                            request.head.expect = null;
                            try request.respond("", .{
                                .keep_alive = false,
                                .status = .expectation_failed,
                            });
                            continue;
                        }
                    }
                    handleRequest(&request) catch |err| {
                        // This message helps the person troubleshooting determine whether
                        // output comes from the server thread or the client thread.
                        std.debug.print("handleRequest failed with '{s}'\n", .{@errorName(err)});
                        return err;
                    };
                }
            }
        }

        fn handleRequest(request: *http.Server.Request) !void {
            //std.debug.print("server received {s} {s} {s}\n", .{
            //    @tagName(request.head.method),
            //    @tagName(request.head.version),
            //    request.head.target,
            //});

            try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
            try expectEqualStrings("text/plain", request.head.content_type.?);

            // head strings expire here
            const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited);
            defer std.testing.allocator.free(body);

            try expectEqualStrings("Hello, World!\n", body);

            var response = try request.respondStreaming(&.{}, .{
                .content_length = switch (request.head.transfer_encoding) {
                    .chunked => null,
                    .none => len: {
                        try expectEqual(14, request.head.content_length.?);
                        break :len 14;
                    },
                },
            });
            try response.flush(); // Test an early flush to send the HTTP headers before the body.
            const w = &response.writer;
            try w.writeAll("Hello, ");
            try w.writeAll("World!\n");
            try response.end();
            //std.debug.print("  server finished responding\n", .{});
        }
    });
    defer test_server.destroy();

    {
        var client: http.Client = .{ .allocator = std.testing.allocator, .io = io };
        defer client.deinit();

        try echoTests(&client, test_server.port());
    }
}

test "Server.Request.respondStreaming non-chunked, unknown content-length" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;

    if (builtin.os.tag == .windows) {
        // https://github.com/ziglang/zig/issues/21457
        return error.SkipZigTest;
    }

    // In this case, the response is expected to stream until the connection is
    // closed, indicating the end of the body.
    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [1000]u8 = undefined;
            var send_buffer: [500]u8 = undefined;
            var remaining: usize = 1;
            while (remaining != 0) : (remaining -= 1) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                var connection_br = stream.reader(io, &recv_buffer);
                var connection_bw = stream.writer(io, &send_buffer);
                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);

                try expectEqual(.ready, server.reader.state);
                var request = try server.receiveHead();
                try expectEqualStrings(request.head.target, "/foo");
                var buf: [30]u8 = undefined;
                var response = try request.respondStreaming(&buf, .{
                    .respond_options = .{
                        .transfer_encoding = .none,
                    },
                });
                const w = &response.writer;
                for (0..500) |i| {
                    try w.print("{d}, ah ha ha!\n", .{i});
                }
                try w.flush();
                try response.end();
                try expectEqual(.closing, server.reader.state);
            }
        }
    });
    defer test_server.destroy();

    const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
    const host_name: net.HostName = try .init("127.0.0.1");
    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
    defer stream.close(io);
    var stream_writer = stream.writer(io, &.{});
    try stream_writer.interface.writeAll(request_bytes);

    var stream_reader = stream.reader(io, &.{});
    const gpa = std.testing.allocator;
    const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
    defer gpa.free(response);

    var expected_response = std.array_list.Managed(u8).init(gpa);
    defer expected_response.deinit();

    try expected_response.appendSlice("HTTP/1.1 200 OK\r\nconnection: close\r\n\r\n");

    {
        var total: usize = 0;
        for (0..500) |i| {
            var buf: [30]u8 = undefined;
            const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
            try expected_response.appendSlice(line);
            total += line.len;
        }
        try expectEqual(7390, total);
    }

    try expectEqualStrings(expected_response.items, response);
}

test "receiving arbitrary http headers from the client" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;

    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [666]u8 = undefined;
            var send_buffer: [777]u8 = undefined;
            var remaining: usize = 1;
            while (remaining != 0) : (remaining -= 1) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                var connection_br = stream.reader(io, &recv_buffer);
                var connection_bw = stream.writer(io, &send_buffer);
                var server = http.Server.init(&connection_br.interface, &connection_bw.interface);

                try expectEqual(.ready, server.reader.state);
                var request = try server.receiveHead();
                try expectEqualStrings("/bar", request.head.target);
                var it = request.iterateHeaders();
                {
                    const header = it.next().?;
                    try expectEqualStrings("CoNneCtIoN", header.name);
                    try expectEqualStrings("close", header.value);
                    try expect(!it.is_trailer);
                }
                {
                    const header = it.next().?;
                    try expectEqualStrings("aoeu", header.name);
                    try expectEqualStrings("asdf", header.value);
                    try expect(!it.is_trailer);
                }
                try request.respond("", .{});
            }
        }
    });
    defer test_server.destroy();

    const request_bytes = "GET /bar HTTP/1.1\r\n" ++
        "CoNneCtIoN:close\r\n" ++
        "aoeu:  asdf \r\n" ++
        "\r\n";
    const host_name: net.HostName = try .init("127.0.0.1");
    var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
    defer stream.close(io);
    var stream_writer = stream.writer(io, &.{});
    try stream_writer.interface.writeAll(request_bytes);

    var stream_reader = stream.reader(io, &.{});
    const gpa = std.testing.allocator;
    const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
    defer gpa.free(response);

    var expected_response = std.array_list.Managed(u8).init(gpa);
    defer expected_response.deinit();

    try expected_response.appendSlice("HTTP/1.1 200 OK\r\n");
    try expected_response.appendSlice("connection: close\r\n");
    try expected_response.appendSlice("content-length: 0\r\n\r\n");
    try expectEqualStrings(expected_response.items, response);
}

test "general client/server API coverage" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;

    if (builtin.os.tag == .windows) {
        // This test was never passing on Windows.
        return error.SkipZigTest;
    }

    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [1024]u8 = undefined;
            var send_buffer: [100]u8 = undefined;

            outer: while (!test_server.shutting_down) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                var connection_br = stream.reader(io, &recv_buffer);
                var connection_bw = stream.writer(io, &send_buffer);
                var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);

                while (http_server.reader.state == .ready) {
                    var request = http_server.receiveHead() catch |err| switch (err) {
                        error.HttpConnectionClosing => continue :outer,
                        else => |e| return e,
                    };

                    try handleRequest(&request, net_server.socket.address.getPort());
                }
            }
        }

        fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
            const log = std.log.scoped(.server);
            const gpa = std.testing.allocator;

            log.info("{t} {t} {s}", .{ request.head.method, request.head.version, request.head.target });
            const target = try gpa.dupe(u8, request.head.target);
            defer gpa.free(target);

            const reader = (try request.readerExpectContinue(&.{}));
            const body = try reader.allocRemaining(gpa, .unlimited);
            defer gpa.free(body);

            if (mem.startsWith(u8, target, "/get")) {
                var response = try request.respondStreaming(&.{}, .{
                    .content_length = if (mem.find(u8, target, "?chunked") == null)
                        14
                    else
                        null,
                    .respond_options = .{
                        .extra_headers = &.{
                            .{ .name = "content-type", .value = "text/plain" },
                        },
                    },
                });
                const w = &response.writer;
                try w.writeAll("Hello, ");
                try w.writeAll("World!\n");
                try response.end();
                // Writing again would cause an assertion failure.
            } else if (mem.startsWith(u8, target, "/large")) {
                var response = try request.respondStreaming(&.{}, .{
                    .content_length = 14 * 1024 + 14 * 10,
                });

                try response.flush(); // Test an early flush to send the HTTP headers before the body.

                const w = &response.writer;

                var i: u32 = 0;
                while (i < 5) : (i += 1) {
                    try w.writeAll("Hello, World!\n");
                }

                var vec: [1][]const u8 = .{"Hello, World!\n"};
                try w.writeSplatAll(&vec, 1024);

                i = 0;
                while (i < 5) : (i += 1) {
                    try w.writeAll("Hello, World!\n");
                }

                try response.end();
            } else if (mem.eql(u8, target, "/redirect/1")) {
                var response = try request.respondStreaming(&.{}, .{
                    .respond_options = .{
                        .status = .found,
                        .extra_headers = &.{
                            .{ .name = "location", .value = "../../get" },
                        },
                    },
                });

                const w = &response.writer;
                try w.writeAll("Hello, ");
                try w.writeAll("Redirected!\n");
                try response.end();
            } else if (mem.eql(u8, target, "/redirect/2")) {
                try request.respond("Hello, Redirected!\n", .{
                    .status = .found,
                    .extra_headers = &.{
                        .{ .name = "location", .value = "/redirect/1" },
                    },
                });
            } else if (mem.eql(u8, target, "/redirect/3")) {
                const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{
                    listen_port,
                });
                defer gpa.free(location);

                try request.respond("Hello, Redirected!\n", .{
                    .status = .found,
                    .extra_headers = &.{
                        .{ .name = "location", .value = location },
                    },
                });
            } else if (mem.eql(u8, target, "/redirect/4")) {
                try request.respond("Hello, Redirected!\n", .{
                    .status = .found,
                    .extra_headers = &.{
                        .{ .name = "location", .value = "/redirect/3" },
                    },
                });
            } else if (mem.eql(u8, target, "/redirect/5")) {
                try request.respond("Hello, Redirected!\n", .{
                    .status = .found,
                    .extra_headers = &.{
                        .{ .name = "location", .value = "/%2525" },
                    },
                });
            } else if (mem.eql(u8, target, "/%2525")) {
                try request.respond("Encoded redirect successful!\n", .{});
            } else if (mem.eql(u8, target, "/redirect/invalid")) {
                const invalid_port = try getUnusedTcpPort();
                const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
                defer gpa.free(location);

                try request.respond("", .{
                    .status = .found,
                    .extra_headers = &.{
                        .{ .name = "location", .value = location },
                    },
                });
            } else if (mem.eql(u8, target, "/empty")) {
                try request.respond("", .{
                    .extra_headers = &.{
                        .{ .name = "empty", .value = "" },
                    },
                });
            } else {
                try request.respond("", .{ .status = .not_found });
            }
        }

        fn getUnusedTcpPort() !u16 {
            const addr = try net.IpAddress.parse("127.0.0.1", 0);
            var s = try addr.listen(io, .{});
            defer s.deinit(io);
            return s.socket.address.getPort();
        }
    });
    defer test_server.destroy();

    const log = std.log.scoped(.client);

    const gpa = std.testing.allocator;
    var client: http.Client = .{ .allocator = gpa, .io = io };
    defer client.deinit();

    const port = test_server.port();

    { // read content-length response
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try expectEqualStrings("text/plain", response.head.content_type.?);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // read large content-length response
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/large", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // send head request and not read chunked
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.HEAD, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try expectEqualStrings("text/plain", response.head.content_type.?);
        try expectEqual(14, response.head.content_length.?);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // read chunked response
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try expectEqualStrings("text/plain", response.head.content_type.?);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // send head request and not read chunked
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.HEAD, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try expectEqualStrings("text/plain", response.head.content_type.?);
        try expect(response.head.transfer_encoding == .chunked);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // read content-length response with connection close
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{
            .keep_alive = false,
        });
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try expectEqualStrings("text/plain", response.head.content_type.?);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been closed
    try expect(client.connection_pool.free_len == 0);

    { // handle empty header field value
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/empty", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{
            .extra_headers = &.{
                .{ .name = "empty", .value = "" },
            },
        });
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        try std.testing.expectEqual(.ok, response.head.status);

        var it = response.head.iterateHeaders();
        {
            const header = it.next().?;
            try expect(!it.is_trailer);
            try expectEqualStrings("content-length", header.name);
            try expectEqualStrings("0", header.value);
        }

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("", body);

        {
            const header = it.next().?;
            try expect(!it.is_trailer);
            try expectEqualStrings("empty", header.name);
            try expectEqualStrings("", header.value);
        }
        try expectEqual(null, it.next());
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // relative redirect
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/1", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // redirect from root
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // absolute redirect
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/3", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // too many redirects
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/4", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        if (req.receiveHead(&redirect_buffer)) |_| {
            return error.TestFailed;
        } else |err| switch (err) {
            error.TooManyHttpRedirects => {},
            else => return err,
        }
    }

    { // redirect to encoded url
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Encoded redirect successful!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // check client without segfault by connection error after redirection
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/invalid", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        log.info("{s}", .{location});
        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        const result = req.receiveHead(&redirect_buffer);

        // a proxy without an upstream is likely to return a 5xx status.
        if (client.http_proxy == null) {
            try expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
        }
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
}

test "Server streams both reading and writing" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;

    const test_server = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [1024]u8 = undefined;
            var send_buffer: [777]u8 = undefined;

            var stream = try net_server.accept(io);
            defer stream.close(io);

            var connection_br = stream.reader(io, &recv_buffer);
            var connection_bw = stream.writer(io, &send_buffer);
            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
            var request = try server.receiveHead();
            var read_buffer: [100]u8 = undefined;
            var br = try request.readerExpectContinue(&read_buffer);
            var response = try request.respondStreaming(&.{}, .{
                .respond_options = .{
                    .transfer_encoding = .none, // Causes keep_alive=false
                },
            });
            const w = &response.writer;

            while (true) {
                try response.flush();
                const buf = br.peekGreedy(1) catch |err| switch (err) {
                    error.EndOfStream => break,
                    error.ReadFailed => |e| return e,
                };
                br.toss(buf.len);
                for (buf) |*b| b.* = std.ascii.toUpper(b.*);
                try w.writeAll(buf);
            }
            try response.end();
        }
    });
    defer test_server.destroy();

    var client: http.Client = .{
        .allocator = std.testing.allocator,
        .io = io,
    };
    defer client.deinit();

    var redirect_buffer: [555]u8 = undefined;
    var req = try client.request(.POST, .{
        .scheme = "http",
        .host = .{ .raw = "127.0.0.1" },
        .port = test_server.port(),
        .path = .{ .percent_encoded = "/" },
    }, .{});
    defer req.deinit();

    req.transfer_encoding = .chunked;
    var body_writer = try req.sendBody(&.{});
    var response = try req.receiveHead(&redirect_buffer);

    try body_writer.writer.writeAll("one ");
    try body_writer.writer.writeAll("fish");
    try body_writer.end();

    const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .unlimited);
    defer std.testing.allocator.free(body);

    try expectEqualStrings("ONE FISH", body);
}

fn echoTests(client: *http.Client, port: u16) !void {
    const gpa = std.testing.allocator;
    var location_buffer: [100]u8 = undefined;

    { // send content-length request
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.POST, uri, .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = "text/plain" },
            },
        });
        defer req.deinit();

        req.transfer_encoding = .{ .content_length = 14 };

        var body_writer = try req.sendBody(&.{});
        try body_writer.writer.writeAll("Hello, ");
        try body_writer.writer.writeAll("World!\n");
        try body_writer.end();

        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // send chunked request
        const uri = try std.Uri.parse(try std.fmt.bufPrint(
            &location_buffer,
            "http://127.0.0.1:{d}/echo-content",
            .{port},
        ));

        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.POST, uri, .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = "text/plain" },
            },
        });
        defer req.deinit();

        req.transfer_encoding = .chunked;

        var body_writer = try req.sendBody(&.{});
        try body_writer.writer.writeAll("Hello, ");
        try body_writer.writer.writeAll("World!\n");
        try body_writer.end();

        var response = try req.receiveHead(&redirect_buffer);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    // connection has been kept alive
    try expect(client.http_proxy != null or client.connection_pool.free_len == 1);

    { // Client.fetch()

        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
        defer gpa.free(location);

        var body: std.Io.Writer.Allocating = .init(gpa);
        defer body.deinit();
        try body.ensureUnusedCapacity(64);

        const res = try client.fetch(.{
            .location = .{ .url = location },
            .method = .POST,
            .payload = "Hello, World!\n",
            .extra_headers = &.{
                .{ .name = "content-type", .value = "text/plain" },
            },
            .response_writer = &body.writer,
        });
        try expectEqual(.ok, res.status);
        try expectEqualStrings("Hello, World!\n", body.written());
    }

    { // expect: 100-continue
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.POST, uri, .{
            .extra_headers = &.{
                .{ .name = "expect", .value = "100-continue" },
                .{ .name = "content-type", .value = "text/plain" },
            },
        });
        defer req.deinit();

        req.transfer_encoding = .chunked;

        var body_writer = try req.sendBody(&.{});
        try body_writer.writer.writeAll("Hello, ");
        try body_writer.writer.writeAll("World!\n");
        try body_writer.end();

        var response = try req.receiveHead(&redirect_buffer);
        try expectEqual(.ok, response.head.status);

        const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("Hello, World!\n", body);
    }

    { // expect: garbage
        const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
        defer gpa.free(location);
        const uri = try std.Uri.parse(location);

        var redirect_buffer: [1024]u8 = undefined;
        var req = try client.request(.POST, uri, .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = "text/plain" },
                .{ .name = "expect", .value = "garbage" },
            },
        });
        defer req.deinit();

        req.transfer_encoding = .chunked;

        var body_writer = try req.sendBody(&.{});
        try body_writer.flush();
        var response = try req.receiveHead(&redirect_buffer);
        try expectEqual(.expectation_failed, response.head.status);
        _ = try response.reader(&.{}).discardRemaining();
    }
}

const TestServer = struct {
    io: Io,
    shutting_down: bool,
    server_thread: std.Thread,
    net_server: net.Server,

    fn destroy(self: *@This()) void {
        const io = self.io;
        self.shutting_down = true;
        var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch
            @panic("shutdown failure");
        stream.close(io);

        self.server_thread.join();
        self.net_server.deinit(io);
        std.testing.allocator.destroy(self);
    }

    fn port(self: @This()) u16 {
        return self.net_server.socket.address.getPort();
    }
};

fn createTestServer(io: Io, S: type) !*TestServer {
    if (builtin.single_threaded) return error.SkipZigTest;
    if (builtin.zig_backend == .stage2_llvm and native_endian == .big) {
        // https://github.com/ziglang/zig/issues/13782
        return error.SkipZigTest;
    }

    const address = try net.IpAddress.parse("127.0.0.1", 0);

    const gpa = std.testing.allocator;

    const test_server = try gpa.create(TestServer);
    errdefer gpa.destroy(test_server);

    var net_server = try address.listen(io, .{ .reuse_address = true });
    errdefer net_server.deinit(io);

    // populate `test_server` first so `S.run` can use it
    test_server.* = .{
        .io = io,
        .net_server = net_server,
        .shutting_down = false,
        .server_thread = undefined, // set below
    };

    test_server.server_thread = try .spawn(.{}, S.run, .{test_server});
    errdefer comptime unreachable;

    return test_server;
}

test "redirect to different connection" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;
    const test_server_new = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [888]u8 = undefined;
            var send_buffer: [777]u8 = undefined;

            var stream = try net_server.accept(io);
            defer stream.close(io);

            var connection_br = stream.reader(io, &recv_buffer);
            var connection_bw = stream.writer(io, &send_buffer);
            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
            var request = try server.receiveHead();
            try expectEqualStrings(request.head.target, "/ok");
            try request.respond("good job, you pass", .{});
        }
    });
    defer test_server_new.destroy();

    const global = struct {
        var other_port: ?u16 = null;
    };
    global.other_port = test_server_new.port();

    const test_server_orig = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [999]u8 = undefined;
            var send_buffer: [100]u8 = undefined;

            var stream = try net_server.accept(io);
            defer stream.close(io);

            var loc_buf: [50]u8 = undefined;
            const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{
                global.other_port.?,
            });

            var connection_br = stream.reader(io, &recv_buffer);
            var connection_bw = stream.writer(io, &send_buffer);
            var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
            var request = try server.receiveHead();
            try expectEqualStrings(request.head.target, "/help");
            try request.respond("", .{
                .status = .found,
                .extra_headers = &.{
                    .{ .name = "location", .value = new_loc },
                },
            });
        }
    });
    defer test_server_orig.destroy();

    const gpa = std.testing.allocator;

    var client: http.Client = .{
        .allocator = gpa,
        .io = io,
    };
    defer client.deinit();

    var loc_buf: [100]u8 = undefined;
    const location = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/help", .{
        test_server_orig.port(),
    });
    const uri = try std.Uri.parse(location);

    {
        var redirect_buffer: [666]u8 = undefined;
        var req = try client.request(.GET, uri, .{});
        defer req.deinit();

        try req.sendBodiless();
        var response = try req.receiveHead(&redirect_buffer);
        var reader = response.reader(&.{});

        const body = try reader.allocRemaining(gpa, .unlimited);
        defer gpa.free(body);

        try expectEqualStrings("good job, you pass", body);
    }
}

test "boot failed connections from the pool" {
    if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257
    if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806

    const io = std.testing.io;
    const gpa = std.testing.allocator;

    const test_server_orig = try createTestServer(io, struct {
        fn run(test_server: *TestServer) anyerror!void {
            const net_server = &test_server.net_server;
            var recv_buffer: [500]u8 = undefined;
            var send_buffer: [500]u8 = undefined;

            accept: while (!test_server.shutting_down) {
                var stream = try net_server.accept(io);
                defer stream.close(io);

                for (0..2) |i| {
                    var connection_br = stream.reader(io, &recv_buffer);
                    var connection_bw = stream.writer(io, &send_buffer);
                    var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
                    var request = server.receiveHead() catch |err| switch (err) {
                        error.HttpConnectionClosing => continue :accept,
                        else => |e| return e,
                    };
                    if (i == 0) try request.respond("hello", .{});
                }
            }
        }
    });
    defer test_server_orig.destroy();

    var client: http.Client = .{
        .allocator = gpa,
        .io = io,
    };
    defer client.deinit();

    var loc_buf: [100]u8 = undefined;
    const location = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/", .{
        test_server_orig.port(),
    });
    const uri = try std.Uri.parse(location);

    {
        const response = try client.fetch(.{ .location = .{ .uri = uri } });
        try expectEqual(.ok, response.status);
    }
    {
        try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
    }
    {
        const response = try client.fetch(.{ .location = .{ .uri = uri } });
        try expectEqual(.ok, response.status);
    }
    {
        try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
    }
}
