//! Implements the standard credential chain:
//! 1. Environment variables
//! 2. Web identity token from STS
//! 3. Credentials/config files
//! 4. ECS Container credentials, using AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
//! 5. EC2 instance profile credentials
const std = @import("std");
const builtin = @import("builtin");
const auth = @import("aws_authentication.zig");

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

pub const Profile = struct {
    /// Credential file. Defaults to AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials
    credential_file: ?[]const u8 = null,
    /// Config file. Defaults to AWS_CONFIG_FILE or ~/.aws/config
    config_file: ?[]const u8 = null,
    /// Config file. Defaults to AWS_PROFILE or default
    profile_name: ?[]const u8 = null,
};

pub const Options = struct {
    profile: Profile = .{},
};

pub fn getCredentials(allocator: std.mem.Allocator, options: Options) !auth.Credentials {
    if (try getEnvironmentCredentials(allocator)) |cred| {
        log.debug("Found credentials in environment. Access key: {s}", .{cred.access_key});
        return cred;
    }
    // Note that boto and Java disagree on where this fits in the order
    // GetWebIdentity is not currently implemented. The rest are tested and gtg
    // Note: Lambda just sets environment variables
    if (try getWebIdentityToken(allocator)) |cred| return cred;
    if (try getProfileCredentials(allocator, options.profile)) |cred| return cred;

    if (try getContainerCredentials(allocator)) |cred| return cred;
    // I don't think we need v1 at all?
    if (try getImdsv2Credentials(allocator)) |cred| return cred;
    return error.CredentialsNotFound;
}

fn getEnvironmentCredentials(allocator: std.mem.Allocator) !?auth.Credentials {
    const secret_key = (try getEnvironmentVariable(allocator, "AWS_SECRET_ACCESS_KEY")) orelse return null;
    defer allocator.free(secret_key); //yes, we're not zeroing. But then, the secret key is in an environment var anyway
    // Use cross-platform API (requires allocation)
    return auth.Credentials.init(
        allocator,
        (try getEnvironmentVariable(allocator, "AWS_ACCESS_KEY_ID")) orelse return null,
        try allocator.dupe(u8, secret_key),
        (try getEnvironmentVariable(allocator, "AWS_SESSION_TOKEN")) orelse
            try getEnvironmentVariable(allocator, "AWS_SECURITY_TOKEN"), // Security token is backward compat only
    );
}

fn getEnvironmentVariable(allocator: std.mem.Allocator, key: []const u8) !?[]const u8 {
    return std.process.getEnvVarOwned(allocator, key) catch |e| switch (e) {
        std.process.GetEnvVarOwnedError.EnvironmentVariableNotFound => return null,
        else => return e,
    };
}

fn getWebIdentityToken(allocator: std.mem.Allocator) !?auth.Credentials {
    _ = allocator;
    // This API does not require signing. We can just use zfetch to
    // shoot a raw request over.
    // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
    // https://github.com/boto/boto3/blob/85b975af30c408f93b654a21930218edd58336ad/docs/source/guide/credentials.rst#assume-role-with-web-identity-provider
    // https://github.com/aws/aws-sdk-java-v2/blob/master/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/WebIdentityTokenFileCredentialsProvider.java
    // TODO: implement
    return null;
}
fn getContainerCredentials(allocator: std.mem.Allocator) !?auth.Credentials {
    // A note on testing: The best way I have found to test this process is
    // the following. Setup an ECS Fargate cluster and create a task definition
    // with the command  ["/bin/bash","-c","while true; do sleep 10; done"].
    //
    // In the console, this would be represented as:
    //
    // /bin/bash,-c,while true; do sleep 10; done
    //
    // Then we run the task with ECS exec-command enabled. The cli for this
    // will look something like the following:
    //
    // aws ecs run-task --enable-execute-command \
    //   --cluster Fargate \
    //   --network-configuration "awsvpcConfiguration={subnets=[subnet-1f3f4278],securityGroups=[sg-0aab58c6b2bde2105],assignPublicIp=ENABLED}" \
    //   --launch-type FARGATE \
    //   --task-definition zig-demo:3
    //
    //   Of course, subnets and security groups will be different. Public
    //   IP is necessary or you won't be able to pull the image. I used
    //   AL2 from the ECR public image:
    //
    //   public.ecr.aws/amazonlinux/amazonlinux:latest
    //
    // With the task running, now we need to execute it. I used CloudShell
    // from the AWS console because everything is already installed and
    // configured, ymmv. You need AWS CLI v2 with the session manager extension.
    //
    // It's good to do a pre-flight check to make sure you can run the
    // execute command. I used this tool to do so:
    //
    // https://github.com/aws-containers/amazon-ecs-exec-checker
    //
    // A couple yellows were ok, but no red.
    //
    // From there, get your task id and Bob's your uncle:
    //
    // aws ecs execute-command --cluster Fargate --command "/bin/bash" --interactive --task ec65b4d9887b429cba5d45ec70a8afa1
    //
    // Compile code, copy to S3, install AWS CLI within the session, download
    // from s3 and run
    const container_relative_uri = (try getEnvironmentVariable(allocator, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) orelse return null;
    defer allocator.free(container_relative_uri);
    const container_uri = try std.fmt.allocPrint(allocator, "http://169.254.170.2{s}", .{container_relative_uri});
    defer allocator.free(container_uri);

    var empty_headers = std.http.Headers.init(allocator);
    defer empty_headers.deinit();
    var cl = std.http.Client{ .allocator = allocator };
    defer cl.deinit(); // I don't belive connection pooling would help much here as it's non-ssl and local
    var req = try cl.request(.GET, try std.Uri.parse(container_uri), empty_headers, .{});
    defer req.deinit();
    try req.start();
    try req.wait();
    if (req.response.status != .ok and req.response.status != .not_found) {
        log.warn("Bad status code received from container credentials endpoint: {}", .{@intFromEnum(req.response.status)});
        return null;
    }
    if (req.response.status == .not_found) return null;
    if (req.response.content_length == null or req.response.content_length.? == 0) return null;

    var resp_payload = try std.ArrayList(u8).initCapacity(allocator, req.response.content_length.?);
    defer resp_payload.deinit();
    try resp_payload.resize(req.response.content_length.?);
    var response_data = try resp_payload.toOwnedSlice();
    defer allocator.free(response_data);
    _ = try req.readAll(response_data);
    log.debug("Read {d} bytes from container credentials endpoint", .{response_data.len});
    if (response_data.len == 0) return null;

    const CredsResponse = struct {
        AccessKeyId: []const u8,
        Expiration: []const u8,
        RoleArn: []const u8,
        SecretAccessKey: []const u8,
        Token: []const u8,
    };
    const creds_response = blk: {
        const res = std.json.parseFromSlice(CredsResponse, allocator, response_data, .{}) catch |e| {
            log.err("Unexpected Json response from container credentials endpoint: {s}", .{response_data});
            log.err("Error parsing json: {}", .{e});
            if (@errorReturnTrace()) |trace| {
                std.debug.dumpStackTrace(trace.*);
            }

            return null;
        };
        break :blk res;
    };
    defer creds_response.deinit();

    return auth.Credentials.init(
        allocator,
        try allocator.dupe(u8, creds_response.value.AccessKeyId),
        try allocator.dupe(u8, creds_response.value.SecretAccessKey),
        try allocator.dupe(u8, creds_response.value.Token),
    );
}

fn getImdsv2Credentials(allocator: std.mem.Allocator) !?auth.Credentials {
    var token: ?[]u8 = null;
    defer if (token) |t| allocator.free(t);
    var cl = std.http.Client{ .allocator = allocator };
    defer cl.deinit(); // I don't belive connection pooling would help much here as it's non-ssl and local
    // Get token
    {
        var headers = std.http.Headers.init(allocator);
        defer headers.deinit();
        try headers.append("X-aws-ec2-metadata-token-ttl-seconds", "21600");
        var req = try cl.request(.PUT, try std.Uri.parse("http://169.254.169.254/latest/api/token"), headers, .{});
        defer req.deinit();
        try req.start();
        try req.wait();
        if (req.response.status != .ok) {
            log.warn("Bad status code received from IMDS v2: {}", .{@intFromEnum(req.response.status)});
            return null;
        }
        if (req.response.content_length == null or req.response.content_length == 0) {
            log.warn("Unexpected zero response from IMDS v2", .{});
            return null;
        }

        var resp_payload = try std.ArrayList(u8).initCapacity(allocator, req.response.content_length.?);
        defer resp_payload.deinit();
        try resp_payload.resize(req.response.content_length.?);
        token = try resp_payload.toOwnedSlice();
        errdefer if (token) |t| allocator.free(t);
        _ = try req.readAll(token.?);
    }
    std.debug.assert(token != null);
    log.debug("Got token from IMDSv2: {s}", .{token.?});
    const role_name = try getImdsRoleName(allocator, &cl, token.?);
    if (role_name == null) {
        log.info("No role is associated with this instance", .{});
        return null;
    }
    defer allocator.free(role_name.?);
    log.debug("Got role name '{s}'", .{role_name.?});
    return getImdsCredentials(allocator, &cl, role_name.?, token.?);
}

fn getImdsRoleName(allocator: std.mem.Allocator, client: *std.http.Client, imds_token: []u8) !?[]const u8 {
    //     {
    //   "Code" : "Success",
    //   "LastUpdated" : "2022-02-09T05:42:09Z",
    //   "InstanceProfileArn" : "arn:aws:iam::550620852718:instance-profile/ec2-dev",
    //   "InstanceProfileId" : "AIPAYAM4POHXCFNKZ7HU2"
    // }
    var headers = std.http.Headers.init(allocator);
    defer headers.deinit();
    try headers.append("X-aws-ec2-metadata-token", imds_token);

    var req = try client.request(.GET, try std.Uri.parse("http://169.254.169.254/latest/meta-data/iam/info"), headers, .{});
    defer req.deinit();

    try req.start();
    try req.wait();

    if (req.response.status != .ok and req.response.status != .not_found) {
        log.warn("Bad status code received from IMDS iam endpoint: {}", .{@intFromEnum(req.response.status)});
        return null;
    }
    if (req.response.status == .not_found) return null;
    if (req.response.content_length == null or req.response.content_length.? == 0) {
        log.warn("Unexpected empty response from IMDS endpoint post token", .{});
        return null;
    }
    // TODO: This is all stupid. We can just allocate a freaking array and be done
    var resp_payload = try std.ArrayList(u8).initCapacity(allocator, req.response.content_length.?);
    defer resp_payload.deinit();
    try resp_payload.resize(req.response.content_length.?);
    // TODO: This feels safer, but can we avoid this?
    const resp = try resp_payload.toOwnedSlice();
    defer allocator.free(resp);
    _ = try req.readAll(resp);

    const ImdsResponse = struct {
        Code: []const u8,
        LastUpdated: []const u8,
        InstanceProfileArn: []const u8,
        InstanceProfileId: []const u8,
    };
    const imds_response = std.json.parseFromSlice(ImdsResponse, allocator, resp, .{}) catch |e| {
        log.err("Unexpected Json response from IMDS endpoint: {s}", .{resp});
        log.err("Error parsing json: {}", .{e});
        if (@errorReturnTrace()) |trace| {
            std.debug.dumpStackTrace(trace.*);
        }
        return null;
    };
    defer imds_response.deinit();

    const role_arn = imds_response.value.InstanceProfileArn;
    const first_slash = std.mem.indexOf(u8, role_arn, "/"); // I think this is valid
    if (first_slash == null) {
        log.err("Could not find role name in arn '{s}'", .{role_arn});
        return null;
    }
    return try allocator.dupe(u8, role_arn[first_slash.? + 1 ..]);
}

/// Note - this internal function assumes zfetch is initialized prior to use
fn getImdsCredentials(allocator: std.mem.Allocator, client: *std.http.Client, role_name: []const u8, imds_token: []u8) !?auth.Credentials {
    var headers = std.http.Headers.init(allocator);
    defer headers.deinit();
    try headers.append("X-aws-ec2-metadata-token", imds_token);

    const url = try std.fmt.allocPrint(allocator, "http://169.254.169.254/latest/meta-data/iam/security-credentials/{s}/", .{role_name});
    defer allocator.free(url);

    var req = try client.request(.GET, try std.Uri.parse(url), headers, .{});
    defer req.deinit();

    try req.start();
    try req.wait();

    if (req.response.status != .ok and req.response.status != .not_found) {
        log.warn("Bad status code received from IMDS role endpoint: {}", .{@intFromEnum(req.response.status)});
        return null;
    }
    if (req.response.status == .not_found) return null;
    if (req.response.content_length == null or req.response.content_length.? == 0) {
        log.warn("Unexpected empty response from IMDS role endpoint", .{});
        return null;
    }
    // TODO: This is still stupid
    var resp_payload = try std.ArrayList(u8).initCapacity(allocator, req.response.content_length.?);
    defer resp_payload.deinit();
    try resp_payload.resize(req.response.content_length.?);
    const resp = try resp_payload.toOwnedSlice();
    defer allocator.free(resp);
    _ = try req.readAll(resp);

    // log.debug("Read {d} bytes from imds v2 credentials endpoint", .{read});
    const ImdsResponse = struct {
        Code: []const u8,
        LastUpdated: []const u8,
        Type: []const u8,
        AccessKeyId: []const u8,
        SecretAccessKey: []const u8,
        Token: []const u8,
        Expiration: []const u8,
    };
    const imds_response = std.json.parseFromSlice(ImdsResponse, allocator, resp, .{}) catch |e| {
        log.err("Unexpected Json response from IMDS endpoint: {s}", .{resp});
        log.err("Error parsing json: {}", .{e});
        if (@errorReturnTrace()) |trace| {
            std.debug.dumpStackTrace(trace.*);
        }

        return null;
    };
    defer imds_response.deinit();

    const ret = auth.Credentials.init(
        allocator,
        try allocator.dupe(u8, imds_response.value.AccessKeyId),
        try allocator.dupe(u8, imds_response.value.SecretAccessKey),
        try allocator.dupe(u8, imds_response.value.Token),
    );
    log.debug("IMDSv2 credentials found. Access key: {s}", .{ret.access_key});

    return ret;

    // {
    //   "Code" : "Success",
    //   "LastUpdated" : "2022-02-08T23:49:02Z",
    //   "Type" : "AWS-HMAC",
    //   "AccessKeyId" : "ASEXAMPLE",
    //   "SecretAccessKey" : "example",
    //   "Token" : "IQoJb==",
    //   "Expiration" : "2022-02-09T06:02:23Z"
    // }

}

fn getProfileCredentials(allocator: std.mem.Allocator, options: Profile) !?auth.Credentials {
    var default_path: ?[]const u8 = null;
    defer if (default_path) |p| allocator.free(p);

    const creds_file_path = try filePath(
        allocator,
        options.credential_file,
        "AWS_SHARED_CREDENTIALS_FILE",
        default_path,
        "credentials",
    );
    defer allocator.free(creds_file_path.evaluated_path);
    default_path = default_path orelse creds_file_path.home;
    const config_file_path = try filePath(
        allocator,
        options.credential_file,
        "AWS_SHARED_CREDENTIALS_FILE",
        default_path,
        "config",
    );
    defer allocator.free(config_file_path.evaluated_path);
    default_path = default_path orelse config_file_path.home;

    // Get active profile
    const profile = (try getEnvironmentVariable(allocator, "AWS_PROFILE")) orelse
        try allocator.dupe(u8, "default");
    defer allocator.free(profile);
    log.debug("Looking for file credentials using profile '{s}'", .{profile});
    log.debug("Checking credentials file: {s}", .{creds_file_path.evaluated_path});
    const credentials_file = std.fs.openFileAbsolute(creds_file_path.evaluated_path, .{}) catch null;
    defer if (credentials_file) |f| f.close();
    // It's much more likely that we'll find credentials in the credentials file
    // so we'll try that first
    const creds_file_creds = try credsForFile(allocator, credentials_file, profile);
    var conf_file_creds = PartialCredentials{};
    if (creds_file_creds.access_key == null or creds_file_creds.secret_key == null) {
        log.debug("Checking config file: {s}", .{config_file_path.evaluated_path});
        const config_file = std.fs.openFileAbsolute(creds_file_path.evaluated_path, .{}) catch null;
        defer if (config_file) |f| f.close();
        conf_file_creds = try credsForFile(allocator, config_file, profile);
    }
    const access_key = keyFrom(allocator, creds_file_creds.access_key, conf_file_creds.access_key);
    const secret_key = keyFrom(allocator, creds_file_creds.secret_key, conf_file_creds.secret_key);
    defer if (secret_key) |k| allocator.free(k);

    if (access_key == null or secret_key == null) {
        const partial = access_key != null or secret_key != null;
        if (partial) {
            log.warn("Could not find credentials in file (partial creds detected)", .{});
        } else {
            log.info("Could not find credentials in file", .{});
        }
        if (access_key) |k| allocator.free(k);
        return null;
    }
    log.debug("Got full credentials from filesystem. Access key: {s}", .{access_key.?});
    return auth.Credentials.init(
        allocator,
        access_key.?,
        try allocator.dupe(u8, secret_key.?),
        null,
    );
}

fn keyFrom(allocator: std.mem.Allocator, priority_1: ?[]const u8, priority_2: ?[]const u8) ?[]const u8 {
    if (priority_1) |p1| {
        if (priority_2) |p2| allocator.free(p2);
        return p1;
    }
    return priority_2;
}

// We could conceivably find different portions of the creds in different
// files, so let's be super-loose here
const PartialCredentials = struct {
    access_key: ?[]const u8 = null,
    secret_key: ?[]const u8 = null,
};
fn credsForFile(allocator: std.mem.Allocator, file: ?std.fs.File, profile: []const u8) !PartialCredentials {
    if (file == null) return PartialCredentials{};
    const text = try file.?.readToEndAlloc(allocator, std.math.maxInt(usize));
    defer allocator.free(text);
    const partial_creds = try credsForText(text, profile);
    var ak: ?[]const u8 = null;
    if (partial_creds.access_key) |k|
        ak = try allocator.dupe(u8, k);
    var sk: ?[]const u8 = null;
    if (partial_creds.secret_key) |k|
        sk = try allocator.dupe(u8, k);

    return PartialCredentials{
        .access_key = ak,
        .secret_key = sk,
    };
}

const LineIterator = struct {
    text: []const u8,
    inx: usize = 0,

    const Self = @This();

    pub fn init(text: []const u8) Self {
        return .{
            .text = text,
            .inx = 0,
        };
    }

    pub fn next(self: *Self) ?[]const u8 {
        if (self.inx >= self.text.len) return null;
        var current = self.inx;
        var start = self.inx;
        for (self.text[self.inx..], 0..) |c, i| {
            if (c == '\n') {
                // log.debug("got \\n: {d}", .{i});
                current += i + 1;
                break;
            }
        }
        // log.debug("{d}:{d}", .{ current, self.inx });
        if (current != self.inx) {
            self.inx = current;
        } else { // no \n found
            self.inx = self.text.len + 1; // add one to capture the last char in return
        }
        return self.text[start .. self.inx - 1];
    }
};
fn credsForText(text: []const u8, profile: []const u8) !PartialCredentials {
    var lines = LineIterator.init(text);
    var is_in_profile = false;
    var was_in_profile = false;
    var done = false;
    var creds: [2]?[]const u8 = [_]?[]const u8{ null, null };

    while (lines.next()) |line| {
        // log.debug("line: {s}", .{line});
        var section_start: ?usize = 0;
        for (line, 0..) |c, i| {
            switch (c) {
                '#' => break,
                '[' => section_start = i + 1,
                ']' => {
                    if (section_start) |s| {
                        const current_section = line[s..i];
                        log.debug("got section: {s}", .{current_section});
                        is_in_profile = std.ascii.eqlIgnoreCase(current_section, profile);
                        if (was_in_profile and !is_in_profile) {
                            done = true;
                            break;
                        }

                        was_in_profile = is_in_profile;
                        break; // got what we need from this line
                    }
                },
                '=' => {
                    if (!is_in_profile) continue;
                    const key = std.mem.trim(u8, line[0..i], " \t"); // other whitespace we care about?
                    log.debug("got key: {s}", .{key});
                    for (&[_][]const u8{
                        "aws_access_key_id",
                        "aws_secret_access_key",
                    }, 0..) |needle, inx| {
                        if (std.ascii.eqlIgnoreCase(key, needle)) {
                            // TODO: Trim this out
                            creds[inx] = trim(line[i + 1 ..]);
                        }
                    }
                },
                else => {},
            }
        }
        if (done) {
            log.debug("no longer in target section: bailing", .{});
            break;
        }
    }
    log.debug("done parsing text", .{});
    return PartialCredentials{
        .access_key = creds[0],
        .secret_key = creds[1],
    };
}

fn trim(text: []const u8) []const u8 {
    // "  myval # yo";
    var start: ?usize = null;
    var end: ?usize = null;

    for (text, 0..) |c, i| switch (c) {
        ' ', '\t' => {},
        '#' => return trimmed(text, start, end),
        else => {
            if (start == null) start = i;
            end = i + 1;
        },
    };
    return trimmed(text, start, end);
}

fn trimmed(text: []const u8, start: ?usize, end: ?usize) []const u8 {
    if (start == null) return "";
    if (end == null) return text[start.?..];
    return text[start.?..end.?];
}

fn filePath(
    allocator: std.mem.Allocator,
    specified_path: ?[]const u8,
    env_var_name: []const u8,
    config_dir: ?[]const u8,
    config_file_name: []const u8,
) !EvaluatedPath {
    if (specified_path) |p| return EvaluatedPath{ .evaluated_path = try allocator.dupe(u8, p) };
    // Not specified. Check environment variable, otherwise, hard coded default
    if (try getEnvironmentVariable(allocator, env_var_name)) |v| return EvaluatedPath{ .evaluated_path = v };

    // Not in environment variable either. Go fish
    return try getDefaultPath(allocator, config_dir, ".aws", config_file_name);
}

const EvaluatedPath = struct {
    home: ?[]const u8 = null,
    evaluated_path: []const u8,
};
fn getDefaultPath(allocator: std.mem.Allocator, home_dir: ?[]const u8, dir: []const u8, file: []const u8) !EvaluatedPath {
    var home = home_dir orelse try getHomeDir(allocator);
    log.debug("Home directory: {s}", .{home});
    const rc = try std.fs.path.join(allocator, &[_][]const u8{ home, dir, file });
    log.debug("Path evaluated as: {s}", .{rc});
    return EvaluatedPath{ .home = home, .evaluated_path = rc };
}

fn getHomeDir(allocator: std.mem.Allocator) ![]const u8 {
    switch (builtin.os.tag) {
        .windows => {
            var dir_path_ptr: [*:0]u16 = undefined;
            // https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid
            const FOLDERID_Profile = std.os.windows.GUID.parse("{5E6C858F-0E22-4760-9AFE-EA3317B67173}");
            switch (std.os.windows.shell32.SHGetKnownFolderPath(
                &FOLDERID_Profile,
                std.os.windows.KF_FLAG_CREATE,
                null,
                &dir_path_ptr,
            )) {
                std.os.windows.S_OK => {
                    defer std.os.windows.ole32.CoTaskMemFree(@as(*anyopaque, @ptrCast(dir_path_ptr)));
                    const global_dir = std.unicode.utf16leToUtf8Alloc(allocator, std.mem.sliceTo(dir_path_ptr, 0)) catch |err| switch (err) {
                        error.UnexpectedSecondSurrogateHalf => return error.HomeDirUnavailable,
                        error.ExpectedSecondSurrogateHalf => return error.HomeDirUnavailable,
                        error.DanglingSurrogateHalf => return error.HomeDirUnavailable,
                        error.OutOfMemory => return error.OutOfMemory,
                    };
                    return global_dir;
                    // defer allocator.free(global_dir);
                },
                std.os.windows.E_OUTOFMEMORY => return error.OutOfMemory,
                else => return error.HomeDirUnavailable,
            }
        },
        .macos, .linux, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris => {
            const home_dir = std.os.getenv("HOME") orelse {
                // TODO look in /etc/passwd
                return error.HomeDirUnavailable;
            };
            return allocator.dupe(u8, home_dir);
        },
        // Code from https://github.com/ziglang/zig/blob/9f9f215305389c08a21730859982b68bf2681932/lib/std/fs/get_app_data_dir.zig
        // be_user_settings magic number is probably different for home directory
        // .haiku => {
        //     var dir_path_ptr: [*:0]u8 = undefined;
        //     // TODO look into directory_which
        //     const be_user_settings = 0xbbe;
        //     const rc = os.system.find_directory(be_user_settings, -1, true, dir_path_ptr, 1);
        //     const settings_dir = try allocator.dupeZ(u8, mem.sliceTo(dir_path_ptr, 0));
        //     defer allocator.free(settings_dir);
        //     switch (rc) {
        //         0 => return fs.path.join(allocator, &[_][]const u8{ settings_dir, appname }),
        //         else => return error.AppDataDirUnavailable,
        //     }
        // },
        else => @compileError("Unsupported OS"),
    }
}

test "filePath" {
    const allocator = std.testing.allocator;
    std.testing.log_level = .debug;
    log.debug("\n", .{});
    const path = try filePath(allocator, null, "NOTHING", null, "hello");
    defer allocator.free(path.evaluated_path);
    defer allocator.free(path.home.?);
    try std.testing.expect(path.evaluated_path.len > 10);
    try std.testing.expectEqualStrings("hello", path.evaluated_path[path.evaluated_path.len - 5 ..]);
    try std.testing.expect(path.home != null);
}

test "ini to creds" {
    std.testing.log_level = .debug;
    log.debug("\n", .{});
    const partial_creds = try credsForText(
        \\
        \\# Amazon Web Services Credentials File used by AWS CLI, SDKs, and tools
        \\# This file was created by the AWS Toolkit for Visual Studio Code extension.
        \\#
        \\# Your AWS credentials are represented by access keys associated with IAM users.
        \\# For information about how to create and manage AWS access keys for a user, see:
        \\# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
        \\#
        \\# This credential file can store multiple access keys by placing each one in a
        \\# named "profile". For information about how to change the access keys in a
        \\# profile or to add a new profile with a different access key, see:
        \\# https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
        \\#
        \\[other_section]
        \\access_key_id = NOTYOURACCESSKEY
        \\
        \\  [default]
        \\  # The access key and secret key pair identify your account and grant access to AWS.
        \\aws_access_key_id = AKIDEXAMPLE # access key
        \\
        \\[another_section]
        \\access_key_id = NOTYOURACCESSKEYEITHER
    , "default");

    try std.testing.expect(partial_creds.access_key != null);
    try std.testing.expectEqualStrings("AKIDEXAMPLE", partial_creds.access_key.?);
    try std.testing.expect(partial_creds.secret_key == null);
}
