%%% 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 `rename function' 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 Elroy Jumpertz <elroy.jumpertz@student.ru.nl>
%%% @author Ely Deckers <e.deckers@gmail.com>
%%% @author Daniel Horpacsi <daniel_h@inf.elte.hu>
%%%
%%% (based on code written by Istvan Bozo)

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

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

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

-record(data, {args_before, arity_before}).

%%% ============================================================================
%%% Interface

%% @spec test_rename_fun() -> qc_result()
%% @doc Performs 100 QuickCheck tests on the loaded files.
test_rename_fun() -> ?QuickCheck:quickcheck(prop_rename_fun()).

%% @spec test_rename_fun((N :: int()) | (Dir :: string())) -> qc_result()
%% @doc Performs QuickCheck tests on the loaded files or over a
%% specific directory.
test_rename_fun(N) when is_integer(N) ->
    ?QuickCheck:quickcheck(?QuickCheck:numtests(N, prop_rename_fun()));
test_rename_fun(Dir) ->
    Files = ?QCCOMMON:get_erl_files(Dir),
    ?QCCOMMON:initialize(files, Files, [{block, true}]),
    test_rename_fun().

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


%%% ============================================================================


get_context(Fun) ->
    Defs = ?Query:exec(Fun, ?Query:seq([?Fun:definition(), ?Form:clauses(), ?Clause:name()])),
    Defs ++ ?QCCOMMON:get_funrefs(Fun).

prop_context({modules, Modules}, ContextBefore) ->
    FunsAfter = lists:flatten([?Query:exec(
        Module, ?Mod:locals()) || Module <- Modules]),
    prop_context({funs, FunsAfter}, ContextBefore); 
prop_context({files, Files}, ContextBefore) ->
    FunsAfter = lists:flatten([?Query:exec(
        File, ?Query:seq([?File:module(), ?Mod:locals()])) || File <- Files]),
    prop_context({funs, FunsAfter}, ContextBefore);
prop_context({funs, FunsAfter}, ContextBefore) ->
    ContextAfter = lists:flatten([get_context(Fun) || Fun <- FunsAfter]),
    (ContextBefore -- ContextAfter) ++ (ContextAfter -- ContextBefore) =:= [].


%% @doc The renamed function must have the same arity as the original function.
%% @spec prop_arity(spgnode(), string(), integer()) -> boolean()
prop_arity(ModNode, GenName, ArityBefore) ->
    ?Query:exec(ModNode, ?Fun:find(GenName,ArityBefore)) /= [].

%% @doc The function arguments' binding structure should remain intact.
%% @spec prop_arg_binding(spgnode(), string(), record(data), [spgnode()]) -> boolean()
prop_arg_binding(ModNode, GenName, Data, ArgBindingStructBefore) ->
    Arity = Data#data.arity_before,
    ArityOk = prop_arity(ModNode, GenName, Arity),
    if ((Arity > 0) and ArityOk) ->
            ArgsBefore = Data#data.args_before,
            [Fun] =  ?Query:exec(ModNode, ?Fun:find(GenName, Arity)),
            ArgsAfter = ?QCCOMMON:get_fun_args(Fun),
            ArgBindingStructAfter =?QCCOMMON:get_arg_binding_struct(ArgsAfter),
            
            if ArgBindingStructBefore =:= ArgBindingStructAfter ->
                    ?QCCOMMON:args_eq(ArgsBefore, ArgsAfter);
               true ->
                    io:format("~nArgBindingError.~n ArgBindingBefore: ~p~nArgBindingAfter: ~p",
                              [ArgBindingStructBefore,ArgBindingStructAfter]),
                    false
            end;
       true ->
            ArityOk % if number of args == 0, then result depends on correct arity.
    end.

%% @doc When renaming a function, and renaming it back, binding
%% structure should remain unchanged ("'Binding structure' here refers
%% to the association of uses of identifiers with their definitions in
%% a program, and is determined by the scope of the identifiers"
%% @@reference Huiqing Li and Simon Thompson, <em>Testing Erlang
%% Refactorings with QuickCheck</em>, Kent University, 2007).
prop_nameback(ModNode, GenName, OldName, Arity) ->
    [Fun] =  ?Query:exec(ModNode, ?Fun:find(GenName, Arity)),
    FunName = GenName, % should hold
    FunName = ?Fun:name(Fun),
    [Mod] = ?Query:exec(Fun, ?Fun:module()),
    Module = ?Mod:name(Mod),

    BindingBefore = ?QCCOMMON:get_binding_struct(Fun),

    %% rename current function node back to its old name
    Args = [{module, Module}, {function, FunName},
            {arity, Arity}, {name, OldName}],
    reftest_utils:exec_transform(rename_fun, Args),
    %% fetch binding structure of the new function node
    [NewFun] =  ?Query:exec(ModNode, ?Fun:find(OldName,Arity)),
    BindingAfter = ?QCCOMMON:get_binding_struct(NewFun),

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

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

prepare(Mods) -> ?LAZY(?QCGEN:gen_args(renfun, {modules, Mods})).

perform_and_check(ModsOrFiles, {_, _, _, Args}) ->
    FunNode = ?Args:function(Args),
    ModNode = ?Args:module(Args),
    NewName = ?Args:name(Args),
    OldName = ?Fun:name(FunNode),
    
    [FileNode] = ?Query:exec(ModNode,?Mod:file()),
    FilePath = ?File:path(FileNode),

    ArgsBefore = ?QCCOMMON:get_fun_args(FunNode),
    ArityBefore = ?Fun:arity(FunNode),
    Data = #data{args_before=ArgsBefore,arity_before=ArityBefore},
    ArgBindingStructBefore = ?QCCOMMON:get_arg_binding_struct(ArgsBefore),
    ContextBefore = lists:flatten([get_context(Fun) || Fun <- ?QCCOMMON:get_funs(ModsOrFiles)]),
    CompResultBefore = compile:file(FilePath, [strong_validation, return_errors]),
    
    %% TODO: maybe not so hard-coded :)
    %% OptionList = [{call_depth, 0}, {abstr_fun_names, true}],
    %% SemTreesBefore = ?QCCOMMON:generate_semtrees(Files, OptionList),

    Result = reftest_utils:exec_transform(rename_fun, Args),
    
    case Result of
        {result, _, _} ->
            ?QCCOMMON:prop_compile(FilePath, CompResultBefore)
                andalso prop_context(ModsOrFiles, ContextBefore)
                andalso prop_arity(ModNode, NewName, ArityBefore)
                %% andalso ?QCCOMMON:prop_semtree(SemTreesBefore, Files, OptionList)
                andalso prop_arg_binding(ModNode,NewName, Data, ArgBindingStructBefore)
                and % rather than andalso!
                prop_nameback(ModNode,NewName, OldName, ArityBefore);
        {abort,{_ErrorDesc, ErrorMessage}} ->
            io:format("Transformation aborted:~n\t~s~n", [ErrorMessage]),
            true
    end.
