%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1997-2017. 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(time_SUITE).
-compile({nowarn_deprecated_function, {erlang,now,0}}).

%% "Time is on my side." -- The Rolling Stones

%% Tests the BIFs:
%%	erlang:localtime_to_universaltime/1
%%	erlang:universaltime_to_localtime/1
%%	date/0
%%	time/0
%%	now/0
%%

-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
	 init_per_group/2,end_per_group/2, univ_to_local/1, local_to_univ/1,
	 bad_univ_to_local/1, bad_local_to_univ/1,
	 univ_to_seconds/1, seconds_to_univ/1,
	 consistency/1,
	 now_unique/1, now_update/1, timestamp/1,
	 time_warp_modes/1,
	 monotonic_time_monotonicity/1,
	 monotonic_time_monotonicity_parallel/1,
	 time_unit_conversion/1,
	 signed_time_unit_conversion/1,
	 erlang_timestamp/1]).

-export([init_per_testcase/2, end_per_testcase/2]).

-export([local_to_univ_utc/1]).

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

-export([linear_time/1]).

%% The following defines the timezone in which the test is run.
%% It is interpreted as the number of hours to be added to UTC
%% to obtain the local time.  The number will be positive east
%% of Greenwhich, negative west of Greenwhich.
%%
%% Allowable range is -12 through 11.

-define(timezone, 1).

%% Similarly to timezone, but the difference when Daylight Saving Time
%% is in use.  [Same range.]

-define(dst_timezone, 2).

init_per_testcase(Func, Config) when is_atom(Func), is_list(Config) ->
    [{testcase, Func}|Config].

end_per_testcase(_Func, _Config) ->
    ok.

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

all() -> 
    [univ_to_local, local_to_univ, local_to_univ_utc,
     bad_univ_to_local, bad_local_to_univ, 
     univ_to_seconds, seconds_to_univ,
     consistency,
     {group, now}, timestamp,
     time_warp_modes,
     monotonic_time_monotonicity,
     monotonic_time_monotonicity_parallel,
     time_unit_conversion,
     signed_time_unit_conversion,
     erlang_timestamp].

groups() -> 
    [{now, [], [now_unique, now_update]}].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.


%% Test that DST = true on timezones without DST is ignored
local_to_univ_utc(Config) when is_list(Config) ->
    case os:type() of
	{unix,_} ->
	    %% TZ variable has a meaning
	    {ok, Node} =
		test_server:start_node(local_univ_utc,peer,
				       [{args, "-env TZ UTC"}]),
	    {{2008,8,1},{0,0,0}} =
		rpc:call(Node,
			 erlang,localtime_to_universaltime,
			 [{{2008, 8, 1}, {0, 0, 0}},
			  false]),
	    {{2008,8,1},{0,0,0}} =
		rpc:call(Node,
			 erlang,localtime_to_universaltime,
			 [{{2008, 8, 1}, {0, 0, 0}},
			  true]),
	    [{{2008,8,1},{0,0,0}}] =
		rpc:call(Node,
			 calendar,local_time_to_universal_time_dst,
			 [{{2008, 8, 1}, {0, 0, 0}}]),
	    test_server:stop_node(Node),
	    ok;
	_ ->
	    {skip,"Only valid on Unix"}
    end.


%% Tests conversion from universal to local time.

univ_to_local(Config) when is_list(Config) ->
    test_univ_to_local(test_data()).

test_univ_to_local([{Utc, Local}|Rest]) ->
    io:format("Testing ~p => ~p~n", [Local, Utc]),
    Local = erlang:universaltime_to_localtime(Utc),
    test_univ_to_local(Rest);
test_univ_to_local([]) ->
    ok.

%% Tests conversion from local to universal time.

local_to_univ(Config) when is_list(Config) ->
    test_local_to_univ(test_data()).

test_local_to_univ([{Utc, Local}|Rest]) ->
    io:format("Testing ~p => ~p~n", [Utc, Local]),
    Utc = erlang:localtime_to_universaltime(Local),
    test_local_to_univ(Rest);
test_local_to_univ([]) ->
    ok.

%% Test bad arguments to erlang:universaltime_to_localtime; should
%% generate a badarg.

bad_univ_to_local(Config) when is_list(Config) ->
    bad_test_univ_to_local(bad_dates()).

bad_test_univ_to_local([Utc|Rest]) ->
    io:format("Testing ~p~n", [Utc]),
    case catch erlang:universaltime_to_localtime(Utc) of
	      {'EXIT', {badarg, _}} -> bad_test_univ_to_local(Rest)
	  end;
bad_test_univ_to_local([]) ->
    ok.

%% Test bad arguments to erlang:localtime_to_universaltime/1; should
%% generate a badarg.

bad_local_to_univ(Config) when is_list(Config) ->
    bad_test_local_to_univ(bad_dates()).

bad_test_local_to_univ([Local|Rest]) ->
    io:format("Testing ~p~n", [Local]),
    case catch erlang:localtime_to_universaltime(Local) of
	      {'EXIT', {badarg, _}} -> bad_test_local_to_univ(Rest)
	  end;
bad_test_local_to_univ([]) ->
    ok.


%% Test universaltime to seconds conversions
univ_to_seconds(Config) when is_list(Config) ->
    test_univ_to_seconds(ok_utc_seconds()).

test_univ_to_seconds([{Datetime, Seconds}|DSs]) ->
    io:format("universaltime = ~p -> seconds = ~p", [Datetime, Seconds]),
    Seconds = erlang:universaltime_to_posixtime(Datetime),
    test_univ_to_seconds(DSs);
test_univ_to_seconds([]) -> 
    ok.

%% Test seconds to universaltime conversions
seconds_to_univ(Config) when is_list(Config) ->
    test_seconds_to_univ(ok_utc_seconds()).

test_seconds_to_univ([{Datetime, Seconds}|DSs]) ->
    io:format("universaltime = ~p <- seconds = ~p", [Datetime, Seconds]),
    Datetime = erlang:posixtime_to_universaltime(Seconds),
    test_seconds_to_univ(DSs);
test_seconds_to_univ([]) -> 
    ok.


%% Test that the the different time functions return
%% consistent results.
consistency(_Config) ->
    %% Test that:
    %% 	 * date() & time() gives the same time as erlang:localtime()
    %%
    %%   * the difference between erlang:universaltime() and
    %%     erlang:localtime() is reasonable (with assuming any
    %%     particular timezone)

    ok = compare_date_time_and_localtime(16),
    compare_local_and_universal(16).

compare_date_time_and_localtime(Times) when Times > 0 ->
    {Year, Mon, Day} = date(),
    {Hour, Min, Sec} = time(),
    case erlang:localtime() of
	{{Year, Mon, Day}, {Hour, Min, Sec}} -> ok;
	_ -> compare_date_time_and_localtime(Times-1)
    end;
compare_date_time_and_localtime(0) ->
    error.

compare_local_and_universal(Times) when Times > 0 ->
    Utc = erlang:universaltime(),
    Local = erlang:localtime(),
    io:format("local = ~p, utc = ~p", [Local,Utc]),

    AcceptableDiff = 14*3600,
    case linear_time(Utc) - linear_time(Local) of
	Diff when abs(Diff) < AcceptableDiff ->
	    ok;
	Diff ->
	    io:format("More than ~p seconds difference betwen "
		      "local and universal time", [Diff]),
	    ct:fail(huge_diff)
    end.

%% This function converts a date and time to a linear time.
%% Two linear times can be subtracted to give their difference
%% in seconds.
%%
%% XXX Limitations: Simplified leap year calc will fail for 2100 :-)

linear_time({{Year, Mon, Day}, {Hour, Min, Sec}}) ->
    86400*(year_to_days(Year) + month_to_days(Year,Mon) + (Day-1)) +
	3600*Hour + 60*Min + Sec.

year_to_days(Year) ->
    Year * 365 + (Year-1) div 4.

month_to_days(Year, Mon) ->
    DoM = [31,days_in_february(Year),31,30,31,30,31,31,30,31,30,31],
    {PastMonths,_} = lists:split(Mon-1, DoM),
    lists:sum(PastMonths).

days_in_february(Year) ->
    case (Year rem 4) of
	0 -> 29;
	_ -> 28
    end.

%% Test (the bif) os:timestamp/0, which is something quite like, but not
%% similar to erlang:now...

%% Test that os:timestamp works.
timestamp(Config) when is_list(Config) ->
    try
	repeating_timestamp_check(100000)
    catch
	throw : {fail, Failure} ->
	    %%
	    %% Our time warping test machines currently warps
	    %% time every 6:th second. If we get a warp during
	    %% 10 seconds, assume this is a time warping test
	    %% and ignore the failure.
	    %%
	    case had_time_warp(10) of
		true ->
		    {skip, "Seems to be time warp test run..."};
		false ->
		    ct:fail(Failure)
	    end
    end.

os_system_time_offset() ->
    erlang:convert_time_unit(os:system_time() - erlang:monotonic_time(),
			     native, microsecond).

had_time_warp(Secs) ->
    had_time_warp(os_system_time_offset(), Secs).

had_time_warp(_OrigOffs, 0) ->
    false;
had_time_warp(OrigOffs, N) ->
    receive after 1000 -> ok end,
    case OrigOffs - os_system_time_offset() of
	Diff when Diff > 500000; Diff < -500000 ->
	    true;
	_Diff ->
	    had_time_warp(OrigOffs, N-1)
    end.

repeating_timestamp_check(0) ->
    ok;
repeating_timestamp_check(N) ->
    {A,B,C} = TS = os:timestamp(),
    if
	is_integer(A),
	is_integer(B),
	is_integer(C),
	B < 1000000,
	C < 1000000 ->
	    ok;
	true ->
	    ct:fail("Strange return from os:timestamp/0 ~w~n",[TS])
    end,
    %% I assume the now and timestamp should not differ more than 1 hour,
    %% which is safe assuming the system has not had a large time-warp
    %% during the testrun...
    Secs = A*1000000+B+round(C/1000000),
    {NA,NB,NC} = erlang:now(),
    NSecs = NA*1000000+NB+round(NC/1000000),
    case Secs - NSecs of
	TooLarge when TooLarge > 3600 ->
	    throw({fail,
	       lists:flatten(
		io_lib:format("os:timestamp/0 is ~w s more than erlang:now/0",
			     [TooLarge]))});
	TooSmall when TooSmall < -3600 ->
	    throw({fail,
	      lists:flatten(
		io_lib:format("os:timestamp/0 is ~w s less than erlang:now/0",
			     [-TooSmall]))});
	_ ->
	    ok
    end,
    repeating_timestamp_check(N-1).
	    

%% Test now/0.


%% Tests that successive calls to now/0 returns different values.
%% Also returns a comment string with the median difference between
%% times (in microseconds).

now_unique(Config) when is_list(Config) ->
    now_unique(1000, now(), []),
    fast_now_unique(100000, now()).

now_unique(N, Previous, Result) when N > 0 ->
    case now() of
	      Previous ->
		  ct:fail("now/0 returned the same value twice");
	      New ->
		  now_unique(N-1, New, [New|Result])
	  end;
now_unique(0, _, [Then|Rest]) ->
    now_calc_increment(Rest, microsecs(Then), []).

now_calc_increment([Then|Rest], Previous, _Result) ->
    This = microsecs(Then),
    now_calc_increment(Rest, This, [Previous-This]);
now_calc_increment([], _, Differences) ->
    {comment, "Median increment: " ++ integer_to_list(median(Differences))}.

fast_now_unique(0, _) -> ok;
fast_now_unique(N, Then) ->
    case now() of
	Then ->
	    ct:fail("now/0 returned the same value twice");
	Now ->
	    fast_now_unique(N-1, Now)
    end.

median(Unsorted_List) ->
    Length = length(Unsorted_List),
    List = lists:sort(Unsorted_List),
    case Length rem 2 of
	0 ->					% Even length.
	    [A, B] = lists:nthtail((Length div 2)-1, List),
	    (A+B)/2;
	1 ->					% Odd list length.
	    lists:nth((Length div 2)+1, List)
    end.

microsecs({Mega_Secs, Secs, Microsecs}) ->
    (Mega_Secs*1000000+Secs)*1000000+Microsecs.

%% Test that the time differences returned by two calls to
%% now/0 one second apart is comparable to the difference of two
%% calls to erlang:localtime().

now_update(Config) when is_list(Config) ->
    case test_server:is_debug() of
	false -> now_update1(10);
	true -> {skip,"Unreliable in DEBUG build"}
    end.


now_update1(N) when N > 0 ->
    T1_linear = linear_time(erlang:localtime()),
    T1_now = microsecs(now()),

    receive after 1008 -> ok end,

    T2_linear = linear_time(erlang:localtime()),
    T2_now = microsecs(now()),

    Linear_Diff = (T2_linear-T1_linear)*1000000,
    Now_Diff = T2_now-T1_now,
    io:format("Localtime diff = ~p; now() diff = ~p", [Linear_Diff, Now_Diff]),
    case abs(Linear_Diff - Now_Diff) of
	      Abs_Delta when Abs_Delta =< 40000 -> ok;
	      _ -> now_update1(N-1)
	  end;
now_update1(0) ->
    ct:fail("now_update zero").

time_warp_modes(Config) when is_list(Config) ->
    %% All time warp modes always supported in
    %% combination with no time correction...
    check_time_warp_mode(Config, false, no_time_warp),
    check_time_warp_mode(Config, false, single_time_warp),
    check_time_warp_mode(Config, false, multi_time_warp),

    erts_debug:set_internal_state(available_internal_state, true),
    try
	case erts_debug:get_internal_state({check_time_config,
					    true, no_time_warp}) of
	    false -> ok;
	    true -> check_time_warp_mode(Config, true, no_time_warp)
	end,
	case erts_debug:get_internal_state({check_time_config,
					    true, single_time_warp}) of
	    false -> ok;
	    true -> check_time_warp_mode(Config, true, single_time_warp)
	end,
	case erts_debug:get_internal_state({check_time_config,
					    true, multi_time_warp}) of
	    false -> ok;
	    true -> check_time_warp_mode(Config, true, multi_time_warp)
	end
    after
	erts_debug:set_internal_state(available_internal_state, false)
    end.

check_time_warp_mode(Config, TimeCorrection, TimeWarpMode) ->
    io:format("~n~n~n***** Testing TimeCorrection=~p TimeWarpMode=~p *****~n",
	      [TimeCorrection, TimeWarpMode]),
    Mon = erlang:monitor(time_offset, clock_service),
    _ = erlang:time_offset(),
    Start = erlang:monotonic_time(1000),
    MonotonicityTimeout = 2000,
    {ok, Node} = start_node(Config,
			    "+c " ++ atom_to_list(TimeCorrection)
			    ++ " +C " ++ atom_to_list(TimeWarpMode)),
    StartTime = rpc:call(Node, erlang, system_info, [start_time]),
    Me = self(),
    MonotincityTestStarted = make_ref(),
    MonotincityTestDone = make_ref(),
    spawn_link(Node,
	       fun () ->
		       Me ! MonotincityTestStarted,
		       cmp_times(erlang:start_timer(MonotonicityTimeout,
						    self(),
						    timeout),
				 erlang:monotonic_time()),
		       Me ! MonotincityTestDone
	       end),
    receive MonotincityTestStarted -> ok end,
    check_time_offset(Node, TimeWarpMode),
    TimeWarpMode = rpc:call(Node, erlang, system_info, [time_warp_mode]),
    TimeCorrection = rpc:call(Node, erlang, system_info, [time_correction]),
    receive MonotincityTestDone -> ok end,
    MonotonicTime = rpc:call(Node, erlang, monotonic_time, []),
    MonotonicTimeUnit = rpc:call(Node,
				       erlang,
				       convert_time_unit,
				       [1, second, native]),
    UpMilliSeconds = erlang:convert_time_unit(MonotonicTime - StartTime,
					      MonotonicTimeUnit,
					      millisecond),
    io:format("UpMilliSeconds=~p~n", [UpMilliSeconds]),
    End = erlang:monotonic_time(millisecond),
    stop_node(Node),
    try
	true = (UpMilliSeconds > (98*MonotonicityTimeout) div 100),
	true = (UpMilliSeconds < (102*(End-Start)) div 100)
    catch
	error:_ ->
	    io:format("Uptime inconsistency", []),
	    case {TimeCorrection, erlang:system_info(time_correction)} of
		{true, true} ->
		    ct:fail(uptime_inconsistency);
		{true, false} ->
		    _ = erlang:time_offset(),
		    receive
			{'CHANGE', Mon, time_offset, clock_service, _} ->
			    ignore
		    after 1000 ->
			    ct:fail(uptime_inconsistency)
		    end;
		_ ->
		    ignore
	    end
    end,
    erlang:demonitor(Mon, [flush]),
    ok.

check_time_offset(Node, no_time_warp) ->
    final = rpc:call(Node, erlang, system_info, [time_offset]),
    final = rpc:call(Node, erlang, system_flag, [time_offset, finalize]),
    final = rpc:call(Node, erlang, system_info, [time_offset]);
check_time_offset(Node, single_time_warp) ->
    preliminary = rpc:call(Node, erlang, system_info, [time_offset]),
    preliminary = rpc:call(Node, erlang, system_flag, [time_offset, finalize]),
    final = rpc:call(Node, erlang, system_info, [time_offset]),
    final = rpc:call(Node, erlang, system_flag, [time_offset, finalize]);
check_time_offset(Node, multi_time_warp) ->
    volatile = rpc:call(Node, erlang, system_info, [time_offset]),
    volatile = rpc:call(Node, erlang, system_flag, [time_offset, finalize]),
    volatile = rpc:call(Node, erlang, system_info, [time_offset]).

monotonic_time_monotonicity(Config) when is_list(Config) ->
    Done = erlang:start_timer(10000,self(),timeout),
    cmp_times(Done, erlang:monotonic_time()).

cmp_times(Done, X0) ->
    X1 = erlang:monotonic_time(),
    X2 = erlang:monotonic_time(),
    X3 = erlang:monotonic_time(),
    X4 = erlang:monotonic_time(),
    X5 = erlang:monotonic_time(),
    true = (X0 =< X1),
    true = (X1 =< X2),
    true = (X2 =< X3),
    true = (X3 =< X4),
    true = (X4 =< X5),
    receive
	{timeout, Done, timeout} ->
	    ok
    after 0 ->
	    cmp_times(Done, X5)
    end.

-define(NR_OF_MONOTONIC_CALLS, 100000).

monotonic_time_monotonicity_parallel(Config) when is_list(Config) ->
    Me = self(),
    Result = make_ref(),
    Go = make_ref(),
    UpAndRunning = make_ref(),
    NoOnlnScheds = erlang:system_info(schedulers_online),
    OffsetUI = erlang:unique_integer([monotonic]),
    OffsetMT = erlang:monotonic_time(),
    MinHSz = ?NR_OF_MONOTONIC_CALLS*(2
				     + 3
				     + erts_debug:flat_size(OffsetUI)
				     + erts_debug:flat_size(OffsetMT)),
    Ps = lists:map(
	   fun (Sched) ->
		   spawn_opt(
		     fun () ->
			     Me ! {self(), UpAndRunning},
			     receive Go -> ok end,
			     Res = fetch_monotonic(?NR_OF_MONOTONIC_CALLS, []),
			     Me ! {self(), Result, Sched, Res}
		     end,
		     [{scheduler, Sched},
		      {priority, max},
		      {min_heap_size, MinHSz}])
	   end,
	   lists:seq(1, NoOnlnScheds)),
    lists:foreach(fun (P) -> receive {P, UpAndRunning} -> ok end end, Ps),
    lists:foreach(fun (P) -> P ! Go end, Ps),
    TMs = recv_monotonics(Result, OffsetMT, OffsetUI, NoOnlnScheds, []),
    true = check_monotonic_result(TMs, OffsetMT, OffsetUI, true).

check_monotonic_result([{_Sched, _PrevUI, _MT, _PostUI}],
		       _OffsetMT, _OffsetUI, Res) ->
    Res;
check_monotonic_result([{_ASched, _APrevUI, AMT, APostUI} = A,
			{_BSched, BPrevUI, BMT, _BPostUI} = B | _] = L,
		       OffsetMT, OffsetUI, Res) ->
    NewRes = case (AMT =< BMT) orelse (BPrevUI < APostUI) of
		 true ->
		     Res;
		 false ->
		     io:format("INCONSISTENCY: ~p ~p~n", [A, B]),
		     false
	     end,
    check_monotonic_result(tl(L), OffsetMT, OffsetUI, NewRes).

recv_monotonics(_Result, _OffsetMT, _OffsetUI, 0, Acc) ->
    lists:keysort(2, Acc);
recv_monotonics(Result, OffsetMT, OffsetUI, N, Acc) ->
    receive
	{_, Result, Sched, Res} ->
	    CRes = convert_monotonic(Sched, OffsetMT, OffsetUI, Res, []),
	    recv_monotonics(Result, OffsetMT, OffsetUI, N-1, CRes ++ Acc)
    end.

convert_monotonic(_Sched, _OffsetMT, _OffsetUI, [{_MT, _UI}], Acc) ->
    Acc;
convert_monotonic(Sched, OffsetMT, OffsetUI,
		  [{MT, UI}, {_PrevMT, PrevUI} | _] = L, Acc) ->
    convert_monotonic(Sched, OffsetMT, OffsetUI, tl(L),
		      [{Sched, PrevUI-OffsetUI, MT-OffsetMT, UI-OffsetUI}
		       | Acc]).

fetch_monotonic(0, Acc) ->
    Acc;
fetch_monotonic(N, Acc) ->
    MT = erlang:monotonic_time(),
    UI = erlang:unique_integer([monotonic]),
    fetch_monotonic(N-1, [{MT, UI} | Acc]).

-define(CHK_RES_CONVS_TIMEOUT, 400).

time_unit_conversion(Config) when is_list(Config) ->
    Mon = erlang:monitor(time_offset, clock_service),
    start_check_res_convs(Mon, 1000000000000),
    start_check_res_convs(Mon, 2333333333333),
    start_check_res_convs(Mon, 5732678356789),
    erlang:demonitor(Mon, [flush]).
    
start_check_res_convs(Mon, Res) ->
    io:format("Checking ~p time_unit~n", [Res]),
    check_res_convs(Mon,
		    erlang:start_timer(?CHK_RES_CONVS_TIMEOUT,
				       self(),
				       timeout),
		    Res).
    

check_res_convs(Mon, Done, Res) ->
    receive
	{timeout, Done, timeout} ->
	    case Res div 10 of
		0 ->
		    ok;
		NewRes ->
		    start_check_res_convs(Mon, NewRes)
	    end
    after 0 ->
	    do_check_res_convs(Mon, Done, Res)
    end.

do_check_res_convs(Mon, Done, Res) ->
    TStart = erlang:monotonic_time(),
    T = erlang:monotonic_time(Res),
    TEnd = erlang:monotonic_time(),
    TMin = erlang:convert_time_unit(TStart, native, Res),
    TMax = erlang:convert_time_unit(TEnd, native, Res),
    %io:format("~p =< ~p =< ~p~n", [TMin, T, TEnd]),
    true = (TMin =< T),
    true = (TMax >= T),
    check_time_offset_res_conv(Mon, Res),
    check_res_convs(Mon, Done, Res).


check_time_offset_res_conv(Mon, Res) ->
    TORes = erlang:time_offset(Res),
    TO = erlang:time_offset(),
    case erlang:convert_time_unit(TO, native, Res) of
	TORes ->
	    ok;
	TORes2 ->
	    case check_time_offset_change(Mon, TO, 1000) of
		{TO, false} ->
		    ct:fail({time_unit_conversion_inconsistency,
			     TO, TORes, TORes2});
		{_NewTO, true} ->
		    io:format("time_offset changed", []),
		    check_time_offset_res_conv(Mon, Res)
	    end
    end.

signed_time_unit_conversion(Config) when is_list(Config) ->
    chk_strc(1000000000, 1000000),
    chk_strc(1000000000, 1000),
    chk_strc(1000000000, 1),
    chk_strc(1000000, 1000),
    chk_strc(1000000, 1),
    chk_strc(1000, 1),
    chk_strc(4711, 17),
    chk_strc(1 bsl 10, 1),
    chk_strc(1 bsl 16, 10),
    chk_strc(1 bsl 17, 1 bsl 8),
    chk_strc((1 bsl 17) + 1, (1 bsl 8) - 1),
    chk_strc(1 bsl 17, 11),
    ok.

chk_strc(Res0, Res1) ->
    case (Res0 /= Res1) andalso (Res0 =< 1000000) andalso (Res1 =< 1000000) of
	true ->
	    {FromRes, ToRes} = case Res0 > Res1 of
				   true -> {Res0, Res1};
				   false -> {Res1, Res0}
			       end,
	    MinFromValuesPerToValue = FromRes div ToRes,
	    MaxFromValuesPerToValue = ((FromRes-1) div ToRes)+1,
	    io:format("~p -> ~p [~p, ~p]~n",
		      [FromRes, ToRes,
		       MinFromValuesPerToValue, MaxFromValuesPerToValue]),
	    chk_values_per_value(FromRes, ToRes,
				 -10*FromRes, 10*FromRes,
				 MinFromValuesPerToValue,
				 MaxFromValuesPerToValue,
				 undefined, MinFromValuesPerToValue);
	_ ->
	    ok
    end,
    chk_random_values(Res0, Res1),
    chk_random_values(Res1, Res0),
    ok.

chk_random_values(FR, TR) ->
    io:format("rand values ~p -> ~p~n", [FR, TR]),
    rand:seed(exsplus, {268438039,268440479,268439161}),
    Values = lists:map(fun (_) -> rand:uniform(1 bsl 65) - (1 bsl 64) end,
		       lists:seq(1, 100000)),
    CheckFun = fun (V) ->
                       CV = erlang:convert_time_unit(V, FR, TR),
                       case {(FR*CV) div TR =< V,
                             (FR*(CV+1)) div TR >= V} of
                           {true, true} ->
                               ok;
                           Failure ->
                               ct:fail({Failure, CV, V, FR, TR})
                       end
               end,
    lists:foreach(CheckFun, Values).
		       

chk_values_per_value(_FromRes, _ToRes,
	 EndValue, EndValue,
	 MinFromValuesPerToValue, MaxFromValuesPerToValue,
	 _ToValue, FromValueCount) ->
%    io:format("~p [~p]~n", [EndValue, FromValueCount]),
    case ((MinFromValuesPerToValue =< FromValueCount)
	  andalso (FromValueCount =< MaxFromValuesPerToValue)) of
	false ->
	    ct:fail({MinFromValuesPerToValue,
		     FromValueCount,
		     MaxFromValuesPerToValue});
	true ->
	    ok
    end;
chk_values_per_value(FromRes, ToRes, Value, EndValue,
		     MinFromValuesPerToValue, MaxFromValuesPerToValue,
		     ToValue, FromValueCount) ->
    case erlang:convert_time_unit(Value, FromRes, ToRes) of
        ToValue ->
            chk_values_per_value(FromRes, ToRes,
                                 Value+1, EndValue,
                                 MinFromValuesPerToValue,
                                 MaxFromValuesPerToValue,
                                 ToValue, FromValueCount+1);
        NewToValue ->
            case ((MinFromValuesPerToValue =< FromValueCount)
                  andalso (FromValueCount =< MaxFromValuesPerToValue)) of
                false ->
                    ct:fail({MinFromValuesPerToValue,
                             FromValueCount,
                             MaxFromValuesPerToValue});
                true ->
                    %		    io:format("~p -> ~p [~p]~n",
                    %			      [Value, NewToValue, FromValueCount]),
                    chk_values_per_value(FromRes, ToRes,
                                         Value+1, EndValue,
                                         MinFromValuesPerToValue,
                                         MaxFromValuesPerToValue,
                                         NewToValue, 1)
            end
    end.

erlang_timestamp(Config) when is_list(Config) ->
    Mon = erlang:monitor(time_offset, clock_service),
    {TO, _} = check_time_offset_change(Mon,
				       erlang:time_offset(),
				       0),
    Done = erlang:start_timer(10000,self(),timeout),
    ok = check_erlang_timestamp(Done, Mon, TO).

check_erlang_timestamp(Done, Mon, TO) ->
    receive
        {timeout, Done, timeout} ->
            erlang:demonitor(Mon, [flush]),
            ok
    after 0 ->
              do_check_erlang_timestamp(Done, Mon, TO)
    end.

do_check_erlang_timestamp(Done, Mon, TO) ->
    MinMon = erlang:monotonic_time(),
    {MegaSec, Sec, MicroSec} = erlang:timestamp(),
    MaxMon = erlang:monotonic_time(),
    TsMin = erlang:convert_time_unit(MinMon+TO,
				     native,
				     microsecond),
    TsMax = erlang:convert_time_unit(MaxMon+TO,
				     native,
				     microsecond),
    TsTime = (MegaSec*1000000+Sec)*1000000+MicroSec,
    case (TsMin =< TsTime) andalso (TsTime =< TsMax) of
	true ->
	    NewTO = case erlang:time_offset() of
			TO ->
			    TO;
			_ ->
			    check_time_offset_change(Mon, TO, 0)
		    end,
	    check_erlang_timestamp(Done, Mon, NewTO);
	false ->
	    io:format("TsMin=~p TsTime=~p TsMax=~p~n", [TsMin, TsTime, TsMax]),
	    io:format("Detected inconsistency; "
		      "checking for time_offset change...", []),
	    case check_time_offset_change(Mon, TO, 1000) of
		{TO, false} ->
		    ct:fail(timestamp_inconsistency);
		{NewTO, true} ->
		    io:format("time_offset changed", []),
		    check_erlang_timestamp(Done, Mon, NewTO)
	    end
    end.

check_time_offset_change(Mon, TO, Wait) ->
    process_changed_time_offset(Mon, TO, false, Wait).

process_changed_time_offset(Mon, TO, Changed, Wait) ->
    receive
	{'CHANGE', Mon, time_offset, clock_service, NewTO} ->
	    process_changed_time_offset(Mon, NewTO, true, Wait)
    after Wait ->
	    case erlang:time_offset() of
		TO ->
		    {TO, Changed};
		_OtherTO ->
		    receive
			{'CHANGE', Mon, time_offset, clock_service, NewTO} ->
			    process_changed_time_offset(Mon, NewTO, true, Wait)
		    end
	    end
    end.
    


%% Returns the test data: a list of {Utc, Local} tuples.

test_data() ->
    {TZ,DSTTZ} = 
	case os:type() of
	    {unix,_} ->
		case os:cmd("date '+%Z'") of
		    "SAST"++_ ->
			{2,2};
		    _ ->
			{?timezone,?dst_timezone}
		end;
	    _ ->
		{?timezone,?dst_timezone}
	end,
    test_data(nondst_dates(), TZ) ++
	test_data(dst_dates(), DSTTZ) ++
	crossover_test_data(crossover_dates(), TZ).    


%% test_data1() ->
%%     test_data(nondst_dates(), ?timezone) ++
%% 	test_data(dst_dates(), ?dst_timezone) ++
%% 	crossover_test_data(crossover_dates(), ?timezone).

crossover_test_data([{Year, Month, Day}|Rest], TimeZone) when TimeZone > 0 ->
    Hour = 23,
    Min = 35,
    Sec = 55,
    Utc = {{Year, Month, Day}, {Hour, Min, Sec}},
    Local = {{Year, Month, Day+1}, {Hour+TimeZone-24, Min, Sec}},
    [{Utc, Local}|crossover_test_data(Rest, TimeZone)];
crossover_test_data([{Year, Month, Day}|Rest], TimeZone) when TimeZone < 0 ->
    Hour = 0,
    Min = 23,
    Sec = 12,
    Utc = {{Year, Month, Day}, {Hour, Min, Sec}},
    Local = {{Year, Month, Day-1}, {Hour+TimeZone+24, Min, Sec}},
    [{Utc, Local}|crossover_test_data(Rest, TimeZone)];
crossover_test_data([], _) ->
    [].

test_data([Date|Rest], TimeZone) ->
    Hour = 12,
    Min = 45,
    Sec = 7,
    Utc = {Date, {Hour, Min, Sec}},
    Local = {Date, {Hour+TimeZone, Min, Sec}},
    [{Utc, Local}|test_data(Rest, TimeZone)];
test_data([], _) ->
    [].
    
nondst_dates() ->
    [{1996, 01, 30},
     {1997, 01, 30},
     {1998, 01, 30},
     {1999, 01, 30},
     {1996, 02, 29},
     {1997, 02, 28},
     {1998, 02, 28},
     {1999, 02, 28},
     {1996, 03, 2},
     {1997, 03, 2},
     {1998, 03, 2},
     {1999, 03, 2}].

dst_dates() ->
    [{1996, 06, 1},
     {1997, 06, 2},
     {1998, 06, 3},
     {1999, 06, 4}].

%% exakt utc {date(), time()} which corresponds to the same seconds since 1 jan 1970 
%% negative seconds are ok
%% generated with date --date='1979-05-28 12:30:35 UTC' +%s
ok_utc_seconds() -> [
	{ {{1970, 1, 1},{ 0, 0, 0}},            0 },
	{ {{1970, 1, 1},{ 0, 0, 1}},            1 },
	{ {{1969,12,31},{23,59,59}},           -1 },
	{ {{1920,12,31},{23,59,59}},  -1546300801 },
	{ {{1600,02,19},{15,14,08}}, -11671807552 },
	{ {{1979,05,28},{12,30,35}},    296742635 },
	{ {{1999,12,31},{23,59,59}},    946684799 },
	{ {{2000, 1, 1},{ 0, 0, 0}},    946684800 },
	{ {{2000, 1, 1},{ 0, 0, 1}},    946684801 },

	{ {{2038, 1,19},{03,14,07}},   2147483647 }, % Sint32 full - 1
	{ {{2038, 1,19},{03,14,08}},   2147483648 }, % Sint32 full
	{ {{2038, 1,19},{03,14,09}},   2147483649 }, % Sint32 full + 1

	{ {{2106, 2, 7},{ 6,28,14}},   4294967294 }, % Uint32 full  0xFFFFFFFF - 1
	{ {{2106, 2, 7},{ 6,28,15}},   4294967295 }, % Uint32 full  0xFFFFFFFF
	{ {{2106, 2, 7},{ 6,28,16}},   4294967296 }, % Uint32 full  0xFFFFFFFF + 1
	{ {{2012,12, 6},{16,28,08}},   1354811288 },
	{ {{2412,12, 6},{16,28,08}},  13977592088 }
    ].


%% The following dates should not be near the end or beginning of
%% a month, because they will be used to test when the dates are
%% different in UTC and local time.

crossover_dates() ->
    [{1996, 01, 25},
     {1997, 01, 25},
     {1998, 01, 25},
     {1999, 01, 25},
     {1996, 02, 27},
     {1997, 02, 27},
     {1998, 02, 27},
     {1999, 02, 27}].

bad_dates() ->
    [{{1900, 7, 1}, {12, 0, 0}},		% Year

     {{1996, 0, 20}, {12, 0, 0}},		% Month
     {{1996, 13, 20}, {12, 0, 0}},

     {{1996, 1, 0}, {12, 0, 0}},		% Date
     {{1996, 1, 32}, {12, 0, 0}},
     {{1996, 2, 30}, {12, 0, 0}},
     {{1997, 2, 29}, {12, 0, 0}},
     {{1998, 2, 29}, {12, 0, 0}},
     {{1999, 2, 29}, {12, 0, 0}},
     {{1996, 4, 31}, {12, 0, 0}},

     {{1996, 4, 30}, {-1, 0, 0}},		% Hour
     {{1996, 4, 30}, {25, 0, 0}},

     {{1996, 4, 30}, {12,-1, 0}},		% Minute
     {{1996, 4, 30}, {12, 60, 0}},

     {{1996, 4, 30}, {12, 0, -1}},		% Sec
     {{1996, 4, 30}, {12, 0, 60}}].

start_node(Config, Args) ->
    TestCase = proplists:get_value(testcase, Config),
    PA = filename:dirname(code:which(?MODULE)),
    ESTime = erlang:monotonic_time(1) + erlang:time_offset(1),
    Unique = erlang:unique_integer([positive]),
    Name = list_to_atom(atom_to_list(?MODULE)
			++ "-"
			++ atom_to_list(TestCase)
			++ "-"
			++ integer_to_list(ESTime)
			++ "-"
			++ integer_to_list(Unique)),
    test_server:start_node(Name,
			   slave,
			   [{args, "-pa " ++ PA ++ " " ++ Args}]).

stop_node(Node) ->
    test_server:stop_node(Node).
