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

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

-define(USER, "chttpd_view_test_admin").
-define(PASS, "pass").
-define(AUTH, {basic_auth, {?USER, ?PASS}}).
-define(CONTENT_JSON, {"Content-Type", "application/json"}).

-define(DOCS, #{
    <<"docs">> => [
        #{<<"_id">> => <<"a">>, <<"key">> => <<"a">>, <<"value">> => 1},
        #{<<"_id">> => <<"b">>, <<"key">> => <<"b">>, <<"value">> => 2},
        #{<<"_id">> => <<"c">>, <<"key">> => <<"c">>, <<"value">> => 3},
        #{<<"_id">> => <<"d">>, <<"key">> => <<"d">>, <<"_deleted">> => true}
    ]
}).
-define(DDOC, #{
    <<"_id">> => <<"_design/ddoc">>,
    <<"views">> => #{
        <<"map">> => #{<<"map">> => <<"function(doc) { emit(doc.key, doc.value) }">>},
        <<"map_reduce">> => #{
            <<"map">> => <<"function(doc) { emit(doc.key, doc.value) }">>,
            <<"reduce">> => <<"_sum">>
        }
    }
}).
-define(ERROR_KEYS_INCOMPATIBLE, #{
    <<"error">> := <<"query_parse_error">>,
    <<"reason">> := <<"`keys` is incompatible with `key`, `start_key` and `end_key`">>
}).

% seconds
-define(TIMEOUT, 60).

setup() ->
    Hashed = couch_passwords:hash_admin_password(?PASS),
    ok = config:set("admins", ?USER, ?b2l(Hashed), false),
    ok = config:set("query_server_config", "query_limit", "100", false),
    Db = ?tempdb(),
    ok = create_db(Db),
    ok = create_docs(Db),
    ok = create_ddoc(Db),
    Db.

teardown(Db) ->
    ok = fabric:delete_db(Db),
    ok = config:delete("admins", ?USER, false),
    ok = config:delete("query_server_config", "query_limit", false).

view_test_() ->
    {
        "chttpd view tests",
        {
            setup,
            fun chttpd_test_util:start_couch/0,
            fun chttpd_test_util:stop_couch/1,
            {
                foreach,
                fun setup/0,
                fun teardown/1,
                [
                    ?TDEF_FE(t_view_with_queries_keys, ?TIMEOUT),
                    ?TDEF_FE(t_view_with_queries_limit_skip, ?TIMEOUT),
                    ?TDEF_FE(t_view_with_multiple_queries, ?TIMEOUT),
                    ?TDEF_FE(t_view_with_key_and_start_key),
                    ?TDEF_FE(t_view_with_key_and_end_key),
                    ?TDEF_FE(t_view_with_single_keys_and_start_key),
                    ?TDEF_FE(t_view_with_keys_and_start_key),
                    ?TDEF_FE(t_view_with_key_non_existent_docs),
                    ?TDEF_FE(t_view_with_keys_non_existent_docs),
                    ?TDEF_FE(t_view_with_key_deleted_docs),
                    ?TDEF_FE(t_view_with_keys_deleted_docs),
                    ?TDEF_FE(t_view_map_reduce_with_key),
                    ?TDEF_FE(t_view_map_reduce_with_single_keys),
                    ?TDEF_FE(t_view_map_reduce_with_single_keys_and_group),
                    ?TDEF_FE(t_view_map_reduce_with_keys),
                    ?TDEF_FE(t_view_map_reduce_with_keys_and_group)
                ]
            }
        }
    }.

all_docs_test_() ->
    {
        "chttpd all docs tests",
        {
            setup,
            fun chttpd_test_util:start_couch/0,
            fun chttpd_test_util:stop_couch/1,
            {
                foreach,
                fun setup/0,
                fun teardown/1,
                [
                    ?TDEF_FE(t_all_docs_with_key_and_start_key),
                    ?TDEF_FE(t_all_docs_with_key_and_end_key),
                    ?TDEF_FE(t_all_docs_with_single_keys_and_start_key),
                    ?TDEF_FE(t_all_docs_with_keys_and_start_key),
                    ?TDEF_FE(t_all_docs_with_key_non_existent_docs),
                    ?TDEF_FE(t_all_docs_with_keys_non_existent_docs),
                    ?TDEF_FE(t_all_docs_with_key_deleted_docs),
                    ?TDEF_FE(t_all_docs_with_keys_deleted_docs),
                    ?TDEF_FE(t_all_docs_with_limit),
                    ?TDEF_FE(t_all_docs_with_skip),
                    ?TDEF_FE(t_all_docs_with_limit_and_skip),
                    ?TDEF_FE(t_all_docs_with_configured_query_limit)
                ]
            }
        }
    }.

t_view_with_queries_keys(Db) ->
    QueryDoc = #{<<"queries">> => [#{<<"keys">> => [<<"a">>, <<"c">>]}]},
    {Code, Res} = req(post, url(Db, "_design/ddoc/_view/map/queries"), QueryDoc),
    ?assertMatch(
        #{
            <<"results">> := [
                #{
                    <<"total_rows">> := 3,
                    <<"offset">> := 1,
                    <<"rows">> := [#{<<"id">> := <<"a">>}, #{<<"id">> := <<"c">>}]
                }
            ]
        },
        Res
    ),
    ?assertEqual(200, Code).

t_view_with_queries_limit_skip(Db) ->
    QueryDoc = #{<<"queries">> => [#{<<"limit">> => 1, <<"skip">> => 1}]},
    {Code, Res} = req(post, url(Db, "_design/ddoc/_view/map/queries/"), QueryDoc),
    ?assertMatch(
        #{
            <<"results">> := [
                #{<<"total_rows">> := 3, <<"offset">> := 1, <<"rows">> := [#{<<"id">> := <<"b">>}]}
            ]
        },
        Res
    ),
    ?assertEqual(200, Code).

t_view_with_multiple_queries(Db) ->
    QueryDoc = #{
        <<"queries">> => [#{<<"keys">> => [<<"a">>, <<"c">>], <<"limit">> => 1, <<"skip">> => 1}]
    },
    {Code, Res} = req(post, url(Db, "_design/ddoc/_view/map/queries/"), QueryDoc),
    ?assertMatch(
        #{
            <<"results">> := [
                #{<<"total_rows">> := 3, <<"offset">> := 2, <<"rows">> := [#{<<"id">> := <<"c">>}]}
            ]
        },
        Res
    ),
    ?assertEqual(200, Code).

t_view_with_key_and_start_key(Db) ->
    {Code1, Res1} = req(get, url(Db, "_design/ddoc/_view/map", "key=\"a\"&startkey=\"b\"")),
    {Code2, Res2} = req(get, url(Db, "_design/ddoc/_view/map", "startkey=\"b\"&key=\"a\"")),
    ?assertMatch(
        #{
            <<"error">> := <<"query_parse_error">>,
            <<"reason">> :=
                <<"No rows can match your key range, reverse your start_key and end_key or set descending=true">>
        },
        Res1
    ),
    ?assertMatch(#{<<"rows">> := [#{<<"id">> := <<"a">>}]}, Res2),
    ?assertEqual(400, Code1),
    ?assertEqual(200, Code2).

t_all_docs_with_key_and_start_key(Db) ->
    {Code1, Res1} = req(get, url(Db, "_all_docs", "key=\"a\"&startkey=\"b\"")),
    {Code2, Res2} = req(get, url(Db, "_all_docs", "startkey=\"b\"&key=\"a\"")),
    ?assertMatch(#{<<"rows">> := []}, Res1),
    ?assertMatch(#{<<"rows">> := [#{<<"id">> := <<"a">>}]}, Res2),
    ?assertEqual(200, Code1),
    ?assertEqual(200, Code2).

t_view_with_key_and_end_key(Db) ->
    test_helper_key_and_end_key(Db, "_design/ddoc/_view/map").

t_all_docs_with_key_and_end_key(Db) ->
    test_helper_key_and_end_key(Db, "_all_docs").

test_helper_key_and_end_key(Db, Path) ->
    {Code1, Res1} = req(get, url(Db, Path, "key=\"a\"&endkey=\"b\"")),
    {Code2, Res2} = req(get, url(Db, Path, "endkey=\"b\"&key=\"a\"")),
    ?assertMatch(#{<<"rows">> := [#{<<"id">> := <<"a">>}, #{<<"id">> := <<"b">>}]}, Res1),
    ?assertMatch(#{<<"rows">> := [#{<<"id">> := <<"a">>}]}, Res2),
    ?assertEqual(200, Code1),
    ?assertEqual(200, Code2).

t_view_with_single_keys_and_start_key(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map?keys=[\"a\"]&startkey=\"b\"")),
    ?assertMatch(
        #{
            <<"error">> := <<"query_parse_error">>,
            <<"reason">> :=
                <<"No rows can match your key range, reverse your start_key and end_key or set descending=true">>
        },
        Res
    ),
    ?assertEqual(400, Code).

t_all_docs_with_single_keys_and_start_key(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs?keys=[\"a\"]&startkey=\"b\"")),
    ?assertMatch(?ERROR_KEYS_INCOMPATIBLE, Res),
    ?assertEqual(400, Code).

t_view_with_keys_and_start_key(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map", "keys=[\"a\",\"b\"]&start_key=\"b\"")),
    ?assertMatch(?ERROR_KEYS_INCOMPATIBLE, Res),
    ?assertEqual(400, Code).

t_all_docs_with_keys_and_start_key(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "keys=[\"a\",\"b\"]&start_key=\"b\"")),
    ?assertMatch(?ERROR_KEYS_INCOMPATIBLE, Res),
    ?assertEqual(400, Code).

t_view_with_key_non_existent_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map", "key=\"not_exist\"")),
    ?assertMatch(#{<<"total_rows">> := 3, <<"offset">> := 3, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map"), #{<<"key">> => <<"not_exist">>}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_all_docs_with_key_non_existent_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "key=\"not_exist\"")),
    ?assertMatch(#{<<"total_rows">> := 4, <<"offset">> := 4, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_all_docs"), #{<<"key">> => <<"not_exist">>}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_with_keys_non_existent_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map", "keys=[\"not_exist\"]")),
    ?assertMatch(#{<<"total_rows">> := 3, <<"offset">> := 3, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map"), #{<<"keys">> => [<<"not_exist">>]}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_all_docs_with_keys_non_existent_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "keys=[\"not_exist\"]")),
    ?assertMatch(
        #{
            <<"total_rows">> := 4,
            <<"offset">> := null,
            <<"rows">> := [#{<<"key">> := <<"not_exist">>, <<"error">> := <<"not_found">>}]
        },
        Res
    ),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_all_docs"), #{<<"keys">> => [<<"not_exist">>]}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_with_key_deleted_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map", "key=\"d\"")),
    ?assertMatch(#{<<"total_rows">> := 3, <<"offset">> := 3, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map"), #{<<"key">> => <<"d">>}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_all_docs_with_key_deleted_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "key=\"d\"")),
    ?assertMatch(#{<<"total_rows">> := 4, <<"offset">> := 4, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_all_docs"), #{<<"key">> => <<"d">>}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_with_keys_deleted_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map", "keys=[\"d\"]")),
    ?assertMatch(#{<<"total_rows">> := 3, <<"offset">> := 3, <<"rows">> := []}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map"), #{<<"keys">> => [<<"d">>]}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_all_docs_with_keys_deleted_docs(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "keys=[\"d\"]")),
    ?assertMatch(
        #{
            <<"total_rows">> := 4,
            <<"offset">> := null,
            <<"rows">> := [
                #{
                    <<"id">> := <<"d">>,
                    <<"key">> := <<"d">>,
                    <<"value">> := #{<<"deleted">> := true}
                }
            ]
        },
        Res
    ),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_all_docs"), #{<<"keys">> => [<<"d">>]}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_map_reduce_with_key(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map_reduce", "key=\"a\"")),
    ?assertMatch(#{<<"rows">> := [#{<<"key">> := null, <<"value">> := 1}]}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map_reduce"), #{<<"key">> => <<"a">>}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_map_reduce_with_single_keys(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map_reduce", "keys=[\"a\"]")),
    ?assertMatch(#{<<"rows">> := [#{<<"key">> := null, <<"value">> := 1}]}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map_reduce"), #{<<"keys">> => [<<"a">>]}),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_map_reduce_with_single_keys_and_group(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map_reduce?keys=[\"a\"]&group=true")),
    ?assertMatch(#{<<"rows">> := [#{<<"key">> := <<"a">>, <<"value">> := 1}]}, Res),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map_reduce"), #{
        <<"keys">> => [<<"a">>], <<"group">> => <<"true">>
    }),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_map_reduce_with_keys(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map_reduce?keys=[\"a\",\"b\"]")),
    ?assertMatch(
        #{
            <<"error">> := <<"query_parse_error">>,
            <<"reason">> := <<"Multi-key fetches for reduce views must use `group=true`">>
        },
        Res
    ),
    ?assertEqual(400, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map_reduce"), #{
        <<"keys">> => [<<"a">>, <<"b">>]
    }),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_view_map_reduce_with_keys_and_group(Db) ->
    {Code, Res} = req(get, url(Db, "_design/ddoc/_view/map_reduce?keys=[\"a\",\"b\"]&group=true")),
    ?assertMatch(
        #{
            <<"rows">> := [
                #{<<"key">> := <<"a">>, <<"value">> := 1},
                #{<<"key">> := <<"b">>, <<"value">> := 2}
            ]
        },
        Res
    ),
    ?assertEqual(200, Code),
    {Code1, Res1} = req(post, url(Db, "_design/ddoc/_view/map_reduce"), #{
        <<"keys">> => [<<"a">>, <<"b">>], <<"group">> => true
    }),
    ?assertEqual(Res, Res1),
    ?assertEqual(Code, Code1).

t_all_docs_with_limit(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "limit=2")),
    ?assertEqual(200, Code),
    ?assertMatch(
        #{
            <<"offset">> := 0,
            <<"total_rows">> := 4,
            <<"rows">> := [
                #{<<"id">> := <<"_design/ddoc">>},
                #{<<"id">> := <<"a">>}
            ]
        },
        Res
    ).

t_all_docs_with_skip(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "skip=2")),
    ?assertEqual(200, Code),
    ?assertMatch(
        #{
            <<"offset">> := 2,
            <<"total_rows">> := 4,
            <<"rows">> := [
                #{<<"id">> := <<"b">>},
                #{<<"id">> := <<"c">>}
            ]
        },
        Res
    ).

t_all_docs_with_limit_and_skip(Db) ->
    {Code, Res} = req(get, url(Db, "_all_docs", "skip=2&limit=1")),
    ?assertEqual(200, Code),
    ?assertMatch(
        #{
            <<"offset">> := 2,
            <<"total_rows">> := 4,
            <<"rows">> := [
                #{<<"id">> := <<"b">>}
            ]
        },
        Res
    ).

t_all_docs_with_configured_query_limit(Db) ->
    config:set("query_server_config", "query_limit", "2", false),
    {Code, Res} = req(get, url(Db, "_all_docs")),
    ?assertEqual(200, Code),
    ?assertMatch(
        #{
            <<"offset">> := 0,
            <<"total_rows">> := 4,
            <<"rows">> := [
                #{<<"id">> := <<"_design/ddoc">>},
                #{<<"id">> := <<"a">>}
            ]
        },
        Res
    ).

%%%%%%%%%%%%%%%%%%%% Utility Functions %%%%%%%%%%%%%%%%%%%%
url(Db) ->
    Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
    Port = mochiweb_socket_server:get(chttpd, port),
    lists:concat(["http://", Addr, ":", Port, "/", ?b2l(Db)]).

url(Db, Path) ->
    url(Db) ++ "/" ++ Path.

url(Db, Path, Parameters) ->
    url(Db) ++ "/" ++ Path ++ "?" ++ Parameters.

create_db(Db) ->
    case req(put, url(Db)) of
        {201, #{}} -> ok;
        Error -> error({failed_to_create_test_db, Db, Error})
    end.

create_docs(Db) ->
    case req(post, url(Db) ++ "/_bulk_docs", ?DOCS) of
        {201, _} -> ok;
        Error -> error({failed_to_create_docs, Db, Error})
    end.

create_ddoc(Db) ->
    case req(post, url(Db), ?DDOC) of
        {201, _} -> ok;
        Error -> error({failed_to_create_ddocs, Db, Error})
    end.

req(Method, Url) ->
    Headers = [?CONTENT_JSON, ?AUTH],
    {ok, Code, _, Res} = test_request:request(Method, Url, Headers),
    {Code, jiffy:decode(Res, [return_maps])}.

req(Method, Url, #{} = Body) ->
    req(Method, Url, jiffy:encode(Body));
req(Method, Url, Body) ->
    Headers = [?CONTENT_JSON, ?AUTH],
    {ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body),
    {Code, jiffy:decode(Res, [return_maps])}.
