% 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(cpse_test_purge_docs).
-compile(export_all).
-compile(nowarn_export_all).

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

-define(REV_DEPTH, 100).

setup_each() ->
    {ok, Db} = cpse_util:create_db(),
    couch_db:name(Db).

teardown_each(DbName) ->
    ok = couch_server:delete(DbName, []).

cpse_purge_simple(DbName) ->
    {ok, Rev} = cpse_util:save_doc(DbName, {[{'_id', foo1}, {vsn, 1.1}]}),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 1},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 2},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_simple_info_check(DbName) ->
    {ok, Rev} = cpse_util:save_doc(DbName, {[{'_id', foo1}, {vsn, 1.1}]}),
    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev]}
    ],
    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev], PRevs),

    {ok, AllInfos} = couch_util:with_db(DbName, fun(Db) ->
        couch_db_engine:fold_purge_infos(Db, 0, fun fold_all_infos/2, [], [])
    end),

    ?assertMatch([{1, <<_/binary>>, <<"foo1">>, [Rev]}], AllInfos).

cpse_purge_empty_db(DbName) ->
    PurgeInfos = [
        {cpse_util:uuid(), <<"foo">>, [{0, <<0>>}]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 1},
        {changes, 0},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_single_docid(DbName) ->
    {ok, [Rev1, _Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1}]},
        {[{'_id', foo2}, {vsn, 2}]}
    ]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 2},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev1]}
    ],
    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev1], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 1},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_multiple_docids(DbName) ->
    {ok, [Rev1, Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1.1}]},
        {[{'_id', foo2}, {vsn, 1.2}]}
    ]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 2},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev1]},
        {cpse_util:uuid(), <<"foo2">>, [Rev2]}
    ],

    {ok, [{ok, PRevs1}, {ok, PRevs2}]} = cpse_util:purge(DbName, PurgeInfos),

    ?assertEqual([Rev1], PRevs1),
    ?assertEqual([Rev2], PRevs2),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 0},
        {purge_seq, 2},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_no_docids(DbName) ->
    {ok, [_Rev1, _Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1}]},
        {[{'_id', foo2}, {vsn, 2}]}
    ]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 2},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    {ok, []} = cpse_util:purge(DbName, []),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 2},
        {purge_seq, 0},
        {purge_infos, []}
    ]).

cpse_purge_rev_path(DbName) ->
    {ok, Rev1} = cpse_util:save_doc(DbName, {[{'_id', foo}, {vsn, 1}]}),
    Update =
        {[
            {<<"_id">>, <<"foo">>},
            {<<"_rev">>, couch_doc:rev_to_str(Rev1)},
            {<<"_deleted">>, true},
            {<<"vsn">>, 2}
        ]},
    {ok, Rev2} = cpse_util:save_doc(DbName, Update),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 1},
        {update_seq, 2},
        {changes, 1},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo">>, [Rev2]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev2], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 0},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_deep_revision_path(DbName) ->
    {ok, InitRev} = cpse_util:save_doc(DbName, {[{'_id', bar}, {vsn, 0}]}),
    LastRev = lists:foldl(
        fun(Count, PrevRev) ->
            Update =
                {[
                    {'_id', bar},
                    {'_rev', couch_doc:rev_to_str(PrevRev)},
                    {vsn, Count}
                ]},
            {ok, NewRev} = cpse_util:save_doc(DbName, Update),
            NewRev
        end,
        InitRev,
        lists:seq(1, ?REV_DEPTH)
    ),

    PurgeInfos = [
        {cpse_util:uuid(), <<"bar">>, [LastRev]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([LastRev], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, ?REV_DEPTH + 2},
        {changes, 0},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_partial_revs(DbName) ->
    {ok, Rev1} = cpse_util:save_doc(DbName, {[{'_id', foo}, {vsn, <<"1.1">>}]}),
    Update =
        {[
            {'_id', foo},
            {'_rev', couch_doc:rev_to_str({1, [couch_hash:md5_hash(<<"1.2">>)]})},
            {vsn, <<"1.2">>}
        ]},
    {ok, [_Rev2]} = cpse_util:save_docs(DbName, [Update], [?REPLICATED_CHANGES]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo">>, [Rev1]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev1], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 1},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_missing_docid(DbName) ->
    {ok, [Rev1, _Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1}]},
        {[{'_id', foo2}, {vsn, 2}]}
    ]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 2},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"baz">>, [Rev1]}
    ],

    {ok, [{ok, []}]} = cpse_util:purge(DbName, PurgeInfos),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 2},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_duplicate_docids(DbName) ->
    {ok, [Rev1, _Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1}]},
        {[{'_id', foo2}, {vsn, 2}]}
    ]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 2},
        {purge_seq, 0},
        {changes, 2},
        {purge_infos, []}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev1]},
        {cpse_util:uuid(), <<"foo1">>, [Rev1]}
    ],

    {ok, Resp} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([{ok, [Rev1]}, {ok, []}], Resp),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 3},
        {purge_seq, 2},
        {changes, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_internal_revision(DbName) ->
    {ok, Rev1} = cpse_util:save_doc(DbName, {[{'_id', foo}, {vsn, 1}]}),
    Update =
        {[
            {'_id', foo},
            {'_rev', couch_doc:rev_to_str(Rev1)},
            {vsn, 2}
        ]},
    {ok, _Rev2} = cpse_util:save_doc(DbName, Update),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo">>, [Rev1]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 1},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_missing_revision(DbName) ->
    {ok, [_Rev1, Rev2]} = cpse_util:save_docs(DbName, [
        {[{'_id', foo1}, {vsn, 1}]},
        {[{'_id', foo2}, {vsn, 2}]}
    ]),

    PurgeInfos = [
        {cpse_util:uuid(), <<"foo1">>, [Rev2]}
    ],

    {ok, [{ok, PRevs}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([], PRevs),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 2},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 2},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

cpse_purge_repeated_revisions(DbName) ->
    {ok, Rev1} = cpse_util:save_doc(DbName, {[{'_id', foo}, {vsn, <<"1.1">>}]}),
    Update =
        {[
            {'_id', foo},
            {'_rev', couch_doc:rev_to_str({1, [couch_hash:md5_hash(<<"1.2">>)]})},
            {vsn, <<"1.2">>}
        ]},
    {ok, [Rev2]} = cpse_util:save_docs(DbName, [Update], [?REPLICATED_CHANGES]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 1},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    PurgeInfos1 = [
        {cpse_util:uuid(), <<"foo">>, [Rev1]},
        {cpse_util:uuid(), <<"foo">>, [Rev1, Rev2]}
    ],

    {ok, [{ok, PRevs1}, {ok, PRevs2}]} = cpse_util:purge(DbName, PurgeInfos1),
    ?assertEqual([Rev1], PRevs1),
    ?assertEqual([Rev2], PRevs2),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 3},
        {changes, 0},
        {purge_seq, 2},
        {purge_infos, PurgeInfos1}
    ]).

cpse_purge_repeated_uuid(DbName) ->
    {ok, Rev} = cpse_util:save_doc(DbName, {[{'_id', foo1}, {vsn, 1.1}]}),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 1},
        {del_doc_count, 0},
        {update_seq, 1},
        {changes, 1},
        {purge_seq, 0},
        {purge_infos, []}
    ]),

    UUID = cpse_util:uuid(),

    PurgeInfos = [
        {UUID, <<"foo1">>, [Rev]}
    ],

    {ok, [{ok, PRevs1}]} = cpse_util:purge(DbName, PurgeInfos),
    ?assertEqual([Rev], PRevs1),

    % Attempting to purge a repeated UUID is a no-op, it could have
    % been a race with the internal replicator
    ?assertEqual({ok, []}, cpse_util:purge(DbName, PurgeInfos)),

    % Purging a repeated UUID with a different Id or Revs is an error
    DifferentId = [{UUID, <<"foo2">>, [Rev]}],
    ?assertThrow({badreq, _}, cpse_util:purge(DbName, DifferentId)),
    DifferentRevs = [{UUID, <<"foo1">>, [Rev, <<"another">>]}],
    ?assertThrow({badreq, _}, cpse_util:purge(DbName, DifferentRevs)),

    % Although we can replicate it in
    {ok, []} = cpse_util:purge(DbName, PurgeInfos, [?REPLICATED_CHANGES]),

    cpse_util:assert_db_props(?MODULE, ?LINE, DbName, [
        {doc_count, 0},
        {del_doc_count, 0},
        {update_seq, 2},
        {changes, 0},
        {purge_seq, 1},
        {purge_infos, PurgeInfos}
    ]).

fold_all_infos(Info, Acc) ->
    {ok, [Info | Acc]}.
