const std = @import("std");

pub fn build(b: *std.Build) void {
    const test_step = b.step("test", "Test it");
    b.default_step = test_step;

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "create-file",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    {
        const run_random_with_sideeffects_first = b.addRunArtifact(exe);
        run_random_with_sideeffects_first.setName("run with side-effects (first)");
        run_random_with_sideeffects_first.has_side_effects = true;

        const run_random_with_sideeffects_second = b.addRunArtifact(exe);
        run_random_with_sideeffects_second.setName("run with side-effects (second)");
        run_random_with_sideeffects_second.has_side_effects = true;

        // ensure that "second" runs after "first"
        run_random_with_sideeffects_second.step.dependOn(&run_random_with_sideeffects_first.step);

        const first_output = run_random_with_sideeffects_first.addOutputFileArg("a.txt");
        const second_output = run_random_with_sideeffects_second.addOutputFileArg("a.txt");

        const expect_uncached_dependencies = CheckOutputCaching.init(b, false, &.{ first_output, second_output });
        test_step.dependOn(&expect_uncached_dependencies.step);

        const expect_unequal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output });
        test_step.dependOn(&expect_unequal_output.step);

        const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} });
        test_step.dependOn(&check_first_output.step);
        const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} });
        test_step.dependOn(&check_second_output.step);
    }

    {
        const run_random_without_sideeffects_1 = b.addRunArtifact(exe);
        run_random_without_sideeffects_1.setName("run without side-effects (A)");

        const run_random_without_sideeffects_2 = b.addRunArtifact(exe);
        run_random_without_sideeffects_2.setName("run without side-effects (B)");

        run_random_without_sideeffects_2.step.dependOn(&run_random_without_sideeffects_1.step);

        const first_output = run_random_without_sideeffects_1.addOutputFileArg("a.txt");
        const second_output = run_random_without_sideeffects_2.addOutputFileArg("a.txt");

        const expect_cached_dependencies = CheckOutputCaching.init(b, true, &.{second_output});
        test_step.dependOn(&expect_cached_dependencies.step);

        const expect_equal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output });
        test_step.dependOn(&expect_equal_output.step);

        const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} });
        test_step.dependOn(&check_first_output.step);
        const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} });
        test_step.dependOn(&check_second_output.step);
    }
}

const CheckOutputCaching = struct {
    step: std.Build.Step,
    expect_caching: bool,

    pub fn init(owner: *std.Build, expect_caching: bool, output_paths: []const std.Build.LazyPath) *CheckOutputCaching {
        const check = owner.allocator.create(CheckOutputCaching) catch @panic("OOM");
        check.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = "check output caching",
                .owner = owner,
                .makeFn = make,
            }),
            .expect_caching = expect_caching,
        };
        for (output_paths) |output_path| {
            output_path.addStepDependencies(&check.step);
        }
        return check;
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
        const check: *CheckOutputCaching = @fieldParentPtr("step", step);

        for (step.dependencies.items) |dependency| {
            if (check.expect_caching) {
                if (dependency.result_cached) continue;
                return step.fail("expected '{s}' step to be cached, but it was not", .{dependency.name});
            } else {
                if (!dependency.result_cached) continue;
                return step.fail("expected '{s}' step to not be cached, but it was", .{dependency.name});
            }
        }
    }
};

const CheckPathEquality = struct {
    step: std.Build.Step,
    expected_equality: bool,
    output_paths: []const std.Build.LazyPath,

    pub fn init(owner: *std.Build, expected_equality: bool, output_paths: []const std.Build.LazyPath) *CheckPathEquality {
        const check = owner.allocator.create(CheckPathEquality) catch @panic("OOM");
        check.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = "check output path equality",
                .owner = owner,
                .makeFn = make,
            }),
            .expected_equality = expected_equality,
            .output_paths = owner.allocator.dupe(std.Build.LazyPath, output_paths) catch @panic("OOM"),
        };
        for (output_paths) |output_path| {
            output_path.addStepDependencies(&check.step);
        }
        return check;
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
        const check: *CheckPathEquality = @fieldParentPtr("step", step);
        std.debug.assert(check.output_paths.len != 0);
        for (check.output_paths[0 .. check.output_paths.len - 1], check.output_paths[1..]) |a, b| {
            try std.testing.expectEqual(check.expected_equality, std.mem.eql(u8, a.getPath(step.owner), b.getPath(step.owner)));
        }
    }
};
