const std = @import("std");
const StringHashMap = std.StringHashMap;
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const GlobalAllocator = std.testing.allocator;
const TailQueue = std.TailQueue;
////Imports////
///
/// The enumeration of types that the mocking framework supports. These include basic types like u8
/// and function types like fn () void.
///
////DataElementType////
///
/// A tagged union of all the data elements that the mocking framework can work with. This can be
/// expanded to add new types. This is needed as need a list of data that all have different types,
/// so this wraps the data into a union, (which is of one type) so can have a list of them.
/// When https://github.com/ziglang/zig/issues/383 and https://github.com/ziglang/zig/issues/2907
/// is done, can programitaclly create types for this. Can use a compile time block that loops
/// through the available basic types and create function types so don't have a long list.
///
////DataElement////
///
/// The type of actions that the mocking framework can perform.
///
const ActionType = enum {
    /// This will test the parameters passed to a function. It will test the correct types and
    /// value of each parameter. This is also used to return a specific value from a function so
    /// can test for returns from a function.
    TestValue,

    /// This action is to replace a function call to be mocked with another function the user
    /// chooses to be replaced. This will consume the function call. This will allow the user to
    /// check that the function is called once or multiple times by added a function to be mocked
    /// multiple times. This also allows the ability for a function to be mocked by different
    /// functions each time it is called.
    ConsumeFunctionCall,

    /// This is similar to the ConsumeFunctionCall action, but will call the mocked function
    /// repeatedly until the mocking is done.
    RepeatFunctionCall,

    // Other actions that could be used

    // This will check that a function isn't called.
    //NoFunctionCall

    // This is a generalisation of ConsumeFunctionCall and RepeatFunctionCall but can specify how
    // many times a function can be called.
    //FunctionCallN
};

///
/// This is a pair of action and data to be actioned on.
///
const Action = struct {
    action: ActionType,
    data: DataElement,
};

///
/// The type for a queue of actions using std.TailQueue.
///
const ActionList = TailQueue(Action);

///
/// The type for linking the function name to be mocked and the action list to be acted on.
///
const NamedActionMap = StringHashMap(ActionList);

///
/// The mocking framework.
///
/// Return: type
///     This returns a struct for adding and acting on mocked functions.
///
fn Mock() type {
    return struct {
        const Self = @This();

        /// The map of function name and action list.
        named_actions: NamedActionMap,

        ///
        /// Create a DataElement from data. This wraps data into a union. This allows the ability
        /// to have a list of different types.
        ///
        /// Arguments:
        ///     IN arg: anytype - The data, this can be a function or basic type value.
        ///
        /// Return: DataElement
        ///     A DataElement with the data wrapped.
        ///
        fn createDataElement(arg: anytype) DataElement {
            return switch (@TypeOf(arg)) {
            ////createDataElement////
            };
        }

        ///
        /// Get the enum that represents the type given.
        ///
        /// Arguments:
        ///     IN comptime T: type - A type.
        ///
        /// Return: DataElementType
        ///     The DataElementType that represents the type given.
        ///
        fn getDataElementType(comptime T: type) DataElementType {
            return switch (T) {
            ////getDataElementType////
            };
        }

        ///
        /// Get the data out of the tagged union
        ///
        /// Arguments:
        ///     IN comptime T: type     - The type of the data to extract. Used to switch on the
        ///                               tagged union.
        ///     IN element: DataElement - The data element to unwrap the data from.
        ///
        /// Return: T
        ///     The data of type T from the DataElement.
        ///
        fn getDataValue(comptime T: type, element: DataElement) T {
            return switch (T) {
            ////getDataValue////
            };
        }

        ///
        /// Create a function type from a return type and its arguments.
        ///
        /// Arguments:
        ///     IN comptime RetType: type - The return type of the function.
        ///     IN params: type           - The parameters of the function. This will be the type
        ///                                 of a anonymous struct to get the fields and types.
        ///
        /// Return: type
        ///     A function type that represents the return type and its arguments.
        ///
        fn getFunctionType(comptime RetType: type, params: type) type {
            const fields = @typeInfo(params).Struct.fields;
            return switch (fields.len) {
                0 => fn () RetType,
                1 => fn (fields[0].field_type) RetType,
                2 => fn (fields[0].field_type, fields[1].field_type) RetType,
                3 => fn (fields[0].field_type, fields[1].field_type, fields[2].field_type) RetType,
                4 => fn (fields[0].field_type, fields[1].field_type, fields[2].field_type, fields[3].field_type) RetType,
                else => @compileError("More than 3 parameters not supported"),
            };
        }

        ///
        /// Call a function with the function definitions and parameters.
        ///
        /// Argument:
        ///     IN comptime RetType: type - The return type of the function.
        ///     IN function_type: anytype - The function pointer to call.
        ///     IN params: anytype        - The parameter(s) of the function.
        ///
        /// Return: RetType
        ///     The return value of the called function. This can be void.
        ///
        fn callFunction(comptime RetType: type, function_type: anytype, params: anytype) RetType {
            return switch (params.len) {
                0 => function_type(),
                1 => function_type(params[0]),
                2 => function_type(params[0], params[1]),
                3 => function_type(params[0], params[1], params[2]),
                4 => function_type(params[0], params[1], params[2], params[3]),
                // Should get to this as `getFunctionType` will catch this
                else => @compileError("More than 3 parameters not supported"),
            };
        }

        ///
        /// Perform a generic function action. This can be part of a ConsumeFunctionCall or
        /// RepeatFunctionCall action. This will perform the function type comparison and
        /// call the function stored in the action list.
        ///
        /// Argument:
        ///     IN comptime RetType: type    - The return type of the function to call.
        ///     IN test_element: DataElement - The test value to compare to the generated function
        ///                                    type. This is also the function that will be called.
        ///     IN params: anytype           - The parameters of the function to call.
        ///
        /// Return: RetType
        ///     The return value of the called function. This can be void.
        ///
        fn performGenericFunction(comptime RetType: type, test_element: DataElement, params: anytype) RetType {
            // Get the expected function type
            const expected_function = getFunctionType(RetType, @TypeOf(params));

            // Test that the types match
            const expect_type = comptime getDataElementType(expected_function);
            expectEqual(expect_type, @as(DataElementType, test_element));

            // Types match, so can use the expected type to get the actual data
            const actual_function = getDataValue(expected_function, test_element);
            return callFunction(RetType, actual_function, params);
        }

        ///
        /// This tests a value passed to a function.
        ///
        /// Arguments:
        ///     IN comptime ExpectedType: type  - The expected type of the value to be tested.
        ///     IN expected_value: ExpectedType - The expected value to be tested. This is what was
        ///                                       passed to the functions.
        ///     IN elem: DataElement            - The wrapped data element to test against the
        ///                                       expected value.
        ///
        fn expectTest(comptime ExpectedType: type, expected_value: ExpectedType, elem: DataElement) void {
            if (ExpectedType == void) {
                // Can't test void as it has no value
                std.debug.panic("Can not test a value for void\n", .{});
            }

            // Test that the types match
            const expect_type = comptime getDataElementType(ExpectedType);
            expectEqual(expect_type, @as(DataElementType, elem));

            // Types match, so can use the expected type to get the actual data
            const actual_value = getDataValue(ExpectedType, elem);

            // Test the values
            expectEqual(expected_value, actual_value);
        }

        ///
        /// This returns a value from the wrapped data element. This will be a test value to be
        /// returned by a mocked function.
        ///
        /// Arguments:
        ///     IN comptime fun_name: []const u8 - The function name to be used to tell the user if
        ///                                        there is no return value set up.
        ///     IN/OUT action_list: *ActionList  - The action list to extract the return value from.
        ///     IN comptime  DataType: type      - The type of the return value.
        ///
        /// Return: RetType
        ///     The return value of the expected value.
        ///
        fn expectGetValue(comptime fun_name: []const u8, action_list: *ActionList, comptime DataType: type) DataType {
            if (DataType == void) {
                return;
            }

            if (action_list.*.popFirst()) |action_node| {
                // Free the node
                defer GlobalAllocator.destroy(action_node);

                const action = action_node.data;

                // Test that the data match
                const expect_data = comptime getDataElementType(DataType);
                expectEqual(expect_data, @as(DataElementType, action.data));
                return getDataValue(DataType, action.data);
            } else {
                std.debug.panic("No more test values for the return of function: " ++ fun_name ++ "\n", .{});
            }
        }

        ///
        /// This adds a action to the action list with ActionType provided. It will create a new
        /// mapping if one doesn't exist for a function name.
        ///
        /// Arguments:
        ///     IN/OUT self: *Self               - Self. This is the mocking object to be modified
        ///                                        to add the test data.
        ///     IN comptime fun_name: []const u8 - The function name to add the test data to.
        ///     IN data: anytype                 - The data to add to the action for the function.
        ///     IN action_type: ActionType       - The action type to add.
        ///
        pub fn addAction(self: *Self, comptime fun_name: []const u8, data: anytype, action_type: ActionType) void {
            // Add a new mapping if one doesn't exist.
            if (!self.named_actions.contains(fun_name)) {
                self.named_actions.put(fun_name, .{}) catch unreachable;
            }

            // Get the function mapping to add the parameter to.
            if (self.named_actions.getEntry(fun_name)) |actions_kv| {
                // Take a reference of the value so the underlying action list will update
                var action_list = &actions_kv.value;
                const action = Action{
                    .action = action_type,
                    .data = createDataElement(data),
                };
                var a = GlobalAllocator.create(TailQueue(Action).Node) catch unreachable;
                a.* = .{ .data = action };
                action_list.*.append(a);
            } else {
                // Shouldn't get here as we would have just added a new mapping
                // But just in case ;)
                std.debug.panic("No function name: " ++ fun_name ++ "\n", .{});
            }
        }

        ///
        /// Perform an action on a function. This can be one of ActionType.
        ///
        /// Arguments:
        ///     IN/OUT self: *Self               - Self. This is the mocking object to be modified
        ///                                        to perform a action.
        ///     IN comptime fun_name: []const u8 - The function name to act on.
        ///     IN comptime RetType: type        - The return type of the function being mocked.
        ///     IN params: anytype               - The list of parameters of the mocked function.
        ///
        /// Return: RetType
        ///     The return value of the mocked function. This can be void.
        ///
        pub fn performAction(self: *Self, comptime fun_name: []const u8, comptime RetType: type, params: anytype) RetType {
            if (self.named_actions.getEntry(fun_name)) |kv_actions_list| {
                // Take a reference of the value so the underlying action list will update
                var action_list = &kv_actions_list.value;
                // Peak the first action to test the action type
                if (action_list.*.first) |action_node| {
                    const action = action_node.data;
                    return switch (action.action) {
                        ActionType.TestValue => ret: {
                            comptime var i = 0;
                            inline while (i < params.len) : (i += 1) {
                                // Now pop the action as we are going to use it
                                // Have already checked that it is not null
                                const test_node = action_list.*.popFirst().?;
                                defer GlobalAllocator.destroy(test_node);

                                const test_action = test_node.data;
                                const param = params[i];
                                const param_type = @TypeOf(params[i]);

                                expectTest(param_type, param, test_action.data);
                            }
                            break :ret expectGetValue(fun_name, action_list, RetType);
                        },
                        ActionType.ConsumeFunctionCall => ret: {
                            // Now pop the action as we are going to use it
                            // Have already checked that it is not null
                            const test_node = action_list.*.popFirst().?;
                            // Free the node once done
                            defer GlobalAllocator.destroy(test_node);
                            const test_element = test_node.data.data;

                            break :ret performGenericFunction(RetType, test_element, params);
                        },
                        ActionType.RepeatFunctionCall => ret: {
                            // Do the same for ActionType.ConsumeFunctionCall but instead of
                            // popping the function, just peak
                            const test_element = action.data;
                            break :ret performGenericFunction(RetType, test_element, params);
                        },
                    };
                } else {
                    std.debug.panic("No action list elements for function: " ++ fun_name ++ "\n", .{});
                }
            } else {
                std.debug.panic("No function name: " ++ fun_name ++ "\n", .{});
            }
        }

        ///
        /// Initialise the mocking framework.
        ///
        /// Return: Self
        ///     An initialised mocking framework.
        ///
        pub fn init() Self {
            return Self{
                .named_actions = StringHashMap(ActionList).init(GlobalAllocator),
            };
        }

        ///
        /// End the mocking session. This will check all test parameters and consume functions are
        /// consumed. Any repeat functions are deinit.
        ///
        /// Arguments:
        ///     IN/OUT self: *Self - Self. This is the mocking object to be modified to finished
        ///                          the mocking session.
        ///
        pub fn finish(self: *Self) void {
            // Make sure the expected list is empty
            var it = self.named_actions.iterator();
            while (it.next()) |next| {
                // Take a reference so the underlying action list will be updated.
                var action_list = &next.value;
                if (action_list.*.popFirst()) |action_node| {
                    const action = action_node.data;
                    switch (action.action) {
                        ActionType.TestValue, ActionType.ConsumeFunctionCall => {
                            // These need to be all consumed
                            std.debug.panic("Unused testing value: Type: {}, value: {} for function '{s}'\n", .{ action.action, @as(DataElementType, action.data), next.key });
                        },
                        ActionType.RepeatFunctionCall => {
                            // As this is a repeat action, the function will still be here
                            // So need to free it
                            GlobalAllocator.destroy(action_node);
                        },
                    }
                }
            }

            // Free the function mapping
            self.named_actions.deinit();
        }
    };
}

/// The global mocking object that is used for a mocking session. Maybe in the future, we can have
/// local mocking objects so can run the tests in parallel.
var mock: ?Mock() = null;

///
/// Get the mocking object and check we have one initialised.
///
/// Return: *Mock()
///     Pointer to the global mocking object so can be modified.
///
fn getMockObject() *Mock() {
    // Make sure we have a mock object
    if (mock) |*m| {
        return m;
    } else {
        std.debug.panic("MOCK object doesn't exists, please initialise this test\n", .{});
    }
}

///
/// Initialise the mocking framework.
///
pub fn initTest() void {
    // Make sure there isn't a mock object
    if (mock) |_| {
        std.debug.panic("MOCK object already exists, please free previous test\n", .{});
    } else {
        mock = Mock().init();
    }
}

///
/// End the mocking session. This will check all test parameters and consume functions are
/// consumed. Any repeat functions are deinit.
///
pub fn freeTest() void {
    getMockObject().finish();

    // This will stop double frees
    mock = null;
}

///
/// Add a list of test parameters to the action list. This will create a list of data
/// elements that represent the list of parameters that will be passed to a mocked
/// function. A mocked function may be called multiple times, so this list may contain
/// multiple values for each call to the same mocked function.
///
/// Arguments:
///     IN comptime fun_name: []const u8 - The function name to add the test parameters to.
///     IN params: anytype               - The parameters to add.
///
pub fn addTestParams(comptime fun_name: []const u8, params: anytype) void {
    var mock_obj = getMockObject();
    comptime var i = 0;
    inline while (i < params.len) : (i += 1) {
        mock_obj.addAction(fun_name, params[i], ActionType.TestValue);
    }
}

///
/// Add a function to mock out another. This will add a consume function action, so once
/// the mocked function is called, this action wil be removed.
///
/// Arguments:
///     IN comptime fun_name: []const u8 - The function name to add the function to.
///     IN function: anytype             - The function to add.
///
pub fn addConsumeFunction(comptime fun_name: []const u8, function: anytype) void {
    getMockObject().addAction(fun_name, function, ActionType.ConsumeFunctionCall);
}

///
/// Add a function to mock out another. This will add a repeat function action, so once
/// the mocked function is called, this action wil be removed.
///
/// Arguments:
///     IN comptime fun_name: []const u8 - The function name to add the function to.
///     IN function: anytype             - The function to add.
///
pub fn addRepeatFunction(comptime fun_name: []const u8, function: anytype) void {
    getMockObject().addAction(fun_name, function, ActionType.RepeatFunctionCall);
}

///
/// Perform an action on a function. This can be one of ActionType.
///
/// Arguments:
///     IN comptime fun_name: []const u8 - The function name to act on.
///     IN comptime RetType: type        - The return type of the function being mocked.
///     IN params: anytype               - The list of parameters of the mocked function.
///
/// Return: RetType
///     The return value of the mocked function. This can be void.
///
pub fn performAction(comptime fun_name: []const u8, comptime RetType: type, params: anytype) RetType {
    return getMockObject().performAction(fun_name, RetType, params);
}