%% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com)
%%
%% Christian Mollekopf (Kolab Systems) <mollekopf@kolabsystems.com>
%%
%% This program is free software: you can redistribute it and/or modify
%% it under the terms of the GNU General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%%
%% You should have received a copy of the GNU General Public License
%% along with this program.  If not, see <http://www.gnu.org/licenses/>.

-module(kolab_guam_session_SUITE).

% easier than exporting by name
-compile(export_all).

% required for common_test to work
-include_lib("common_test/include/ct.hrl").
-include("../src/kolab_guam_session.hrl").


%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% common test callbacks %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Specify a list of all unit test functions
all() -> [
          kolab_guam_session_test_client,
          kolab_guam_session_test_server,
          kolab_guam_session_test_client_benchmark
         ].

% required, but can just return Config. this is a suite level setup function.
init_per_suite(Config) ->
        Config.

% required, but can just return Config. this is a suite level tear down function.
end_per_suite(Config) ->
        Config.

% optional, can do function level setup for all functions,
% or for individual functions by matching on TestCase.
init_per_testcase(_TestCase, Config) ->
        Config.

% optional, can do function level tear down for all functions,
% or for individual functions by matching on TestCase.
end_per_testcase(_TestCase, Config) ->
        Config.


kolab_guam_session_test_client(_TestConfig) ->
    %% setup boilerplate
    ServerConfig = kolab_guam_sup:default_imap_server_config(),
    { ok, ImapSession } = eimap:start_link(ServerConfig),
    { ok, Socket } = gen_tcp:listen(9964, [ { reuseaddr, true }, {active, false}, inet6 ]),

    lager:start(),
    lager:set_loglevel(lager_console_backend, debug),

    ActiveRules = [{ kolab_guam_rule_filter_groupware, kolab_guam_rule_filter_groupware:new({}) }],

    State = #state{
      socket = Socket,
      server_config = ServerConfig,
      imap_session = ImapSession
    },

    %handle_info
    {noreply, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = undefined,
      command_split_reset_trigger = [] %FIXME ?
     }} = kolab_guam_session:handle_info({tcp, Socket, <<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>}, State#state{ rules_deciding = [] }),

    % Run without rules
    {<<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = undefined,
      command_split_reset_trigger = [] %FIXME ?
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>, State#state{ rules_deciding = [] }),


    % Activate filtering
    {<<"y1 ID (\"name\" \"Test\")\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test\")">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 ID (\"name\" \"Test\")\r\n">>, State#state{ rules_deciding = ActiveRules }),


    % Append
    {<<"y1 APPEND INBOX {30}\r\n0123">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {30}">>},
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState4} = kolab_guam_session:process_client_data(Socket, <<"y1 APPEND INBOX {30}\r\n0123">>, State#state{ rules_deciding = ActiveRules }),

    {<<"456789">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>, %This ensures we aren't buffering during the continuation
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {30}">>},
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState5} = kolab_guam_session:process_client_data(Socket, <<"456789">>, IntermediateState4),

    {<<"0123456789012345678">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>, %This ensures we aren't buffering during the continuation
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {30}">>},
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState6} = kolab_guam_session:process_client_data(Socket, <<"0123456789012345678">>, IntermediateState5),

    {<<"9\r\ny2 ENABLE QRESYNC\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y2">>,<<"ENABLE">>,<<"QRESYNC">>},
      command_split_reset_trigger = reset_for_next_client_command
     }} = kolab_guam_session:process_client_data(Socket, <<"9\r\ny2 ENABLE QRESYNC\r\n">>, IntermediateState6),

    % Append2
    {<<"y1 APPEND INBOX {36}\r\n0123">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {36}">>},
      command_split_reset_trigger = reset_for_next_client_command,
      continuation_bytes = 32
     } = IntermediateState7} = kolab_guam_session:process_client_data(Socket, <<"y1 APPEND INBOX {36}\r\n0123">>, State#state{ rules_deciding = ActiveRules }),

    {<<"456789\r\n0123456789\r\n0123456789">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>, %This ensures we aren't buffering during the continuation
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {36}">>},
      command_split_reset_trigger = reset_for_next_client_command,
      continuation_bytes = 2
     } = IntermediateState8} = kolab_guam_session:process_client_data(Socket, <<"456789\r\n0123456789\r\n0123456789">>, IntermediateState7),

    {<<"\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>, %This ensures we aren't buffering during the continuation
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {36}">>},
      command_split_reset_trigger = reset_for_next_client_command,
      continuation_bytes = 0
     }} = kolab_guam_session:process_client_data(Socket, <<"\r\n">>, IntermediateState8),

    % 0 length literal
    {<<"y1 APPEND INBOX {0}\r\n\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {0}">>},
      command_split_reset_trigger = reset_for_next_client_command
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 APPEND INBOX {0}\r\n\r\n">>, State#state{ rules_deciding = ActiveRules }),

    % 0 length literal in two lines
    {<<"y1 APPEND INBOX {0}\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {0}">>},
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState9} = kolab_guam_session:process_client_data(Socket, <<"y1 APPEND INBOX {0}\r\n">>, State#state{ rules_deciding = ActiveRules }),

    {<<"\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>, %This ensures we aren't buffering during the continuation
      current_command_split = {<<"y1">>,<<"APPEND">>,<<"INBOX {0}">>},
      command_split_reset_trigger = reset_for_next_client_command,
      continuation_bytes = 0
     }} = kolab_guam_session:process_client_data(Socket, <<"\r\n">>, IntermediateState9),

    % Don't activate filtering
    {<<"y1 ID (\"name\" \"Test/KOLAB\")\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test/KOLAB\")">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = []
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 ID (\"name\" \"Test/KOLAB\")\r\n">>, State#state{ rules_deciding = ActiveRules }),


    % Lone tag in a packet. Can/Could happen with iPhone apparently. (See commit 89f9dc93757c68032ed17f42838858bdfaefa408)
    {<<>>, #state{ buffered_client_data = <<"y1">>, rules_active = [] } = IntermediateState1} = kolab_guam_session:process_client_data(Socket, <<"y1">>, State#state{ rules_deciding = ActiveRules }),
    {<<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y2">>,<<"ENABLE">>,<<"QRESYNC">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
     }} = kolab_guam_session:process_client_data(Socket, <<" ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\n">>, IntermediateState1),


    % Lone tag in a packet, but with a \r\n before. See guam-0.9.2-stalling-client-buffer-and-split-command-handling.patch (This triggers the odd List buffering case)
    {<<"\r\n">>, #state{ buffered_client_data = <<"y1 ">>, rules_active = [] } = IntermediateState2} = kolab_guam_session:process_client_data(Socket, <<"\r\ny1 ">>, State#state{ rules_deciding = ActiveRules }),
    {<<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y2">>,<<"ENABLE">>,<<"QRESYNC">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
     }} = kolab_guam_session:process_client_data(Socket, <<"ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\n">>, IntermediateState2),


    % Empty line (should be considered a complete command and should not be buffered)
    {<<"\r\n">>, #state{ buffered_client_data = <<>>, rules_active = [] } = IntermediateState3} = kolab_guam_session:process_client_data(Socket, <<"\r\n">>, State#state{ rules_deciding = ActiveRules }),
    {<<"y1 ID (\"name\" \"Test\")\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test\")">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 ID (\"name\" \"Test\")\r\n">>, IntermediateState3),


    % Activate with multi-line packet
    {<<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y3">>,<<"LIST">>,<<"\"\" \"%\" RETURN (SUBSCRIBED))">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      % Make sure that we have processed y3 in here (and don't stop processing at y1)
      rules_active = [{kolab_guam_rule_filter_groupware, {state,undefined,<<"y3">>,true,<<>>,[<<"LIST">>,<<"list">>,<<"XLIST">>,<<"xlist">>,<<"LSUB">>,<<"lsub">>]}}]
      % rules_active = [{kolab_guam_rule_filter_groupware, _}]
     }} = kolab_guam_session:process_client_data(Socket, <<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>, State#state{ rules_deciding = ActiveRules }),


    % Test various packet splits
    PacketsSplits = [
        [<<"y1 ID (\"name\" \"Test\")\r\n">>, <<"y2 ENABLE QRESYNC\r\n">>, <<"y3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>],
        [<<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>],
        [<<"y1 ID (\"name\" \"Test\")">>, <<"\r\ny2">>, <<" ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>],
        [<<"y1 ID (\"name\" \"Test\")">>, <<"\r\n">>, <<"y2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>],
        [<<"y1">>, <<" ">>, <<"ID">>, <<" (\"name\" \"Test\")\r\n">>, <<"y2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>]
    ],

     lists:foldl(fun(Packets, _) ->
                    try_packets(Packets, Socket, ServerConfig, State#state{ rules_deciding = ActiveRules })
                end,
                undefined,
                PacketsSplits),


    lager:info("Done").


try_packets(Packets, Socket, ServerConfig, State) ->
    #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      current_command_split = {<<"y3">>,<<"LIST">>,<<"\"\" \"%\" RETURN (SUBSCRIBED))">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      % Make sure that we have processed y3 in here (and don't stop processing at y1)
      rules_active = [{kolab_guam_rule_filter_groupware, {state,undefined,<<"y3">>,true,<<>>,[<<"LIST">>,<<"list">>,<<"XLIST">>,<<"xlist">>,<<"LSUB">>,<<"lsub">>]}}]
     } = lists:foldl(fun(Packet, _State) ->
                        {_, NewState} = kolab_guam_session:handle_info({tcp, Socket, Packet}, _State),
                        NewState
                    end,
                    State,
                    Packets).


kolab_guam_session_test_server(_TestConfig) ->
    %% setup boilerplate
    % gen_tcp:connect
    ServerConfig = kolab_guam_sup:default_imap_server_config(),
    { ok, ImapSession } = eimap:start_link(ServerConfig),
    { ok, Socket } = gen_tcp:listen(9964, [ { reuseaddr, true }, {active, false}, inet6 ]),

    lager:start(),
    lager:set_loglevel(lager_console_backend, debug),

    ActiveRules = [{ kolab_guam_rule_filter_groupware, kolab_guam_rule_filter_groupware:new({}) }],

    State = #state{
      socket = Socket,
      server_config = ServerConfig,
      imap_session = ImapSession
    },


    % This should trigger a metadata request
    {noreply, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      % current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test\")">>},
      current_command_split = {<<"y3">>,<<"LIST">>,<<"\"\" \"%\" RETURN (SUBSCRIBED))">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
    } = IntermediateState1} = kolab_guam_session:handle_info({tcp, Socket, <<"y1 ID (\"name\" \"Test\")\r\ny2 ENABLE QRESYNC\r\ny3 LIST \"\" \"%\" RETURN (SUBSCRIBED))\r\n">>}, State#state{ rules_deciding = ActiveRules }),


    {noreply, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      % current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test\")">>},
      current_command_split = undefined,
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
    } = IntermediateState2} = kolab_guam_session:handle_info({imap_server_response, <<"* ID (\"name\" \"Cyrus IMAPD\")\n\ry1 OK Completed\r\n">>}, IntermediateState1),

    {noreply, #state{
      server_config = ServerConfig,
      buffered_client_data = <<>>,
      % current_command_split = {<<"y1">>,<<"ID">>,<<"(\"name\" \"Test\")">>},
      command_split_reset_trigger = reset_for_next_client_command,
      rules_deciding = [],
      rules_active = [{kolab_guam_rule_filter_groupware, _}]
    }} = kolab_guam_session:handle_info({imap_server_response, <<"* METADATA INBOX (/shared/vendor/kolab/folder-type \"mail\")\r\n">>}, IntermediateState2),

% <<"* METADATA Archive (/shared/vendor/kolab/folder-type NIL)\r\n">
% <<"* METADATA Calendar (/shared/vendor/kolab/folder-type \"event\")\r\n">
% <<"EG0001 OK Completed\r\n">>

    lager:info("Done").



kolab_guam_session_test_client_benchmark(_TestConfig) ->
    %% setup boilerplate
    ServerConfig = kolab_guam_sup:default_imap_server_config(),
    { ok, ImapSession } = eimap:start_link(ServerConfig),
    { ok, Socket } = gen_tcp:listen(9964, [ { reuseaddr, true }, {active, false}, inet6 ]),

    lager:start(),
    lager:set_loglevel(lager_console_backend, debug),

    ActiveRules = [{ kolab_guam_rule_filter_groupware, kolab_guam_rule_filter_groupware:new({}) }],

    State = #state{
      socket = Socket,
      server_config = ServerConfig,
      imap_session = ImapSession
    },

    Count = 70,
    % We used to have a factor of 10 in runtime between \r\n (causing line splits), and just \n (not causing line splits)
    Payload = <<"0123456789\r\n">>,
    Data = list_to_binary(lists:duplicate(Count, Payload)),
    BinaryCount = list_to_binary(integer_to_list(Count * size(Payload) + 36)),
    ExpectedCommandSplit = {<<"y1">>, <<"APPEND">>, list_to_binary([<<"INBOX {">>, BinaryCount, <<"}">>])},

    S = os:timestamp(),

    % Append
    {noreply, #state{
      server_config = ServerConfig,
      current_command_split = ExpectedCommandSplit,
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState4} = kolab_guam_session:handle_info({tcp, Socket, list_to_binary([<<"y1 APPEND INBOX {">>, BinaryCount, <<"}\r\n0123">>])}, State#state{ rules_deciding = ActiveRules }),

    {noreply, #state{
      server_config = ServerConfig,
      current_command_split = ExpectedCommandSplit,
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState5} = kolab_guam_session:handle_info({tcp, Socket, <<"456789\r\n0123456789\r\n0123456789">>}, IntermediateState4),


    {noreply, #state{
      server_config = ServerConfig,
      current_command_split = ExpectedCommandSplit,
      command_split_reset_trigger = reset_for_next_client_command
     } = IntermediateState6} = kolab_guam_session:handle_info({tcp, Socket, <<"\r\n">>}, IntermediateState5),

    {noreply, #state{
      server_config = ServerConfig,
      current_command_split = ExpectedCommandSplit,
      command_split_reset_trigger = reset_for_next_client_command
     }} = kolab_guam_session:handle_info({tcp, Socket, Data}, IntermediateState6),


    E = os:timestamp(),
    X = timer:now_diff(E,S),
    lager:info("Runtime: ~p usecs", [X]),


    lager:info("Done").
