% 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(couch_mrview_ddoc_updated_tests).

-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").

-define(TIMEOUT, 1000).

setup() ->
    Name = ?tempdb(),
    couch_server:delete(Name, [?ADMIN_CTX]),
    {ok, Db} = couch_db:create(Name, [?ADMIN_CTX]),
    DDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, <<"_design/bar">>},
            {<<"views">>,
                {[
                    {<<"baz">>,
                        {[
                            {<<"map">>, <<
                                "function(doc) {\n"
                                "    emit(doc.val, doc.val);\n"
                                "}"
                            >>}
                        ]}}
                ]}}
        ]}
    ),
    [Doc1 | Docs999] = couch_mrview_test_util:make_docs(map, 100),
    {ok, _} = couch_db:update_docs(Db, [DDoc, Doc1], []),
    {ok, Db2} = couch_db:reopen(Db),

    % run a query with 1 doc to initialize couch_index process
    CB = fun
        ({row, _}, Count) -> {ok, Count + 1};
        (_, Count) -> {ok, Count}
    end,
    {ok, _} =
        couch_mrview:query_view(Db2, <<"_design/bar">>, <<"baz">>, [], CB, 0),

    meck:new(couch_index_updater, [passthrough]),
    meck:expect(couch_index_updater, update, fun(Idx, Mod, IdxSt) ->
        timer:sleep(5000),
        meck:passthrough([Idx, Mod, IdxSt])
    end),

    % add more docs
    {ok, _} = couch_db:update_docs(Db2, Docs999, []),
    {ok, Db3} = couch_db:reopen(Db2),
    Db3.

teardown(Db) ->
    meck:unload(couch_index_updater),
    couch_db:close(Db),
    couch_server:delete(couch_db:name(Db), [?ADMIN_CTX]),
    ok.

ddoc_update_test_() ->
    {
        "Check ddoc update actions",
        {
            setup,
            fun test_util:start_couch/0,
            fun test_util:stop_couch/1,
            {
                foreach,
                fun setup/0,
                fun teardown/1,
                [
                    fun check_indexing_stops_on_ddoc_change/1
                ]
            }
        }
    }.

check_indexing_stops_on_ddoc_change(Db) ->
    ?_test(begin
        DDocID = <<"_design/bar">>,

        IndexesBefore = get_indexes_by_ddoc(couch_db:name(Db), DDocID, 1),
        ?assertEqual(1, length(IndexesBefore)),
        AliveBefore = lists:filter(fun is_process_alive/1, IndexesBefore),
        ?assertEqual(1, length(AliveBefore)),

        {ok, DDoc} = couch_db:open_doc(Db, DDocID, [ejson_body, ?ADMIN_CTX]),
        DDocJson2 = couch_doc:from_json_obj(
            {[
                {<<"_id">>, DDocID},
                {<<"_deleted">>, true},
                {<<"_rev">>, couch_doc:rev_to_str(DDoc#doc.revs)}
            ]}
        ),

        % spawn a process for query
        Self = self(),
        QPid = spawn(fun() ->
            {ok, Result} = couch_mrview:query_view(
                Db, <<"_design/bar">>, <<"baz">>, []
            ),
            Self ! {self(), Result}
        end),

        % while indexing for the query is in progress, delete DDoc
        {ok, _} = couch_db:update_doc(Db, DDocJson2, []),
        receive
            {QPid, Msg} ->
                ?assertEqual(Msg, ddoc_updated)
        after ?TIMEOUT ->
            error(
                {assertion_failed, [
                    {module, ?MODULE},
                    {line, ?LINE},
                    {reason, "test failed"}
                ]}
            )
        end,

        %% assert that previously running indexes are gone
        IndexesAfter = get_indexes_by_ddoc(couch_db:name(Db), DDocID, 0),
        ?assertEqual(0, length(IndexesAfter)),
        AliveAfter = lists:filter(fun is_process_alive/1, IndexesBefore),
        ?assertEqual(0, length(AliveAfter))
    end).

get_indexes_by_ddoc(DbName0, DDocID, N) ->
    Indexes = test_util:wait(fun() ->
        Indxs = ets:match_object(
            couch_index_server:by_db(DbName0), {'$1', {DDocID, '$2'}}
        ),
        case length(Indxs) == N of
            true ->
                Indxs;
            false ->
                wait
        end
    end),
    lists:foldl(
        fun({DbName, {_DDocID, Sig}}, Acc) ->
            case ets:lookup(couch_index_server:by_sig(DbName), {DbName, Sig}) of
                [{_, Pid}] -> [Pid | Acc];
                _ -> Acc
            end
        end,
        [],
        Indexes
    ).
