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

-define(i2l(I), integer_to_list(I)).

test_db_name() -> <<"couch_test_update_conflicts">>.


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

    etap:plan(35),
    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()),
    couch_config:set("couchdb", "delayed_commits", "true", false),

    lists:foreach(
        fun(NumClients) -> test_concurrent_doc_update(NumClients) end,
        [100, 500, 1000, 2000, 5000]),

    test_bulk_delete_create(),

    couch_server_sup:stop(),
    ok.


% Verify that if multiple clients try to update the same document
% simultaneously, only one of them will get success response and all
% the other ones will get a conflict error. Also validate that the
% client which got the success response got its document version
% persisted into the database.
test_concurrent_doc_update(NumClients) ->
    {ok, Db} = create_db(test_db_name()),
    Doc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"foobar">>},
        {<<"value">>, 0}
    ]}),
    {ok, Rev} = couch_db:update_doc(Db, Doc, []),
    ok = couch_db:close(Db),
    RevStr = couch_doc:rev_to_str(Rev),
    etap:diag("Created first revision of test document"),

    etap:diag("Spawning " ++ ?i2l(NumClients) ++
        " clients to update the document"),
    Clients = lists:map(
        fun(Value) ->
            ClientDoc = couch_doc:from_json_obj({[
                {<<"_id">>, <<"foobar">>},
                {<<"_rev">>, RevStr},
                {<<"value">>, Value}
            ]}),
            Pid = spawn_client(ClientDoc),
            {Value, Pid, erlang:monitor(process, Pid)}
        end,
        lists:seq(1, NumClients)),

    lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients),
    etap:diag("Waiting for clients to finish"),

    {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} ->
                etap:bail("Client " ++ ?i2l(Value) ++
                    " got update error: " ++ couch_util:to_list(Error))
            after 60000 ->
                etap:bail("Timeout waiting for client " ++ ?i2l(Value) ++ " to die")
            end
        end,
        {0, nil},
        Clients),

    etap:diag("Verifying client results"),
    etap:is(
        NumConflicts,
        NumClients - 1,
        "Got " ++ ?i2l(NumClients - 1) ++ " client conflicts"),

    {ok, Db2} = couch_db:open_int(test_db_name(), []),
    {ok, Leaves} = couch_db:open_doc_revs(Db2, <<"foobar">>, all, []),
    ok = couch_db:close(Db2),
    etap:is(length(Leaves), 1, "Only one document revision was persisted"),
    [{ok, Doc2}] = Leaves,
    {JsonDoc} = couch_doc:to_json_obj(Doc2, []),
    etap:is(
        couch_util:get_value(<<"value">>, JsonDoc),
        SavedValue,
        "Persisted doc has the right value"),

    ok = timer:sleep(1000),
    etap:diag("Restarting the server"),
    couch_server_sup:stop(),
    ok = timer:sleep(1000),
    couch_server_sup:start_link(test_util:config_files()),

    {ok, Db3} = couch_db:open_int(test_db_name(), []),
    {ok, Leaves2} = couch_db:open_doc_revs(Db3, <<"foobar">>, all, []),
    ok = couch_db:close(Db3),
    etap:is(length(Leaves2), 1, "Only one document revision was persisted"),
    [{ok, Doc3}] = Leaves,
    etap:is(Doc3, Doc2, "Got same document after server restart"),

    delete_db(Db3).


% COUCHDB-188
test_bulk_delete_create() ->
    {ok, Db} = create_db(test_db_name()),
    Doc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"foobar">>},
        {<<"value">>, 0}
    ]}),
    {ok, Rev} = couch_db:update_doc(Db, Doc, []),

    DeletedDoc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"foobar">>},
        {<<"_rev">>, couch_doc:rev_to_str(Rev)},
        {<<"_deleted">>, true}
    ]}),
    NewDoc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"foobar">>},
        {<<"value">>, 666}
    ]}),

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

    etap:is(length([ok || {ok, _} <- Results]), 2,
        "Deleted and non-deleted versions got an ok reply"),

    [{ok, Rev1}, {ok, Rev2}] = Results,
    {ok, Db2} = couch_db:open_int(test_db_name(), []),

    {ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
        Db2, <<"foobar">>, [Rev1], [conflicts, deleted_conflicts]),
    {ok, [{ok, Doc2}]} = couch_db:open_doc_revs(
        Db2, <<"foobar">>, [Rev2], [conflicts, deleted_conflicts]),
    ok = couch_db:close(Db2),

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

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

    etap:is(element(1, Rev1), 2, "Deleted revision has position 2"),
    etap:is(element(1, Rev2), 1, "New leaf revision has position 1"),

    delete_db(Db2).


spawn_client(Doc) ->
    spawn(fun() ->
        {ok, Db} = couch_db:open_int(test_db_name(), []),
        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).


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


delete_db(Db) ->
    ok = couch_server:delete(
        couch_db:name(Db), [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]).
