#!/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.

-record(user_ctx, {
    name = null,
    roles = [],
    handler
}).

-record(db, {
    main_pid = nil,
    update_pid = nil,
    compactor_pid = nil,
    instance_start_time, % number of microsecs since jan 1 1970 as a binary string
    fd,
    updater_fd,
    fd_ref_counter,
    header = nil,
    committed_update_seq,
    fulldocinfo_by_id_btree,
    docinfo_by_seq_btree,
    local_docs_btree,
    update_seq,
    name,
    filepath,
    validate_doc_funs = [],
    validate_doc_read_funs = [],
    security = [],
    security_ptr = nil,
    user_ctx = #user_ctx{},
    waiting_delayed_commit = nil,
    revs_limit = 1000,
    fsync_options = [],
    options = [],
    compression,
    before_doc_update,
    after_doc_read
}).

main_db_name() -> <<"couch_test_view_group_shutdown">>.


main(_) ->
    etap:plan(13),
    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() ->
    test_util:start_couch(),
    ok = couch_config:set("couchdb", "delayed_commits", "false", false),
    crypto:start(),

    % Test that while a view group is being compacted its database can not
    % be closed by the database LRU system.
    test_view_group_compaction(),

    test_util:stop_couch(),
    ok.


test_view_group_compaction() ->
    {ok, DbWriter3} = create_db(<<"couch_test_view_group_shutdown_w3">>),
    ok = couch_db:close(DbWriter3),

    {ok, MainDb} = create_main_db(),
    ok = couch_db:close(MainDb),

    {ok, DbWriter1} = create_db(<<"couch_test_view_group_shutdown_w1">>),
    ok = couch_db:close(DbWriter1),

    {ok, DbWriter2} = create_db(<<"couch_test_view_group_shutdown_w2">>),
    ok = couch_db:close(DbWriter2),

    Writer1 = spawn_writer(DbWriter1#db.name),
    Writer2 = spawn_writer(DbWriter2#db.name),
    etap:is(is_process_alive(Writer1), true, "Spawned writer 1"),
    etap:is(is_process_alive(Writer2), true, "Spawned writer 2"),

    etap:is(get_writer_status(Writer1), ok, "Writer 1 opened his database"),
    etap:is(get_writer_status(Writer2), ok, "Writer 2 opened his database"),

    {ok, MonRef} = couch_mrview:compact(MainDb#db.name, <<"_design/foo">>, [monitor]),

    % Add some more docs to database and trigger view update
    {ok, MainDb2} = couch_db:open_int(MainDb#db.name, []),
    ok = populate_main_db(MainDb2, 3, 3),
    update_view(MainDb2#db.name, <<"_design/foo">>, <<"foo">>),
    ok = couch_db:close(MainDb2),

    % Assuming the view compaction takes more than 50ms to complete
    ok = timer:sleep(50),
    Writer3 = spawn_writer(DbWriter3#db.name),

    etap:is(is_process_alive(Writer1), true, "Writer 1 still alive"),
    etap:is(is_process_alive(Writer2), true, "Writer 2 still alive"),
    etap:is(is_process_alive(Writer3), true, "Writer 3 still alive"),

    receive
    {'DOWN', MonRef, process, _, normal} ->
         etap:diag("View group compaction successful"),
         ok;
    {'DOWN', MonRef, process, _, _Reason} ->
         etap:bail("Failure compacting view group")
    end,

    etap:is(is_process_alive(Writer1), true, "Writer 1 still alive"),
    etap:is(is_process_alive(Writer2), true, "Writer 2 still alive"),
    etap:is(is_process_alive(Writer3), true, "Writer 3 still alive"),

    etap:is(stop_writer(Writer1), ok, "Stopped writer 1"),
    etap:is(stop_writer(Writer2), ok, "Stopped writer 2"),
    etap:is(stop_writer(Writer3), ok, "Stopped writer 3"),

    delete_db(MainDb),
    delete_db(DbWriter1),
    delete_db(DbWriter2),
    delete_db(DbWriter3).


create_main_db() ->
    {ok, Db} = create_db(main_db_name()),
    DDoc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"_design/foo">>},
        {<<"language">>, <<"javascript">>},
        {<<"views">>, {[
            {<<"foo">>, {[
                {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
            ]}},
            {<<"foo2">>, {[
                {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
            ]}},
            {<<"foo3">>, {[
                {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
            ]}},
            {<<"foo4">>, {[
                {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
            ]}},
            {<<"foo5">>, {[
                {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
            ]}}
        ]}}
    ]}),
    {ok, _} = couch_db:update_doc(Db, DDoc, []),
    ok = populate_main_db(Db, 1000, 20000),
    update_view(Db#db.name, <<"_design/foo">>, <<"foo">>),
    {ok, Db}.


populate_main_db(Db, BatchSize, N) when N > 0 ->
    Docs = lists:map(
        fun(_) ->
            couch_doc:from_json_obj({[
                {<<"_id">>, couch_uuids:new()},
                {<<"value">>, base64:encode(crypto:rand_bytes(1000))}
            ]})
        end,
        lists:seq(1, BatchSize)),
    {ok, _} = couch_db:update_docs(Db, Docs, []),
    populate_main_db(Db, BatchSize, N - length(Docs));
populate_main_db(_Db, _, _) ->
    ok.


update_view(DbName, DDocName, ViewName) ->
    {ok, Db} = couch_db:open_int(DbName, []),
    {ok, DDoc} = couch_db:open_doc(Db, DDocName, [ejson_body]),
    couch_mrview:query_view(Db, DDoc, ViewName, [{stale, false}]),
    ok = couch_db:close(Db),
    etap:diag("View group updated").


create_db(DbName) ->
    {ok, Db} = couch_db:create(
        DbName,
        [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]),
    {ok, Db}.


delete_db(#db{name = DbName, main_pid = Pid}) ->
    ok = couch_server:delete(
        DbName, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]),
    MonRef = erlang:monitor(process, Pid),
    receive
    {'DOWN', MonRef, process, Pid, _Reason} ->
        ok
    after 30000 ->
        etap:bail("Timeout deleting database")
    end.


spawn_writer(DbName) ->
    Parent = self(),
    spawn(fun() ->
        process_flag(priority, high),
        writer_loop(DbName, Parent)
    end).


get_writer_status(Writer) ->
    Ref = make_ref(),
    Writer ! {get_status, Ref},
    receive
    {db_open, Ref} ->
        ok;
    {db_open_error, Error, Ref} ->
        Error
    after 5000 ->
        timeout
    end.


writer_try_again(Writer) ->
    Ref = make_ref(),
    Writer ! {try_again, Ref},
    receive
    {ok, Ref} ->
        ok
    after 5000 ->
        timeout
    end.


stop_writer(Writer) ->
    Ref = make_ref(),
    Writer ! {stop, Ref},
    receive
    {ok, Ref} ->
        ok
    after 5000 ->
        etap:bail("Timeout stopping writer process")
    end.


% Just keep the database open, no need to actually do something on it.
writer_loop(DbName, Parent) ->
    case couch_db:open_int(DbName, []) of
    {ok, Db} ->
        writer_loop_1(Db, Parent);
    Error ->
        writer_loop_2(DbName, Parent, Error)
    end.

writer_loop_1(Db, Parent) ->
    receive
    {get_status, Ref} ->
        Parent ! {db_open, Ref},
        writer_loop_1(Db, Parent);
    {stop, Ref} ->
        ok = couch_db:close(Db),
        Parent ! {ok, Ref}
    end.

writer_loop_2(DbName, Parent, Error) ->
    receive
    {get_status, Ref} ->
        Parent ! {db_open_error, Error, Ref},
        writer_loop_2(DbName, Parent, Error);
    {try_again, Ref} ->
        Parent ! {ok, Ref},
        writer_loop(DbName, Parent)
    end.
