%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates.  All rights reserved.
%%

-module(unit_gm_SUITE).

-behaviour(gm).

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

-include("gm_specs.hrl").

-compile(export_all).

-define(RECEIVE_OR_THROW(Body, Bool, Error),
        receive Body ->
                true = Bool,
                passed
        after 5000 ->
                throw(Error)
        end).

all() ->
    [
      join_leave,
      broadcast,
      confirmed_broadcast,
      member_death,
      receive_in_order,
      unexpected_msg,
      down_in_members_change
    ].

init_per_suite(Config) ->
    ok = application:set_env(mnesia, dir, ?config(priv_dir, Config)),
    ok = application:start(mnesia),
    {ok, FHC} = file_handle_cache:start_link(),
    unlink(FHC),
    {ok, WPS} = worker_pool_sup:start_link(),
    unlink(WPS),
    rabbit_ct_helpers:set_config(Config, [
        {file_handle_cache_pid, FHC},
        {worker_pool_sup_pid, WPS}
      ]).

end_per_suite(Config) ->
    exit(?config(worker_pool_sup_pid, Config), shutdown),
    exit(?config(file_handle_cache_pid, Config), shutdown),
    ok = application:stop(mnesia),
    Config.

%% ---------------------------------------------------------------------------
%% Functional tests
%% ---------------------------------------------------------------------------

join_leave(_Config) ->
    passed = with_two_members(fun (_Pid, _Pid2) -> passed end).

broadcast(_Config) ->
    passed = do_broadcast(fun gm:broadcast/2).

confirmed_broadcast(_Config) ->
    passed = do_broadcast(fun gm:confirmed_broadcast/2).

member_death(_Config) ->
    passed = with_two_members(
      fun (Pid, Pid2) ->
              {ok, Pid3} = gm:start_link(
                             ?MODULE, ?MODULE, self(),
                             fun rabbit_misc:execute_mnesia_transaction/1),
              passed = receive_joined(Pid3, [Pid, Pid2, Pid3],
                                      timeout_joining_gm_group_3),
              passed = receive_birth(Pid, Pid3, timeout_waiting_for_birth_3_1),
              passed = receive_birth(Pid2, Pid3, timeout_waiting_for_birth_3_2),

              unlink(Pid3),
              exit(Pid3, kill),

              %% Have to do some broadcasts to ensure that all members
              %% find out about the death.
              BFun = broadcast_fun(fun gm:confirmed_broadcast/2),
              passed = BFun(Pid, Pid2),
              passed = BFun(Pid, Pid2),

              passed = receive_death(Pid, Pid3, timeout_waiting_for_death_3_1),
              passed = receive_death(Pid2, Pid3, timeout_waiting_for_death_3_2),

              passed
      end).

receive_in_order(_Config) ->
    passed = with_two_members(
      fun (Pid, Pid2) ->
              Numbers = lists:seq(1,1000),
              [begin ok = gm:broadcast(Pid, N), ok = gm:broadcast(Pid2, N) end
               || N <- Numbers],
              passed = receive_numbers(
                         Pid, Pid, {timeout_for_msgs, Pid, Pid}, Numbers),
              passed = receive_numbers(
                         Pid, Pid2, {timeout_for_msgs, Pid, Pid2}, Numbers),
              passed = receive_numbers(
                         Pid2, Pid, {timeout_for_msgs, Pid2, Pid}, Numbers),
              passed = receive_numbers(
                         Pid2, Pid2, {timeout_for_msgs, Pid2, Pid2}, Numbers),
              passed
      end).

unexpected_msg(_Config) ->
    passed = with_two_members(
	       fun(Pid, _) ->
		       Pid ! {make_ref(), old_gen_server_answer},
		       true = erlang:is_process_alive(Pid),
		       passed
	       end).

down_in_members_change(_Config) ->
    %% Setup
    ok = gm:create_tables(),
    {ok, Pid} = gm:start_link(?MODULE, ?MODULE, self(),
                              fun rabbit_misc:execute_mnesia_transaction/1),
    passed = receive_joined(Pid, [Pid], timeout_joining_gm_group_1),
    {ok, Pid2} = gm:start_link(?MODULE, ?MODULE, self(),
                               fun rabbit_misc:execute_mnesia_transaction/1),
    passed = receive_joined(Pid2, [Pid, Pid2], timeout_joining_gm_group_2),
    passed = receive_birth(Pid, Pid2, timeout_waiting_for_birth_2),

    %% Test. Simulate that the gm group is deleted (forget_group) while
    %% processing the 'DOWN' message from the neighbour
    process_flag(trap_exit, true),
    ok = meck:new(mnesia, [passthrough]),
    ok = meck:expect(mnesia, read, fun({gm_group, ?MODULE}) ->
					   [];
				      (Key) ->
					   meck:passthrough([Key])
				   end),
    gm:leave(Pid2),
    Passed = receive
		 {'EXIT', Pid, shutdown} ->
		     passed;
		 {'EXIT', Pid, _} ->
		     crashed
	     after 15000 ->
		     timeout
	     end,
    %% Cleanup
    meck:unload(mnesia),
    process_flag(trap_exit, false),
    passed = Passed.


do_broadcast(Fun) ->
    with_two_members(broadcast_fun(Fun)).

broadcast_fun(Fun) ->
    fun (Pid, Pid2) ->
            ok = Fun(Pid, magic_message),
            passed = receive_or_throw({msg, Pid, Pid, magic_message},
                                      timeout_waiting_for_msg),
            passed = receive_or_throw({msg, Pid2, Pid, magic_message},
                                      timeout_waiting_for_msg)
    end.

with_two_members(Fun) ->
    ok = gm:create_tables(),

    {ok, Pid} = gm:start_link(?MODULE, ?MODULE, self(),
                              fun rabbit_misc:execute_mnesia_transaction/1),
    passed = receive_joined(Pid, [Pid], timeout_joining_gm_group_1),

    {ok, Pid2} = gm:start_link(?MODULE, ?MODULE, self(),
                               fun rabbit_misc:execute_mnesia_transaction/1),
    passed = receive_joined(Pid2, [Pid, Pid2], timeout_joining_gm_group_2),
    passed = receive_birth(Pid, Pid2, timeout_waiting_for_birth_2),

    passed = Fun(Pid, Pid2),

    ok = gm:leave(Pid),
    passed = receive_death(Pid2, Pid, timeout_waiting_for_death_1),
    passed =
        receive_termination(Pid, normal, timeout_waiting_for_termination_1),

    ok = gm:leave(Pid2),
    passed =
        receive_termination(Pid2, normal, timeout_waiting_for_termination_2),

    receive X -> throw({unexpected_message, X})
    after 0 -> passed
    end.

receive_or_throw(Pattern, Error) ->
    ?RECEIVE_OR_THROW(Pattern, true, Error).

receive_birth(From, Born, Error) ->
    ?RECEIVE_OR_THROW({members_changed, From, Birth, Death},
                      ([Born] == Birth) andalso ([] == Death),
                      Error).

receive_death(From, Died, Error) ->
    ?RECEIVE_OR_THROW({members_changed, From, Birth, Death},
                      ([] == Birth) andalso ([Died] == Death),
                      Error).

receive_joined(From, Members, Error) ->
    ?RECEIVE_OR_THROW({joined, From, Members1},
                      lists:usort(Members) == lists:usort(Members1),
                      Error).

receive_termination(From, Reason, Error) ->
    ?RECEIVE_OR_THROW({termination, From, Reason1},
                      Reason == Reason1,
                      Error).

receive_numbers(_Pid, _Sender, _Error, []) ->
    passed;
receive_numbers(Pid, Sender, Error, [N | Numbers]) ->
    ?RECEIVE_OR_THROW({msg, Pid, Sender, M},
                      M == N,
                      Error),
    receive_numbers(Pid, Sender, Error, Numbers).

%% -------------------------------------------------------------------
%% gm behavior callbacks.
%% -------------------------------------------------------------------

joined(Pid, Members) ->
    Pid ! {joined, self(), Members},
    ok.

members_changed(Pid, Births, Deaths) ->
    Pid ! {members_changed, self(), Births, Deaths},
    ok.

handle_msg(Pid, From, Msg) ->
    Pid ! {msg, self(), From, Msg},
    ok.

handle_terminate(Pid, Reason) ->
    Pid ! {termination, self(), Reason},
    ok.
