%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2011-2016. All Rights Reserved.
%%
%% 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.
%%
%% %CopyrightEnd%
%%

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


-compile(export_all).

-define(SUP,rb_SUITE_sup).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
all() ->
    no_group_cases() ++ [{group,running_error_logger}].

no_group_cases() ->
    [help,
     start_error_stop].

groups() ->
    [{running_error_logger,[shuffle],[show,
				      list,
				      rescan,
				      start_stop_log,
				      grep,
				      filter_filter,
				      filter_date,
				      filter_filter_and_date,
				      filter_re_no
				     ]}].


init_per_suite(Config) ->
    PrivDir = ?config(priv_dir,Config),
    RbDir = filename:join(PrivDir,rb),
    ok = file:make_dir(RbDir),
    NewConfig = [{rb_dir,RbDir}|Config],
    reset_sasl(NewConfig),
    NewConfig.

end_per_suite(_Config) ->
    ok.

init_per_group(running_error_logger,Config) ->
    %% Install log_mf_h
    RbDir = ?config(rb_dir,Config),
    ok = application:set_env(sasl,error_logger_mf_dir,RbDir),
    ok = application:set_env(sasl,error_logger_mf_maxbytes,5000),
    ok = application:set_env(sasl,error_logger_mf_maxfiles,2),
    restart_sasl(),
    Config.

end_per_group(running_error_logger,_Config) ->
    %% Remove log_mf_h???
    ok.


init_per_testcase(_Case,Config) ->
    case whereis(?SUP) of
	undefined ->
	    ok;
	Sup ->
	    Server = whereis(?MODULE),
	    exit(Sup,kill),
	    wait_for_down([Server,Sup])
    end,
    empty_error_logs(Config),
    Config.

wait_for_down([]) ->
    ok;
wait_for_down([undefined|Rest]) ->
    wait_for_down(Rest);
wait_for_down([Pid|Rest]) ->
    Ref = erlang:monitor(process,Pid),
    receive {'DOWN', Ref, process, Pid, _Info} -> ok end,
    wait_for_down(Rest).

end_per_testcase(Case,Config) ->
    try apply(?MODULE,Case,[cleanup,Config])
    catch error:undef -> ok
    end,
    ok.


%%%-----------------------------------------------------------------
%%% Test cases

help(_Config) ->
    Help = capture(fun() -> rb:h() end),
    %% Check that first and last line is there
    true = lists:member("Report Browser Tool - usage", Help),
    true = lists:member("rb:stop            - stop the rb_server", Help),
    ok.

%% Test that all three sasl env vars must be set for a successful start of rb
%% Then stop rb.
start_error_stop(Config) ->
    RbDir = ?config(rb_dir,Config),

    {error,{"cannot locate report directory",_}} = rb:start(),


    ok = application:set_env(sasl,error_logger_mf_dir,"invaliddir"),
    ok = application:set_env(sasl,error_logger_mf_maxbytes,1000),
    ok = application:set_env(sasl,error_logger_mf_maxfiles,2),
    restart_sasl(),
    {error,{"cannot read the index file",_}} = rb:start(),
    ok = application:set_env(sasl,error_logger_mf_dir,RbDir),
    restart_sasl(),
    {ok,_} = rb:start(),

    ok = rb:stop(),
    ok.

show(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Insert some reports in the error log and start rb
    init_error_logs(),
    ok = start_rb(OutFile),

    %% Show all reports
    All = check_report(fun() -> rb:show() end,OutFile),

    %% Show by number
    [{_,First}] = check_report(fun() -> rb:show(1) end,OutFile),
    {1,First} = lists:keyfind(1,1,All),

    %% Show by type
    [{_,CR}] = check_report(fun() -> rb:show(crash_report) end,OutFile),
    true = contains(CR,"rb_test_crash"),
    [{_,EC},{_,EM}] = check_report(fun() -> rb:show(error) end,OutFile),
    true = contains(EC,"rb_test_crash"),
    true = contains(EM,"rb_test_error_msg"),
    [{_,ER}] = check_report(fun() -> rb:show(error_report) end,OutFile),
    true = contains(ER,"rb_test_error"),
    [{_,IR}] = check_report(fun() -> rb:show(info_report) end,OutFile),
    true = contains(IR,"rb_test_info"),
    [{_,IM}] = check_report(fun() -> rb:show(info_msg) end,OutFile),
    true = contains(IM,"rb_test_info_msg"),
    [_|_] = check_report(fun() -> rb:show(progress) end,OutFile),
    [{_,SR}] = check_report(fun() -> rb:show(supervisor_report) end,
			    OutFile),
    true = contains(SR,"child_terminated"),
    true = contains(SR,"{rb_SUITE,rb_test_crash}"),

    ok.

list(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Insert some reports in the error log and start rb
    init_error_logs(),
    ok = start_rb(OutFile),

    All = capture(fun() -> rb:list() end),
    [{crash_report,[_]=CR},
     {error,[_,_]=EM},
     {error_report,[_]=ER},
     {info_msg,[_]=IM},
     {info_report,[_]=IR},
     {progress,[_|_]=P},
     {supervisor_report,[_]=SR}] = sort_list(All),

    [{crash_report,CR}] =
	sort_list(capture(fun() -> rb:list(crash_report) end)),
    [{error,EM}] =
	sort_list(capture(fun() -> rb:list(error) end)),
    [{error_report,ER}] =
	sort_list(capture(fun() -> rb:list(error_report) end)),
    [{info_msg,IM}] =
	sort_list(capture(fun() -> rb:list(info_msg) end)),
    [{info_report,IR}] =
	sort_list(capture(fun() -> rb:list(info_report) end)),
    [{progress,P}] =
	sort_list(capture(fun() -> rb:list(progress) end)),
    [{supervisor_report,SR}] =
	sort_list(capture(fun() -> rb:list(supervisor_report) end)),

    ok.

grep(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Insert some reports in the error log and start rb
    init_error_logs(),
    ok = start_rb(OutFile),

    [{_,S},
     {_,CR},
     {_,EC},
     {_,IM},
     {_,IR},
     {_,EM},
     {_,ER}]= check_report(fun() -> rb:grep("rb_test_") end,OutFile),
    true = contains(S, "rb_test_crash"),
    true = contains(CR, "rb_test_crash"),
    true = contains(EC, "rb_test_crash"),
    true = contains(IM, "rb_test_info_msg"),
    true = contains(IR, "rb_test_info"),
    true = contains(EM, "rb_test_error_msg"),
    true = contains(ER, "rb_test_error"),
    ok.

filter_filter(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Insert some reports in the error log and start rb
    init_error_logs(),
    ok = start_rb(OutFile),

    All = check_report(fun() -> rb:show() end,OutFile),

    ER = [_] = rb_filter([{rb_SUITE,rb_test_error}],OutFile),
    [] = rb_filter([{rb_SUITE,rb_test}],OutFile),
    _E = [_,_] = rb_filter([{rb_SUITE,"rb_test",re}],OutFile),
    AllButER = rb_filter([{rb_SUITE,rb_test_error,no}],OutFile),

    {_,AllRep} = lists:unzip(All),
    {_,ERRep} = lists:unzip(ER),
    {_,AllButERRep} = lists:unzip(AllButER),
    AllButERRep = AllRep -- ERRep,

    ok.

filter_date(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),


    %% Insert some reports in the error log and start rb
    init_error_logs(),
    Between1 = calendar:local_time(),
    timer:sleep(1000),
    Between2 = calendar:local_time(),
    ok = start_rb(OutFile),

    All = check_report(fun() -> rb:show() end,OutFile),

    Before = calendar:gregorian_seconds_to_datetime(
	       calendar:datetime_to_gregorian_seconds(calendar:local_time()) - 10),
    After = calendar:gregorian_seconds_to_datetime(
	      calendar:datetime_to_gregorian_seconds(calendar:local_time()) + 1),

    All = rb_filter([],{Before,from},OutFile),
    All = rb_filter([],{After,to},OutFile),
    [] = rb_filter([],{Before,to},OutFile),
    [] = rb_filter([],{After,from},OutFile),
    All = rb_filter([],{Before,After},OutFile),

    %%?t:format("~p~n",[All]),
    AllButLast = [{N-1,R} || {N,R} <- tl(All)],
    AllButLast = rb_filter([],{Before,Between1},OutFile),

    Last = hd(All),
    [Last] = rb_filter([],{Between2,After},OutFile),

    ok.

filter_filter_and_date(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),


    %% Insert some reports in the error log and start rb
    init_error_logs(),
    Between1 = calendar:local_time(),
    timer:sleep(1000),
    Between2 = calendar:local_time(),
    error_logger:error_report([{rb_SUITE,rb_test_filter}]),
    ok = start_rb(OutFile),

    Before = calendar:gregorian_seconds_to_datetime(
	       calendar:datetime_to_gregorian_seconds(calendar:local_time()) - 10),
    After = calendar:gregorian_seconds_to_datetime(
	      calendar:datetime_to_gregorian_seconds(calendar:local_time()) + 1),

    All = check_report(fun() -> rb:show() end,OutFile),
    Last = hd(All),

    [_,_,_] = rb_filter([{rb_SUITE,"rb_test",re}],{Before,After},OutFile),
    [_,_] = rb_filter([{rb_SUITE,"rb_test",re}],{Before,Between1},OutFile),
    [_] = rb_filter([{rb_SUITE,"rb_test",re}],{Between2,After},OutFile),
    [_] = rb_filter([{rb_SUITE,rb_test_filter}],{Before,After},OutFile),
    [] = rb_filter([{rb_SUITE,rb_test_filter}],{Before,Between1},OutFile),
    [Last] = rb_filter([{rb_SUITE,rb_test_filter,no}],{Between2,After},OutFile),
    {_,Str} = Last,
    false = contains(Str,"rb_test_filter"),

    ok.


filter_re_no(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Insert some reports in the error log and start rb
    init_error_logs(),
    ok = start_rb(OutFile),

    All = check_report(fun() -> rb:show() end,OutFile),

    E = [_,_] = rb_filter([{rb_SUITE,"rb_test",re}],OutFile),
    AllButE = rb_filter([{rb_SUITE,"rb_test",re,no}],OutFile),

    {_,AllRep} = lists:unzip(All),
    {_,ERep} = lists:unzip(E),
    {_,AllButERep} = lists:unzip(AllButE),
    AllButERep = AllRep -- ERep,

    ok.


rescan(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),

    %% Start rb
    ok = start_rb(OutFile),

    %% Insert one more report and check that the list is longer. Note
    %% that there might be two more reports, since the progress report
    %% from starting rb_server might not be included before the rescan.
    AllBefore = capture(fun() -> rb:list() end),
    error_logger:error_report([{rb_SUITE,rb_test_rescan}]),
    ok = rb:rescan(),
    AllAfter = capture(fun() -> rb:list() end),
    Diff = length(AllAfter) - length(AllBefore),
    true = (Diff >= 1),

    ok.


start_stop_log(Config) ->
    PrivDir = ?config(priv_dir,Config),
    OutFile = filename:join(PrivDir,"rb_SUITE_log.txt"),
    ok = file:write_file(OutFile,[]),

    %% Start rb and check that show is printed to standard_io
    ok = start_rb(),
    StdioResult = [_|_] = capture(fun() -> rb:show(1) end),
    {ok,<<>>} = file:read_file(OutFile),

    %% Start log and check that show is printed to log and not to standard_io
    ok = rb:start_log(OutFile),
    [] = capture(fun() -> rb:show(1) end),
    {ok,Bin} = file:read_file(OutFile),
    true = (Bin =/= <<>>),

    %% Start log with atom standard_io and check that show is printed to standard_io
    ok = rb:stop_log(),
    ok = file:write_file(OutFile,[]),
    ok = rb:start_log(standard_io),
    StdioResult = [_|_] = capture(fun() -> rb:show(1) end),
    {ok,<<>>} = file:read_file(OutFile),

    %% Start log and check that show is printed to iodevice log and not to standard_io
    ok = rb:stop_log(),
    ok = file:write_file(OutFile,[]),
    {ok, IoOutFile} = file:open(OutFile,[write]),
    ok = rb:start_log(IoOutFile),
    [] = capture(fun() -> rb:show(1) end),
    {ok,Bin} = file:read_file(OutFile),
    true = (Bin =/= <<>>),
    ok = file:close(IoOutFile),

    %% Stop log and check that show is printed to standard_io and not to log
    ok = rb:stop_log(),
    ok = file:write_file(OutFile,[]),
    StdioResult = capture(fun() -> rb:show(1) end),
    {ok,<<>>} = file:read_file(OutFile),

    %% Start log and check that list is printed to log and not to standard_io
    ok = file:write_file(OutFile,[]),
    ok = rb:start_log(OutFile),
    [] = capture(fun() -> rb:log_list() end),
    {ok,Bin2} = file:read_file(OutFile),
    true = (Bin2 =/= <<>>),

    %% Stop log and check that list is printed to standard_io and not to log
    ok = rb:stop_log(),
    ok = file:write_file(OutFile,[]),
    StdioResult2 = capture(fun() -> rb:log_list() end),
    {ok,<<>>} = file:read_file(OutFile),
    
    %% Test that standard_io is used if log file can not be opened
    ok = rb:start_log(filename:join(nonexistingdir,"newfile.txt")),
    StdioResult = capture(fun() -> rb:show(1) end),
    {ok,<<>>} = file:read_file(OutFile),

    ok.


%%%-----------------------------------------------------------------
%%% INTERNAL FUNCTIONS

restart_sasl() ->
    application:stop(sasl),
    ok = application:start(sasl),
    wait_for_sasl().

reset_sasl(Config) ->
    application:unset_env(sasl,error_logger_mf_dir),
    application:unset_env(sasl,error_logger_mf_maxbytes),
    application:unset_env(sasl,error_logger_mf_maxfiles),
    empty_error_logs(Config).

empty_error_logs(Config) ->
    application:stop(sasl),
    catch delete_content(?config(rb_dir, Config)),
    ok = application:start(sasl),
    wait_for_sasl().

wait_for_sasl() ->
    wait_for_sasl(50).
wait_for_sasl(0) ->
    ?t:fail("sasl application did not start within 5 seconds");
wait_for_sasl(N) ->
    case lists:keymember(sasl,1,application:which_applications()) of
	true ->
	    ok;
	false ->
	    timer:sleep(100),
	    wait_for_sasl(N-1)
    end.

start_rb(OutFile) ->
    do_start_rb([{start_log,OutFile}]).
start_rb() ->
    do_start_rb([]).

do_start_rb(Opts) ->
    {ok,Pid} = rb:start(Opts),

    %% Wait for process to started, then wait a little bit more
    sys:get_status(Pid),
    timer:sleep(500),

    %% Make sure printouts (e.g. from rb:list(), come to the test log,
    %% and that they can be captured.
    group_leader(group_leader(),Pid),
    ok.


delete_tree(Dir) ->
    case filelib:is_dir(Dir) of
	true ->
	    delete_content(Dir),
	    file:del_dir(Dir);
	false ->
	    ok = file:delete(Dir)
    end.

delete_content(Dir) ->
    {ok,Files} = file:list_dir(Dir),
    lists:foreach(fun(File) -> delete_tree(filename:join(Dir,File)) end, 
		  Files).

init_error_logs() ->        
    error_logger:error_report([{rb_SUITE,rb_test_error}]),
    error_logger:error_msg("rb_test_error_msg"),
    error_logger:info_report([{rb_SUITE,rb_test_info}]),
    error_logger:info_msg("rb_test_info_msg"),
    _Pid = start(),
    Ref = erlang:monitor(process,?MODULE),
    gen_server:cast(?MODULE,crash),
    receive {'DOWN',Ref,process,_,{rb_SUITE,rb_test_crash}} -> ok
    after 2000 ->
	    ?t:format("Got: ~p~n",[process_info(self(),messages)]),
	    ?t:fail("rb_SUITE server never died")
    end,
    erlang:demonitor(Ref),
    wait_for_server(),
    ok.

wait_for_server() ->
    case whereis(?MODULE) of
	undefined ->
	    wait_for_server();
	Pid ->
	    timer:sleep(100), % allow the supervisor report to be written
	    Pid
    end.

capture(Fun) ->
    ?t:capture_start(),
    ok = Fun(),
    timer:sleep(1000),
    ?t:capture_stop(),
    string:tokens(lists:append(?t:capture_get()),"\n").


rb_filter(Filter,OutFile) ->
    check_report(fun() -> rb:filter(Filter) end, OutFile).
rb_filter(Filter,Dates,OutFile) ->
    check_report(fun() -> rb:filter(Filter,Dates) end, OutFile).


%% This function first empties the given report file, then executes
%% the fun and returns a list of {N,Report}, where Report is a report
%% read from the file and N is an integer. The newest report has the
%% lowest number.
%% If the fun was a call to rb:show() (i.e. with no arguments), then
%% the numbering (N) will be the same as rb's own numbering (as shown
%% by rb:list()).
check_report(Fun,File) ->
    file:delete(File),
    rb:rescan([{start_log,File}]),
    ok = Fun(),
    {ok,Bin} = file:read_file(File),
    Reports = split_reports(binary_to_list(Bin),[],[]),
    lists:zip(lists:seq(1,length(Reports)),Reports).

-define(report_header_line,"\n===============================================================================\n").
split_reports([],Report,Reports) ->
    add_report(Report,Reports);
split_reports(Text,Report,Reports) ->
    case Text of
	?report_header_line++Rest ->
	    {Heading,PrevReport} = lists:splitwith(fun($\n) -> false;
						      (_) -> true
						   end,
						   Report),
	    split_reports(Rest,
			  ?report_header_line++Heading,
			  add_report(PrevReport,Reports));
	[Ch|Rest] ->
	    split_reports(Rest,[Ch|Report],Reports)
    end.

add_report(Report,Reports) ->    
    case string:strip(Report,both,$\n) of
	[] -> Reports;
	Report1 -> [lists:reverse(Report1)|Reports]
    end.

%% Returns true if Substr is a substring of Str.
contains(Str,Substr) ->
    0 =/= string:str(Str,Substr).

%% Sort the result of rb_list after report type
sort_list(List) ->
    sort_list(List,dict:new()).
sort_list([H|T],D) ->
    case re:run(H,"\s+[0-9]+\s+([a-z_]+)",[{capture,all_but_first,list}]) of
	nomatch ->
	    sort_list(T,D);
	{match,[TypeStr]} ->
	    sort_list(T,dict:append(list_to_atom(TypeStr),H,D))
    end;
sort_list([],D) ->
    lists:sort(dict:to_list(D)).


%%%-----------------------------------------------------------------
%%% A dummy supervisor and gen_server used for creating crash- and
%%% supervisor reports
start() ->
    {ok,Pid} =
	supervisor:start_link({local, ?SUP}, ?MODULE, i_am_supervisor),
    unlink(Pid),
    Pid.
start_server() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, i_am_server, []).
init(i_am_server) ->
    {ok, state};
init(i_am_supervisor) ->
    AChild = {?SUP,{?MODULE,start_server,[]},
	      permanent,2000,worker,[?MODULE]},
    {ok,{{one_for_all,1,1}, [AChild]}}.
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.
handle_cast(crash, State) ->
    exit({rb_SUITE,rb_test_crash}),
    {noreply, State}.
handle_info(_Info, State) ->
    {noreply, State}.
terminate(_Reason, _State) ->
    ok.

