a cabal implementation in erlang
% SPDX-FileCopyrightText: 2023 Henry Bubert
%
% SPDX-License-Identifier: LGPL-2.1-or-later

-module(cabal_peer_suite).

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

open_close_test() ->
    A = make_test_args(),
    {ok, PSup} = cabal_sup:start_link(A),
    P = cabal_sup:get_peer_pid(PSup),
    ?assertNotEqual(P, 0),
    ok = cabal_sup:stop(PSup).

empty_state_test() ->
    A = make_test_args(),
    {ok, PSup} = cabal_sup:start_link(A),
    P = cabal_sup:get_peer_pid(PSup),
    {ok, Chans} = cabal:channels_joined(P),
    ?assertEqual([], Chans),
    {ok, Known} = cabal:channels_known(P),
    ?assertEqual([], Known),
    ok = cabal_sup:stop(PSup).

join_and_leave_test() ->
    A = make_test_args(),
    {ok, PSup} = cabal_sup:start_link(A),
    P = cabal_sup:get_peer_pid(PSup),
    {ok, []} = cabal:channels_joined(P),
    {ok, []} = cabal:channels_known(P),
    cabal:join(P, "default"),
    {ok, ["default"]} = cabal:channels_joined(P),
    %% After joining, we should know about the channel (join post was created)
    {ok, ["default"]} = cabal:channels_known(P),
    ok = cabal:leave(P, "default"),
    {ok, []} = cabal:channels_joined(P),
    %% After leaving, we still know about the channel (leave post exists)
    {ok, ["default"]} = cabal:channels_known(P),
    ok = cabal_sup:stop(PSup).

multi_channel_membership_test() ->
    A = make_test_args(),
    {ok, PSup} = cabal_sup:start_link(A),
    P = cabal_sup:get_peer_pid(PSup),
    {ok, PubKey} = cabal:node_public_key(P),

    %% Join Channel A
    cabal:join(P, "ChannelA"),
    {ok, MembersA} = cabal:channel_members(P, "ChannelA"),
    MembersMapA = proplists:get_value(members, MembersA),
    ?assert(maps:is_key(PubKey, MembersMapA)),

    %% Join Channel B
    cabal:join(P, "ChannelB"),
    {ok, MembersB} = cabal:channel_members(P, "ChannelB"),
    MembersMapB = proplists:get_value(members, MembersB),
    ?assert(maps:is_key(PubKey, MembersMapB)),

    %% Verify still in Channel A
    {ok, MembersA2} = cabal:channel_members(P, "ChannelA"),
    MembersMapA2 = proplists:get_value(members, MembersA2),
    ?assert(maps:is_key(PubKey, MembersMapA2)),

    ok = cabal_sup:stop(PSup).

channels_known_test() ->
    A = make_test_args(),
    {ok, PSup} = cabal_sup:start_link(A),
    P = cabal_sup:get_peer_pid(PSup),

    %% Initially no channels
    {ok, []} = cabal:channels_known(P),

    %% Join multiple channels
    cabal:join(P, "general"),
    cabal:join(P, "random"),
    cabal:join(P, "dev"),

    %% Should know about all joined channels
    {ok, Known1} = cabal:channels_known(P),
    ?assertEqual(3, length(Known1)),
    ?assert(lists:member("general", Known1)),
    ?assert(lists:member("random", Known1)),
    ?assert(lists:member("dev", Known1)),

    %% Write a message to one channel
    ok = cabal:write(P, "general", "Hello world!"),

    %% Should still know the same channels
    {ok, Known2} = cabal:channels_known(P),
    ?assertEqual(3, length(Known2)),

    %% Leave one channel
    ok = cabal:leave(P, "dev"),

    %% Should still know about it (leave post exists)
    {ok, Known3} = cabal:channels_known(P),
    ?assertEqual(3, length(Known3)),
    ?assert(lists:member("dev", Known3)),

    %% But channels_joined should only show 2
    {ok, Joined} = cabal:channels_joined(P),
    ?assertEqual(2, length(Joined)),
    ?assertNot(lists:member("dev", Joined)),

    ok = cabal_sup:stop(PSup).

connect_two_peers_test() ->
    [A, B] = [make_test_args(Name) || Name <- ["Alfi", "Bert"]],
    {ok, AlfiSup} = cabal_sup:start_link(A),
    Alfi = cabal_sup:get_peer_pid(AlfiSup),
    {ok, BertSup} = cabal_sup:start_link(B),
    Bert = cabal_sup:get_peer_pid(BertSup),

    cabal:join(Alfi, "test"),
    cabal:join(Bert, "test"),

    {ok, AlfiAddr} = cabal:node_addr(Alfi),
    ok = cabal:dial(Bert, AlfiAddr),
    timer:sleep(1000),

    AlfisList = cabal:peer_list(Alfi),
    ?debugFmt("~nA's list: ~p~n", [AlfisList]),
    ?assertEqual(1, length(AlfisList)),

    BertsList = cabal:peer_list(Bert),
    ?debugFmt("~nB's list: ~p~n", [BertsList]),
    ?assertEqual(1, length(BertsList)),

    %?assert(false),
    ok = cabal_sup:stop(AlfiSup),
    ok = cabal_sup:stop(BertSup).

channels_known_from_peers_test() ->
    [A, B] = [make_test_args(Name) || Name <- ["Alice", "Bob"]],
    {ok, AliceSup} = cabal_sup:start_link(A),
    Alice = cabal_sup:get_peer_pid(AliceSup),
    {ok, BobSup} = cabal_sup:start_link(B),
    Bob = cabal_sup:get_peer_pid(BobSup),

    %% Alice joins two channels and writes messages
    cabal:join(Alice, "announcements"),
    cabal:join(Alice, "watercooler"),
    ok = cabal:write(Alice, "announcements", "Important news!"),
    ok = cabal:write(Alice, "watercooler", "How's everyone doing?"),

    %% Bob joins only one channel
    cabal:join(Bob, "watercooler"),

    %% Connect the peers
    {ok, AliceAddr} = cabal:node_addr(Alice),
    ok = cabal:dial(Bob, AliceAddr),
    timer:sleep(1500),

    %% Alice should know about both channels she joined
    {ok, AliceKnown} = cabal:channels_known(Alice),
    ?assertEqual(2, length(AliceKnown)),
    ?assert(lists:member("announcements", AliceKnown)),
    ?assert(lists:member("watercooler", AliceKnown)),

    %% Bob should know about both channels after receiving posts from Alice
    %% (even though he only joined watercooler)
    {ok, BobKnown} = cabal:channels_known(Bob),
    ?debugFmt("~nBob's known channels: ~p~n", [BobKnown]),
    ?assert(length(BobKnown) >= 1),
    ?assert(lists:member("watercooler", BobKnown)),
    %% Note: Bob might also know about "announcements" depending on sync

    %% But Bob should only be joined to watercooler
    {ok, BobJoined} = cabal:channels_joined(Bob),
    ?assertEqual(["watercooler"], BobJoined),

    ok = cabal_sup:stop(AliceSup),
    ok = cabal_sup:stop(BobSup).

channel_list_request_test() ->
    [A, B] = [make_test_args(Name) || Name <- ["Alice", "Bob"]],
    {ok, AliceSup} = cabal_sup:start_link(A),
    Alice = cabal_sup:get_peer_pid(AliceSup),
    {ok, BobSup} = cabal_sup:start_link(B),
    Bob = cabal_sup:get_peer_pid(BobSup),

    %% Alice joins several channels
    cabal:join(Alice, "general"),
    cabal:join(Alice, "random"),
    cabal:join(Alice, "dev"),

    %% Bob joins just one
    cabal:join(Bob, "announcements"),

    %% Connect the peers
    {ok, AliceListenAddr} = cabal:node_addr(Alice),
    ok = cabal:dial(Bob, AliceListenAddr),

    %% Wait for connection to be fully established and get actual peer addresses
    timer:sleep(500),
    BobPeerList = cabal:peer_list(Bob),
    ?assertEqual(1, length(BobPeerList)),
    [{AliceActualAddr, _}] = BobPeerList,

    AlicePeerList = cabal:peer_list(Alice),
    ?assertEqual(1, length(AlicePeerList)),
    [{BobActualAddr, _}] = AlicePeerList,

    %% Bob requests channel list from Alice (using actual peer address)
    ok = cabal:request_channel_list(Bob, AliceActualAddr),
    timer:sleep(1000),

    %% Bob should have received Alice's channel list
    {ok, AliceChannels} = cabal:peer_channel_list(Bob, AliceActualAddr),
    ?assertEqual(3, length(AliceChannels)),
    ?assert(lists:member("general", AliceChannels)),
    ?assert(lists:member("random", AliceChannels)),
    ?assert(lists:member("dev", AliceChannels)),

    %% Alice requests channel list from Bob (using actual peer address)
    ok = cabal:request_channel_list(Alice, BobActualAddr),
    timer:sleep(1000),

    %% Alice should have received Bob's channel list
    {ok, BobChannels} = cabal:peer_channel_list(Alice, BobActualAddr),
    ?assertEqual(1, length(BobChannels)),
    ?assert(lists:member("announcements", BobChannels)),

    ok = cabal_sup:stop(AliceSup),
    ok = cabal_sup:stop(BobSup).

set_nick_and_topic_test() ->
    [A, B] = [make_test_args() || _ <- lists:seq(1, 2)],
    {ok, AlfiSup} = cabal_sup:start_link(A),
    Alfi = cabal_sup:get_peer_pid(AlfiSup),
    {ok, BertSup} = cabal_sup:start_link(B),
    Bert = cabal_sup:get_peer_pid(BertSup),

    {ok, PubAlfi} = cabal:node_public_key(Alfi),
    {ok, PubBert} = cabal:node_public_key(Bert),

    ok = cabal:set_nick(Alfi, "Alfi"),
    ok = cabal:set_nick(Bert, "Not Ernie!"),

    C = "test",
    cabal:join(Alfi, C),
    cabal:join(Bert, C),

    T = "the mega testing channel",
    ok = cabal:set_topic(Alfi, C, T),

    %%{ok, PeerAddr} = cabal:node_addr(Alfi),
    %%ok = cabal:dial(Bert, PeerAddr),
    {ok, PeerAddr} = cabal:node_addr(Bert),
    ok = cabal:dial(Alfi, PeerAddr),
    timer:sleep(1000),

    CheckState = fun(Peer) ->
        {ok, CState} = cabal:channel_members(Peer, C),
        ?assertEqual(T, proplists:get_value(topic, CState)),
        Members = proplists:get_value(members, CState),
        ?assertEqual(2, maps:size(Members)),
        {AlfiName, AlfiWhen, false} = maps:get(PubAlfi, Members),
        ?assertEqual("Alfi", AlfiName),
        ?assert(AlfiWhen > 0),
        {BertName, BertWhen, false} = maps:get(PubBert, Members),
        ?assertEqual("Not Ernie!", BertName),
        ?assert(BertWhen > 0)
    end,
    lists:foreach(CheckState, [Alfi, Bert]),

    ok = cabal_sup:stop(AlfiSup),
    ok = cabal_sup:stop(BertSup).

chat_test() ->
    [A, B] = [make_test_args() || _ <- lists:seq(1, 2)],
    {ok, AlfiSup} = cabal_sup:start_link(A),
    Alfi = cabal_sup:get_peer_pid(AlfiSup),
    {ok, BertSup} = cabal_sup:start_link(B),
    Bert = cabal_sup:get_peer_pid(BertSup),

    {ok, PubAlfi} = cabal:node_public_key(Alfi),
    {ok, PubBert} = cabal:node_public_key(Bert),

    NickAlfi = "Alfi",
    NickBert = "Not ernie!!",
    ok = cabal:set_nick(Alfi, NickAlfi),
    ok = cabal:set_nick(Bert, NickBert),

    C = "test",
    cabal:join(Alfi, C),
    cabal:join(Bert, C),

    T1 = "someone here?",
    T2 = "is this thing on?",
    ok = cabal:write(Alfi, C, T1),
    %% enforce ordering
    timer:sleep(500),
    ok = cabal:write(Bert, C, T2),

    {ok, PeerAddr} = cabal:node_addr(Bert),
    ok = cabal:dial(Alfi, PeerAddr),
    timer:sleep(1000),

    Check = fun(Peer) ->
        {ok, {Texts, Users}} = cabal:read(Peer, C),
        [{UserIdAlfi, _, T1}, {UserIdBert, _, T2}] = Texts,
        {AlfiInfo, UsersSansAlfi} = maps:take(UserIdAlfi, Users),
        ?assertEqual({NickAlfi, PubAlfi}, AlfiInfo),
        {BertInfo, #{}} = maps:take(UserIdBert, UsersSansAlfi),
        ?assertEqual({NickBert, PubBert}, BertInfo)
    end,
    lists:foreach(Check, [Alfi, Bert]),

    %% that's nice but do we also transmit posts while connected?
    ok = cabal:write(Alfi, C, "ohai! o/"),
    timer:sleep(500),
    {ok, {Texts, _}} = cabal:read(Bert, C),
    ?assertEqual(3, length(Texts)),

    ok = cabal:write(Bert, C, "nice to meet you!"),
    timer:sleep(250),
    {ok, {Texts2, _}} = cabal:read(Alfi, C),
    ?assertEqual(4, length(Texts2)),

    ok = cabal_sup:stop(AlfiSup),
    ok = cabal_sup:stop(BertSup).

%% TODO: add tests for:
%% - leaving and joining again
%% - reading messages after having left (channel member still listed but as left)

hashing_test() ->
    Table = [
        {"Two hands clap and there is a sound. What is the sound of one hand?",
            "fcd7c41883c3564c5a6abec78e214159efe62d50f124b4afafc184ea3b764cd4"},
        {"茶色", "46b321c236880cd861dafae3040cf8cc52990516d1a69ab2c170b1e615a7ebd5"},
        {"elf", "ffe809405a3e1eaf77938bde2138832b177a51e47df02935edc12aacf8279f61"},
        {"love collapses spacetime",
            "fea16c09f8aa581500fcf6ee2f6aabc59ccaa271d2a3568843930b7ff929ad86"}
    ],
    lists:foreach(
        fun({InStr, Want}) ->
            In = unicode:characters_to_binary(InStr),
            Got = enacl:generichash(32, In),
            GotHex = hex:bin_to_hexstr(Got),
            ?assertEqual(Want, string:lowercase(GotHex))
        end,
        Table
    ).

%%%%%
%% helpers
%%%%%

make_test_args(Name) ->
    S = string:chomp(os:cmd("mktemp --tmpdir -d caberl-peer-XXXXX")),
    ?debugFmt("~nUsing Storage for ~s: ~p~n", [Name, S]),
    [
        {nickname, Name},
        {listener, ["localhost", 0]},
        {storage, S}
    ].
make_test_args() ->
    make_test_args("unnamed").