#!/usr/bin/env escript
%% -*- erlang -*-
% 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.

main(_) ->
    test_util:init_code_path(),

    etap:plan(55),
    case (catch test()) of
        ok ->
            etap:end_tests();
        Other ->
            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
            etap:bail(Other)
    end,
    ok.


test() ->
    couch_server_sup:start_link(test_util:config_files()),
    ibrowse:start(),

    test_pool_full(),
    test_worker_dead_pool_non_full(),
    test_worker_dead_pool_full(),

    couch_server_sup:stop(),
    ok.


test_pool_full() ->
    Pool = spawn_pool(),
    Client1 = spawn_client(Pool),
    Client2 = spawn_client(Pool),
    Client3 = spawn_client(Pool),

    etap:diag("Check that we can spawn the max number of connections."),
    etap:is(ping_client(Client1), ok, "Client 1 started ok."),
    etap:is(ping_client(Client2), ok, "Client 2 started ok."),
    etap:is(ping_client(Client3), ok, "Client 3 started ok."),

    Worker1 = get_client_worker(Client1, "1"),
    Worker2 = get_client_worker(Client2, "2"),
    Worker3 = get_client_worker(Client3, "3"),
    etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."),
    etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."),
    etap:is(is_process_alive(Worker3), true, "Client's 3 worker is alive."),

    etap:isnt(Worker1, Worker2, "Clients 1 and 2 got different workers."),
    etap:isnt(Worker2, Worker3, "Clients 2 and 3 got different workers."),
    etap:isnt(Worker1, Worker3, "Clients 1 and 3 got different workers."),

    etap:diag("Check that client 4 blocks waiting for a worker."),
    Client4 = spawn_client(Pool),
    etap:is(ping_client(Client4), timeout, "Client 4 blocked while waiting."),

    etap:diag("Check that stopping a client gives up its worker."),
    etap:is(stop_client(Client1), ok, "First client stopped."),

    etap:diag("And check that our blocked client has been unblocked."),
    etap:is(ping_client(Client4), ok, "Client 4 was unblocked."),

    Worker4 = get_client_worker(Client4, "4"),
    etap:is(is_process_alive(Worker4), true, "Client's 4 worker is alive."),
    etap:is(Worker4, Worker1, "Client 4 got worker that client 1 got before."),

    lists:foreach(fun(C) -> ok = stop_client(C) end, [Client2, Client3, Client4]),
    stop_pool(Pool).


test_worker_dead_pool_non_full() ->
    Pool = spawn_pool(),
    Client1 = spawn_client(Pool),

    etap:is(ping_client(Client1), ok, "Client 1 started ok."),
    Worker1 = get_client_worker(Client1, "1"),
    etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."),

    etap:diag("Kill client's 1 worker."),
    etap:is(kill_client_worker(Client1), ok, "Killed client's 1 worker."),
    etap:is(is_process_alive(Worker1), false, "Client's 1 worker process is dead."),

    etap:is(stop_client(Client1), ok, "First client stopped and released its worker."),

    Client2 = spawn_client(Pool),
    etap:is(ping_client(Client2), ok, "Client 2 started ok."),
    Worker2 = get_client_worker(Client2, "2"),
    etap:isnt(Worker2, Worker1, "Client 2 got a different worker from client 1"),
    etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."),

    etap:is(stop_client(Client2), ok, "Second client stopped."),
    stop_pool(Pool).


test_worker_dead_pool_full() ->
    Pool = spawn_pool(),
    Client1 = spawn_client(Pool),
    Client2 = spawn_client(Pool),
    Client3 = spawn_client(Pool),

    etap:diag("Check that we can spawn the max number of connections."),
    etap:is(ping_client(Client1), ok, "Client 1 started ok."),
    etap:is(ping_client(Client2), ok, "Client 2 started ok."),
    etap:is(ping_client(Client3), ok, "Client 3 started ok."),

    Worker1 = get_client_worker(Client1, "1"),
    Worker2 = get_client_worker(Client2, "2"),
    Worker3 = get_client_worker(Client3, "3"),
    etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."),
    etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."),
    etap:is(is_process_alive(Worker3), true, "Client's 3 worker is alive."),

    etap:isnt(Worker1, Worker2, "Clients 1 and 2 got different workers."),
    etap:isnt(Worker2, Worker3, "Clients 2 and 3 got different workers."),
    etap:isnt(Worker1, Worker3, "Clients 1 and 3 got different workers."),

    etap:diag("Check that client 4 blocks waiting for a worker."),
    Client4 = spawn_client(Pool),
    etap:is(ping_client(Client4), timeout, "Client 4 blocked while waiting."),

    etap:diag("Kill client's 1 worker."),
    etap:is(kill_client_worker(Client1), ok, "Killed client's 1 worker."),
    etap:is(is_process_alive(Worker1), false, "Client's 1 worker process is dead."),

    etap:diag("Check client 4 got unblocked after first worker's death"),
    etap:is(ping_client(Client4), ok, "Client 4 not blocked anymore."),

    Worker4 = get_client_worker(Client4, "4"),
    etap:is(is_process_alive(Worker4), true, "Client's 4 worker is alive."),
    etap:isnt(Worker4, Worker1, "Client 4 got a worker different from client 1."),
    etap:isnt(Worker4, Worker2, "Client 4 got a worker different from client 2."),
    etap:isnt(Worker4, Worker3, "Client 4 got a worker different from client 3."),

    etap:diag("Check that stopping client 1 is a noop."),
    etap:is(stop_client(Client1), ok, "First client stopped."),

    etap:is(is_process_alive(Worker2), true, "Client's 2 worker still alive."),
    etap:is(is_process_alive(Worker3), true, "Client's 3 worker still alive."),
    etap:is(is_process_alive(Worker4), true, "Client's 4 worker still alive."),

    etap:diag("Check that client 5 blocks waiting for a worker."),
    Client5 = spawn_client(Pool),
    etap:is(ping_client(Client5), timeout, "Client 5 blocked while waiting."),

    etap:diag("Check that stopping client 2 gives up its worker."),
    etap:is(stop_client(Client2), ok, "Second client stopped."),

    etap:diag("Now check that client 5 has been unblocked."),
    etap:is(ping_client(Client5), ok, "Client 5 was unblocked."),

    Worker5 = get_client_worker(Client5, "5"),
    etap:is(is_process_alive(Worker5), true, "Client's 5 worker is alive."),
    etap:isnt(Worker5, Worker1, "Client 5 got a worker different from client 1."),
    etap:is(Worker5, Worker2, "Client 5 got same worker as client 2."),
    etap:isnt(Worker5, Worker3, "Client 5 got a worker different from client 3."),
    etap:isnt(Worker5, Worker4, "Client 5 got a worker different from client 4."),

    etap:is(is_process_alive(Worker3), true, "Client's 3 worker still alive."),
    etap:is(is_process_alive(Worker4), true, "Client's 4 worker still alive."),
    etap:is(is_process_alive(Worker5), true, "Client's 5 worker still alive."),

    lists:foreach(fun(C) -> ok = stop_client(C) end, [Client3, Client4, Client5]),
    stop_pool(Pool).


spawn_client(Pool) ->
    Parent = self(),
    Ref = make_ref(),
    Pid = spawn(fun() ->
        {ok, Worker} = couch_replicator_httpc_pool:get_worker(Pool),
        loop(Parent, Ref, Worker, Pool)
    end),
    {Pid, Ref}.


ping_client({Pid, Ref}) ->
    Pid ! ping,
    receive
        {pong, Ref} ->
            ok
    after 3000 ->
        timeout
    end.


get_client_worker({Pid, Ref}, ClientName) ->
    Pid ! get_worker,
    receive
        {worker, Ref, Worker} ->
            Worker
    after 3000 ->
        etap:bail("Timeout getting client " ++ ClientName ++ " worker.")
    end.


stop_client({Pid, Ref}) ->
    Pid ! stop,
    receive
        {stop, Ref} ->
            ok
    after 3000 ->
        timeout
    end.


kill_client_worker({Pid, Ref}) ->
    Pid ! get_worker,
    receive
        {worker, Ref, Worker} ->
            exit(Worker, kill),
            ok
    after 3000 ->
        timeout
    end.


loop(Parent, Ref, Worker, Pool) ->
    receive
        ping ->
            Parent ! {pong, Ref},
            loop(Parent, Ref, Worker, Pool);
        get_worker  ->
            Parent ! {worker, Ref, Worker},
            loop(Parent, Ref, Worker, Pool);
        stop ->
            couch_replicator_httpc_pool:release_worker(Pool, Worker),
            Parent ! {stop, Ref}
    end.


spawn_pool() ->
    Host = couch_config:get("httpd", "bind_address", "127.0.0.1"),
    Port = couch_config:get("httpd", "port", "5984"),
    {ok, Pool} = couch_replicator_httpc_pool:start_link(
        "http://" ++ Host ++ ":5984", [{max_connections, 3}]),
    Pool.


stop_pool(Pool) ->
    ok = couch_replicator_httpc_pool:stop(Pool).
