%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2007-2010. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%

-module(tftp_test_lib).

-compile(export_all).

-include("tftp_test_lib.hrl").

%%
%% -----
%%

init_per_testcase(_Case, Config) when is_list(Config) ->
    io:format("\n ", []),
    ?IGNORE(application:stop(inets)),   
    Config.

end_per_testcase(_Case, Config) when is_list(Config) ->
    ?IGNORE(application:stop(inets)),   
    Config.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Infrastructure for test suite
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

error(Actual, Mod, Line) ->
    (catch global:send(tftp_global_logger, {failed, Mod, Line})),
    log("<ERROR> Bad result: ~p\n", [Actual], Mod, Line),
    Label = lists:concat([Mod, "(", Line, ") unexpected result"]),
    et:report_event(60, Mod, Mod, Label,
			[{line, Mod, Line}, {error, Actual}]),
    case global:whereis_name(tftp_test_case_sup) of
	undefined -> 
	    ignore;
	Pid -> 
	    Fail = #'REASON'{mod = Mod, line = Line, desc = Actual},
	    Pid ! {fail, self(), Fail}
    end,
    Actual.

log(Format, Args, Mod, Line) ->
    case global:whereis_name(tftp_global_logger) of
	undefined ->
	    io:format(user, "~p(~p): " ++ Format, 
		      [Mod, Line] ++ Args);
	Pid ->
	    io:format(Pid, "~p(~p): " ++ Format, 
		      [Mod, Line] ++ Args)
    end.

default_config() ->
    [].

t() -> 
    t([{?MODULE, all}]).

t(Cases) ->
    t(Cases, default_config()).

t(Cases, Config) ->
    process_flag(trap_exit, true),
    Res = lists:flatten(do_test(Cases, Config)),
    io:format("Res: ~p\n", [Res]),
    display_result(Res),
    Res.

do_test({Mod, Fun}, Config) when is_atom(Mod), is_atom(Fun) ->
    case catch apply(Mod, Fun, [suite]) of
	[] ->
	    io:format("Eval:   ~p:", [{Mod, Fun}]),
	    Res = eval(Mod, Fun, Config),
	    {R, _, _} = Res,
	    io:format(" ~p\n", [R]),
	    Res;

	Cases when is_list(Cases) ->
	    io:format("Expand: ~p ...\n", [{Mod, Fun}]),
	    Map = fun(Case) when is_atom(Case)-> {Mod, Case};
		     (Case) -> Case
		  end,
	    do_test(lists:map(Map, Cases), Config);

        {req, _, {conf, Init, Cases, Finish}} ->
	    case (catch apply(Mod, Init, [Config])) of
		Conf when is_list(Conf) ->
		    io:format("Expand: ~p ...\n", [{Mod, Fun}]),
		    Map = fun(Case) when is_atom(Case)-> {Mod, Case};
			     (Case) -> Case
			  end,
		    Res = do_test(lists:map(Map, Cases), Conf),
		    (catch apply(Mod, Finish, [Conf])),
		    Res;
		    
		{'EXIT', {skipped, Reason}} ->
		    io:format(" => skipping: ~p\n", [Reason]),
		    [{skipped, {Mod, Fun}, Reason}];
		    
		Error ->
		    io:format(" => failed: ~p\n", [Error]),
		    [{failed, {Mod, Fun}, Error}]
	    end;
		    
        {'EXIT', {undef, _}} ->
	    io:format("Undefined:   ~p\n", [{Mod, Fun}]),
	    [{nyi, {Mod, Fun}, ok}];
		    
        Error ->
	    io:format("Ignoring:   ~p: ~p\n", [{Mod, Fun}, Error]),
	    [{failed, {Mod, Fun}, Error}]
    end;
do_test(Mod, Config) when is_atom(Mod) ->
    Res = do_test({Mod, all}, Config),
    Res;
do_test(Cases, Config) when is_list(Cases) ->
    [do_test(Case, Config) || Case <- Cases];
do_test(Bad, _Config) ->
    [{badarg, Bad, ok}].

eval(Mod, Fun, Config) ->
    TestCase = {?MODULE, Mod, Fun},
    Label = lists:concat(["TEST CASE: ", Fun]),
    et:report_event(40, ?MODULE, Mod, Label ++ " started",
			[TestCase, Config]),
    global:register_name(tftp_test_case_sup, self()),
    Flag = process_flag(trap_exit, true),
    Config2 = Mod:init_per_testcase(Fun, Config),
    Pid = spawn_link(?MODULE, do_eval, [self(), Mod, Fun, Config2]),
    R = wait_for_evaluator(Pid, Mod, Fun, Config2, []),
    Mod:end_per_testcase(Fun, Config2),
    global:unregister_name(tftp_test_case_sup),
    process_flag(trap_exit, Flag),
    R.

wait_for_evaluator(Pid, Mod, Fun, Config, Errors) ->
    TestCase = {?MODULE, Mod, Fun},
    Label = lists:concat(["TEST CASE: ", Fun]),
    receive
	{done, Pid, ok} when Errors == [] ->
	    et:report_event(40, Mod, ?MODULE, Label ++ " ok",
				[TestCase, Config]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, {ok, _}} when Errors == [] ->
	    et:report_event(40, Mod, ?MODULE, Label ++ " ok",
				[TestCase, Config]),
	    {ok, {Mod, Fun}, Errors};
	{done, Pid, Fail} ->
	    et:report_event(20, Mod, ?MODULE, Label ++ " failed",
				[TestCase, Config, {return, Fail}, Errors]),
	    {failed, {Mod,Fun}, Fail};
	{'EXIT', Pid, {skipped, Reason}} -> 
	    et:report_event(20, Mod, ?MODULE, Label ++ " skipped",
				[TestCase, Config, {skipped, Reason}]),
	    {skipped, {Mod, Fun}, Errors};
	{'EXIT', Pid, Reason} -> 
	    et:report_event(20, Mod, ?MODULE, Label ++ " crashed",
				[TestCase, Config, {'EXIT', Reason}]),
	    {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors]};
	{fail, Pid, Reason} ->
	    wait_for_evaluator(Pid, Mod, Fun, Config, Errors ++ [Reason])
    end.

do_eval(ReplyTo, Mod, Fun, Config) ->
    case (catch apply(Mod, Fun, [Config])) of
	{'EXIT', {skipped, Reason}} ->
	    ReplyTo ! {'EXIT', self(), {skipped, Reason}};
	Other ->
	    ReplyTo ! {done, self(), Other}
    end,
    unlink(ReplyTo),
    exit(shutdown).

display_result([]) ->    
    io:format("OK\n", []);
display_result(Res) when is_list(Res) ->
    Ok      = [MF || {ok, MF, _}  <- Res],
    Nyi     = [MF || {nyi, MF, _} <- Res],
    Skipped = [{MF, Reason} || {skipped, MF, Reason} <- Res],
    Failed  = [{MF, Reason} || {failed, MF, Reason} <- Res],
    Crashed = [{MF, Reason} || {crashed, MF, Reason} <- Res],
    display_summary(Ok, Nyi, Skipped, Failed, Crashed),
    display_skipped(Skipped),
    display_failed(Failed),
    display_crashed(Crashed).

display_summary(Ok, Nyi, Skipped, Failed, Crashed) ->
    io:format("\nTest case summary:\n", []),
    display_summary(Ok,      "successful"),
    display_summary(Nyi,     "not yet implemented"),
    display_summary(Skipped, "skipped"),
    display_summary(Failed,  "failed"),
    display_summary(Crashed, "crashed"),
    io:format("\n", []).
   
display_summary(Res, Info) ->
    io:format("  ~w test cases ~s\n", [length(Res), Info]).
    
display_skipped([]) ->
    ok;
display_skipped(Skipped) ->
    io:format("Skipped test cases:\n", []),
    F = fun({MF, Reason}) -> io:format("  ~p => ~p\n", [MF, Reason]) end,
    lists:foreach(F, Skipped),
    io:format("\n", []).
    

display_failed([]) ->
    ok;
display_failed(Failed) ->
    io:format("Failed test cases:\n", []),
    F = fun({MF, Reason}) -> io:format("  ~p => ~p\n", [MF, Reason]) end,
    lists:foreach(F, Failed),
    io:format("\n", []).

display_crashed([]) ->
    ok;
display_crashed(Crashed) ->
    io:format("Crashed test cases:\n", []),
    F = fun({MF, Reason}) -> io:format("  ~p => ~p\n", [MF, Reason]) end,
    lists:foreach(F, Crashed),
    io:format("\n", []).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% generic callback
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-record(generic_state, {state, prepare, open, read, write, abort}).

prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
    State   = lookup_option(state,   mandatory, Initial),
    Prepare = lookup_option(prepare, mandatory, Initial),
    Open    = lookup_option(open,    mandatory, Initial),
    Read    = lookup_option(read,    mandatory, Initial),
    Write   = lookup_option(write,   mandatory, Initial),
    Abort   = lookup_option(abort,   mandatory, Initial),
    case Prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of
	{ok, AcceptedOptions, NewState} ->
	    {ok,
	     AcceptedOptions,
	     #generic_state{state   = NewState,
			    prepare = Prepare,
			    open    = Open,
			    read    = Read,
			    write   = Write,
			    abort   = Abort}};
	Other ->
	    Other
    end.

open(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
    case prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) of
 	{ok, SuggestedOptions2, GenericState} ->
	    open(Peer, Access, LocalFilename, Mode, SuggestedOptions2, GenericState);
	Other ->
	    Other
    end;
open(Peer, Access, LocalFilename, Mode, SuggestedOptions, #generic_state{state = State, open = Open} = GenericState) ->
    case Open(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of
	{ok, SuggestedOptions2, NewState} ->
	    {ok, SuggestedOptions2, GenericState#generic_state{state = NewState}};
	Other ->
	    Other
    end.

read(#generic_state{state = State, read = Read} = GenericState) ->
    case Read(State) of
	{more, DataBlock, NewState} ->
	    {more, DataBlock, GenericState#generic_state{state = NewState}};
	Other ->
	    Other
    end.

write(DataBlock, #generic_state{state = State, write = Write} = GenericState) ->
    case Write(DataBlock, State) of
	{more, NewState} ->
	    {more, GenericState#generic_state{state = NewState}};
	Other ->
	    Other
    end.

abort(Code, Text, #generic_state{state = State, abort = Abort}) ->
    Abort(Code, Text, State).

lookup_option(Key, Default, Options) ->
    case lists:keysearch(Key, 1, Options) of
	{value, {_, Val}} ->
	    Val;
	false ->
	    Default
    end.