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

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

-define(TIMEOUT, 1000).

setup() ->
    DbName = ?tempdb(),
    {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
    ok = couch_db:close(Db),
    create_design_doc(DbName, <<"_design/foo">>, <<"bar">>),
    tracer_new(),
    DbName.

teardown(DbName) ->
    tracer_delete(),
    couch_server:delete(DbName, [?ADMIN_CTX]).

couch_index_ioq_priority_test_() ->
    {
        "Test ioq_priority for views",
        {
            setup,
            fun test_util:start_couch/0,
            fun test_util:stop_couch/1,
            {
                foreach,
                fun setup/0,
                fun teardown/1,
                [
                    fun check_io_priority_for_updater/1,
                    fun check_io_priority_for_compactor/1
                ]
            }
        }
    }.

check_io_priority_for_updater(DbName) ->
    ?_test(begin
        {ok, IndexerPid} = couch_index_server:get_index(
            couch_mrview_index, DbName, <<"_design/foo">>
        ),
        CouchIndexUpdaterPid = updater_pid(IndexerPid),
        tracer_record(CouchIndexUpdaterPid),

        create_docs(DbName),

        CommittedSeq = couch_util:with_db(DbName, fun(Db) -> couch_db:get_update_seq(Db) end),
        couch_index:get_state(IndexerPid, CommittedSeq),
        [UpdaterPid] = wait_spawn_event_for_pid(CouchIndexUpdaterPid),

        [UpdaterMapProcess] = wait_spawn_by_anonymous_fun(
            UpdaterPid, '-start_update/4-fun-0-'
        ),

        ?assert(
            wait_set_io_priority(
                UpdaterMapProcess, {view_update, DbName, <<"_design/foo">>}
            )
        ),

        [UpdaterWriterProcess] = wait_spawn_by_anonymous_fun(
            UpdaterPid, '-start_update/4-fun-1-'
        ),
        ?assert(
            wait_set_io_priority(
                UpdaterWriterProcess, {view_update, DbName, <<"_design/foo">>}
            )
        ),

        ok
    end).

check_io_priority_for_compactor(DbName) ->
    ?_test(begin
        {ok, IndexerPid} = couch_index_server:get_index(
            couch_mrview_index, DbName, <<"_design/foo">>
        ),
        {ok, CompactorPid} = couch_index:get_compactor_pid(IndexerPid),
        tracer_record(CompactorPid),

        create_docs(DbName),

        couch_index:compact(IndexerPid),
        wait_spawn_event_for_pid(CompactorPid),

        [CompactorProcess] = wait_spawn_by_anonymous_fun(
            CompactorPid, '-handle_call/3-fun-0-'
        ),
        ?assert(
            wait_set_io_priority(
                CompactorProcess, {view_compact, DbName, <<"_design/foo">>}
            )
        ),
        ok
    end).

create_docs(DbName) ->
    {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
    Doc1 = couch_doc:from_json_obj(
        {[
            {<<"_id">>, <<"doc1">>},
            {<<"value">>, 1}
        ]}
    ),
    Doc2 = couch_doc:from_json_obj(
        {[
            {<<"_id">>, <<"doc2">>},
            {<<"value">>, 2}
        ]}
    ),
    Doc3 = couch_doc:from_json_obj(
        {[
            {<<"_id">>, <<"doc3">>},
            {<<"value">>, 3}
        ]}
    ),
    {ok, _} = couch_db:update_docs(Db, [Doc1, Doc2, Doc3]),
    couch_db:close(Db).

create_design_doc(DbName, DDName, ViewName) ->
    {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
    DDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, DDName},
            {<<"language">>, <<"javascript">>},
            {<<"views">>,
                {[
                    {ViewName,
                        {[
                            {<<"map">>, <<"function(doc) { emit(doc.value, null); }">>}
                        ]}}
                ]}}
        ]}
    ),
    {ok, Rev} = couch_db:update_doc(Db, DDoc, []),
    couch_db:close(Db),
    Rev.

wait_set_io_priority(Pid, IOPriority) ->
    test_util:wait_value(
        fun() ->
            does_process_set_io_priority(Pid, IOPriority)
        end,
        true
    ).

does_process_set_io_priority(Pid, IOPriority) ->
    PutCallsArgs = find_calls_to_fun(Pid, {erlang, put, 2}),
    lists:any(fun([_, Priority]) -> Priority =:= IOPriority end, PutCallsArgs).

wait_events(MatchSpec) ->
    test_util:wait_other_value(fun() -> select(MatchSpec) end, []).

find_spawned_by_anonymous_fun(ParentPid, Name) ->
    AnonymousFuns = select(
        ets:fun2ms(fun({spawned, Pid, _TS, _Name, _Dict, [PPid, {erlang, apply, [Fun, _]}]}) when
            is_function(Fun) andalso PPid =:= ParentPid
        ->
            {Pid, Fun}
        end)
    ),
    lists:filtermap(
        fun({Pid, Fun}) ->
            case erlang:fun_info(Fun, name) of
                {name, Name} -> {true, Pid};
                _ -> false
            end
        end,
        AnonymousFuns
    ).

find_calls_to_fun(Pid, {Module, Function, Arity}) ->
    select(
        ets:fun2ms(fun({call, P, _TS, _Name, _Dict, [{M, F, Args}]}) when
            length(Args) =:= Arity andalso
                M =:= Module andalso
                F =:= Function andalso
                P =:= Pid
        ->
            Args
        end)
    ).

wait_spawn_event_for_pid(ParentPid) ->
    wait_events(
        ets:fun2ms(fun({spawned, Pid, _TS, _Name, _Dict, [P, _]}) when P =:= ParentPid -> Pid end)
    ).

wait_spawn_by_anonymous_fun(ParentPid, Name) ->
    test_util:wait_other_value(
        fun() ->
            find_spawned_by_anonymous_fun(ParentPid, Name)
        end,
        []
    ).

updater_pid(IndexerPid) ->
    {links, Links} = process_info(IndexerPid, links),
    [Pid] = select_process_by_name_prefix(Links, "couch_index_updater:init/1"),
    Pid.

select_process_by_name_prefix(Pids, Name) ->
    lists:filter(
        fun(Pid) ->
            Key = couch_debug:process_name(Pid),
            string:str(Key, Name) =:= 1
        end,
        Pids
    ).

select(MatchSpec) ->
    lists:filtermap(
        fun(Event) ->
            case ets:test_ms(Event, MatchSpec) of
                {ok, false} -> false;
                {ok, Result} -> {true, Result};
                _ -> false
            end
        end,
        tracer_events()
    ).

%% ========================
%% Tracer related functions
%% ------------------------
tracer_new() ->
    ets:new(?MODULE, [public, named_table]),
    {ok, _Tracer} = dbg:tracer(process, {fun tracer_collector/2, 0}),
    ok.

tracer_delete() ->
    dbg:stop(),
    (catch ets:delete(?MODULE)),
    ok.

tracer_record(Pid) ->
    {ok, _} = dbg:tp(erlang, put, x),
    {ok, _} = dbg:p(Pid, [c, p, sos]),
    ok.

tracer_events() ->
    Events = [{Idx, E} || [Idx, E] <- ets:match(?MODULE, {{trace, '$1'}, '$2'})],
    {_, Sorted} = lists:unzip(lists:keysort(1, Events)),
    Sorted.

tracer_collector(Msg, Seq) ->
    ets:insert(?MODULE, {{trace, Seq}, normalize_trace_msg(Msg)}),
    Seq + 1.

normalize_trace_msg(TraceMsg) ->
    case tuple_to_list(TraceMsg) of
        [trace_ts, Pid, Type | Info] ->
            {TraceInfo, [Timestamp]} = lists:split(length(Info) - 1, Info),
            {Type, Pid, Timestamp, couch_debug:process_name(Pid), process_info(Pid), TraceInfo};
        [trace, Pid, Type | TraceInfo] ->
            {Type, Pid, os:timestamp(), couch_debug:process_name(Pid), process_info(Pid), TraceInfo}
    end.
