// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const URL = @import("browser/URL.zig");

const TestHTTPServer = @This();

shutdown: std.atomic.Value(bool),
listener: ?std.net.Server,
handler: Handler,

const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;

pub fn init(handler: Handler) TestHTTPServer {
    return .{
        .shutdown = .init(true),
        .listener = null,
        .handler = handler,
    };
}

pub fn deinit(self: *TestHTTPServer) void {
    self.listener = null;
}

pub fn stop(self: *TestHTTPServer) void {
    self.shutdown.store(true, .release);
    if (self.listener) |*listener| {
        switch (@import("builtin").target.os.tag) {
            .linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},
            else => std.posix.close(listener.stream.handle),
        }
    }
}

pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
    const address = try std.net.Address.parseIp("127.0.0.1", 9582);

    self.listener = try address.listen(.{ .reuse_address = true });
    var listener = &self.listener.?;
    self.shutdown.store(false, .release);

    wg.finish();

    while (true) {
        const conn = listener.accept() catch |err| {
            if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
                return;
            }
            return err;
        };
        const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
        thrd.detach();
    }
}

fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
    defer conn.stream.close();

    var req_buf: [2048]u8 = undefined;
    var conn_reader = conn.stream.reader(&req_buf);
    var conn_writer = conn.stream.writer(&req_buf);

    var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);

    while (true) {
        var req = http_server.receiveHead() catch |err| switch (err) {
            error.ReadFailed => continue,
            error.HttpConnectionClosing => continue,
            else => {
                std.debug.print("Test HTTP Server error: {}\n", .{err});
                return err;
            },
        };

        self.handler(&req) catch |err| {
            std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
            try req.respond("server error", .{ .status = .internal_server_error });
            return;
        };
    }
}

pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
    var url_buf: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&url_buf);
    var unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
    if (std.mem.indexOfScalarPos(u8, unescaped_file_path, 0, '?')) |pos| {
        unescaped_file_path = unescaped_file_path[0..pos];
    }
    var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {
        error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
        else => return err,
    };
    defer file.close();

    const stat = try file.stat();
    var send_buffer: [4096]u8 = undefined;

    var res = try req.respondStreaming(&send_buffer, .{
        .content_length = stat.size,
        .respond_options = .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = getContentType(unescaped_file_path) },
            },
        },
    });

    var read_buffer: [4096]u8 = undefined;
    var reader = file.reader(&read_buffer);
    _ = try res.writer.sendFileAll(&reader, .unlimited);
    try res.writer.flush();
    try res.end();
}

fn getContentType(file_path: []const u8) []const u8 {
    if (std.mem.endsWith(u8, file_path, ".js")) {
        return "application/json";
    }

    if (std.mem.endsWith(u8, file_path, ".GB2312.html")) {
        return "text/html; charset=GB2312";
    }

    if (std.mem.endsWith(u8, file_path, ".html")) {
        return "text/html";
    }

    if (std.mem.endsWith(u8, file_path, ".htm")) {
        return "text/html";
    }

    if (std.mem.endsWith(u8, file_path, ".xml")) {
        // some wpt tests do this
        return "text/xml";
    }

    if (std.mem.endsWith(u8, file_path, ".mjs")) {
        // mjs are ECMAScript modules
        return "application/json";
    }

    std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
    return "text/html";
}
