%% -------------------------------------------------------------------
%%
%% cuttlefish_mapping: models a single mapping
%%
%% Copyright (c) 2013 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you 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(cuttlefish_mapping).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

-type collection_type() :: list
                      | {map, atom}
                      | {map, binary}
                      | {proplist, atom}.

-record(mapping, {
        variable::cuttlefish_variable:variable(),
        mapping::string(),
        default::term(),
        commented::term(),
        datatype = [string] :: cuttlefish_datatypes:datatype_list(),
        level = basic :: basic | intermediate | advanced,
        doc = [] :: list(),
        include_default = undefined :: string() | undefined,
        new_conf_value = undefined :: string() | undefined,
        validators = [] :: [string()],
        is_merge = false :: boolean(),
        see = [] :: [cuttlefish_variable:variable()],
        hidden = false :: boolean(),
        aliases = [] :: [{cuttlefish_variable:variable(), string() | undefined}],
        collect = undefined :: collection_type() | undefined
    }).

-type mapping() :: #mapping{}.
-type raw_mapping() :: {mapping, string(), string(), [proplists:property()]}.
-export_type([mapping/0, collection_type/0]).

-export([
    parse/1,
    parse_and_merge/2,
    is_mapping/1,
    variable/1,
    is_fuzzy_variable/1,
    mapping/1,
    default/1,
    has_default/1,
    commented/1,
    datatype/1,
    set_datatype/2,
    level/1,
    hidden/1,
    doc/1,
    see/1,
    include_default/1,
    new_conf_value/1,
    replace/2,
    validators/1,
    validators/2,
    remove_all_but_first/2,
    aliases/1,
    aliases_with_messages/1,
    collect/1
    ]).

-spec parse(raw_mapping()) -> mapping() | cuttlefish_error:error().
parse({mapping, Variable, Mapping, Proplist}) ->

    Datatype = case proplists:get_value(datatype, Proplist, string) of
        {enum, Enums} ->
            AtomEnums = [ begin
                case is_list(E) of
                    true -> list_to_atom(E);
                    _ -> E
                end
            end || E <- Enums ],
            [{enum, AtomEnums}];
        L when is_list(L) ->
            case cuttlefish_datatypes:is_valid_list(L) of
                true -> L;
                _ -> {error, {mapping_types, L}}
            end;
        D -> [D]
    end,

    VarTokenized = cuttlefish_variable:tokenize(Variable),
    IsMerge = proplists:is_defined(merge, Proplist),
    AliasesResult = parse_aliases(Variable, VarTokenized, Proplist, IsMerge),
    CollectResult = parse_collect(proplists:get_value(collect, Proplist)),

    case {Datatype, AliasesResult, CollectResult} of
        {{error, _}, _, _} ->
            Datatype;
        {_, {error, _} = AliasError, _} ->
            AliasError;
        {_, _, {error, _} = CollectError} ->
            CollectError;
        {_, TokenizedAliases, CollectType} ->
            WildcardCount = length([S || S <- VarTokenized, hd(S) =:= $$]),
            CollectError = case {CollectType, WildcardCount} of
                {undefined, _}  -> ok;
                {_, 0}          -> {error, {collect_on_non_fuzzy, Variable}};
                {_, N} when N > 1 -> {error, {collect_multi_wildcard, Variable, N}};
                {_, 1}          -> ok
            end,
            case CollectError of
                {error, _} ->
                    CollectError;
                ok ->
                    #mapping{
                        variable = VarTokenized,
                        default = proplists:get_value(default, Proplist),
                        commented = proplists:get_value(commented, Proplist),
                        mapping = Mapping,
                        level = proplists:get_value(level, Proplist, basic),
                        datatype = Datatype,
                        doc = proplists:get_value(doc, Proplist, []),
                        see = proplists:get_value(see, Proplist, []),
                        include_default = proplists:get_value(include_default, Proplist),
                        new_conf_value = proplists:get_value(new_conf_value, Proplist),
                        validators = proplists:get_value(validators, Proplist, []),
                        hidden = proplists:get_value(hidden, Proplist, false),
                        aliases = TokenizedAliases,
                        collect = CollectType
                    }
            end
    end;
parse(X) ->
    {error, {mapping_parse, X}}.

-spec parse_aliases(string(), cuttlefish_variable:variable(), [proplists:property()], boolean()) ->
    [{cuttlefish_variable:variable(), string() | undefined}] | cuttlefish_error:error().
parse_aliases(Variable, VarTokenized, Proplist, IsMerge) ->
    RawAliases = case {proplists:get_value(alias, Proplist),
                       proplists:get_value(aliases, Proplist)} of
        {undefined, undefined} -> undefined;
        {Single, undefined} when is_list(Single) -> [Single];
        {{S, M} = Single, undefined} when is_list(S), is_list(M) -> [Single];
        {Single, undefined} -> {error, {alias_not_a_string, Variable, Single}};
        {undefined, Multi} -> Multi;
        {_, _} -> {error, {alias_and_aliases_both_set, Variable}}
    end,
    validate_raw_aliases(Variable, VarTokenized, RawAliases, IsMerge).

validate_raw_aliases(_Variable, _VarTokenized, undefined, _IsMerge) ->
    [];
validate_raw_aliases(_Variable, _VarTokenized, [] = _RawAliases, true = _IsMerge) ->
    [];
validate_raw_aliases(Variable, _VarTokenized, [], _IsMerge) ->
    {error, {aliases_empty, Variable}};
validate_raw_aliases(_Variable, _VarTokenized, {error, _} = Error, _IsMerge) ->
    Error;
validate_raw_aliases(Variable, _VarTokenized, [H|_], _IsMerge) when is_integer(H) ->
    %% Bare string: {aliases, "old.key"} instead of {aliases, ["old.key"]}
    {error, {aliases_is_bare_string, Variable}};
validate_raw_aliases(Variable, VarTokenized, RawAliases, _IsMerge) when is_list(RawAliases) ->
    case normalize_alias_entries(Variable, RawAliases) of
        {error, _} = E -> E;
        Entries ->
            TokenizedEntries = [{cuttlefish_variable:tokenize(A), Msg}
                                || {A, Msg} <- Entries],
            TokenizedVars = [Var || {Var, _} <- TokenizedEntries],
            case lists:member(VarTokenized, TokenizedVars) of
                true ->
                    {error, {alias_is_self, Variable}};
                false ->
                    case length(lists:usort(TokenizedVars)) < length(TokenizedVars) of
                        true ->
                            {error, {aliases_contain_duplicates, Variable}};
                        false ->
                            case first_fuzzy_alias(TokenizedVars) of
                                {fuzzy, FuzzyAlias} ->
                                    {error, {fuzzy_alias_unsupported, Variable,
                                             cuttlefish_variable:format(FuzzyAlias)}};
                                ok ->
                                    TokenizedEntries
                            end
                    end
            end
    end;
validate_raw_aliases(Variable, _VarTokenized, BadValue, _IsMerge) ->
    {error, {aliases_invalid_value, Variable, BadValue}}.

normalize_alias_entries(_Variable, []) -> [];
normalize_alias_entries(Variable, [H|T]) ->
    case normalize_alias_entry(Variable, H) of
        {error, _} = E -> E;
        Entry ->
            case normalize_alias_entries(Variable, T) of
                {error, _} = E2 -> E2;
                Rest -> [Entry | Rest]
            end
    end.

normalize_alias_entry(_Variable, AliasStr) when is_list(AliasStr) ->
    {AliasStr, undefined};
normalize_alias_entry(_Variable, {AliasStr, Message})
  when is_list(AliasStr), is_list(Message) ->
    {AliasStr, Message};
normalize_alias_entry(Variable, Bad) ->
    {error, {alias_not_a_string, Variable, Bad}}.

-spec first_fuzzy_alias([cuttlefish_variable:variable()]) -> ok | {fuzzy, cuttlefish_variable:variable()}.
first_fuzzy_alias([]) ->
    ok;
first_fuzzy_alias([Alias|Rest]) ->
    IsFuzzy = lists:any(fun([$$|_]) -> true; (_) -> false end, Alias),
    case IsFuzzy of
        true -> {fuzzy, Alias};
        false -> first_fuzzy_alias(Rest)
    end.

%% If this mapping exists, do something.
%% That something is usually a simple replace, unless the proplist in
%% the raw mapping contains the atom 'merge'.
%%
%% This function assumes it's run as part of a foldl over new schema elements
%% in which case, there's only ever one instance of a key in the list
%% so keyreplace works fine.
-spec parse_and_merge(
    raw_mapping(), [mapping()]) -> [mapping()] | cuttlefish_error:error().
parse_and_merge({mapping, Variable, _Mapping, Props} = MappingSource, Mappings) ->
    Var = cuttlefish_variable:tokenize(Variable),
    case lists:keyfind(Var, #mapping.variable, Mappings) of
        false ->
            [ parse(MappingSource) | Mappings];
        OldMapping ->
            MaybeMergedMapping = case proplists:is_defined(merge, Props) of
                true ->
                    merge(MappingSource, OldMapping);
                _ ->
                    parse(MappingSource)
            end,
            case MaybeMergedMapping of
                {error, _} = Error -> Error;
                _ -> lists:keyreplace(Var, #mapping.variable, Mappings, MaybeMergedMapping)
            end
    end.

-spec merge(raw_mapping(), mapping()) -> mapping() | cuttlefish_error:error().
merge(NewMappingSource, OldMapping) ->
    case parse(NewMappingSource) of
        {error, _} = Error ->
            Error;
        MergeMapping ->
            #mapping{
                variable = variable(MergeMapping),
                mapping = mapping(MergeMapping),
                default = choose(default, NewMappingSource, MergeMapping, OldMapping),
                commented = choose(commented, NewMappingSource, MergeMapping, OldMapping),
                datatype = choose(datatype, NewMappingSource, MergeMapping, OldMapping),
                level = choose(level, NewMappingSource, MergeMapping, OldMapping),
                doc = choose(doc, NewMappingSource, MergeMapping, OldMapping),
                include_default = choose(include_default, NewMappingSource, MergeMapping, OldMapping),
                new_conf_value = choose(new_conf_value, NewMappingSource, MergeMapping, OldMapping),
                validators = choose(validators, NewMappingSource, MergeMapping, OldMapping),
                see = choose(see, NewMappingSource, MergeMapping, OldMapping),
                hidden = choose(hidden, NewMappingSource, MergeMapping, OldMapping),
                aliases = choose(aliases, NewMappingSource, MergeMapping, OldMapping),
                collect = choose(collect, NewMappingSource, MergeMapping, OldMapping)
            }
    end.

choose(Field, {_, _, _, PreParseMergeProps}, MergeMapping, OldMapping) ->
    IsDefined = proplists:is_defined(Field, PreParseMergeProps) orelse
                (Field =:= aliases andalso proplists:is_defined(alias, PreParseMergeProps)),
    Value = proplists:get_value(Field, PreParseMergeProps),
    %% Empty see/doc in a merge mapping is treated as "not specified" (inherit),
    %% but empty aliases is an intentional "clear all aliases" directive.
    Which = case {Field, IsDefined, Value} of
                {see, _, []} -> old;
                {doc, _, []} -> old;
                {_, true, _} -> new;
                _ -> old
    end,
    case {Which, Field} of
        {new, aliases} -> aliases_with_messages(MergeMapping);
        {old, aliases} -> aliases_with_messages(OldMapping);
        {new, _}       -> ?MODULE:Field(MergeMapping);
        {old, _}       -> ?MODULE:Field(OldMapping)
    end.

-spec is_mapping(any()) -> boolean().
is_mapping(M) ->
    is_tuple(M) andalso element(1, M) =:= mapping.

-spec variable(mapping()) -> [string()].
variable(M) -> M#mapping.variable.

-spec is_fuzzy_variable(mapping()) -> boolean().
is_fuzzy_variable(#mapping{variable=VariableDef}) ->
    lists:any(fun(X) -> hd(X) =:= $$ end, VariableDef).

-spec mapping(mapping()) -> string().
mapping(M) -> M#mapping.mapping.

-spec default(mapping()) -> term().
default(M) -> M#mapping.default.

-spec has_default(mapping()) -> boolean().
has_default(MappingRecord) ->
    default(MappingRecord) =/= undefined.

-spec commented(mapping()) -> term().
commented(M)        -> M#mapping.commented.

-spec datatype(mapping()) -> cuttlefish_datatypes:datatype_list().
datatype(M) -> M#mapping.datatype.

%% Pipeline-internal: swaps the datatype list in place so a later
%% stage can substitute resolved `{validator, Fun}' for the parsed
%% `{validator, Name}' entries. Not intended for schema authors.
-spec set_datatype(mapping(), cuttlefish_datatypes:datatype_list()) -> mapping().
set_datatype(M, DT) -> M#mapping{datatype = DT}.

-spec level(mapping()) -> basic | intermediate | advanced.
level(M) -> M#mapping.level.

-spec hidden(mapping()) -> boolean().
hidden(M) -> M#mapping.hidden.

-spec doc(mapping()) -> [string()].
doc(M) -> M#mapping.doc.

-spec see(mapping()) -> [cuttlefish_variable:variable()].
see(M) -> M#mapping.see.

-spec include_default(mapping()) -> string() | undefined.
include_default(M) -> M#mapping.include_default.

-spec new_conf_value(mapping()) -> string() | undefined.
new_conf_value(M) -> M#mapping.new_conf_value.

-spec validators(mapping()) -> [string()].
validators(M) -> M#mapping.validators.

-spec aliases(mapping()) -> [cuttlefish_variable:variable()].
aliases(M) -> [Var || {Var, _Msg} <- M#mapping.aliases].

-spec aliases_with_messages(mapping()) ->
    [{cuttlefish_variable:variable(), string() | undefined}].
aliases_with_messages(M) -> M#mapping.aliases.

-spec collect(mapping()) -> collection_type() | undefined.
collect(M) -> M#mapping.collect.

-spec validators(mapping(), [cuttlefish_validator:validator()]) -> [cuttlefish_validator:validator()].
validators(M, Validators) ->
    lists:foldr(fun(VName, Vs) ->
                        case find_validator(VName, Validators) of
                            false -> Vs;
                            V -> [V|Vs]
                        end
                end, [], M#mapping.validators).

%% Look up by canonical name first, then by alias. Canonical match
%% wins so a validator named "X" beats an alias "X" on a different
%% validator — the same precedence rule mapping aliases use.
find_validator(Name, Validators) ->
    case lists:keyfind(Name, 2, Validators) of
        false ->
            find_by_alias(Name, Validators);
        V -> V
    end.

find_by_alias(_Name, []) -> false;
find_by_alias(Name, [V | Rest]) ->
    case lists:member(Name, cuttlefish_validator:aliases(V)) of
        true  -> V;
        false -> find_by_alias(Name, Rest)
    end.

-spec replace(mapping(), [mapping()]) -> [mapping()].
replace(Mapping, ListOfMappings) ->
    Exists = lists:keymember(variable(Mapping), #mapping.variable, ListOfMappings),
    case Exists of
        true ->
            lists:keyreplace(variable(Mapping), #mapping.variable, ListOfMappings, Mapping);
        _ ->
            [Mapping | ListOfMappings]
    end.

-spec remove_all_but_first(string(), [mapping()]) -> [mapping()].
remove_all_but_first(MappingName, Mappings) ->
    lists:foldr(
        fun(#mapping{mapping=MN}=M, Acc) when MN =:= MappingName->
                [M|lists:keydelete(MN, #mapping.mapping, Acc)];
            (M, Acc) ->
                [M|Acc]
        end, [], Mappings).

-spec parse_collect(term()) -> collection_type() | undefined | cuttlefish_error:error().
parse_collect(undefined)        -> undefined;
parse_collect(list)             -> list;
parse_collect({map, atom})      -> {map, atom};
parse_collect({map, binary})    -> {map, binary};
parse_collect({proplist, atom}) -> {proplist, atom};
%% If {proplist, binary} is given, reject it; use {map, binary} for binary keys.
parse_collect({proplist, binary}) -> {error, {unsupported_collect_type, {proplist, binary}}};
parse_collect(Other)            -> {error, {invalid_collect_type, Other}}.

-ifdef(TEST).

-define(XLATE(X), lists:flatten(cuttlefish_error:xlate(X))).

mapping_test() ->

    SampleMapping = {
        mapping,
        "conf.key",
        "erlang.key",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "config_file_val"},
            {doc, ["documentation", "for feature"]},
            {validators, ["valid.the.impailer"]},
            hidden
        ]
    },

    Record = parse(SampleMapping),

    ?assertEqual(["conf","key"], Record#mapping.variable),
    ?assertEqual("default value", Record#mapping.default),
    ?assertEqual("erlang.key", Record#mapping.mapping),
    ?assertEqual(advanced, Record#mapping.level),
    ?assertEqual([{enum, [on, off]}], Record#mapping.datatype),
    ?assertEqual(["documentation", "for feature"], Record#mapping.doc),
    ?assertEqual("default_substitution", Record#mapping.include_default),
    ?assertEqual("config_file_val", Record#mapping.new_conf_value),
    ?assertEqual(["valid.the.impailer"], Record#mapping.validators),
    ?assertEqual(true, Record#mapping.hidden),

    %% function tests
    ?assertEqual(["conf","key"], variable(Record)),
    ?assertEqual("default value", default(Record)),
    ?assertEqual("erlang.key", mapping(Record)),
    ?assertEqual(advanced, level(Record)),
    ?assertEqual([{enum, [on, off]}], datatype(Record)),
    ?assertEqual(["documentation", "for feature"], doc(Record)),
    ?assertEqual("default_substitution", include_default(Record)),
    ?assertEqual("config_file_val", new_conf_value(Record)),
    ?assertEqual(["valid.the.impailer"], validators(Record)),
    ?assertEqual(true, hidden(Record)),

    ok.

replace_test() ->
    Element1 = parse({
        mapping,
        "conf.key18",
        "erlang.key4",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "conf_val18"},
            {doc, ["documentation", "for feature"]}
        ]
    }),

    SampleMappings = [Element1,
    parse({
        mapping,
        "conf.key",
        "erlang.key2",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "conf_val_A"},
            {doc, ["documentation", "for feature"]}
        ]
    })
    ],

    Override = parse({
        mapping,
        "conf.key",
        "erlang.key",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "conf_val_B"},
            {doc, ["documentation", "for feature"]}
        ]
    }),

    NewMappings = replace(Override, SampleMappings),
    ?assertEqual([Element1, Override], NewMappings),
    ok.

validators_test() ->
    Validators = [
    cuttlefish_validator:parse(
        {validator, "a", "a desc", fun(_X) -> true end}
    ),
    cuttlefish_validator:parse({
        validator, "b", "b desc", fun(_X) -> true end
        }),
    cuttlefish_validator:parse({
        validator, "c", "c desc", fun(_X) -> true end
        })
    ],

    %% Hack for coverage
    [ begin
        Fun = cuttlefish_validator:func(V),
        ?assert(Fun(x))
    end || V <- Validators],
    Mapping = parse({
        mapping,
        "conf.key",
        "erlang.key1",
        [
            {validators, ["a", "b"]}
        ]
    }),

    [A, B, _C] = Validators,

    ?assertEqual([A,B], validators(Mapping, Validators)),

    MappingWithMissingValidator = parse({
        mapping,
        "conf.key",
        "erlang.key1",
        [
            {validators, ["a", "d"]} %% There is no "d"
        ]
    }),
    ?assertEqual([A], validators(MappingWithMissingValidator, Validators)),

    ok.

parse_and_merge_test() ->
    SampleMappings = [parse({
        mapping,
        "conf.key",
        "erlang.key1",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "conf_val_A"},
            {doc, ["documentation", "for feature"]},
            hidden
        ]
    }),
    parse({
        mapping,
        "conf.key2",
        "erlang.key2",
        [
            {level, advanced},
            {default, "default value"},
            {datatype, {enum, [on, off]}},
            {commented, "commented value"},
            {include_default, "default_substitution"},
            {new_conf_value, "conf_val_B"},
            {doc, ["documentation", "for feature"]}
        ]
    })
    ],

    NewMappings = parse_and_merge({mapping, "conf.key", "erlang.key3", [{hidden, false}]}, SampleMappings),

    ?assertEqual("erlang.key3", mapping(hd(NewMappings))),
    ?assertEqual(false, hidden(hd(NewMappings))),
    ok.

smart_merge_test() ->
    OldM1 = parse({mapping, "thing.to.merge", "some.key", [
            {default, 7},
            {datatype, integer},
            {doc, ["documentation", "for feature"]}
        ]}),
    OldM2 = parse({mapping, "thing.not.merged", "some.other_key", [{default, 6}, {datatype, integer}]}),

    OriginalMappings = [OldM1, OldM2],

    NewRawNoMergeMapping = {mapping, "thing.to.merge", "some.new_other_key", [{level, advanced}]},
    [NewUnMergedMapping, OldM2] = parse_and_merge(
        NewRawNoMergeMapping,
        OriginalMappings),

    ?assertEqual(["thing", "to", "merge"], variable(NewUnMergedMapping)),
    ?assertEqual(undefined, default(NewUnMergedMapping)),
    ?assertEqual("some.new_other_key", mapping(NewUnMergedMapping)),
    ?assertEqual(advanced, level(NewUnMergedMapping)),
    ?assertEqual([string], datatype(NewUnMergedMapping)),
    ?assertEqual([], doc(NewUnMergedMapping)),
    ?assertEqual(undefined, include_default(NewUnMergedMapping)),
    ?assertEqual([], validators(NewUnMergedMapping)),

    NewRawMergeMapping = {mapping, "thing.to.merge", "some.new_key", [merge, {level, advanced}]},
    [NewMergedMapping, OldM2] = NewMappings = parse_and_merge(
        NewRawMergeMapping,
        OriginalMappings),

    ?assertEqual(["thing", "to", "merge"], variable(NewMergedMapping)),
    ?assertEqual(7, default(NewMergedMapping)),
    ?assertEqual("some.new_key", mapping(NewMergedMapping)),
    ?assertEqual(advanced, level(NewMergedMapping)),
    ?assertEqual([integer], datatype(NewMergedMapping)),
    ?assertEqual(["documentation", "for feature"], doc(NewMergedMapping)),
    ?assertEqual(undefined, include_default(NewMergedMapping)),
    ?assertEqual([], validators(NewMergedMapping)),

    NewerRawMergeMapping = {mapping, "thing.to.merge", "some.third_key", [merge, {default, 42}]},

    [NewerMergedMapping, OldM2] = parse_and_merge(
        NewerRawMergeMapping,
        NewMappings),

    ?assertEqual(["thing", "to", "merge"], variable(NewerMergedMapping)),
    ?assertEqual(42, default(NewerMergedMapping)),
    ?assertEqual("some.third_key", mapping(NewerMergedMapping)),
    ?assertEqual(advanced, level(NewerMergedMapping)),
    ?assertEqual([integer], datatype(NewerMergedMapping)),
    ?assertEqual(["documentation", "for feature"], doc(NewerMergedMapping)),
    ?assertEqual(undefined, include_default(NewerMergedMapping)),
    ?assertEqual([], validators(NewerMergedMapping)),
    ok.

accidentally_used_strings_for_enums_test() ->
    Mapping = parse({
        mapping,
        "conf.key2",
        "erlang.key2",
        [
            {datatype, {enum, ["on", "off"]}}
        ]
    }),
    ?assertEqual([{enum, [on, off]}], cuttlefish_mapping:datatype(Mapping)),
    ok.

parse_error_test() ->
    {ErrorAtom, ErrorTuple} = parse(not_a_raw_mapping),
    ?assertEqual(error, ErrorAtom),
    ?assertEqual(
        "Poorly formatted input to cuttlefish_mapping:parse/1 : not_a_raw_mapping",
        ?XLATE(ErrorTuple)),
    ok.

is_mapping_test() ->
    ?assert(not(is_mapping(not_a_mapping))),

    M = parse({
        mapping,
        "conf.key2",
        "erlang.key2",
        [
            {datatype, {enum, ["on", "off"]}}
        ]
    }),
    ?assert(is_mapping(M)),
    ok.

%% conf.key can be any integer or the atom undefined.
extended_types_parse_test() ->
    Mapping = parse({
        mapping,
        "conf.key",
        "erlang.key",
        [
         {datatype, [integer, {atom, undefined}]}
        ]
    }),

    ?assertEqual([integer, {atom, undefined}], cuttlefish_mapping:datatype(Mapping)),
    ok.

datatype_cannot_be_empty_list_test() ->
    Mapping = parse({
        mapping,
        "conf.key",
        "erlang.key",
        [
         {datatype, []}
        ]
    }),
    ?assertMatch({error, _}, Mapping),
    ok.

%% Aliases tests

aliases_parse_test() ->
    M = parse({mapping, "new.key", "app.setting", [
        {aliases, ["old.key", "older.key"]}
    ]}),
    ?assertEqual([["old", "key"], ["older", "key"]], aliases(M)),
    ?assertEqual([{["old", "key"], undefined}, {["older", "key"], undefined}],
                 aliases_with_messages(M)).

aliases_omitted_test() ->
    M = parse({mapping, "new.key", "app.setting", []}),
    ?assertEqual([], aliases(M)),
    ?assertEqual([], aliases_with_messages(M)).

aliases_empty_list_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, []}]}),
    ?assertMatch({error, {aliases_empty, _}}, Result).

aliases_self_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, ["a.b"]}]}),
    ?assertMatch({error, {alias_is_self, _}}, Result).

aliases_self_among_multiple_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, ["old.key", "a.b"]}
    ]}),
    ?assertMatch({error, {alias_is_self, _}}, Result).

alias_singular_test() ->
    M = parse({mapping, "new.key", "app.setting", [
        {alias, "old.key"}
    ]}),
    ?assertEqual([["old", "key"]], aliases(M)),
    ?assertEqual([{["old", "key"], undefined}], aliases_with_messages(M)).

alias_singular_self_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{alias, "a.b"}]}),
    ?assertMatch({error, {alias_is_self, _}}, Result).

alias_and_aliases_both_set_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {alias, "old.key"}, {aliases, ["older.key"]}
    ]}),
    ?assertMatch({error, {alias_and_aliases_both_set, _}}, Result).

aliases_bare_string_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, "old.key"}]}),
    ?assertMatch({error, {aliases_is_bare_string, _}}, Result).

alias_singular_not_a_string_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{alias, 42}]}),
    ?assertMatch({error, {alias_not_a_string, "a.b", 42}}, Result).

aliases_duplicate_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, ["old.key", "old.key"]}
    ]}),
    ?assertMatch({error, {aliases_contain_duplicates, _}}, Result).

aliases_invalid_value_atom_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, deprecated}]}),
    ?assertMatch({error, {aliases_invalid_value, "a.b", deprecated}}, Result).

aliases_invalid_value_integer_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, 42}]}),
    ?assertMatch({error, {aliases_invalid_value, "a.b", 42}}, Result).

aliases_invalid_value_tuple_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [{aliases, {"a", "b"}}]}),
    ?assertMatch({error, {aliases_invalid_value, "a.b", {"a", "b"}}}, Result).

aliases_fuzzy_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, ["old.$name.key"]}
    ]}),
    ?assertMatch({error, {fuzzy_alias_unsupported, _, _}}, Result).

%% Alias with message parsing tests

aliases_with_message_test() ->
    M = parse({mapping, "new.key", "app.setting", [
        {aliases, [{"old.key", "renamed in 4.0"}]}
    ]}),
    ?assertEqual([["old", "key"]], aliases(M)),
    ?assertEqual([{["old", "key"], "renamed in 4.0"}],
                 aliases_with_messages(M)).

aliases_mixed_with_and_without_message_test() ->
    M = parse({mapping, "new.key", "app.setting", [
        {aliases, ["old.key", {"older.key", "legacy name"}]}
    ]}),
    ?assertEqual([["old", "key"], ["older", "key"]], aliases(M)),
    ?assertEqual([{["old", "key"], undefined},
                  {["older", "key"], "legacy name"}],
                 aliases_with_messages(M)).

alias_singular_with_message_test() ->
    M = parse({mapping, "new.key", "app.setting", [
        {alias, {"old.key", "consolidated in 4.0"}}
    ]}),
    ?assertEqual([["old", "key"]], aliases(M)),
    ?assertEqual([{["old", "key"], "consolidated in 4.0"}],
                 aliases_with_messages(M)).

aliases_with_message_self_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, [{"a.b", "msg"}]}
    ]}),
    ?assertMatch({error, {alias_is_self, _}}, Result).

aliases_with_message_duplicate_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, [{"old.key", "msg1"}, {"old.key", "msg2"}]}
    ]}),
    ?assertMatch({error, {aliases_contain_duplicates, _}}, Result).

aliases_with_message_fuzzy_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, [{"old.$name.key", "msg"}]}
    ]}),
    ?assertMatch({error, {fuzzy_alias_unsupported, _, _}}, Result).

aliases_with_bad_message_type_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, [{"old.key", 42}]}
    ]}),
    ?assertMatch({error, {alias_not_a_string, "a.b", {"old.key", 42}}}, Result).

aliases_tuple_not_string_error_test() ->
    Result = parse({mapping, "a.b", "app.setting", [
        {aliases, [{42, "msg"}]}
    ]}),
    ?assertMatch({error, {alias_not_a_string, "a.b", {42, "msg"}}}, Result).

%% Collect parse tests

collect_omitted_test() ->
    M = parse({mapping, "prefix.$name", "app.key", [{datatype, integer}]}),
    ?assertEqual(undefined, collect(M)).

collect_list_test() ->
    M = parse({mapping, "prefix.$name", "app.key", [
        {datatype, integer}, {collect, list}
    ]}),
    ?assertEqual(list, collect(M)).

collect_map_atom_test() ->
    M = parse({mapping, "prefix.$name", "app.key", [
        {collect, {map, atom}}
    ]}),
    ?assertEqual({map, atom}, collect(M)).

collect_map_binary_test() ->
    M = parse({mapping, "prefix.$name", "app.key", [
        {collect, {map, binary}}
    ]}),
    ?assertEqual({map, binary}, collect(M)).

collect_proplist_atom_test() ->
    M = parse({mapping, "prefix.$name", "app.key", [
        {collect, {proplist, atom}}
    ]}),
    ?assertEqual({proplist, atom}, collect(M)).

collect_invalid_type_test() ->
    Result = parse({mapping, "prefix.$name", "app.key", [
        {collect, not_valid}
    ]}),
    ?assertMatch({error, {invalid_collect_type, not_valid}}, Result).

collect_proplist_binary_unsupported_test() ->
    Result = parse({mapping, "prefix.$name", "app.key", [
        {collect, {proplist, binary}}
    ]}),
    ?assertMatch({error, {unsupported_collect_type, {proplist, binary}}}, Result).

collect_on_non_fuzzy_test() ->
    Result = parse({mapping, "no.wildcard", "app.key", [
        {collect, list}
    ]}),
    ?assertMatch({error, {collect_on_non_fuzzy, "no.wildcard"}}, Result).

collect_multi_wildcard_test() ->
    Result = parse({mapping, "a.$x.b.$y", "app.key", [
        {collect, list}
    ]}),
    ?assertMatch({error, {collect_multi_wildcard, "a.$x.b.$y", 2}}, Result).

collect_merge_inherits_test() ->
    OldM = parse({mapping, "p.$n", "app.x", [
        {default, 1}, {datatype, integer}, {collect, list}
    ]}),
    NewRaw = {mapping, "p.$n", "app.x", [merge, {default, 2}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual(list, collect(Merged)),
    ?assertEqual(2, default(Merged)).

collect_merge_replaces_test() ->
    OldM = parse({mapping, "p.$n", "app.x", [
        {default, 1}, {datatype, integer}, {collect, list}
    ]}),
    NewRaw = {mapping, "p.$n", "app.x", [merge, {collect, {map, atom}}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual({map, atom}, collect(Merged)).

aliases_merge_replaces_test() ->
    OldM = parse({mapping, "a.b", "app.x", [
        {default, 1}, {datatype, integer}, {aliases, ["old.ab"]}
    ]}),
    NewRaw = {mapping, "a.b", "app.x", [merge, {aliases, ["older.ab"]}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual([["older", "ab"]], aliases(Merged)),
    ?assertEqual(1, default(Merged)).

aliases_merge_inherits_test() ->
    OldM = parse({mapping, "a.b", "app.x", [
        {default, 1}, {datatype, integer}, {aliases, ["old.ab"]}
    ]}),
    NewRaw = {mapping, "a.b", "app.x", [merge, {default, 2}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual([["old", "ab"]], aliases(Merged)),
    ?assertEqual(2, default(Merged)).

aliases_merge_singular_replaces_test() ->
    OldM = parse({mapping, "a.b", "app.x", [
        {default, 1}, {datatype, integer}, {aliases, ["old.ab"]}
    ]}),
    NewRaw = {mapping, "a.b", "app.x", [merge, {alias, "newer.ab"}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual([["newer", "ab"]], aliases(Merged)),
    ?assertEqual(1, default(Merged)).

aliases_merge_empty_clears_test() ->
    OldM = parse({mapping, "a.b", "app.x", [
        {default, 1}, {datatype, integer}, {aliases, ["old.ab"]}
    ]}),
    NewRaw = {mapping, "a.b", "app.x", [merge, {aliases, []}]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual([], aliases(Merged)),
    ?assertEqual(1, default(Merged)),
    ?assertEqual([integer], datatype(Merged)).

aliases_merge_with_message_test() ->
    OldM = parse({mapping, "a.b", "app.x", [
        {default, 1}, {datatype, integer}, {aliases, ["old.ab"]}
    ]}),
    NewRaw = {mapping, "a.b", "app.x", [merge,
        {aliases, [{"old.ab", "renamed in 4.0"}]}
    ]},
    [Merged] = parse_and_merge(NewRaw, [OldM]),
    ?assertEqual([["old", "ab"]], aliases(Merged)),
    ?assertEqual([{["old", "ab"], "renamed in 4.0"}],
                 aliases_with_messages(Merged)),
    ?assertEqual(1, default(Merged)).

-endif.
