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

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

main(_) ->
    etap:plan(10),
    case (catch test()) of
        ok ->
            etap:end_tests();
        Other ->
            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
            etap:bail(Other)
    end,
    ok.

%% NOTE: since during the test we stop the server,
%%       a huge and ugly but harmless stack trace is sent to stderr
%%
test() ->
    test_util:start_couch(),

    timer:sleep(1000),
    delete_db(),
    create_db(),

    create_docs(),
    create_design_doc(),

    % make DB file backup
    backup_db_file(),

    put(addr, couch_config:get("httpd", "bind_address", "127.0.0.1")),
    put(port, integer_to_list(mochiweb_socket_server:get(couch_http, port))),

    create_new_doc(),
    query_view_before_restore_backup(),

    % restore DB file backup after querying view
    restore_backup_db_file(),

    query_view_after_restore_backup(),

    delete_db(),
    catch test_util:stop_couch(),
    ok.

admin_user_ctx() ->
    {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.

create_db() ->
    {ok, _} = couch_db:create(test_db_name(), [admin_user_ctx()]).

delete_db() ->
    couch_server:delete(test_db_name(), [admin_user_ctx()]).

create_docs() ->
    {ok, Db} = couch_db:open(test_db_name(), [admin_user_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:ensure_full_commit(Db),
    couch_db:close(Db).

create_design_doc() ->
    {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
    DDoc = couch_doc:from_json_obj({[
        {<<"_id">>, <<"_design/foo">>},
        {<<"language">>, <<"javascript">>},
        {<<"views">>, {[
            {<<"bar">>, {[
                {<<"map">>, <<"function(doc) { emit(doc.value, 1); }">>}
            ]}}
        ]}}
    ]}),
    {ok, _} = couch_db:update_docs(Db, [DDoc]),
    couch_db:ensure_full_commit(Db),
    couch_db:close(Db).

backup_db_file() ->
    DbFile = test_util:test_file("data/" ++
        binary_to_list(test_db_name()) ++ ".couch"),
    {ok, _} = file:copy(DbFile, DbFile ++ ".backup"),
    ok.

create_new_doc() ->
    {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
    Doc666 = couch_doc:from_json_obj({[
        {<<"_id">>, <<"doc666">>},
        {<<"value">>, 999}

    ]}),
    {ok, _} = couch_db:update_docs(Db, [Doc666]),
    couch_db:ensure_full_commit(Db),
    couch_db:close(Db).

db_url() ->
    "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/" ++
    binary_to_list(test_db_name()).

query_view_before_restore_backup() ->
    {ok, Code, _Headers, Body} = test_util:request(
        db_url() ++ "/_design/foo/_view/bar", [], get),
    etap:is(Code, 200, "Got view response before restoring backup."),
    ViewJson = couch_util:json_decode(Body),
    Rows = couch_util:get_nested_json_value(ViewJson, [<<"rows">>]),
    HasDoc1 = has_doc("doc1", Rows),
    HasDoc2 = has_doc("doc2", Rows),
    HasDoc3 = has_doc("doc3", Rows),
    HasDoc666 = has_doc("doc666", Rows),
    etap:is(HasDoc1, true, "Before backup restore, view has doc1"),
    etap:is(HasDoc2, true, "Before backup restore, view has doc2"),
    etap:is(HasDoc3, true, "Before backup restore, view has doc3"),
    etap:is(HasDoc666, true, "Before backup restore, view has doc666"),
    ok.

has_doc(DocId1, Rows) ->
    DocId = iolist_to_binary(DocId1),
    lists:any(
        fun({R}) -> lists:member({<<"id">>, DocId}, R) end,
        Rows
    ).

restore_backup_db_file() ->
    test_util:stop_couch(),
    timer:sleep(3000),
    DbFile = test_util:test_file("data/" ++
    binary_to_list(test_db_name()) ++ ".couch"),
    ok = file:delete(DbFile),
    ok = file:rename(DbFile ++ ".backup", DbFile),
    test_util:start_couch(),
    timer:sleep(1000),
    put(port, integer_to_list(mochiweb_socket_server:get(couch_http, port))),
    ok.

query_view_after_restore_backup() ->
    {ok, Code, _Headers, Body} = test_util:request(
        db_url() ++ "/_design/foo/_view/bar", [], get),
    etap:is(Code, 200, "Got view response after restoring backup."),
    ViewJson = couch_util:json_decode(Body),
    Rows = couch_util:get_nested_json_value(ViewJson, [<<"rows">>]),
    HasDoc1 = has_doc("doc1", Rows),
    HasDoc2 = has_doc("doc2", Rows),
    HasDoc3 = has_doc("doc3", Rows),
    HasDoc666 = has_doc("doc666", Rows),
    etap:is(HasDoc1, true, "After backup restore, view has doc1"),
    etap:is(HasDoc2, true, "After backup restore, view has doc2"),
    etap:is(HasDoc3, true, "After backup restore, view has doc3"),
    etap:is(HasDoc666, false, "After backup restore, view does not have doc666"),
    ok.
