%% Copyright (c) Meta Platforms, Inc. and affiliates.
%%
%% This source code is licensed under both the MIT license found in the
%% LICENSE-MIT file in the root directory of this source tree and the Apache
%% License, Version 2.0 found in the LICENSE-APACHE file in the root directory
%% of this source tree.

%%%-------------------------------------------------------------------
%%% @doc
%%% Helper module to format errors arising from common_test executions,
%%% in particular as generated by the assert.hrl module.
%%% @end
%%% % @format

-module(ct_error_printer).

%% Public API
-export([format_error/2, format_error/3, format_reason/1]).
-spec format_error(term(), boolean()) -> [unicode:chardata()].
format_error(Reason, FormatStackTrace) -> format_error(error, Reason, FormatStackTrace).

-spec format_error(atom(), term(), boolean()) -> [unicode:chardata()].
format_error('EXIT', Reason, FormatStackTrace) ->
    format_error(Reason, FormatStackTrace);
format_error(ErrType, {'EXIT', Reason}, FormatStackTrace) ->
    format_error(ErrType, Reason, FormatStackTrace);
format_error(ErrType, Reason, FormatStackTrace) ->
    case format_reason(Reason) of
        {ok, Output} ->
            case FormatStackTrace of
                true ->
                    {SubReason, StackTrace} = Reason,
                    ErrReason =
                        case SubReason of
                            {Type, _} -> Type;
                            _ -> exception
                        end,
                    ["\n", Output, [erl_error:format_exception(ErrType, ErrReason, StackTrace)]];
                false ->
                    Output
            end;
        unrecognized_error ->
            case Reason of
                {SubReason, StackTrace} ->
                    [erl_error:format_exception(ErrType, SubReason, StackTrace)];
                _ ->
                    [io_lib:format("~p", [Reason])]
            end
    end.

-spec format_reason(term()) -> {ok, unicode:chardata()}.
format_reason(Reason) ->
    lists:foldl(
        fun
            (_Formatter, Acc = {ok, _Formatted}) -> Acc;
            (Formatter, _Err) -> Formatter(Reason)
        end,
        not_formatted,
        [
            fun maybe_proper_format/1,
            fun maybe_assert_format/1
        ]
    ).

-spec maybe_assert_format(term()) -> unrecognized_error | [unicode:chardata()].
maybe_assert_format({{Type, Props}, _StackTrace}) -> format_assert(Type, Props);
maybe_assert_format(_Reason) -> unrecognized_error.

%% @doc Try to correctly format an error arising from a
%% failed assertion, see
%% https://github.com/erlang/otp/blob/master/bootstrap/lib/stdlib/include/assert.hrl
%% for the different type of errors raised depending of which macro was used.
-spec format_assert(term(), term()) -> unrecognized_error | [unicode:chardata()].
format_assert(Type, Props) when is_list(Props) andalso is_atom(Type) ->
    try proplists:to_map(Props) of
        Map -> format_assert0(Type, Map)
    catch
        _:_:_ -> unrecognized_error
    end;
format_assert(_Type, _Props) ->
    unrecognized_error.

% For this macro to work we expect as local variable
% Type, Module, Line and Props
-define(FORMAT_ASSSERTION(Format, Expressions, SpecificExpressions),
    {ok,
        [print_type_expression(Type, Format, Expressions) ++ print_location(Module, Line)] ++
            SpecificExpressions ++
            print_comments(
                Props, string:chr(unicode:characters_to_list(lists:nth(1, SpecificExpressions)), $:)
            )}
).

-spec format_assert0(atom(), map()) -> unrecognized_error | [unicode:chardata()].
format_assert0(Type, Props = #{formatter := Formatter}) ->
    try
        Formatter:format_assert(Type, Props)
    catch
        E:R:ST ->
            {ok, [
                io_lib:format(
                    "unexpected error when formatting assertion: ~n"
                    "~s~n",
                    [erl_error:format_exception(E, R, ST)]
                ),
                io_lib:format("original assertion: ~n" "~p~n", {Type, Props})
            ]}
    end;
format_assert0(
    assert = Type,
    #{not_boolean := Value} = Props
) ->
    NewProps = maps:remove(not_boolean, Props),
    format_assert0(Type, NewProps#{value => Value});
format_assert0(
    assert = Type1,
    #{
        line := Line,
        module := Module,
        expression := Expression,
        expected := Expected,
        value := Value
    } = Props
) ->
    % asssertNot as formatted as assert, so we need to debug that here.
    Type =
        case Expected of
            true -> Type1;
            false -> assertNot
        end,
    ?FORMAT_ASSSERTION(
        "~s",
        [Expression],
        [
            io_lib:format("   expected: ~p~n", [Expected]),
            io_lib:format("        got: ~p~n", [Value])
        ]
    );
format_assert0(
    assertEqual = Type,
    #{
        line := Line,
        module := Module,
        expression := Expression,
        expected := Expected,
        value := Value
    } = Props
) ->
    ?FORMAT_ASSSERTION(
        "~0p, ~s",
        [Expected, Expression],
        [
            io_lib:format("   expected: ~p~n", [Expected]),
            io_lib:format("        got: ~p~n", [Value])
        ]
    );
format_assert0(
    Type,
    #{line := Line, module := Module, expression := Expression, pattern := Pattern, value := Value} =
        Props
) when
    Type =:= assertMatch orelse Type =:= assertNotMatch
->
    ?FORMAT_ASSSERTION(
        "~s, ~s",
        [Pattern, Expression],
        [
            case Type of
                assertMatch ->
                    io_lib:format("     expected: ~s~n", [Pattern]);
                assertNotMatch ->
                    io_lib:format(" expected not: ~s~n", [Pattern])
            end,
            io_lib:format("          got: ~p~n", [Value])
        ]
    );
format_assert0(
    assertNotEqual = Type,
    #{line := Line, module := Module, expression := Expression, value := Value} = Props
) ->
    ?FORMAT_ASSSERTION(
        "~p, ~s",
        [Value, Expression],
        [
            io_lib:format(" expected not: ~p~n", [Value]),
            io_lib:format("          got: ~p~n", [Value])
        ]
    );
format_assert0(
    assertException = Type,
    #{module := Module, line := Line, expression := Expression, pattern := Pattern} = Props
) ->
    ValueLine =
        case Props of
            #{unexpected_success := Value} ->
                io_lib:format("          got value: ~p~n", [Value]);
            #{unexpected_exception := {Class, Reason, StackTrace} = _Exception} ->
                io_lib:format(
                    "      got exception: ~s ~n",
                    [erl_error:format_exception(Class, Reason, StackTrace)]
                );
            _ ->
                unrecognized_error
        end,
    case ValueLine of
        unrecognized_error ->
            ValueLine;
        _ ->
            ?FORMAT_ASSSERTION(
                "~s, ~p",
                [Pattern, Expression],
                [
                    io_lib:format(" expected exception: ~s~n", [Pattern]),
                    ValueLine
                ]
            )
    end;
format_assert0(
    assertNotException = Type,
    #{
        module := Module,
        line := Line,
        expression := Expression,
        pattern := Pattern,
        unexpected_exception := {Class, Reason, StackTrace}
    } = Props
) ->
    ?FORMAT_ASSSERTION(
        "~p",
        [Expression],
        [
            io_lib:format(" expected no exception: ~p~n", [Pattern]),
            io_lib:format("         got exception: ~s~n", [
                erl_error:format_exception(Class, Reason, StackTrace)
            ])
        ]
    );
format_assert0(_Type, _Props) ->
    unrecognized_error.

-spec print_type_expression(atom(), string(), [term()]) -> unicode:chardata().
print_type_expression(Type, Format, Args) ->
    FormatStr = unicode:characters_to_list(io_lib:format("Failure ?~p(~s)", [Type, Format])),
    io_lib:format(FormatStr, Args).

-spec print_location(atom(), integer()) -> unicode:chardata().
print_location(Module, Line) ->
    io_lib:format(" in ~p.erl:~p~n", [Module, Line]).

-spec print_comments(map(), integer()) -> [unicode:chardata()].
print_comments(#{comment := Comment}, Width) ->
    [io_lib:format("~*.. s ~p~n", [Width, "comment:", Comment])];
print_comments(_Map, _Width) ->
    [].

% This is inherited from former module. It's very unclear how it is used
% / if it is used.
-spec maybe_proper_format(term()) -> no_property | {ok, unicode:chardata()}.
maybe_proper_format({{property_failed, Property, Counterexample}, _ST}) ->
    {ok,
        io_lib:format(
            "Property ~s failed with Counterexample: ~n~p~n",
            [Property, Counterexample]
        )};
maybe_proper_format(_) ->
    no_property.
