const std = @import("std");

const string = []const u8;

const RED = "\x1b[31;1m";
const GREEN = "\x1b[32;1m";
const CYAN = "\x1b[36;1m";
const WHITE = "\x1b[37;1m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";

pub const Tester = struct {
    pass: std.ArrayList(Expectation),
    fail: std.ArrayList(Expectation),
    allocator: std.mem.Allocator,

    pub fn t(allocator: std.mem.Allocator) Tester {
        return Tester{
            .allocator = allocator,
            .pass = std.ArrayList(Expectation).init(allocator),
            .fail = std.ArrayList(Expectation).init(allocator),
        };
    }

    pub const Expectation = struct {
        expected: string,
        result: string,
        source: std.builtin.SourceLocation,

        pub fn init(expected: string, result: string, src: std.builtin.SourceLocation) Expectation {
            return Expectation{
                .expected = expected,
                .result = result,
                .source = src,
            };
        }
        const PADDING = 0;
        pub fn print(self: *const @This()) void {
            var pad = &([_]u8{' '} ** PADDING);
            var stderr = std.io.getStdErr();

            stderr.writeAll(RESET) catch unreachable;
            stderr.writeAll(pad) catch unreachable;
            stderr.writeAll(DIM) catch unreachable;
            std.fmt.format(stderr.writer(), "{s}:{d}:{d}", .{ self.source.file, self.source.line, self.source.column }) catch unreachable;
            stderr.writeAll(RESET) catch unreachable;
            stderr.writeAll("\n") catch unreachable;

            stderr.writeAll(pad) catch unreachable;
            stderr.writeAll("Expected: ") catch unreachable;
            stderr.writeAll(RESET) catch unreachable;
            stderr.writeAll(GREEN) catch unreachable;
            std.fmt.format(stderr.writer(), "\"{s}\"", .{self.expected}) catch unreachable;
            stderr.writeAll(GREEN) catch unreachable;
            stderr.writeAll(RESET) catch unreachable;

            stderr.writeAll("\n") catch unreachable;
            stderr.writeAll(pad) catch unreachable;
            stderr.writeAll("Received: ") catch unreachable;
            stderr.writeAll(RESET) catch unreachable;
            stderr.writeAll(RED) catch unreachable;
            std.fmt.format(stderr.writer(), "\"{s}\"", .{self.result}) catch unreachable;
            stderr.writeAll(RED) catch unreachable;
            stderr.writeAll(RESET) catch unreachable;
            stderr.writeAll("\n") catch unreachable;
        }
        const strings = @import("../string_immutable.zig");
        pub fn evaluate_outcome(self: *const @This()) Outcome {
            if (strings.eql(self.expected, self.result)) {
                return .pass;
            } else {
                return .fail;
            }
        }
    };

    pub const Outcome = enum {
        pass,
        fail,
    };

    pub inline fn expect(tester: *Tester, expected: string, result: string, src: std.builtin.SourceLocation) bool {
        var expectation = Expectation.init(expected, result, src);
        switch (expectation.evaluate_outcome()) {
            .pass => {
                tester.pass.append(expectation) catch unreachable;
                return true;
            },
            .fail => {
                tester.fail.append(expectation) catch unreachable;
                return false;
            },
        }
    }

    const ReportType = enum {
        none,
        pass,
        fail,
        some_fail,

        pub fn init(tester: *Tester) ReportType {
            if (tester.fail.items.len == 0 and tester.pass.items.len == 0) {
                return .none;
            } else if (tester.fail.items.len == 0) {
                return .pass;
            } else if (tester.pass.items.len == 0) {
                return .fail;
            } else {
                return .some_fail;
            }
        }
    };

    pub fn report(tester: *Tester, src: std.builtin.SourceLocation) void {
        var stderr = std.io.getStdErr();

        if (tester.fail.items.len > 0) {
            std.fmt.format(stderr.writer(), "\n\n", .{}) catch unreachable;
        }

        for (tester.fail.items) |item| {
            item.print();
            std.fmt.format(stderr.writer(), "\n", .{}) catch unreachable;
        }

        switch (ReportType.init(tester)) {
            .none => {
                std.log.info("No expectations.\n\n", .{});
            },
            .pass => {
                std.fmt.format(stderr.writer(), "{s}All {d} expectations passed.{s}\n", .{ GREEN, tester.pass.items.len, GREEN }) catch unreachable;
                std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
                std.testing.expect(true) catch std.debug.panic("Test failure", .{});
            },
            .fail => {
                std.fmt.format(stderr.writer(), "{s}All {d} expectations failed.{s}\n\n", .{ RED, tester.fail.items.len, RED }) catch unreachable;
                std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
                std.testing.expect(false) catch std.debug.panic("Test failure", .{});
            },
            .some_fail => {
                std.fmt.format(stderr.writer(), "{s}{d} failed{s} and {s}{d} passed{s} of {d} expectations{s}\n\n", .{
                    RED,
                    tester.fail.items.len,
                    RED ++ RESET,
                    GREEN,
                    tester.pass.items.len,
                    GREEN ++ RESET,
                    tester.fail.items.len + tester.pass.items.len,
                    RESET,
                }) catch unreachable;
                std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
                std.testing.expect(false) catch std.debug.panic("Test failure in {s}: {s}:{d}:{d}", .{ src.fn_name, src.file, src.line, src.column });
            },
        }
    }
};
