%%% This file is part of RefactorErl.
%%%
%%% RefactorErl is free software: you can redistribute it and/or modify
%%% it under the terms of the GNU Lesser General Public License as published
%%% by the Free Software Foundation, either version 3 of the License, or
%%% (at your option) any later version.
%%%
%%% RefactorErl is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%%% GNU Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public License
%%% along with RefactorErl.  If not, see <http://plc.inf.elte.hu/erlang/>.
%%%
%%% The Original Code is RefactorErl.
%%%
%%% The Initial Developer of the Original Code is Eötvös Loránd University.
%%% Portions created  by Eötvös Loránd University and ELTE-Soft Ltd.
%%% are Copyright 2007-2025 Eötvös Loránd University, ELTE-Soft Ltd.
%%% and Ericsson Hungary. All Rights Reserved.


%%% ============================================================================
%%% Module information
%%% @doc This module performs random `reorder funpar' refactorings on files
%%% in the database, with arguments generated by QuickCheck. It then tests a
%%% number of properties that should hold before and after refactoring.

%%% @author Ely Deckers <e.deckers@gmail.com>
%%% @author Daniel Horpacsi <daniel_h@inf.elte.hu>


%% TODO:
%% change_order: [{[391],[]},{[393],[398,400]}] [1,2]
%%         [{[391],[]},{[393],[398,400]}]

%% ArgBindingError.
%% ArgBindingBefore: [{[391],[]},{[393],[398,400]}]
%% ArgBindingAfter: [{[391],[398,400]},{[393],[]}]Failed! After 1 tests.

%% Perhaps the reorder fun transformation does a swap even in the case
%% of specified order [1, 2].

-module(refqc_reorder_funpar).
-vsn("$Rev: 17551 $").

%% Random module based testing callbacks
-export([prepare/1, perform_and_check/2]).
%% Interface
-export([prop_reorder_funpar/0]).

-include_lib("referl_qc/include/prop_based_testing.hrl").
-include_lib("referl_qc/include/qc.hrl").

prop_reorder_funpar() ->
    Files = ?QCCOMMON:get_files_in_database(),
    ?FORALL(Args, ?LAZY(gen_args(?QCCOMMON:get_funs(Files))),
            perform_and_check({files, Files}, Args)).

%%% ============================================================================
%%% Generators and helpers

%% @doc generate a (random) order for the function arguments of the function.
%%
%% @spec gen_order(Fun :: funnode()) -> [int()]
gen_order(Fun) ->
    random_order(lists:seq(1, ?Fun:arity(Fun))).

get_arg_for_fun(Fun) ->
    [Mod] = ?Query:exec(Fun,?Fun:module()),
    [{module,   ?Mod:name(Mod)},
     {function, ?Fun:name(Fun)},
     {arity,    ?Fun:arity(Fun)}].

%% @doc generate arguments for testing
%%
%% @spec gen_args([fun_spgnode()]) -> generator
gen_args(Funs) ->
    noshrink(
        ?LET(Fun, oneof(Funs),
            begin
                Arg =  get_arg_for_fun(Fun),
                Order = gen_order(Fun),
                Arg ++ [{order, Order}, {ask_missing, false}] % ask_missing required by ?Args:function...
            end)).

change_order(Patterns, Order)->
    MapN = fun(Args, N) ->
                   NewPos = lists:nth(N, Order),
                   {_, RLocs} = lists:nth(NewPos, Args),
                   {BLocNew, _} = lists:nth(N, Args),
                   {BLocNew, RLocs}
           end,
    [[MapN(Args, N) || N <- lists:seq(1,length(Order))] || Args <- Patterns].

%% @doc Randomly order the elements of a list, based on Erlang's random
%% generator.
%%
%% @spec random_order(List :: [any()]) -> [any()]
random_order(List) ->
    %% `sort' function which randomly returns `true' or `false'
    OrderFun = fun(_,_) -> lists:nth(rand:uniform(2),[true,false]) end,
    lists:sort(OrderFun,List).

%%% ============================================================================
%%% QuickCheck properties

%% @doc The renamed function must have the same arity as the original function.
%%
%% @spec prop_arity(FunNode :: spgnode(), ArityBefore :: integer() ) -> boolean()
prop_arity(FunNode, ArityBefore) ->
    ?Fun:arity(FunNode) == ArityBefore.

%% @doc The function arguments' binding structure should remain intact.
%%
%% @todo verify equality of argument types and atom values.
%%
%% @spec prop_argument_bindings( FunNode :: spgnode(),
%%   ArgBindingStructBefore :: [spgnode()] ) -> boolean()
prop_argument_bindings(FunNode, ArgBindingStructBefore) ->
    Arity = ?Fun:arity(FunNode),
    ArityOk = prop_arity(FunNode, Arity),

    if ((Arity > 0) and ArityOk) ->
            ArgsAfter = ?QCCOMMON:get_fun_args(FunNode),
            %% TODO ?QCCOMMON:args_eq(ArgsBefore,ArgsAfter),
            ArgBindingStructAfter = ?QCCOMMON:get_arg_binding_struct(ArgsAfter),

            if ArgBindingStructBefore =:= ArgBindingStructAfter ->
                    true;
               true ->
                    io:format("~nArgBindingError.~nArgBindingBefore: ~p~nArgBindingAfter: ~p",
                              [ArgBindingStructBefore,ArgBindingStructAfter]),
                    false
            end;
       true ->
            ArityOk %if number of args == 0, then result depends on correct arity.
    end.

%% @@doc When function arguments are swapped, they should be swapped in the same
%% fashion in that function's applications.
%%
%% @@spec prop_applications(FunNode,ExpectedFunAppArgLocs) -> [{int(),[int()]}]
%prop_applications(FunNode,ExpectedFunAppArgLocs) ->
% find implicits, where definition of Arg.
    %ImpCalls = ?Query:exec(FunNode, ?Fun:implicits())
    %Clauses = [{C, ?Query:exec(C,?Clause:patterns())}|| C <- Funcl],
    %make_new_order(Clauses, Order, pattern),
    %[?Transform:touch(Node) || Node <- Funcl],
    %ok.
% swap their token positions, and determine what the new token position will be.
%    true.

%% @doc When arguments are reversed two times, the argument binding structure
%% should remain unchanged.
%%
%% @spec prop_inverse( FunNode :: spgnode() ) -> boolean()
prop_inverse(FunNode) ->
    Arity = ?Fun:arity(FunNode),
    Args1 = ?QCCOMMON:get_fun_args(FunNode),
    Binding1 = ?QCCOMMON:get_arg_binding_struct(Args1),

    Args = get_arg_for_fun(FunNode) ++
        [{order, lists:reverse(lists:seq(1,Arity))}, {ask_missing, false}],

    reftest_utils:exec_transform(reorder_funpar, Args),
    reftest_utils:exec_transform(reorder_funpar, Args),

    Fun2 = ?Args:function(Args),
    Args2 = ?QCCOMMON:get_fun_args(Fun2),
    Binding2 = ?QCCOMMON:get_arg_binding_struct(Args2),

    if Binding1 =:= Binding2 -> true;
       true ->
            io:format( "~nBinding structure broken.~nBefore:~p ~nAfter:~p",
                       [Binding1,Binding2] ),
            false
    end.

%% =============================================================================
%% Random module based testing callbacks

prepare(_Mods) -> ?LAZY(gen_args(?QCCOMMON:get_funs(?QCCOMMON:get_files_in_database()))).

perform_and_check(_ModsOrFiles, Args) ->
    ?IMPLIES(?Fun:arity(?Args:function(Args)) > 0,
             begin
                 FunNode = ?Args:function(Args),

                 [FileNode] = ?Query:exec(FunNode,?Query:seq(?Fun:module(), ?Mod:file())),                 
                 FilePath = ?File:path(FileNode),
                 
                 FunName = ?Fun:name(FunNode),
                 FunArgs = ?QCCOMMON:get_fun_args(FunNode),
                 Arity = ?Fun:arity(FunNode),
                 NewOrder = ?Args:order(Args),
                 
                 %% determine function's binding structure for `binding' property.
                 ArgBindingStruct = ?QCCOMMON:get_arg_binding_struct(FunArgs),

                 ExpectedBindingStructAfter =
                     change_order(ArgBindingStruct,NewOrder),
                 
                 %% check if the current module compiles for `compile' property.
                 CompResult = compile:file(
                                FilePath, [strong_validation, return_errors]),
                 
                 %% perform the reorder funpar transformation.
                 Result = reftest_utils:exec_transform(reorder_funpar, Args),
                 
                 %% retrieve the post-transformation function node.

                 [NewFun] = ?Query:exec(FileNode, ?Query:seq(?File:module(), ?Mod:local(FunName, Arity))),
                 
                 %% if the transformation was succesful verify the properties.
                 case Result of
                     {result, _, _} ->
                         ?QCCOMMON:prop_compile(FilePath, CompResult) andalso
                             prop_argument_bindings(NewFun, ExpectedBindingStructAfter) and %rather than andalso!
                             prop_inverse(NewFun);
                     {abort,{_ErrorDesc, ErrorMessage}} ->
                         io:format("Transformation aborted:~n\t~s~n", [ErrorMessage]),
                         true
                 end
             end).
