%% ``Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''

-module(syntax_tools_SUITE).

-include_lib("common_test/include/ct.hrl").

%% Test server specific exports
-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
	 init_per_group/2,end_per_group/2]).

%% Test cases
-export([app_test/1,appup_test/1,smoke_test/1,revert/1,revert_map/1,
         revert_map_type/1,
	t_abstract_type/1,t_erl_parse_type/1,t_type/1, t_epp_dodger/1,
	t_comment_scan/1,t_igor/1,t_erl_tidy/1]).

suite() -> [{ct_hooks,[ts_install_cth]}].

all() -> 
    [app_test,appup_test,smoke_test,revert,revert_map,revert_map_type,
    t_abstract_type,t_erl_parse_type,t_type,t_epp_dodger,
    t_comment_scan,t_igor,t_erl_tidy].

groups() -> 
    [].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.

app_test(Config) when is_list(Config) ->
    ok = ?t:app_test(syntax_tools).

appup_test(Config) when is_list(Config) ->
    ok = ?t:appup_test(syntax_tools).

%% Read and parse all source in the OTP release.
smoke_test(Config) when is_list(Config) ->
    Dog = ?t:timetrap(?t:minutes(12)),
    Wc = filename:join([code:lib_dir(),"*","src","*.erl"]),
    Fs = filelib:wildcard(Wc) ++ test_files(Config),
    io:format("~p files\n", [length(Fs)]),
    case p_run(fun smoke_test_file/1, Fs) of
        0 -> ok;
        N -> ?t:fail({N,errors})
    end,
    ?t:timetrap_cancel(Dog).

smoke_test_file(File) ->
    case epp_dodger:parse_file(File) of
	{ok,Forms} ->
	    [print_error_markers(F, File) || F <- Forms],
	    ok;
	{error,Reason} ->
	    io:format("~s: ~p\n", [File,Reason]),
	    error
    end.

print_error_markers(F, File) ->
    case erl_syntax:type(F) of
	error_marker ->
	    {L,M,Info} = erl_syntax:error_marker_info(F),
	    io:format("~ts:~p: ~s", [File,L,M:format_error(Info)]);
	_ ->
	    ok
    end.
    

%% Read with erl_parse, wrap and revert with erl_syntax and check for equality.
revert(Config) when is_list(Config) ->
    Dog = ?t:timetrap(?t:minutes(12)),
    Wc = filename:join([code:lib_dir("stdlib"),"src","*.erl"]),
    Fs = filelib:wildcard(Wc) ++ test_files(Config),
    Path = [filename:join(code:lib_dir(stdlib), "include"),
            filename:join(code:lib_dir(kernel), "include")],
    io:format("~p files\n", [length(Fs)]),
    case p_run(fun (File) -> revert_file(File, Path) end, Fs) of
        0 -> ok;
        N -> ?t:fail({N,errors})
        end,
    ?t:timetrap_cancel(Dog).

revert_file(File, Path) ->
    case epp:parse_file(File, Path, []) of
        {ok,Fs0} ->
            Fs1 = erl_syntax:form_list(Fs0),
            Fs2 = erl_syntax_lib:map(fun (Node) -> Node end, Fs1),
            Fs3 = erl_syntax:form_list_elements(Fs2),
            Fs4 = [ erl_syntax:revert(Form) || Form <- Fs3 ],
            {ok,_} = compile:forms(Fs4, [report,strong_validation]),
            ok
    end.

%% Testing bug fix for reverting map_field_assoc
revert_map(Config) when is_list(Config) ->
    Dog = ?t:timetrap(?t:minutes(1)),
    [{map_field_assoc,16,{atom,17,name},{var,18,'Value'}}] =
    erl_syntax:revert_forms([{tree,map_field_assoc,
                             {attr,16,[],none},
			     {map_field_assoc,{atom,17,name},{var,18,'Value'}}}]),
    ?t:timetrap_cancel(Dog).

%% Testing bug fix for reverting map_field_assoc in types
revert_map_type(Config) when is_list(Config) ->
    Dog = ?t:timetrap(?t:minutes(1)),
    Form1 = {attribute,4,record,
             {state,
              [{typed_record_field,
                {record_field,5,{atom,5,x}},
                {type,5,map,
                 [{type,5,map_field_exact,[{atom,5,y},{atom,5,z}]}]}}]}},
    Mapped1 = erl_syntax_lib:map(fun(X) -> X end, Form1),
    Form1 = erl_syntax:revert(Mapped1),
    Form2 = {attribute,4,record,
             {state,
              [{typed_record_field,
                {record_field,5,{atom,5,x}},
                {type,5,map,
                 [{type,5,map_field_assoc,[{atom,5,y},{atom,5,z}]}]}}]}},
    Mapped2 = erl_syntax_lib:map(fun(X) -> X end, Form2),
    Form2 = erl_syntax:revert(Mapped2),
    ?t:timetrap_cancel(Dog).

%% api tests

t_type(Config) when is_list(Config) ->
    F0 = fun validate_basic_type/1,
    Appl0 = fun(Name) ->
                    Atom = erl_syntax:atom(Name),
                    erl_syntax:type_application(none, Atom, [])
            end,
    User0 = fun(Name) ->
                    Atom = erl_syntax:atom(Name),
                    erl_syntax:user_type_application(Atom, [])
            end,
    ok = validate(F0,[{"tuple()", erl_syntax:tuple_type()}
                     ,{"{}", erl_syntax:tuple_type([])}
                     ,{"integer()", Appl0(integer)}
                     ,{"foo()", User0(foo)}
                     ,{"map()", erl_syntax:map_type()}
                     ,{"#{}", erl_syntax:map_type([])}
                     ,{"1..2", erl_syntax:integer_range_type
                          (erl_syntax:integer(1), erl_syntax:integer(2))}
                     ,{"<<_:1,_:_*2>>", erl_syntax:bitstring_type
                          (erl_syntax:integer(1), erl_syntax:integer(2))}
                     ,{"fun()", erl_syntax:fun_type()}
                     ]),

    F = fun validate_type/1,
    ok = validate(F,[{"{}", tuple_type, false}
                    ,{"tuple()", tuple_type, true}
                    ,{"{atom()}", tuple_type, false}
                    ,{"{atom(),integer()}", tuple_type, false}
                    ,{"integer()", type_application, false}
                    ,{"foo()", user_type_application, false}
                    ,{"foo(integer())", user_type_application, false}
                    ,{"module:function()", type_application, false}
                    ,{"map()", map_type, true}
                    ,{"#{}", map_type, false}
                    ,{"#{atom() => integer()}", map_type, false}
                    ,{"#{atom() := integer()}", map_type, false}
                    ,{"#r{}", record_type, false}
                    ,{"#r{a :: integer()}", record_type, false}
                    ,{"[]", type_application, false}
                    ,{"nil()", type_application, false}
                    ,{"[atom()]", type_application, false}
                    ,{"1..2", integer_range_type, false}
                    ,{"<<_:1,_:_*2>>", bitstring_type, false}
                    ,{"fun()", fun_type, true}
                    ,{"integer() | atom()", type_union, false}
                    ,{"A :: fun()", annotated_type, false}
                    ,{"fun((...) -> atom())", function_type, false}
                    ,{"fun((integer()) -> atom())", function_type, false}
                    ,{"V", variable, true}
                    ]),
    ok.

validate_basic_type({String, Tree}) ->
    ErlT = string_to_type(String),
    ErlT = erl_syntax:revert(Tree),
    ok.

validate_type({String, Type, Leaf}) ->
    ErlT = string_to_type(String),
    Type = erl_syntax:type(ErlT),
    Leaf = erl_syntax:is_leaf(ErlT),
    Tree = erl_syntax_lib:map(fun(Node) -> Node end, ErlT),
    Type = erl_syntax:type(Tree),
    _    = erl_syntax:meta(Tree),
    RevT = erl_syntax:revert(Tree),
    Type = erl_syntax:type(RevT),
    ok.

t_abstract_type(Config) when is_list(Config) ->
    F = fun validate_abstract_type/1,
    ok = validate(F,[{hi,atom},
		     {1,integer},
		     {1.0,float},
		     {$a,integer},
		     {[],nil},
		     {[<<1,2>>,a,b],list},
		     {[2,3,<<1,2>>,a,b],list},
		     {[$a,$b,$c],string},
		     {"hello world",string},
		     {<<1,2,3>>,binary},
                     {<<1,2,3:4>>,binary},
		     {#{a=>1,"b"=>2},map_expr},
		     {#{#{i=>1}=>1,"b"=>#{v=>2}},map_expr},
		     {{a,b,c},tuple}]),
    ok.

t_erl_parse_type(Config) when is_list(Config) ->
    F = fun validate_erl_parse_type/1,
    %% leaf types
    ok = validate(F,[{"1",integer,true},
		     {"123456789",integer,true},
		     {"$h", char,true},
		     {"3.1415", float,true},
		     {"1.33e36", float,true},
		     {"\"1.33e36: hello\"", string,true},
		     {"Var1", variable,true},
		     {"_", underscore,true},
		     {"[]", nil,true},
		     {"{}", tuple,true},
		     {"#{}",map_expr,true},
		     {"'some atom'", atom, true}]),
    %% composite types
    ok = validate(F,[{"case X of t -> t; f -> f end", case_expr,false},
		     {"try X of t -> t catch C:R -> error end", try_expr,false},
		     {"receive X -> X end", receive_expr,false},
		     {"receive M -> X1 after T -> X2 end", receive_expr,false},
		     {"catch (X)", catch_expr,false},
		     {"fun(X) -> X end", fun_expr,false},
		     {"fun Foo(X) -> X end", named_fun_expr,false},
		     {"fun foo/2", implicit_fun,false},
		     {"fun bar:foo/2", implicit_fun,false},
		     {"if X -> t; true -> f end", if_expr,false},
		     {"<<1,2,3,4>>", binary,false},
		     {"<<1,2,3,4:5>>", binary,false},
		     {"<<V1:63,V2:22/binary, V3/bits>>", binary,false},
		     {"begin X end", block_expr,false},
		     {"foo(X1,X2)", application,false},
		     {"bar:foo(X1,X2)", application,false},
		     {"[1,2,3,4]", list,false},
		     {"[1|4]", list, false},
		     {"[<<1>>,<<2>>,-2,<<>>,[more,list]]", list,false},
		     {"[1|[2|[3|[4|[]]]]]", list,false},
		     {"#{ a=>1, b=>2 }", map_expr,false},
		     {"#{3=>3}#{ a=>1, b=>2 }", map_expr,false},
		     {"#{ a:=1, b:=2 }", map_expr,false},
		     {"M#{ a=>1, b=>2 }", map_expr,false},
		     {"[V||V <- Vs]", list_comp,false},
		     {"<< <<B>> || <<B>> <= Bs>>", binary_comp,false},
		     {"#state{ a = A, b = B}", record_expr,false},
		     {"#state{}", record_expr,false},
		     {"#s{ a = #def{ a=A }, b = B}", record_expr,false},
		     {"State#state{ a = A, b = B}", record_expr,false},
		     {"State#state.a", record_access,false},
		     {"#state.a", record_index_expr,false},
		     {"-X", prefix_expr,false},
		     {"X1 + X2", infix_expr,false},
		     {"(X1 + X2) * X3", infix_expr,false},
		     {"X1 = X2", match_expr,false},
		     {"{a,b,c}", tuple,false}]),
    ok.

%% the macro ?MODULE seems faulty
t_epp_dodger(Config) when is_list(Config) ->
    DataDir   = ?config(data_dir, Config),
    PrivDir   = ?config(priv_dir, Config),
    Filenames = test_files(),
    ok = test_epp_dodger(Filenames,DataDir,PrivDir),
    ok.

t_comment_scan(Config) when is_list(Config) ->
    DataDir   = ?config(data_dir, Config),
    Filenames = test_files(),
    ok = test_comment_scan(Filenames,DataDir),
    ok.

test_files(Config) ->
    DataDir = ?config(data_dir, Config),
    [ filename:join(DataDir,Filename) || Filename <- test_files() ].

test_files() ->
    ["syntax_tools_SUITE_test_module.erl",
     "syntax_tools_test.erl",
     "type_specs.erl"].

t_igor(Config) when is_list(Config) ->
    DataDir   = ?config(data_dir, Config),
    PrivDir   = ?config(priv_dir, Config),
    FileM1  = filename:join(DataDir,"m1.erl"),
    FileM2  = filename:join(DataDir,"m2.erl"),
    ["m.erl",_]=R = igor:merge(m,[FileM1,FileM2],[{outdir,PrivDir}]),
    io:format("igor:merge/3 = ~p~n", [R]),

    FileTypeSpecs = filename:join(DataDir,"igor_type_specs.erl"),
    Empty = filename:join(DataDir,"empty.erl"),
    ["n.erl",_]=R2 = igor:merge(n,[FileTypeSpecs,Empty],[{outdir,PrivDir}]),
    io:format("igor:merge/3 = ~p~n", [R2]),

    ok.

t_erl_tidy(Config) when is_list(Config) ->
    DataDir   = ?config(data_dir, Config),
    File  = filename:join(DataDir,"erl_tidy_tilde.erl"),
    ok = erl_tidy:file(File, [{stdout, true}]),

    %% OTP-14471.
    Old = process_flag(trap_exit, true),
    NonExisting  = filename:join(DataDir,"non_existing_file.erl"),
    {'EXIT',{error,{0,file,enoent}}} = (catch erl_tidy:file(NonExisting)),
    true = process_flag(trap_exit, Old),
    ok.

test_comment_scan([],_) -> ok;
test_comment_scan([File|Files],DataDir) ->
    Filename  = filename:join(DataDir,File),
    {ok, Fs0} = epp:parse_file(Filename, [], []),
    Comments  = erl_comment_scan:file(Filename),
    Fun = fun(Node) ->
		  case erl_syntax:is_form(Node) of
		      true ->
			  C1    = erl_syntax:comment(2,[" This is a form."]),
			  Node1 = erl_syntax:add_precomments([C1],Node),
			  Node1;
		      false ->
			  Node
		  end
	  end,
    Fs1 = erl_recomment:recomment_forms(Fs0, Comments),
    Fs2 = erl_syntax_lib:map(Fun, Fs1),
    io:format("File: ~s~n", [Filename]),
    io:put_chars(erl_prettypr:format(Fs2, [{paper,  120},
					   {ribbon, 110}])),
    test_comment_scan(Files,DataDir).


test_epp_dodger([], _, _) -> ok;
test_epp_dodger([Filename|Files],DataDir,PrivDir) ->
    io:format("Parsing ~p~n", [Filename]),
    InFile   = filename:join(DataDir, Filename),
    Parsers  = [{fun epp_dodger:parse_file/1,parse_file},
		{fun epp_dodger:quick_parse_file/1,quick_parse_file},
		{fun (File) ->
			{ok,Dev} = file:open(File,[read]),
			Res = epp_dodger:parse(Dev),
			file:close(File),
			Res
		 end, parse},
		{fun (File) ->
			{ok,Dev} = file:open(File,[read]),
			Res = epp_dodger:quick_parse(Dev),
			file:close(File),
			Res
		 end, quick_parse}],
    FsForms  = parse_with(Parsers, InFile),
    ok = pretty_print_parse_forms(FsForms,PrivDir,Filename),
    test_epp_dodger(Files,DataDir,PrivDir).

parse_with([],_) -> [];
parse_with([{Fun,ParserType}|Funs],File) ->
    {ok, Fs} = Fun(File),
    [{Fs,ParserType}|parse_with(Funs,File)].

pretty_print_parse_forms([],_,_) -> ok;
pretty_print_parse_forms([{Fs0,Type}|FsForms],PrivDir,Filename) ->
    Parser  = atom_to_list(Type),
    OutFile = filename:join(PrivDir, Parser ++"_" ++ Filename),
    io:format("Pretty print ~p (~w) to ~p~n", [Filename,Type,OutFile]),
    Comment = fun (Node,{CntCase,CntTry}=Cnt) ->
		      case erl_syntax:type(Node) of
			  case_expr ->
			      C1    = erl_syntax:comment(2,["Before a case expression"]),
			      Node1 = erl_syntax:add_precomments([C1],Node),
			      C2    = erl_syntax:comment(2,["After a case expression"]),
			      Node2 = erl_syntax:add_postcomments([C2],Node1),
			      {Node2,{CntCase+1,CntTry}};
			  try_expr ->
			      C1    = erl_syntax:comment(2,["Before a try expression"]),
			      Node1 = erl_syntax:set_precomments(Node,
						     erl_syntax:get_precomments(Node) ++ [C1]),
			      C2    = erl_syntax:comment(2,["After a try expression"]),
			      Node2 = erl_syntax:set_postcomments(Node1,
						     erl_syntax:get_postcomments(Node1) ++ [C2]),
			      {Node2,{CntCase,CntTry+1}};
			  _ ->
			      {Node,Cnt}
		      end
	      end,
    Fs1 = erl_syntax:form_list(Fs0),
    {Fs2,{CC,CT}} = erl_syntax_lib:mapfold(Comment,{0,0}, Fs1),
    io:format("Commented on ~w cases and ~w tries~n", [CC,CT]),
    PP  = erl_prettypr:format(Fs2),
    ok  = file:write_file(OutFile,iolist_to_binary(PP)),
    pretty_print_parse_forms(FsForms,PrivDir,Filename).


validate(_,[]) -> ok;
validate(F,[V|Vs]) ->
    ok = F(V),
    validate(F,Vs).


validate_abstract_type({Lit,Type}) ->
    Tree = erl_syntax:abstract(Lit),
    ok   = validate_special_type(Type,Tree),
    Type = erl_syntax:type(Tree),
    true = erl_syntax:is_literal(Tree),
    ErlT = erl_syntax:revert(Tree),
    Type = erl_syntax:type(ErlT),
    ok   = validate_special_type(Type,ErlT),
    Conc = erl_syntax:concrete(Tree),
    Lit  = Conc,
    ok.

validate_erl_parse_type({String,Type,Leaf}) ->
    ErlT = string_to_expr(String),
    ok   = validate_special_type(Type,ErlT),
    Type = erl_syntax:type(ErlT),
    Leaf = erl_syntax:is_leaf(ErlT),
    Tree = erl_syntax_lib:map(fun(Node) -> Node end, ErlT),
    Type = erl_syntax:type(Tree),
    _    = erl_syntax:meta(Tree),
    ok   = validate_special_type(Type,Tree),
    RevT = erl_syntax:revert(Tree),
    ok   = validate_special_type(Type,RevT),
    Type = erl_syntax:type(RevT),
    ok.

validate_special_type(string,Node) ->
    Val  = erl_syntax:string_value(Node),
    true = erl_syntax:is_string(Node,Val),
    _    = erl_syntax:string_literal(Node),
    ok;
validate_special_type(variable,Node) ->
    _ = erl_syntax:variable_literal(Node),
    ok;
validate_special_type(fun_expr,Node) ->
    A = erl_syntax:fun_expr_arity(Node),
    true = is_integer(A),
    ok;
validate_special_type(named_fun_expr,Node) ->
    A = erl_syntax:named_fun_expr_arity(Node),
    true = is_integer(A),
    ok;
validate_special_type(tuple,Node) ->
    Size = erl_syntax:tuple_size(Node),
    true = is_integer(Size),
    ok;
validate_special_type(float,Node) ->
    Str   = erl_syntax:float_literal(Node),
    Val   = list_to_float(Str),
    Val   = erl_syntax:float_value(Node),
    false = erl_syntax:is_proper_list(Node),
    false = erl_syntax:is_list_skeleton(Node),
    ok;
validate_special_type(integer,Node) ->
    Str   = erl_syntax:integer_literal(Node),
    Val   = list_to_integer(Str),
    true  = erl_syntax:is_integer(Node,Val),
    Val   = erl_syntax:integer_value(Node),
    false = erl_syntax:is_proper_list(Node),
    ok;
validate_special_type(nil,Node) ->
    true  = erl_syntax:is_proper_list(Node),
    ok;
validate_special_type(list,Node) ->
    true  = erl_syntax:is_list_skeleton(Node),
    _     = erl_syntax:list_tail(Node),
    ErrV  = erl_syntax:list_head(Node),
    false = erl_syntax:is_string(Node,ErrV),
    Norm  = erl_syntax:normalize_list(Node),
    list  = erl_syntax:type(Norm),
    case erl_syntax:is_proper_list(Node) of
	true ->
	    true = erl_syntax:is_list_skeleton(Node),
	    Compact = erl_syntax:compact_list(Node),
	    list = erl_syntax:type(Compact),
	    [_|_] = erl_syntax:list_elements(Node),
	    _  = erl_syntax:list_elements(Node),
	    N  = erl_syntax:list_length(Node),
	    true = N > 0,
	    ok;
	false ->
	    ok
    end;
validate_special_type(_,_) ->
    ok.

%%% scan_and_parse

string_to_expr(String) ->
    io:format("Str: ~p~n", [String]),
    {ok, Ts, _} = erl_scan:string(String++"."),
    {ok,[Expr]} = erl_parse:parse_exprs(Ts),
    Expr.

string_to_type(String) ->
    io:format("Str: ~p~n", [String]),
    {ok,Ts,_} = erl_scan:string("-type foo() :: "++String++".", 0),
    {ok,Form} = erl_parse:parse_form(Ts),
    {attribute,_,type,{foo,Type,_NoParms=[]}} = Form,
    Type.


p_run(Test, List) ->
    N = erlang:system_info(schedulers),
    p_run_loop(Test, List, N, [], 0).

p_run_loop(_, [], _, [], Errors) ->
    Errors;
p_run_loop(Test, [H|T], N, Refs, Errors) when length(Refs) < N ->
    {_,Ref} = erlang:spawn_monitor(fun() -> exit(Test(H)) end),
    p_run_loop(Test, T, N, [Ref|Refs], Errors);
p_run_loop(Test, List, N, Refs0, Errors0) ->
    receive
	{'DOWN',Ref,process,_,Res} ->
	    Errors = case Res of
			 ok -> Errors0;
			 error -> Errors0+1
		     end,
	    Refs = Refs0 -- [Ref],
	    p_run_loop(Test, List, N, Refs, Errors)
    end.
