const std = @import("std");
const testing = std.testing;

const c = @cImport({
    @cInclude("stdlib.h");
});

const src = @import("src");
const utils = src.utils;

fn isScript(data: []const u8) bool {
    return data.len >= 2 and data[0] == '#' and data[1] == '!';
}

test "embedded rsync binary creation" {
    const allocator = testing.allocator;

    // Ensure the override doesn't influence this test.
    _ = c.unsetenv("ML_RSYNC_PATH");

    // Force embedded fallback by clearing PATH for this process so system rsync isn't found.
    const old_path_z = std.posix.getenv("PATH");
    if (c.setenv("PATH", "", 1) != 0) return error.Unexpected;
    defer {
        if (old_path_z) |z| {
            _ = c.setenv("PATH", z, 1);
        } else {
            _ = c.unsetenv("PATH");
        }
    }

    var embedded_rsync = utils.rsync_embedded.EmbeddedRsync{ .allocator = allocator };

    const rsync_path = try embedded_rsync.extractRsyncBinary();
    defer allocator.free(rsync_path);

    try testing.expect(rsync_path.len > 0);

    // File exists and is executable
    const file = try std.fs.openFileAbsolute(rsync_path, .{});
    defer file.close();
    const stat = try file.stat();
    try testing.expect(stat.mode & 0o111 != 0);

    // Contents match embedded payload
    const data = try file.readToEndAlloc(allocator, 1024 * 1024 * 4);
    defer allocator.free(data);

    var digest: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(data, &digest, .{});
    const embedded_digest = utils.rsync_embedded_binary.rsyncBinarySha256();
    try testing.expect(std.mem.eql(u8, &digest, &embedded_digest));

    // Sanity: if we're not using the release binary, it should be the placeholder script.
    if (!utils.rsync_embedded_binary.USING_RELEASE_BINARY) {
        try testing.expect(isScript(data));
    }
}

test "embedded rsync honors ML_RSYNC_PATH override" {
    const allocator = testing.allocator;

    // Use a known executable. We don't execute it; we only verify the override is returned.
    const override_path = "/bin/sh";
    try testing.expect(std.fs.openFileAbsolute(override_path, .{}) != error.FileNotFound);

    if (c.setenv("ML_RSYNC_PATH", override_path, 1) != 0) return error.Unexpected;
    defer _ = c.unsetenv("ML_RSYNC_PATH");

    var embedded_rsync = utils.rsync_embedded.EmbeddedRsync{ .allocator = allocator };
    const rsync_path = try embedded_rsync.extractRsyncBinary();
    defer allocator.free(rsync_path);

    try testing.expectEqualStrings(override_path, rsync_path);
}

test "embedded rsync prefers system rsync when available" {
    const allocator = testing.allocator;

    _ = c.unsetenv("ML_RSYNC_PATH");

    // Find rsync on PATH (simple scan). If not present, skip.
    const path_z = std.posix.getenv("PATH") orelse return;
    const path = std.mem.sliceTo(path_z, 0);

    var sys_rsync: ?[]u8 = null;
    defer if (sys_rsync) |p| allocator.free(p);

    var it = std.mem.splitScalar(u8, path, ':');
    while (it.next()) |dir| {
        if (dir.len == 0) continue;
        const candidate = std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir, "rsync" }) catch return;
        errdefer allocator.free(candidate);

        const file = std.fs.openFileAbsolute(candidate, .{}) catch {
            allocator.free(candidate);
            continue;
        };
        defer file.close();

        const st = file.stat() catch {
            allocator.free(candidate);
            continue;
        };
        if (st.mode & 0o111 == 0) {
            allocator.free(candidate);
            continue;
        }

        sys_rsync = candidate;
        break;
    }

    if (sys_rsync == null) return;

    var embedded_rsync = utils.rsync_embedded.EmbeddedRsync{ .allocator = allocator };
    const rsync_path = try embedded_rsync.extractRsyncBinary();
    defer allocator.free(rsync_path);

    try testing.expectEqualStrings(sys_rsync.?, rsync_path);
}
