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

filename() -> test_util:build_file("test/etap/temp.011").
sizeblock() -> 4096. % Need to keep this in sync with couch_file.erl

main(_) ->
    test_util:init_code_path(),
    {S1, S2, S3} = now(),
    random:seed(S1, S2, S3),

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

test() ->
    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),

    etap:is({ok, 0}, couch_file:bytes(Fd),
        "File should be initialized to contain zero bytes."),

    etap:is(ok, couch_file:write_header(Fd, {<<"some_data">>, 32}),
        "Writing a header succeeds."),

    {ok, Size1} = couch_file:bytes(Fd),
    etap:is_greater(Size1, 0,
        "Writing a header allocates space in the file."),

    etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
        "Reading the header returns what we wrote."),

    etap:is(ok, couch_file:write_header(Fd, [foo, <<"more">>]),
        "Writing a second header succeeds."),

    {ok, Size2} = couch_file:bytes(Fd),
    etap:is_greater(Size2, Size1,
        "Writing a second header allocates more space."),

    etap:is({ok, [foo, <<"more">>]}, couch_file:read_header(Fd),
        "Reading the second header does not return the first header."),

    % Delete the second header.
    ok = couch_file:truncate(Fd, Size1),

    etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
        "Reading the header after a truncation returns a previous header."),

    couch_file:write_header(Fd, [foo, <<"more">>]),
    etap:is({ok, Size2}, couch_file:bytes(Fd),
        "Rewriting the same second header returns the same second size."),

    couch_file:write_header(Fd, erlang:make_tuple(5000, <<"CouchDB">>)),
    etap:is(
        couch_file:read_header(Fd),
        {ok, erlang:make_tuple(5000, <<"CouchDB">>)},
        "Headers larger than the block size can be saved (COUCHDB-1319)"
    ),

    ok = couch_file:close(Fd),

    % Now for the fun stuff. Try corrupting the second header and see
    % if we recover properly.

    % Destroy the 0x1 byte that marks a header
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        file:pwrite(RawFd, HeaderPos, <<0>>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the byte marker should read the previous header.")
    end),

    % Corrupt the size.
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +1 for 0x1 byte marker
        file:pwrite(RawFd, HeaderPos+1, <<10/integer>>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the size should read the previous header.")
    end),

    % Corrupt the MD5 signature
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +5 = +1 for 0x1 byte and +4 for term size.
        file:pwrite(RawFd, HeaderPos+5, <<"F01034F88D320B22">>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the MD5 signature should read the previous header.")
    end),

    % Corrupt the data
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig
        file:pwrite(RawFd, HeaderPos+21, <<"some data goes here!">>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the header data should read the previous header.")
    end),

    ok.

check_header_recovery(CheckFun) ->
    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
    {ok, RawFd} = file:open(filename(), [read, write, raw, binary]),

    {ok, _} = write_random_data(Fd),
    ExpectHeader = {some_atom, <<"a binary">>, 756},
    ok = couch_file:write_header(Fd, ExpectHeader),

    {ok, HeaderPos} = write_random_data(Fd),
    ok = couch_file:write_header(Fd, {2342, <<"corruption! greed!">>}),

    CheckFun(Fd, RawFd, {ok, ExpectHeader}, HeaderPos),

    ok = file:close(RawFd),
    ok = couch_file:close(Fd),
    ok.

write_random_data(Fd) ->
    write_random_data(Fd, 100 + random:uniform(1000)).

write_random_data(Fd, 0) ->
    {ok, Bytes} = couch_file:bytes(Fd),
    {ok, (1 + Bytes div sizeblock()) * sizeblock()};
write_random_data(Fd, N) ->
    Choices = [foo, bar, <<"bizzingle">>, "bank", ["rough", stuff]],
    Term = lists:nth(random:uniform(4) + 1, Choices),
    {ok, _, _} = couch_file:append_term(Fd, Term),
    write_random_data(Fd, N-1).

