%%% 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 Prop based testing using random modules

%%% @author D�niel Horp�csi <daniel_h@inf.elte.hu>

-module(refqc_randmod).
-vsn("$Rev: 4877 $ ").

-export([test/1, test/2, test_beheqv/1, test_beheqv/2, qc/1, qc/2, qc_with_io/2,
         prop/1]).

-include_lib("referl_qc/include/prop_based_testing.hrl").
-include_lib("referl_lib/include/lib_export.hrl").
-include_lib("referl_core/include/core_export.hrl").

-define(DD, refqc_beheqv).
-define(LA, refqc_libanal_test).
-define(DF, reftest_dataflow).
-define(CFG, reftest_cfg).
-define(Iodev, refqc_dd_iodev).
-define(ArgGen, refqc_generators).
-define(POST, reftest_postest).

-ifdef(development_mode).
  -define(out(S), io:format(S)).
  -define(out2(S,L), io:format(S,L)).
-else.
  -define(out(S), ok).
  -define(out2(S,L), ok).
-endif.

%% -----------------------------------------------------------------------------
%% Interface of random module based testing

%% Uses predefined properties
test(X)                  -> test(1, X).
test(Numtests, X)        -> qc(Numtests, prop(X)).
test_beheqv(X)           -> test_beheqv(1, X).
test_beheqv(Numtests, X) -> qc_with_io(Numtests, prop({beheqv, X})).

%% Custom property tested with random modules
qc(Prop) -> qc(1, Prop).
qc(Numtests, Prop) ->
    file:make_dir(?TestDir),
    ?QuickCheck:quickcheck(?QuickCheck:numtests(Numtests, Prop)),
    {ok, Files} = file:list_dir(?TestDir),
    [file:delete(filename:join(?TestDir, File)) || File <- Files],
    file:del_dir(?TestDir).
qc_with_io(Numtests, Prop) ->
    ?Iodev:start_link(),
    qc(Numtests, Prop),
    ?Iodev:stop().

%% -----------------------------------------------------------------------------
%% Properties

%% Specific props

refac_name(renfun)  -> rename_fun;
refac_name(extfun)  -> extract_fun;
refac_name(renvar)  -> rename_var;
refac_name(reorder) -> reorder_funpar; 
refac_name(elimvar) -> elim_var; 
refac_name(tuple)   -> tuple_funpar.

-ifdef(ets_mode).
-define(etstest,
        prop_randmod(fun erl_ets_gen:mods/1,
                     {fun reftest_ets:prepare/1,
                      fun reftest_ets:perform_and_check/2})).
-else.
-define(etstest,
        io:format("ETS analysis is disabled in this RefactorErl instance. "
                  "Please set the ets_mode macro and try again.~n")).
-endif.

prop(libanal_test) ->
    prop_randmod(fun erl_seq_gen:mods/1, {fun ?LA:prepare/1, fun ?LA:perform_and_check/2});
prop(dataflow_test) ->
    prop_randmod(fun erl_seq_gen:mods/1, {fun ?DF:prepare/1, fun ?DF:perform_and_check/2});
prop(postest) ->
    prop_randmod(fun erl_seq_gen:mods/1, {fun ?POST:prepare/1, fun ?POST:perform_and_check/2});
prop(ets_test) ->
    ?etstest;
prop(cfg_test) ->
    prop_randmod(fun erl_seq_gen:mods/1, {fun ?CFG:prepare/1, fun ?CFG:perform_and_check/2});
prop(Tag) when Tag == elimvar;
               Tag == extfun;
               Tag == renfun;
               %% Tag == renmod;
               Tag == renvar;
               Tag == reorder ->
    QcMod = list_to_atom("refqc_" ++ atom_to_list(refac_name(Tag))),
    prop_randmod(fun erl_seq_gen:mods/1, {fun(P) -> QcMod:prepare(P) end, 
                  fun(P1, P2) -> QcMod:perform_and_check(P1, P2) end});
prop({beheqv, Tag}) when Tag == elimvar;
                         Tag == extfun;
                         Tag == renfun;
                         Tag == renvar;
                         %% Tag == reorder;
                         Tag == tuple ->
    prop({dd, refac_name(Tag), fun(File) -> ?ArgGen:gen_args(Tag, File) end, 2});
prop({dd, Ref, Gen}) ->
    prop({dd, Ref, Gen, 1});
prop({dd, Ref, Gen, N}) ->
    prop_randmod(fun erl_seq_gen:mods/1, [before, 'after'], N,
                 {fun(_) -> ?DD:prepare(Gen) end,
                  fun(_,Args) -> ?DD:perform_and_check(N,Ref,Args) end}).

%% Generic props

prop_randmod(Generator, CallBacks)        -> prop_randmod(Generator, testmodule, CallBacks).
prop_randmod(Generator, Names, CallBacks) -> prop_randmod(Generator, Names, 2, CallBacks).
prop_randmod(Generator, Names, N, CallBacks) ->
    F = fun(Mods) -> prop_transformation(Mods, CallBacks) end,
    ?FORALL(_, gen_modules(Generator, N, Names), F(load_modules(Names, N))).

prop_transformation(Mods, {Prepare, PerformAndCheck}) ->
    %%?FORALL(Param, Prepare(Mods), PerformAndCheck({modules, Mods}, Param ++ [{ask_missing, false}])).
    ?out("Checking the property...~n"),
    ?FORALL(Param, Prepare(Mods), PerformAndCheck({modules, Mods}, Param)).

%%% ============================================================================
%%% Generating and loading modules

%% @private
%% @spec gen_modules(fun(), int(), Name | [Name]) -> any()
%%           Name = atom() | string()
gen_modules(Generator, N, [Name|_] = Names) when is_integer(N), is_atom(Name) ->
    gen_modules(Generator, N, [atom_to_list(NameAtom) || NameAtom <- Names]);
gen_modules(Generator, N, Name) when is_integer(N), is_atom(Name) ->
    gen_modules(Generator, N, [atom_to_list(Name)]);
gen_modules(Generator, N, [Char|_] = Name) when is_integer(N), is_integer(Char) ->
    gen_modules(Generator, N, [Name]);
gen_modules(Generator, N, Names) when is_integer(N), is_list(Names) ->
    ?LET(PropList,
         %% noshrink(erl_seq_gen:mods(N)),
         eqc_gen:lazy(fun() ->
                              io:nl(),
                              ?out2("~nGenerating ~p random modules...~n", [N]),
                              noshrink(Generator(N))
                      end),
         begin
             Trees = eval(proplists:get_value(value, PropList)),
             Strs = lists:map(fun erl_prettypr:format/1, Trees),
             Indices = [integer_to_list(I) || I <- lists:seq(1,N)],
             Code = lists:zip(Indices, Strs),
             {ok, Cwd} = file:get_cwd(),
             ?out2("Saving generated modules into files (in ~s)...~n", [Cwd]),
             R = [[saveas(Name ++ Ind, replace_names(Str, Name, N)) ||
                      {Ind, Str} <- Code] ||
                     Name <- Names],
             R
         end).

%% @private
saveas(ModuleName, Body) ->
    {ok, IoDevice} = file:open(filename:join(?TestDir, fileof(ModuleName)), [write]),
    io:format(IoDevice, "~s", [Body]),
    %% ?out2("\t(~s: ~p characters)~n", [fileof(ModuleName), length(Body)]),
    file:close(IoDevice).

%% @private
replace_names(Str, _ModuleName, 0) ->
    Str;
replace_names(Str, ModuleName, N) ->
    {From, To} = name_pair(ModuleName, N),
    Res = re:replace(Str, From, To, [global, {return, list}]),
    replace_names(Res, ModuleName, N - 1).

%% @private
name_pair(ModuleName, Index) when is_list(ModuleName), is_integer(Index) ->
    Ind = integer_to_list(Index),
    {"module" ++ Ind, ModuleName ++ Ind}.

fileof(Module) when is_atom(Module) -> atom_to_list(Module) ++ ".erl";
fileof(Module) when is_list(Module) -> Module ++ ".erl".


%% @private
%% @spec load_modules(atom() | [atom()], int()) -> [node(#module{})]
load_modules([_, Name], N) ->
    load_modules(Name, N);
load_modules(Name, N) when is_atom(Name) ->
    ?out("Clearing the database...~n"),
    %% refqc_common:clear_database(),
    reftest_utils:reset_db(),
    ?out("Adding the randomly generated files to the database...~n"),
    [?FileMan:add_file(filename:join(?TestDir, fileof(atom_to_list(Name) ++ integer_to_list(I))))
     || I <- lists:seq(1, N)],
    %% ?ESG:finalize(),
    [?Query:exec(?Mod:find(list_to_atom(atom_to_list(Name) ++ integer_to_list(I)))) ||
        I <- lists:seq(1, N)];
load_modules(_, _) -> throw("Too many module names, not supported.").
