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

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

-define(i2l(I), integer_to_list(I)).
-define(DOC_ID, <<"foobar">>).
-define(LOCAL_DOC_ID, <<"_local/foobar">>).
-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]).
-define(TIMEOUT, 20000).

start() ->
    test_util:start_couch().

setup() ->
    DbName = ?tempdb(),
    {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX, overwrite]),
    Doc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, ?DOC_ID},
            {<<"value">>, 0}
        ]}
    ),
    {ok, Rev} = couch_db:update_doc(Db, Doc, []),
    ok = couch_db:close(Db),
    RevStr = couch_doc:rev_to_str(Rev),
    {DbName, RevStr}.
setup(_) ->
    setup().

teardown({DbName, _}) ->
    ok = couch_server:delete(DbName, []),
    ok.
teardown(_, {DbName, _RevStr}) ->
    teardown({DbName, _RevStr}).

view_indexes_cleanup_test_() ->
    {
        "Update conflicts",
        {
            setup,
            fun start/0,
            fun test_util:stop_couch/1,
            [
                concurrent_updates(),
                bulk_docs_updates()
            ]
        }
    }.

concurrent_updates() ->
    {
        "Concurrent updates",
        {
            foreachx,
            fun setup/1,
            fun teardown/2,
            [
                {NumClients, fun should_concurrently_update_doc/2}
             || NumClients <- ?NUM_CLIENTS
            ]
        }
    }.

bulk_docs_updates() ->
    {
        "Bulk docs updates",
        {
            foreach,
            fun setup/0,
            fun teardown/1,
            [
                fun should_bulk_create_delete_doc/1,
                fun should_bulk_create_local_doc/1,
                fun should_ignore_invalid_local_doc/1
            ]
        }
    }.

should_concurrently_update_doc(NumClients, {DbName, InitRev}) ->
    {
        ?i2l(NumClients) ++ " clients",
        {inorder, [
            {"update doc",
                {timeout, ?TIMEOUT div 1000,
                    ?_test(concurrent_doc_update(NumClients, DbName, InitRev))}},
            {"ensure in single leaf", ?_test(ensure_in_single_revision_leaf(DbName))}
        ]}
    }.

should_bulk_create_delete_doc({DbName, InitRev}) ->
    ?_test(bulk_delete_create(DbName, InitRev)).

should_bulk_create_local_doc({DbName, _}) ->
    ?_test(bulk_create_local_doc(DbName)).

should_ignore_invalid_local_doc({DbName, _}) ->
    ?_test(ignore_invalid_local_doc(DbName)).

concurrent_doc_update(NumClients, DbName, InitRev) ->
    Clients = lists:map(
        fun(Value) ->
            ClientDoc = couch_doc:from_json_obj(
                {[
                    {<<"_id">>, ?DOC_ID},
                    {<<"_rev">>, InitRev},
                    {<<"value">>, Value}
                ]}
            ),
            Pid = spawn_client(DbName, ClientDoc),
            {Value, Pid, monitor(process, Pid)}
        end,
        lists:seq(1, NumClients)
    ),

    lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients),

    {NumConflicts, SavedValue} = lists:foldl(
        fun({Value, Pid, MonRef}, {AccConflicts, AccValue}) ->
            receive
                {'DOWN', MonRef, process, Pid, {ok, _NewRev}} ->
                    {AccConflicts, Value};
                {'DOWN', MonRef, process, Pid, conflict} ->
                    {AccConflicts + 1, AccValue};
                {'DOWN', MonRef, process, Pid, Error} ->
                    error(
                        {assertion_failed, [
                            {module, ?MODULE},
                            {line, ?LINE},
                            {reason,
                                "Client " ++ ?i2l(Value) ++
                                    " got update error: " ++
                                    couch_util:to_list(Error)}
                        ]}
                    )
            after ?TIMEOUT div 2 ->
                error(
                    {assertion_failed, [
                        {module, ?MODULE},
                        {line, ?LINE},
                        {reason,
                            "Timeout waiting for client " ++
                                ?i2l(Value) ++ " to die"}
                    ]}
                )
            end
        end,
        {0, nil},
        Clients
    ),
    ?assertEqual(NumClients - 1, NumConflicts),

    {ok, Db} = couch_db:open_int(DbName, []),
    {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
    ok = couch_db:close(Db),
    ?assertEqual(1, length(Leaves)),

    [{ok, Doc2}] = Leaves,
    {JsonDoc} = couch_doc:to_json_obj(Doc2, []),
    ?assertEqual(SavedValue, couch_util:get_value(<<"value">>, JsonDoc)).

ensure_in_single_revision_leaf(DbName) ->
    {ok, Db} = couch_db:open_int(DbName, []),
    {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
    ok = couch_db:close(Db),
    [{ok, Doc}] = Leaves,

    %% FIXME: server restart won't work from test side
    %% stop(ok),
    %% start(),

    {ok, Db2} = couch_db:open_int(DbName, []),
    {ok, Leaves2} = couch_db:open_doc_revs(Db2, ?DOC_ID, all, []),
    ok = couch_db:close(Db2),
    ?assertEqual(1, length(Leaves2)),

    [{ok, Doc2}] = Leaves,
    ?assertEqual(Doc, Doc2).

bulk_delete_create(DbName, InitRev) ->
    {ok, Db} = couch_db:open_int(DbName, []),

    DeletedDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, ?DOC_ID},
            {<<"_rev">>, InitRev},
            {<<"_deleted">>, true}
        ]}
    ),
    NewDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, ?DOC_ID},
            {<<"value">>, 666}
        ]}
    ),

    {ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []),
    ok = couch_db:close(Db),

    ?assertEqual(2, length([ok || {ok, _} <- Results])),
    [{ok, Rev1}, {ok, Rev2}] = Results,

    {ok, Db2} = couch_db:open_int(DbName, []),
    {ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
        Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]
    ),
    {ok, [{ok, Doc2}]} = couch_db:open_doc_revs(
        Db2, ?DOC_ID, [Rev2], [conflicts, deleted_conflicts]
    ),
    ok = couch_db:close(Db2),

    {Doc1Props} = couch_doc:to_json_obj(Doc1, []),
    {Doc2Props} = couch_doc:to_json_obj(Doc2, []),

    %% Document was deleted
    ?assert(couch_util:get_value(<<"_deleted">>, Doc1Props)),
    %% New document not flagged as deleted
    ?assertEqual(
        undefined,
        couch_util:get_value(
            <<"_deleted">>,
            Doc2Props
        )
    ),
    %% New leaf revision has the right value
    ?assertEqual(
        666,
        couch_util:get_value(
            <<"value">>,
            Doc2Props
        )
    ),
    %% Deleted document has no conflicts
    ?assertEqual(
        undefined,
        couch_util:get_value(
            <<"_conflicts">>,
            Doc1Props
        )
    ),
    %% Deleted document has no deleted conflicts
    ?assertEqual(
        undefined,
        couch_util:get_value(
            <<"_deleted_conflicts">>,
            Doc1Props
        )
    ),
    %% New leaf revision doesn't have conflicts
    ?assertEqual(
        undefined,
        couch_util:get_value(
            <<"_conflicts">>,
            Doc1Props
        )
    ),
    %% New leaf revision doesn't have deleted conflicts
    ?assertEqual(
        undefined,
        couch_util:get_value(
            <<"_deleted_conflicts">>,
            Doc1Props
        )
    ),

    %% Deleted revision has position 2
    ?assertEqual(2, element(1, Rev1)),
    %% New leaf revision has position 3
    ?assertEqual(3, element(1, Rev2)).

bulk_create_local_doc(DbName) ->
    {ok, Db} = couch_db:open_int(DbName, []),

    LocalDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, ?LOCAL_DOC_ID},
            {<<"_rev">>, <<"0-1">>}
        ]}
    ),

    {ok, Results} = couch_db:update_docs(
        Db,
        [LocalDoc],
        [],
        ?REPLICATED_CHANGES
    ),
    ok = couch_db:close(Db),
    ?assertEqual([], Results),

    {ok, Db2} = couch_db:open_int(DbName, []),
    {ok, LocalDoc1} = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
    ok = couch_db:close(Db2),
    ?assertEqual(?LOCAL_DOC_ID, LocalDoc1#doc.id),
    ?assertEqual({0, [<<"2">>]}, LocalDoc1#doc.revs).

ignore_invalid_local_doc(DbName) ->
    {ok, Db} = couch_db:open_int(DbName, []),

    LocalDoc = couch_doc:from_json_obj(
        {[
            {<<"_id">>, ?LOCAL_DOC_ID},
            {<<"_rev">>, <<"0-abcdef">>}
        ]}
    ),

    {ok, Results} = couch_db:update_docs(
        Db,
        [LocalDoc],
        [],
        ?REPLICATED_CHANGES
    ),
    ok = couch_db:close(Db),
    ?assertEqual([], Results),

    {ok, Db2} = couch_db:open_int(DbName, []),
    Result2 = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
    ok = couch_db:close(Db2),
    ?assertEqual({not_found, missing}, Result2).

spawn_client(DbName, Doc) ->
    spawn(fun() ->
        {ok, Db} = couch_db:open_int(DbName, []),
        receive
            go -> ok
        end,
        erlang:yield(),
        Result =
            try
                couch_db:update_doc(Db, Doc, [])
            catch
                _:Error ->
                    Error
            end,
        ok = couch_db:close(Db),
        exit(Result)
    end).
