%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2004-2018. All Rights Reserved.
%% 
%% 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.
%% 
%% %CopyrightEnd%
%%

%% This module tests the ordsets, sets, and gb_sets modules.
%%

-module(sets_SUITE).

-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
	 init_per_group/2,end_per_group/2,
	 init_per_testcase/2,end_per_testcase/2,
	 create/1,add_element/1,del_element/1,
	 subtract/1,intersection/1,union/1,is_subset/1,
	 is_set/1,is_empty/1,fold/1,filter/1,
	 take_smallest/1,take_largest/1, iterate/1]).

-include_lib("common_test/include/ct.hrl").

-import(lists, [foldl/3,reverse/1]).

init_per_testcase(_Case, Config) ->
    Config.

end_per_testcase(_Case, _Config) ->
    ok.

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap,{minutes,5}}].

all() -> 
    [create, add_element, del_element, subtract,
     intersection, union, is_subset, is_set, fold, filter,
     take_smallest, take_largest, iterate, is_empty].

groups() -> 
    [].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.


create(Config) when is_list(Config) ->
    test_all(fun create_1/1).

create_1(M) ->
    S0 = M(empty, []),
    [] = M(to_list, S0),
    0 = M(size, S0),
    true = M(is_empty, S0),
    E = make_ref(),
    One = M(singleton, E),
    1 = M(size, One),
    false = M(is_empty, One),
    [E] = M(to_list, One),
    S0.

add_element(Config) when is_list(Config) ->
    test_all([{0,132},{253,258},{510,514}], fun add_element_1/2).

add_element_1(List, M) ->
    S = M(from_list, List),
    SortedSet = lists:usort(List),
    SortedSet = lists:sort(M(to_list, S)),

    %% Make sure that we get the same result by inserting
    %% elements one at the time.
    S2 = foldl(fun(El, Set) -> M(add_element, {El,Set}) end,
	       M(empty, []), List),
    true = M(equal, {S,S2}),

    %% Insert elements, randomly delete inserted elements,
    %% and re-inserted all deleted elements at the end.
    S3 = add_element_del(List, M, M(empty, []), [], []),
    true = M(equal, {S2,S3}),
    true = M(equal, {S,S3}),
    S.

add_element_del([H|T], M, S, Del, []) ->
    add_element_del(T, M, M(add_element, {H,S}), Del, [H]);
add_element_del([H|T], M, S0, Del, Inserted) ->
    S1 = M(add_element, {H,S0}),
    case rand:uniform(3) of
	1 ->
	    OldEl = lists:nth(rand:uniform(length(Inserted)), Inserted),
	    S = M(del_element, {OldEl,S1}),
	    add_element_del(T, M, S, [OldEl|Del], [H|Inserted]);
	_ ->
	    add_element_del(T, M, S1, Del, [H|Inserted])
    end;
add_element_del([], M, S, Del, _) ->
    M(union, {S,M(from_list, Del)}).

del_element(Config) when is_list(Config) ->
    test_all([{0,132},{253,258},{510,514},{1022,1026}], fun del_element_1/2).

del_element_1(List, M) ->    
    S0 = M(from_list, List),
    Empty = foldl(fun(El, Set) -> M(del_element, {El,Set}) end, S0, List),
    Empty = M(empty, []),
    true = M(is_empty, Empty),
    S1 = foldl(fun(El, Set) ->
		       M(add_element, {El,Set})
	       end, S0, reverse(List)),
    true = M(equal, {S0,S1}),
    S1.

subtract(Config) when is_list(Config) ->
    test_all(fun subtract_empty/1),

    %% Note: No empty set.
    test_all([{2,69},{126,130},{253,258},511,512,{1023,1030}], fun subtract_1/2).

subtract_empty(M) ->
    Empty = M(empty, []),
    true = M(is_empty, M(subtract, {Empty,Empty})),
    M(subtract, {Empty,Empty}).

subtract_1(List, M) ->
    S0 = M(from_list, List),
    Empty = M(empty, []),

    %% Trivial cases.
    true = M(is_empty, M(subtract, {Empty,S0})),
    true = M(equal, {S0,M(subtract, {S0,Empty})}),

    %% Not so trivial.
    subtract_check(List, mutate_some(remove_some(List, 0.4)), M),
    subtract_check(List, rnd_list(length(List) div 2 + 5), M),
    subtract_check(List, rnd_list(length(List) div 7 + 9), M),
    subtract_check(List, mutate_some(List), M).

subtract_check(A, B, M) ->
    one_subtract_check(B, A, M),
    one_subtract_check(A, B, M).

one_subtract_check(A, B, M) ->
    ASorted = lists:usort(A),
    BSorted = lists:usort(B),
    ASet = M(from_list, A),
    BSet = M(from_list, B),
    DiffSet = M(subtract, {ASet,BSet}),
    Diff = ASorted -- BSorted,
    true = M(equal, {DiffSet,M(from_list, Diff)}),
    Diff = lists:sort(M(to_list, DiffSet)),
    DiffSet.

intersection(Config) when is_list(Config) ->
    %% Note: No empty set.
    test_all([{1,65},{126,130},{253,259},{499,513},{1023,1025}], fun intersection_1/2).

intersection_1(List, M) ->
    S0 = M(from_list, List),

    %% Intersection with self.
    true = M(equal, {S0,M(intersection, {S0,S0})}),
    true = M(equal, {S0,M(intersection, [S0,S0])}),
    true = M(equal, {S0,M(intersection, [S0,S0,S0])}),
    true = M(equal, {S0,M(intersection, [S0])}),

    %% Intersection with empty.
    Empty = M(empty, []),
    true = M(equal, {Empty,M(intersection, {S0,Empty})}),
    true = M(equal, {Empty,M(intersection, [S0,Empty,S0,Empty])}),

    %% The intersection of no sets is undefined.
    {'EXIT',_} = (catch M(intersection, [])),

    %% Disjoint sets.
    Disjoint = [{El} || El <- List],
    DisjointSet = M(from_list, Disjoint),
    true = M(is_empty, M(intersection, {S0,DisjointSet})),

    %% Disjoint, different sizes.
    [begin
	 SomeRemoved = M(from_list, remove_some(Disjoint, HowMuch)),
	 true = M(is_empty, M(intersection, {S0,SomeRemoved})),
	 MoreRemoved = M(from_list, remove_some(List, HowMuch)),
	 true = M(is_empty, M(intersection, {MoreRemoved,DisjointSet}))
     end || HowMuch <- [0.3,0.5,0.7,0.9]],

    %% Partial overlap (one or more elements in result set).
    %% The sets have almost the same size. (Almost because a duplicated
    %% element in the original list could be mutated and not mutated
    %% at the same time.)
    PartialOverlap = mutate_some(List, []),
    IntersectionSet = check_intersection(List, PartialOverlap, M),
    false = M(is_empty, IntersectionSet),

    %% Partial overlap, different set sizes. (Intersection possibly empty.)
    check_intersection(List, remove_some(PartialOverlap, 0.1), M),
    check_intersection(List, remove_some(PartialOverlap, 0.3), M),
    check_intersection(List, remove_some(PartialOverlap, 0.5), M),
    check_intersection(List, remove_some(PartialOverlap, 0.7), M),
    check_intersection(List, remove_some(PartialOverlap, 0.9), M),

    IntersectionSet.

check_intersection(Orig, Mutated, M) ->
    OrigSet = M(from_list, Orig),
    MutatedSet = M(from_list, Mutated),
    Intersection = [El || El <- Mutated, not is_tuple(El)],
    SortedIntersection = lists:usort(Intersection),
    IntersectionSet = M(intersection, {OrigSet,MutatedSet}),
    true = M(equal, {IntersectionSet,M(from_list, SortedIntersection)}),
    SortedIntersection = lists:sort(M(to_list, IntersectionSet)),

    IntersectionSet.


union(Config) when is_list(Config) ->
    %% Note: No empty set.
    test_all([{1,71},{125,129},{254,259},{510,513},{1023,1025}], fun union_1/2).

union_1(List, M) ->
    S = M(from_list, List),

    %% Union with self and empty.
    Empty = M(empty, []),
    true = M(equal, {S,M(union, {S,S})}),
    true = M(equal, {S,M(union, [S,S])}),
    true = M(equal, {S,M(union, [S,S,Empty])}),
    true = M(equal, {S,M(union, [S,Empty,S])}),
    true = M(equal, {S,M(union, {S,Empty})}),
    true = M(equal, {S,M(union, [S])}),
    true = M(is_empty, M(union, [])),

    %% Partial overlap.
    check_union(List, remove_some(mutate_some(List), 0.9), M),
    check_union(List, remove_some(mutate_some(List), 0.7), M),
    check_union(List, remove_some(mutate_some(List), 0.5), M),
    check_union(List, remove_some(mutate_some(List), 0.3), M),
    check_union(List, remove_some(mutate_some(List), 0.1), M),

    check_union(List, mutate_some(remove_some(List, 0.9)), M),
    check_union(List, mutate_some(remove_some(List, 0.7)), M),
    check_union(List, mutate_some(remove_some(List, 0.5)), M),
    check_union(List, mutate_some(remove_some(List, 0.3)), M),
    check_union(List, mutate_some(remove_some(List, 0.1)), M).

check_union(Orig, Other, M) ->
    OrigSet = M(from_list, Orig),
    OtherSet = M(from_list, Other),
    Union = Orig++Other,
    SortedUnion = lists:usort(Union),
    UnionSet = M(union, {OrigSet,OtherSet}),
    SortedUnion = lists:sort(M(to_list, UnionSet)),
    M(equal, {UnionSet,M(from_list, Union)}),
    UnionSet.

is_subset(Config) when is_list(Config) ->
    test_all([{1,132},{253,270},{299,311}], fun is_subset_1/2).

is_subset_1(List, M) ->
    S = M(from_list, List),
    Empty = M(empty, []),

    %% Subset of empty and self.
    true = M(is_subset, {Empty,Empty}),
    true = M(is_subset, {Empty,S}),
    false = M(is_subset, {S,Empty}),
    true = M(is_subset, {S,S}),

    %% Other cases.
    Res = [false = M(is_subset, {M(singleton, make_ref()),S}),
	   true = M(is_subset, {M(singleton, hd(List)),S}),
	   true = check_subset(remove_some(List, 0.1), List, M),
	   true = check_subset(remove_some(List, 0.5), List, M),
	   true = check_subset(remove_some(List, 0.9), List, M),
	   check_subset(mutate_some(List), List, M),
	   check_subset(rnd_list(length(List) div 2 + 5), List, M),
	   subtract_check(List, rnd_list(length(List) div 7 + 9), M)
	  ],
    res_to_set(Res, M, 0, []).

check_subset(X, Y, M) ->
    check_one_subset(Y, X, M),
    check_one_subset(X, Y, M).

check_one_subset(X, Y, M) ->
    XSet = M(from_list, X),
    YSet = M(from_list, Y),
    SortedX = lists:usort(X),
    SortedY = lists:usort(Y),
    IsSubSet = length(SortedY--SortedX) =:= length(SortedY) - length(SortedX),
    IsSubSet = M(is_subset, {XSet,YSet}),
    IsSubSet.

%% Encode all test results as a set to return.
res_to_set([true|T], M, I, Acc) ->
    res_to_set(T, M, I+1, [I|Acc]);
res_to_set([_|T], M, I, Acc) ->
    res_to_set(T, M, I+1, Acc);
res_to_set([], M, _, Acc) -> M(from_list, Acc).

is_set(Config) when is_list(Config) ->
    %% is_set/1 is tested in the other test cases when its argument
    %% is a set. Here test some arguments that makes it return false.

    false = gb_sets:is_set([a,b]),
    false = gb_sets:is_set({a,very,bad,tuple}),

    false = sets:is_set([a,b]),
    false = sets:is_set({a,very,bad,tuple}),

    false = ordsets:is_set([b,a]),
    false = ordsets:is_set({bad,tuple}),

    %% Now test values that are known to be bad for all set representations.
    test_all(fun is_set_1/1).

is_set_1(M) ->
    false = M(is_set, self()),
    false = M(is_set, blurf),
    false = M(is_set, make_ref()),
    false = M(is_set, <<1,2,3>>),
    false = M(is_set, 42),
    false = M(is_set, math:pi()),
    false = M(is_set, {}),
    M(empty, []).

is_empty(Config) when is_list(Config) ->
    test_all(fun is_empty_1/1).

is_empty_1(M) ->
    S = M(from_list, [blurf]),
    Empty = M(empty, []),

    true = M(is_empty, Empty),
    false = M(is_empty, S),
    M(empty, []).

fold(Config) when is_list(Config) ->
    test_all([{0,71},{125,129},{254,259},{510,513},{1023,1025},{9999,10001}],
	     fun fold_1/2).

fold_1(List, M) ->
    S = M(from_list, List),
    L = M(fold, {fun(E, A) -> [E|A] end,[],S}),
    true = lists:sort(L) =:= lists:usort(List),
    M(empty, []).

filter(Config) when is_list(Config) ->
    test_all([{0,69},{126,130},{254,259},{510,513},{1023,1025},{7999,8000}],
	     fun filter_1/2).

filter_1(List, M) ->
    S = M(from_list, List),
    IsNumber = fun(X) -> is_number(X) end,
    M(equal, {M(from_list, lists:filter(IsNumber, List)),
	      M(filter, {IsNumber,S})}),
    M(filter, {fun(X) -> is_atom(X) end,S}).

%%%
%%% Test specifics for gb_sets.
%%%

take_smallest(Config) when is_list(Config) ->
    test_all([{1,71},{125,129},{254,259},{510,513},{1023,1025}],
	     fun take_smallest_1/2).

take_smallest_1(List, M) ->
    case M(module, []) of
	gb_sets -> take_smallest_2(List, M);
	_ -> ok
    end,
    M(empty, []).

take_smallest_2(List0, M) ->
    List = lists:usort(List0),
    S = M(from_list, List0),
    take_smallest_3(S, List, M).

take_smallest_3(S0, List0, M) ->
    case M(is_empty, S0) of
	true -> ok;
	false ->
	    Smallest = hd(List0),
	    Smallest = gb_sets:smallest(S0),
	    {Smallest,S} = gb_sets:take_smallest(S0),
	    List = tl(List0),
	    true = gb_sets:to_list(S) =:= List,
	    take_smallest_3(S, List, M)
    end.

take_largest(Config) when is_list(Config) ->
    test_all([{1,71},{125,129},{254,259},{510,513},{1023,1025}],
	     fun take_largest_1/2).

take_largest_1(List, M) ->
    case M(module, []) of
	gb_sets -> take_largest_2(List, M);
	_ -> ok
    end,
    M(empty, []).

take_largest_2(List0, M) ->
    List = reverse(lists:usort(List0)),
    S = M(from_list, List0),
    take_largest_3(S, List, M).

take_largest_3(S0, List0, M) ->
    case M(is_empty, S0) of
	true -> ok;
	false ->
	    Largest = hd(List0),
	    Largest = gb_sets:largest(S0),
	    {Largest,S} = gb_sets:take_largest(S0),
	    List = tl(List0),
	    true = gb_sets:to_list(S) =:= reverse(List),
	    take_largest_3(S, List, M)
    end.

iterate(Config) when is_list(Config) ->
    test_all(fun iterate_1/1).

iterate_1(M) ->
    case M(module, []) of
	gb_sets -> iterate_2(M);
	_ -> ok
    end,
    M(empty, []).

iterate_2(M) ->
    rand:seed(exsplus, {1,2,42}),
    iter_set(M, 1000).

iter_set(_M, 0) ->
    ok;
iter_set(M, N) ->
    L = [I || I <- lists:seq(1, N)],
    T = M(from_list, L),
    L = lists:reverse(iterate_set(M, T)),
    R = rand:uniform(N),
    S = lists:reverse(iterate_set(M, R, T)),
    S = [E || E <- L, E >= R],
    iter_set(M, N-1).

iterate_set(M, Set) ->
    I = M(iterator, Set),
    iterate_set_1(M, M(next, I), []).

iterate_set(M, Start, Set) ->
    I = M(iterator_from, {Start, Set}),
    iterate_set_1(M, M(next, I), []).

iterate_set_1(_, none, R) ->
    R;
iterate_set_1(M, {E, I}, R) ->
    iterate_set_1(M, M(next, I), [E | R]).

%%%
%%% Helper functions.
%%%

sets_mods() ->
    Ordsets = sets_test_lib:new(ordsets, fun(X, Y) -> X == Y end),
    Sets = sets_test_lib:new(sets, fun(X, Y) ->
					   lists:sort(sets:to_list(X)) == 
					       lists:sort(sets:to_list(Y)) end),
    Gb = sets_test_lib:new(gb_sets, fun(X, Y) ->
					    gb_sets:to_list(X) == 
						gb_sets:to_list(Y) end),
    [Ordsets,Sets,Gb].

test_all(Tester) ->
    Res = [begin
	       rand:seed(exsplus, {1,2,42}),
	       S = Tester(M),
	       {M(size, S),lists:sort(M(to_list, S))}
	   end || M <- sets_mods()],
    all_same(Res).

test_all([{Low,High}|T], Tester) ->
    test_all(lists:seq(Low, High)++T, Tester);
test_all([Sz|T], Tester) when is_integer(Sz) ->
    List = rnd_list(Sz),
    Res = [begin
		     rand:seed(exsplus, {19,2,Sz}),
		     S = Tester(List, M),
		     {M(size, S),lists:sort(M(to_list, S))}
		 end || M <- sets_mods()],
    all_same(Res),
    test_all(T, Tester);
test_all([], _) -> ok.


all_same([H|T]) ->
    all_same_1(T, H).

all_same_1([H|T], H) ->
    all_same_1(T, H);
all_same_1([], _) -> ok.

rnd_list(Sz) ->
    rnd_list_1(Sz, []).
    
atomic_rnd_term() ->
    case rand:uniform(3) of
	1 -> list_to_atom(integer_to_list($\s+rand:uniform(94))++"rnd");
	2 -> rand:uniform();
	3 -> rand:uniform(50)-37
    end.

rnd_list_1(0, Acc) -> Acc;
rnd_list_1(N, Acc) -> rnd_list_1(N-1, [atomic_rnd_term()|Acc]).

mutate_some(List) ->
    mutate_some(List, []).
    
mutate_some([X,Y,Z|T], Acc) ->
    %% Intentionally change order. (Order should not matter.)
    mutate_some(T, [{X},Z,Y|Acc]);
mutate_some([H|T], Acc) ->
    mutate_some(T, [H|Acc]);
mutate_some([], Acc) ->
    %% Intentionally not reversing.
    Acc.

%% Removes at least one element.
remove_some(List0, P) ->
    case remove_some(List0, P, []) of
	List when length(List0) =:= length(List) ->
	    tl(List);
	List ->
	    List
    end.

remove_some([H|T], P, Acc) ->
    case rand:uniform() of
	F when F < P ->				%Remove.
	    remove_some(T, P, Acc);
	_ ->
	    remove_some(T, P, [H|Acc])
    end;
remove_some([], _, Acc) ->
    %% Intentionally no reverse. Order should not matter.
    Acc.
