% 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.

-module(couchdb_os_daemons_tests).

-include("couch_eunit.hrl").

%% keep in sync with couchdb/couch_os_daemons.erl
-record(daemon, {
    port,
    name,
    cmd,
    kill,
    status=running,
    cfg_patterns=[],
    errors=[],
    buf=[]
}).

-define(DAEMON_CONFIGER, "os_daemon_configer.escript").
-define(DAEMON_LOOPER, "os_daemon_looper.escript").
-define(DAEMON_BAD_PERM, "os_daemon_bad_perm.sh").
-define(DAEMON_CAN_REBOOT, "os_daemon_can_reboot.sh").
-define(DAEMON_DIE_ON_BOOT, "os_daemon_die_on_boot.sh").
-define(DAEMON_DIE_QUICKLY, "os_daemon_die_quickly.sh").
-define(DAEMON_CFGREG, "test_cfg_register").
-define(DELAY, 100).
-define(FIXTURES_BUILDDIR,
        filename:join([?BUILDDIR, "test", "couchdb", "fixtures"])).
-define(TIMEOUT, 1000).


setup(DName) ->
    {ok, CfgPid} = couch_config:start_link(?CONFIG_CHAIN),
    {ok, OsDPid} = couch_os_daemons:start_link(),
    Path = case DName of
        ?DAEMON_CONFIGER ->
            filename:join([?FIXTURES_BUILDDIR, DName]);
        ?DAEMON_CFGREG ->
            filename:join([?FIXTURES_BUILDDIR, DName]);
        _ ->
            filename:join([?FIXTURESDIR, DName])
    end,
    couch_config:set("os_daemons", DName, Path, false),
    timer:sleep(?DELAY),  % sleep a bit to let daemon set kill flag
    {CfgPid, OsDPid}.

teardown(_, {CfgPid, OsDPid}) ->
    erlang:monitor(process, CfgPid),
    couch_config:stop(),
    receive
        {'DOWN', _, _, CfgPid, _} ->
            ok
    after ?TIMEOUT ->
        throw({timeout, config_stop})
    end,

    erlang:monitor(process, OsDPid),
    exit(OsDPid, normal),
    receive
        {'DOWN', _, _, OsDPid, _} ->
            ok
    after ?TIMEOUT ->
        throw({timeout, os_daemon_stop})
    end.


os_daemons_test_() ->
    {
        "OS Daemons tests",
        {
            foreachx,
            fun setup/1, fun teardown/2,
            [{?DAEMON_LOOPER, Fun} || Fun <- [
                fun should_check_daemon/2,
                fun should_check_daemon_table_form/2,
                fun should_clean_tables_on_daemon_remove/2,
                fun should_spawn_multiple_daemons/2,
                fun should_keep_alive_one_daemon_on_killing_other/2
            ]]
        }
    }.

configuration_reader_test_() ->
    {
        "OS Daemon requests CouchDB configuration",
        {
            foreachx,
            fun setup/1, fun teardown/2,
            [{?DAEMON_CONFIGER,
              fun should_read_write_config_settings_by_daemon/2}]

        }
    }.

error_test_() ->
    {
        "OS Daemon process error tests",
        {
            foreachx,
            fun setup/1, fun teardown/2,
            [{?DAEMON_BAD_PERM, fun should_fail_due_to_lack_of_permissions/2},
             {?DAEMON_DIE_ON_BOOT, fun should_die_on_boot/2},
             {?DAEMON_DIE_QUICKLY, fun should_die_quickly/2},
             {?DAEMON_CAN_REBOOT, fun should_not_being_halted/2}]
        }
    }.

configuration_register_test_() ->
    {
        "OS daemon subscribed to config changes",
        {
            foreachx,
            fun setup/1, fun teardown/2,
            [{?DAEMON_CFGREG, Fun} || Fun <- [
                fun should_start_daemon/2,
                fun should_restart_daemon_on_section_change/2,
                fun should_not_restart_daemon_on_changing_ignored_section_key/2,
                fun should_restart_daemon_on_section_key_change/2
            ]]
        }
    }.


should_check_daemon(DName, _) ->
    ?_test(begin
        {ok, [D]} = couch_os_daemons:info([table]),
        check_daemon(D, DName)
    end).

should_check_daemon_table_form(DName, _) ->
    ?_test(begin
        {ok, Tab} = couch_os_daemons:info(),
        [D] = ets:tab2list(Tab),
        check_daemon(D, DName)
    end).

should_clean_tables_on_daemon_remove(DName, _) ->
    ?_test(begin
        couch_config:delete("os_daemons", DName, false),
        {ok, Tab2} = couch_os_daemons:info(),
        ?_assertEqual([], ets:tab2list(Tab2))
    end).

should_spawn_multiple_daemons(DName, _) ->
    ?_test(begin
        couch_config:set("os_daemons", "bar",
                         filename:join([?FIXTURESDIR, DName]), false),
        couch_config:set("os_daemons", "baz",
                         filename:join([?FIXTURESDIR, DName]), false),
        timer:sleep(?DELAY),
        {ok, Daemons} = couch_os_daemons:info([table]),
        lists:foreach(fun(D) ->
            check_daemon(D)
        end, Daemons),
        {ok, Tab} = couch_os_daemons:info(),
        lists:foreach(fun(D) ->
            check_daemon(D)
        end, ets:tab2list(Tab))
    end).

should_keep_alive_one_daemon_on_killing_other(DName, _) ->
    ?_test(begin
        couch_config:set("os_daemons", "bar",
                         filename:join([?FIXTURESDIR, DName]), false),
        timer:sleep(?DELAY),
        {ok, Daemons} = couch_os_daemons:info([table]),
        lists:foreach(fun(D) ->
            check_daemon(D)
        end, Daemons),

        couch_config:delete("os_daemons", "bar", false),
        timer:sleep(?DELAY),
        {ok, [D2]} = couch_os_daemons:info([table]),
        check_daemon(D2, DName),

        {ok, Tab} = couch_os_daemons:info(),
        [T] = ets:tab2list(Tab),
        check_daemon(T, DName)
    end).

should_read_write_config_settings_by_daemon(DName, _) ->
    ?_test(begin
        % have to wait till daemon run all his tests
        % see daemon's script for more info
        timer:sleep(?TIMEOUT),
        {ok, [D]} = couch_os_daemons:info([table]),
        check_daemon(D, DName)
    end).

should_fail_due_to_lack_of_permissions(DName, _) ->
    ?_test(should_halts(DName, 1000)).

should_die_on_boot(DName, _) ->
    ?_test(should_halts(DName, 1000)).

should_die_quickly(DName, _) ->
    ?_test(should_halts(DName, 4000)).

should_not_being_halted(DName, _) ->
    ?_test(begin
        timer:sleep(1000),
        {ok, [D1]} = couch_os_daemons:info([table]),
        check_daemon(D1, DName, running, 0),

        % Should reboot every two seconds. We're at 1s, so wait
        % until 3s to be in the middle of the next invocation's
        % life span.

        timer:sleep(2000),
        {ok, [D2]} = couch_os_daemons:info([table]),
        check_daemon(D2, DName, running, 1),

        % If the kill command changed, that means we rebooted the process.
        ?assertNotEqual(D1#daemon.kill, D2#daemon.kill)
    end).

should_halts(DName, Time) ->
    timer:sleep(Time),
    {ok, [D]} = couch_os_daemons:info([table]),
    check_dead(D, DName),
    couch_config:delete("os_daemons", DName, false).

should_start_daemon(DName, _) ->
    ?_test(begin
        wait_for_start(10),
        {ok, [D]} = couch_os_daemons:info([table]),
        check_daemon(D, DName, running, 0, [{"s1"}, {"s2", "k"}])
    end).

should_restart_daemon_on_section_change(DName, _) ->
    ?_test(begin
        wait_for_start(10),
        {ok, [D1]} = couch_os_daemons:info([table]),
        couch_config:set("s1", "k", "foo", false),
        wait_for_restart(10),
        {ok, [D2]} = couch_os_daemons:info([table]),
        check_daemon(D2, DName, running, 0, [{"s1"}, {"s2", "k"}]),
        ?assertNotEqual(D1, D2)
    end).

should_not_restart_daemon_on_changing_ignored_section_key(_, _) ->
    ?_test(begin
        wait_for_start(10),
        {ok, [D1]} = couch_os_daemons:info([table]),
        couch_config:set("s2", "k2", "baz", false),
        timer:sleep(?DELAY),
        {ok, [D2]} = couch_os_daemons:info([table]),
        ?assertEqual(D1, D2)
    end).

should_restart_daemon_on_section_key_change(DName, _) ->
    ?_test(begin
        wait_for_start(10),
        {ok, [D1]} = couch_os_daemons:info([table]),
        couch_config:set("s2", "k", "bingo", false),
        wait_for_restart(10),
        {ok, [D2]} = couch_os_daemons:info([table]),
        check_daemon(D2, DName, running, 0, [{"s1"}, {"s2", "k"}]),
        ?assertNotEqual(D1, D2)
    end).


wait_for_start(0) ->
    erlang:error({assertion_failed,
                  [{module, ?MODULE},
                   {line, ?LINE},
                   {reason, "Timeout on waiting daemon for start"}]});
wait_for_start(N) ->
    case couch_os_daemons:info([table]) of
        {ok, []} ->
            timer:sleep(?DELAY),
            wait_for_start(N - 1);
        _ ->
            timer:sleep(?TIMEOUT)
    end.

wait_for_restart(0) ->
    erlang:error({assertion_failed,
                  [{module, ?MODULE},
                   {line, ?LINE},
                   {reason, "Timeout on waiting daemon for restart"}]});
wait_for_restart(N) ->
    {ok, [D]} = couch_os_daemons:info([table]),
    case D#daemon.status of
        restarting ->
            timer:sleep(?DELAY),
            wait_for_restart(N - 1);
        _ ->
            timer:sleep(?TIMEOUT)
    end.

check_daemon(D) ->
    check_daemon(D, D#daemon.name).

check_daemon(D, Name) ->
    check_daemon(D, Name, running).

check_daemon(D, Name, Status) ->
    check_daemon(D, Name, Status, 0).

check_daemon(D, Name, Status, Errs) ->
    check_daemon(D, Name, Status, Errs, []).

check_daemon(D, Name, Status, Errs, CfgPatterns) ->
    ?assert(is_port(D#daemon.port)),
    ?assertEqual(Name, D#daemon.name),
    ?assertNotEqual(undefined, D#daemon.kill),
    ?assertEqual(Status, D#daemon.status),
    ?assertEqual(CfgPatterns, D#daemon.cfg_patterns),
    ?assertEqual(Errs, length(D#daemon.errors)),
    ?assertEqual([], D#daemon.buf).

check_dead(D, Name) ->
    ?assert(is_port(D#daemon.port)),
    ?assertEqual(Name, D#daemon.name),
    ?assertNotEqual(undefined, D#daemon.kill),
    ?assertEqual(halted, D#daemon.status),
    ?assertEqual(nil, D#daemon.errors),
    ?assertEqual(nil, D#daemon.buf).
