%% -------------------------------------------------------------------
%%
%% cuttlefish_generator: this is where the action is
%%
%% 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_generator).

-include_lib("kernel/include/logger.hrl").

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

-define(LSUB, "$(").
-define(RSUB, ")").
-define(LSUBLEN, 2).
-define(RSUBLEN, 1).

-export([map/2, map/3, find_mapping/2, add_defaults/2, minimal_map/2, resolve_aliases/2, validate/2, validate/3]).

-spec map(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
                 [proplists:property()] |
                 {error, atom(), cuttlefish_error:errorlist()}.
map(Schema, Config) ->
    map(Schema, Config, []).

-spec map(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()]) ->
                 [proplists:property()] |
                 {error, atom(), cuttlefish_error:errorlist()}.
map(Schema, Config, ParsedArgs) ->
    map_add_defaults(Schema, Config, ParsedArgs).

%% @doc Generates an Erlang config that only includes the settings
%% encompassed by the passed Config, excluding defaults from the
%% schema for unspecified settings.
-spec minimal_map(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
                         [proplists:property()] | {error, atom(), cuttlefish_error:errorlist()}.
minimal_map({AllTranslations,AllMappings,V}, Config) ->
    ConfigKeys = sets:from_list([K || {K, _} <- Config]),
    {RestrictedMappings, MappingKeys} = lists:foldr(fun(M,Acc) ->
                                                            restrict_mappings(M, Acc, ConfigKeys)
                                                    end, {[], sets:new()}, AllMappings),
    RestrictedTranslations = [ T || T <- AllTranslations,
                                    sets:is_element(cuttlefish_translation:mapping(T), MappingKeys)],
    map({RestrictedTranslations,RestrictedMappings,V}, Config, []).

restrict_mappings(M, {Mappings, Keys}, ConfigKeys) ->
    Variable = cuttlefish_mapping:variable(M),
    Aliases = cuttlefish_mapping:aliases(M),
    HasMatch = sets:is_element(Variable, ConfigKeys) orelse
               lists:any(fun(A) -> sets:is_element(A, ConfigKeys) end, Aliases),
    case HasMatch of
        true ->
            {[M|Mappings], sets:add_element(cuttlefish_mapping:mapping(M), Keys)};
        false ->
            {Mappings, Keys}
    end.

-spec map_add_defaults(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()]) ->
                              [proplists:property()] |
                              {error, atom(), cuttlefish_error:errorlist()}.
map_add_defaults({_, Mappings, _} = Schema, Config, ParsedArgs) ->
    %% Config at this point is just what's in the .conf file.
    %% First resolve any aliases to their canonical keys
    _ = ?LOG_DEBUG("Resolving aliases"),
    AliasResolvedConfig = resolve_aliases(Config, Mappings),
    %% add_defaults/2 rolls the default values in from the schema
    _ = ?LOG_DEBUG("Adding Defaults"),
    DConfig = add_defaults(AliasResolvedConfig, Mappings),
    case cuttlefish_error:errorlist_maybe(DConfig) of
        {errorlist, EList} ->
            {error, add_defaults, {errorlist, EList}};
        _ ->
            map_value_sub(Schema, DConfig, ParsedArgs)
    end.

-spec map_value_sub(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()]) ->
                           [proplists:property()] |
                           {error, atom(), cuttlefish_error:errorlist()}.
map_value_sub(Schema, Config, ParsedArgs) ->
    _ = ?LOG_DEBUG("Right Hand Side Substitutions"),
    {_, Mappings, _} = Schema,
     case value_sub(Config) of
         {SubbedConfig, []} ->
            map_transform_datatypes(Schema, SubbedConfig, ParsedArgs);
         {_, EList} ->
            {error, rhs_subs, {errorlist, enrich_alias_sub_errors(EList, Mappings)}}
    end.

-spec map_transform_datatypes(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()]) ->
                                     [proplists:property()] |
                                     {error, atom(), cuttlefish_error:errorlist()}.
map_transform_datatypes({_, Mappings, _} = Schema, DConfig, ParsedArgs) ->
    %% Everything in DConfig is of datatype "string",
    %% transform_datatypes turns them into other erlang terms
    %% based on the schema
    _ = ?LOG_DEBUG("Applying Datatypes"),
    case transform_datatypes(DConfig, Mappings, ParsedArgs) of
        {NewConf, []} ->
            map_validate(Schema, NewConf);
        {_, EList} ->
            {error, transform_datatypes, {errorlist, EList}}
    end.

-spec map_validate(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
                          [proplists:property()] |
                          {error, atom(), cuttlefish_error:errorlist()}.
map_validate(Schema, Conf) ->
    %% Any more advanced validators
    _ = ?LOG_DEBUG("Validation"),
    case cuttlefish_error:errorlist_maybe(run_validations(Schema, Conf)) of
        {errorlist, EList} ->
            {error, validation, {errorlist, EList}};
        true ->
            {DirectMappings, TranslationsToDrop} = apply_mappings(Schema, Conf),
            apply_translations(Schema, Conf, DirectMappings, TranslationsToDrop)
    end.

-spec apply_mappings(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
                            {[proplists:property()], [string()]}.
apply_mappings({Translations, Mappings, _Validators}, Conf) ->
    %% This fold handles 1:1 mappings, that have no corresponding translations.
    %% The accumulator is the app.config proplist that we start building from
    %% these 1:1 mappings, hence the return "DirectMappings".
    %% It also builds a list of "TranslationsToDrop". It's basically saying that
    %% if a user didn't actually configure this setting in the .conf file and
    %% there's no default in the schema, then there won't be enough information
    %% during the translation phase to succeed, so we'll earmark it to be skipped.
    %% If a fuzzy mapping has a collect property and no translation, its values are auto-collected.
    {DirectMappings, {TranslationsToMaybeDrop, TranslationsToKeep}} = lists:foldr(
        fun(MappingRecord, {ConfAcc, {MaybeDrop, Keep}}) ->
            Mapping = cuttlefish_mapping:mapping(MappingRecord),
            Default = cuttlefish_mapping:default(MappingRecord),
            Variable = cuttlefish_mapping:variable(MappingRecord),
            HasTranslation = lists:any(
                fun(T) -> cuttlefish_translation:mapping(T) =:= Mapping end,
                Translations),
            CollectType = cuttlefish_mapping:collect(MappingRecord),
            IsFuzzy = cuttlefish_mapping:is_fuzzy_variable(MappingRecord),
            case {HasTranslation, CollectType =/= undefined andalso IsFuzzy} of
                {false, true} ->
                    collect_fuzzy_values(Variable, Mapping, CollectType, Conf,
                                         ConfAcc, MaybeDrop, Keep);
                _ ->
                    case {Default =/= undefined orelse
                          cuttlefish_conf:is_variable_defined(Variable, Conf),
                          HasTranslation} of
                        {true, false} ->
                            Tokens = cuttlefish_variable:tokenize(Mapping),
                            NewValue = proplists:get_value(Variable, Conf),
                            {set_value(Tokens, ConfAcc, NewValue),
                             {MaybeDrop, ordsets:add_element(Mapping, Keep)}};
                        {true, true} ->
                            {ConfAcc, {MaybeDrop, ordsets:add_element(Mapping, Keep)}};
                        _ ->
                            {ConfAcc, {ordsets:add_element(Mapping, MaybeDrop), Keep}}
                    end
            end
        end,
        {[], {ordsets:new(), ordsets:new()}},
        Mappings),
    _ = ?LOG_DEBUG("Applied 1:1 Mappings"),

    TranslationsToDrop = TranslationsToMaybeDrop -- TranslationsToKeep,
    {DirectMappings, TranslationsToDrop}.

%% If there are no conf values matching the pattern, write an empty collection.
-spec collect_fuzzy_values(
        cuttlefish_variable:variable(), string(),
        cuttlefish_mapping:collection_type(),
        cuttlefish_conf:conf(),
        [proplists:property()], ordsets:ordset(string()), ordsets:ordset(string())) ->
    {[proplists:property()], {ordsets:ordset(string()), ordsets:ordset(string())}}.
collect_fuzzy_values(Variable, Mapping, CollectType, Conf, ConfAcc, MaybeDrop, Keep) ->
    Matches = cuttlefish_variable:fuzzy_matches(Variable, Conf),
    Tokens = cuttlefish_variable:tokenize(Mapping),
    Value = build_collect(Variable, Matches, CollectType, Conf),
    {set_value(Tokens, ConfAcc, Value),
     {MaybeDrop, ordsets:add_element(Mapping, Keep)}}.

%% If multiple conf values match, assemble them into a collection, sorted by
%% the wildcard segment value for list and proplist types.
-spec build_collect(
        cuttlefish_variable:variable(),
        [{string(), string()}],
        cuttlefish_mapping:collection_type(),
        cuttlefish_conf:conf()) -> term().
build_collect(Variable, Matches, list, Conf) ->
    Entries = [{SegVal,
                proplists:get_value(
                    cuttlefish_variable:replace_match(Variable, SegVal), Conf)}
               || {_WildcardSeg, SegVal} <- Matches],
    [V || {_, V} <- lists:sort(Entries)];
build_collect(Variable, Matches, {map, atom}, Conf) ->
    maps:from_list(
        [{list_to_atom(SegVal),
          proplists:get_value(
              cuttlefish_variable:replace_match(Variable, SegVal), Conf)}
         || {_, SegVal} <- Matches]);
build_collect(Variable, Matches, {map, binary}, Conf) ->
    maps:from_list(
        [{list_to_binary(SegVal),
          proplists:get_value(
              cuttlefish_variable:replace_match(Variable, SegVal), Conf)}
         || {_, SegVal} <- Matches]);
build_collect(Variable, Matches, {proplist, atom}, Conf) ->
    [{list_to_atom(SegVal),
      proplists:get_value(
          cuttlefish_variable:replace_match(Variable, SegVal), Conf)}
     || {_, SegVal} <- lists:sort(Matches)].

-spec apply_translations(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()], [string()]) ->
                                [proplists:property()] |
                                {error, atom(), cuttlefish_error:errorlist()}.
apply_translations({Translations, _, _} = Schema, Conf, DirectMappings, TranslationsToDrop) ->
    %% The fold handles the translations. After we've build the DirectMappings,
    %% we use that to seed this fold's accumulator. As we go through each translation
    %% we write that to the `app.config` that lives in the accumutator.
    {Proplist, Errorlist} = lists:foldl(fold_apply_translation(Conf, Schema, TranslationsToDrop),
                                        {DirectMappings, []}, Translations),
    case Errorlist of
        [] ->
            _ = ?LOG_DEBUG("Applied Translations"),
            Proplist;
        Es ->
            {error, apply_translations, {errorlist, Es}}
    end.


fold_apply_translation(Conf, Schema, TranslationsToDrop) ->
    fun(TranslationRecord, {Acc, Errors}) ->
            Mapping = cuttlefish_translation:mapping(TranslationRecord),
            Xlat = cuttlefish_translation:func(TranslationRecord),
            case lists:member(Mapping, TranslationsToDrop) of
                false ->
                    {XlatFun, XlatArgs} = prepare_translation_fun(Conf, Schema,
                                                                  Mapping, Xlat),
                    _ = ?LOG_DEBUG("Running translation for ~ts", [Mapping]),
                    case try_apply_translation(Mapping, XlatFun, XlatArgs) of
                        unset ->
                            {Acc, Errors};
                        {set, NewValue} ->
                            {set_value(cuttlefish_variable:tokenize(Mapping), Acc, NewValue), Errors};
                        {error, Term} ->
                            {Acc, [{error, Term}|Errors]}
                    end;
                _ ->
                    _ = ?LOG_DEBUG("~tp in Translations to drop...", [Mapping]),
                    {Acc, Errors}
            end
        end.

prepare_translation_fun(Conf, Schema, Mapping, Xlat) ->
    case proplists:get_value(arity, erlang:fun_info(Xlat)) of
        1 -> {Xlat, [Conf]};
        2 -> {Xlat, [Conf, Schema]};
        OtherArity ->
            {fun(_) ->
                     {error, {translation_arity, {Mapping, OtherArity}}}
             end, error}
    end.

try_apply_translation(Mapping, XlatFun, XlatArgs) ->
    try erlang:apply(XlatFun, XlatArgs) of
        {ok, Value} ->
            {set, Value};
        X ->
            {set, X}
    catch
        %% cuttlefish:conf_get/2 threw not_found
        throw:{not_found, NotFound} ->
            {error, {translation_missing_setting,
                     {Mapping, cuttlefish_variable:format(NotFound)}}};
        %% For explicitly omitting an output setting.
        %% See cuttlefish:unset/0
        throw:unset ->
            unset;
        %% For translations that found invalid
        %% settings, even after mapping. See
        %% cuttlefish:invalid/1.
        throw:{invalid, Invalid} ->
            {error, {translation_invalid_configuration,
                     {Mapping, Invalid}}};
        %% Any unknown error, perhaps caused by stdlib
        %% stuff.
        E:R ->
            {error, {translation_unknown_error,
                     {Mapping, {E, R}}}}
    end.

%for each token, is it special?
%
%if yes, special processing
%if no, recurse into this with the value from the proplist and tail of tokens
%
%unless the tail of tokens is []

%% This is the last token, so things ends with replacing the proplist value.
set_value([LastToken], Acc, NewValue) ->
    cuttlefish_util:replace_proplist_value(list_to_atom(LastToken), NewValue, Acc);
%% This is the case of all but the last token.
%% recurse until you hit a leaf.
set_value([HeadToken|MoreTokens], PList, NewValue) ->
    Token = list_to_atom(HeadToken),
    OldValue = proplists:get_value(Token, PList, []),
    cuttlefish_util:replace_proplist_value(
        Token,
        set_value(MoreTokens, OldValue, NewValue),
        PList).

%% @doc Rewrites substitution_missing_config errors where the missing variable
%% is a known alias key, to produce a more helpful error message.
-spec enrich_alias_sub_errors([cuttlefish_error:error()], [cuttlefish_mapping:mapping()]) ->
    [cuttlefish_error:error()].
enrich_alias_sub_errors(EList, Mappings) ->
    AliasIndex = lists:flatmap(fun(M) ->
        Canonical = cuttlefish_mapping:variable(M),
        [{Alias, Canonical} || Alias <- cuttlefish_mapping:aliases(M)]
    end, Mappings),
    [case E of
        {error, {substitution_missing_config, {Src, Var}}} ->
            VarTokenized = cuttlefish_variable:tokenize(Var),
            case lists:keyfind(VarTokenized, 1, AliasIndex) of
                {_, Canonical} ->
                    {error, {substitution_alias_key,
                             {Src, Var, cuttlefish_variable:format(Canonical)}}};
                false ->
                    E
            end;
        _ -> E
    end || E <- EList].

%% @doc Resolves alias keys to their canonical keys.
%% Alias values are rewritten to the canonical key with a deprecation warning.
%% Canonical takes precedence. If multiple aliases are set, the first one
%% listed in the mapping's `aliases' property wins. Every set alias warns.
%% If the alias carries a custom message, it is appended to the warning.
-spec resolve_aliases(cuttlefish_conf:conf(), [cuttlefish_mapping:mapping()]) ->
    cuttlefish_conf:conf().
resolve_aliases(Conf, Mappings) ->
    lists:foldl(fun(Mapping, ConfAcc) ->
        case cuttlefish_mapping:aliases_with_messages(Mapping) of
            [] -> ConfAcc;
            Aliases -> resolve_aliases_for_single_mapping(Mapping, Aliases, ConfAcc)
        end
    end, Conf, Mappings).

resolve_aliases_for_single_mapping(Mapping, Aliases, Conf) ->
    Canonical = cuttlefish_mapping:variable(Mapping),
    CanonicalStr = cuttlefish_variable:format(Canonical),
    lists:foldl(fun({AliasVar, CustomMsg}, ConfAcc) ->
        %% Recheck on every iteration: a previous alias in this
        %% fold may have inserted the canonical key.
        CanonicalSet = lists:keymember(Canonical, 1, ConfAcc),
        case lists:keyfind(AliasVar, 1, ConfAcc) of
            false ->
                ConfAcc;
            {_, _} when CanonicalSet ->
                AliasStr = cuttlefish_variable:format(AliasVar),
                _ = ?LOG_WARNING("~ts", [format_alias_warning(
                    ignored, AliasStr, CanonicalStr, CustomMsg)]),
                lists:keydelete(AliasVar, 1, ConfAcc);
            {_, Value} ->
                AliasStr = cuttlefish_variable:format(AliasVar),
                _ = ?LOG_WARNING("~ts", [format_alias_warning(
                    use_canonical, AliasStr, CanonicalStr, CustomMsg)]),
                [{Canonical, Value} | lists:keydelete(AliasVar, 1, ConfAcc)]
        end
    end, Conf, Aliases).

format_alias_warning(Type, AliasStr, CanonicalStr, CustomMsg) ->
    Base = case Type of
        use_canonical ->
            io_lib:format("~ts was renamed to ~ts", [AliasStr, CanonicalStr]);
        ignored ->
            io_lib:format("~ts was renamed to ~ts, both are set, using ~ts",
                          [AliasStr, CanonicalStr, CanonicalStr])
    end,
    case CustomMsg of
        undefined -> Base;
        _         -> [Base, io_lib:format(" (~ts)", [CustomMsg])]
    end.

%% @doc adds default values from the schema when something's not
%% defined in the Conf, to give a complete app.config
add_defaults(Conf, Mappings) ->
    Prefixes = get_possible_values_for_fuzzy_matches(Conf, Mappings),

    lists:foldl(
      fun(MappingRecord, Acc) ->
              case cuttlefish_mapping:has_default(MappingRecord) of
                  false -> Acc;
                  true  -> add_default(Conf, Prefixes, MappingRecord, Acc)
              end
      end,
      Conf, Mappings).

add_default(Conf, Prefixes, MappingRecord, Acc) ->
    Default = cuttlefish_mapping:default(MappingRecord),
    VariableDef = cuttlefish_mapping:variable(MappingRecord),
    IsFuzzyMatch = cuttlefish_mapping:is_fuzzy_variable(MappingRecord),
    IsStrictMatch = lists:keymember(VariableDef, 1, Conf),

    %% No, then plug in the default
    case {IsStrictMatch, IsFuzzyMatch} of
        %% Strict match means we have the setting already
        {true, false} -> Acc;

        %% If IsStrictMatch =:= false, IsFuzzyMatch =:= true, we've got a setting, but
        %% it's part of a complex data structure.
        {false, true} ->
            add_fuzzy_default(Prefixes, Acc, Default, VariableDef);

        %% If Match =:= FuzzyMatch =:= false, use the default, key not set in .conf
        {false, false} -> [{VariableDef, Default}|Acc];

        %% If Match =:= true, do nothing, the value is set in the .conf file
        _ ->
            %% Both fuzzy and strict match should not happen
            _ = ?LOG_ERROR("Both fuzzy and strict match! should not happen"),
            [{error, {map_multiple_match, VariableDef}}|Acc]
    end.

is_strict_prefix([H|T1], [H|T2]) ->
    is_strict_prefix(T1, T2);
is_strict_prefix([], [H2|_]) when hd(H2) =:= $$ ->
    true;
is_strict_prefix(_, _) ->
    false.

add_fuzzy_default(Prefixes, Conf, Default, VariableDef) ->
    PotentialMatch = lists:dropwhile(fun({Prefix, _}) ->
                                             not is_strict_prefix(Prefix, VariableDef)
                                     end, Prefixes),
    case PotentialMatch of
        %% None of the prefixes match, so we don't generate a default.
        [] -> Conf;
        [{_Prefix, Substitutions}|_] ->
            %% This means that we found the key.
            %% ToAdd will be the list of all the things we're adding to the defaults.
            %% So, let's say you have the following mappings defined:
            %% namespace.$named_thing.a
            %% namespace.$named_thing.b
            %% namespace.$named_thing.c

            %% and in your conf, you defined the following:
            %% namespace.strong_bad.a = 10
            %% namespace.senor_cardgage.b = percent_sign
            %% namespace.trogdor.c = burninate

            %% Well, Prefixes would look like this:
            %% [{"namespace", ["strong_bad", "senor_cardgage", "trogdor"]}]

            %% The ToAdd list comp is going through and saying: ok, I know there are
            %% defaults for namespace.$named_thing.a, b, and c. And I know the possible
            %% values of $named_thing are strong_bad, senor_cardgage, and trogdor.
            %% so I want to ensure that there are values for the following:
            %%
            %% namespace.strong_bad.a
            %% namespace.strong_bad.b
            %% namespace.strong_bad.c
            %% namespace.senor_cardgage.a
            %% namespace.senor_cardgage.b
            %% namespace.senor_cardgage.c
            %% namespace.trogdor.a
            %% namespace.trogdor.b
            %% namespace.trogdor.c

            %% So, we go through the List of possible substitutions
            %% and apply the substitution to the variable. If it
            %% already exists in the Conf, then we skip it, otherwise
            %% we include the Default value.
            ToAdd = [ {VariableToAdd, Default}
                      || Subst <- Substitutions,
                       VariableToAdd <- [cuttlefish_variable:replace_match(VariableDef, Subst)],
                       not lists:keymember(VariableToAdd, 1, Conf)],
            Conf ++ ToAdd
    end.

%%%%%%%%%%%%%%%%%%%%%%%%
%% Prefixes is the thing we need for defaults of named keys
%% it looks like this:
%%
%% Prefixes: [{"riak_control.user",["user"]},
%%     {"listener.https",["internal"]},
%%     {"listener.protobuf",["internal"]},
%%     {"listener.http",["internal"]},
%%     {"multi_backend",
%%      ["bitcask_mult","leveldb_mult","leveldb_mult2","memory_mult"}]
%%%%%%%%%%%%%%%%%%%%%%%%
-spec get_possible_values_for_fuzzy_matches(cuttlefish_conf:conf(), [cuttlefish_mapping:mapping()]) -> [{string(), [string()]}].
get_possible_values_for_fuzzy_matches(Conf, Mappings) ->
    %% Now, get all the variables that could match, i.e. all the names
    %% it found referenced in the Conf proplist. It may look something
    %% like this: [{"n",["ck","ak","bk"]}]
    lists:foldl(
      fun(Mapping, FuzzyMatches) ->
              case cuttlefish_mapping:is_fuzzy_variable(Mapping) of
                  false -> FuzzyMatches; %% Strict match
                  true ->
                      %% Fuzzy match, extract the matching settings from the conf
                      VD = cuttlefish_mapping:variable(Mapping),
                      ListOfVars = [Var || {_, Var} <- cuttlefish_variable:fuzzy_matches(VD, Conf)],
                      {Prefix, _, _} = cuttlefish_variable:split_on_match(VD),
                      orddict:append_list(Prefix, ListOfVars, FuzzyMatches)
              end
      end,
      orddict:new(),
      Mappings).

-spec transform_datatypes(
        cuttlefish_conf:conf(),
        [cuttlefish_mapping:mapping()],
        [proplists:property()]
      ) -> {cuttlefish_conf:conf(), [cuttlefish_error:error()]}.
transform_datatypes(Conf, Mappings, ParsedArgs) ->
    lists:foldl(
        fun({Variable, Value}, {Acc, ErrorAcc}) ->
            AllowExtra = proplists:get_value(allow_extra, ParsedArgs, false),
            %% Look up mapping from schema
            case find_mapping(Variable, Mappings) of
                {error, _} when AllowExtra ->
                    %% user asked for us to tolerate variables
                    %% that are not present in the mapping so
                    %% do nothing here
                    {Acc, ErrorAcc};
                {error, _}  ->
                    %% So, this error message isn't so performant (s/o @argv0)
                    %% but it shouldn't happen too often, and I think it's important
                    %% to give users this feedback.

                    %% It will prevent anything from starting, and will let you know
                    %% that you're trying to set something that has no effect
                    VarName = cuttlefish_variable:format(Variable),
                    _ = ?LOG_ERROR("You've tried to set ~ts, but there is no setting with that name.", [VarName]),
                    _ = ?LOG_ERROR("  Did you mean one of these?"),

                    Possibilities = [ begin
                        MapVarName = cuttlefish_variable:format(cuttlefish_mapping:variable(M)),
                        {cuttlefish_util:levenshtein(VarName, MapVarName), MapVarName}
                    end || M <- Mappings],
                    Sorted = lists:sort(Possibilities),
                    _ = [ _ = ?LOG_ERROR("    ~ts", [T]) || {_, T} <- lists:sublist(Sorted, 3) ],
                    {Acc, [ {error, {unknown_variable, VarName}} | ErrorAcc ]};
                MappingRecord ->
                    DTs = cuttlefish_mapping:datatype(MappingRecord),

                    %% DTs is a list now, which means we'll receive an
                    %% errorlist, not a single error
                    case transform_type(DTs, Value) of
                        {ok, NewValue} ->
                            {[{Variable, NewValue}|Acc], ErrorAcc};
                        {errorlist, EList} ->
                            NewError = {transform_type,
                                        cuttlefish_variable:format(Variable)},
                            {Acc, [{error, NewError}] ++ EList ++ ErrorAcc}
                    end
            end
        end,
        {[], []},
        Conf).

-spec value_sub(cuttlefish_conf:conf()) -> {cuttlefish_conf:conf(), [cuttlefish_error:error()]}.
value_sub(Conf) ->
    lists:foldr(
        fun({Var, Val}, {Acc, ErrorAcc}) ->
            case value_sub(Var, Val, Conf) of
                {error, _E} = Error -> {Acc, [Error|ErrorAcc]};
                {NewVal, _NewConf} -> {[{Var, NewVal}|Acc], ErrorAcc}
            end
        end,
        {[],[]},
        Conf).

-spec value_sub(cuttlefish_variable:variable(),
                string(),
                cuttlefish_conf:conf()) ->
      {string(), cuttlefish_conf:conf()} | cuttlefish_error:error().
value_sub(Var, Value, Conf) ->
    value_sub(Var, Value, Conf, []).

-spec value_sub(cuttlefish_variable:variable(),
                string(),
                cuttlefish_conf:conf(),
                [string()]) ->
      {string(), cuttlefish_conf:conf()} | cuttlefish_error:error().
value_sub(Var, Value, Conf, History) when is_list(Value) ->
     %% Check if history contains duplicates. if so error
     case erlang:length(History) == sets:size(sets:from_list(History)) of
         false ->
             {error, {circular_rhs, History}};
         _ ->
             case head_sub(Value) of
                 none -> {Value, Conf};
                 {sub, NextVar, {SubFront, SubBack}} ->
                    case subbed_value(NextVar, Conf) of
                        undefined ->
                            {error, {substitution_missing_config,
                                     {cuttlefish_variable:format(Var),
                                      cuttlefish_variable:format(NextVar)}}};
                        SubVal ->
                            %% Do a sub-subsitution, in case the substituted
                            %% value contains substitutions itself. Do this as
                            %% its own separate recursion so that circular
                            %% substitutions can be detected.
                            case value_sub(NextVar, SubVal, Conf, [Var|History]) of
                                {error, _} = Error ->
                                    Error;
                                {NewSubVal, NewConf} ->
                                    NewValue = SubFront ++ NewSubVal ++ SubBack,
                                    value_sub(Var, NewValue, NewConf, History)
                            end
                    end
             end
     end;
value_sub(_Var, Value, Conf, _History) ->
    {Value, Conf}.

-spec subbed_value(Var :: cuttlefish_variable:variable(),
                   Conf :: cuttlefish_conf:conf()) -> undefined | string().
subbed_value(Var, Conf) ->
    case proplists:get_value(Var, Conf) of
        undefined ->
            % we couldn't find the var in the conf file, let's
            % look at the environment and check for it
            case os:getenv(cuttlefish_variable:format(Var)) of
                false ->
                    undefined;
                Value ->
                    Value
            end;
        Value ->
            Value
    end.

-spec head_sub(string()) -> none | {sub, cuttlefish_variable:variable(), {string(), string()}}.
head_sub(Value) ->
    L = string:str(Value, ?LSUB),
    case L > 0 of
        false ->
            none;
        _ ->
            R = string:str(string:substr(Value, L + ?RSUBLEN), ?RSUB) + L,
            case L < R of
                false ->
                    none;
                _ ->
                    Var = cuttlefish_variable:tokenize(string:strip(string:substr(Value, L+?LSUBLEN, R-L-?LSUBLEN))),
                    Front = string:substr(Value, 1, L-1),
                    Back =  string:substr(Value, R+?RSUBLEN),
                    {sub, Var, {Front, Back}}
            end
    end.

%% If transform_type takes a list as first argument, foldm_either will
%% give us back an errorlist for a single error
-spec transform_type(cuttlefish_datatypes:datatype_list() | cuttlefish_datatypes:datatype(), term()) ->
                            {ok, term()} | cuttlefish_error:error() | cuttlefish_error:errorlist().
transform_type(DTs, Value) when is_list(DTs) ->
    foldm_either(fun(DT) -> transform_type(DT, Value) end, DTs);

transform_type(DT, Value) ->
    Supported = cuttlefish_datatypes:is_supported(DT),
    Extended = cuttlefish_datatypes:is_extended(DT),
    if
        Supported -> transform_supported_type(DT, Value);
        Extended  -> transform_extended_type(DT, Value);
        true ->
            {error, {unsupported_type, DT}}
    end.

-spec transform_supported_type(cuttlefish_datatypes:datatype(), any()) ->
                                     {ok, term()} | cuttlefish_error:error().
transform_supported_type(DT, Value) ->
    try cuttlefish_datatypes:from_string(Value, DT) of
        {error, Message} -> {error, Message};
        NewValue -> {ok, NewValue}
    catch
        Class:Error:_Stacktrace ->
            %% io:format("Failed to transform a type. Stacktrace: ~p~n", [Stacktrace]),
            {error, {transform_type_exception, {DT, {Class, Error}}}}
    end.

-spec transform_extended_type(cuttlefish_datatypes:extended(), any()) ->
                                     {ok, term()} | cuttlefish_error:error().
transform_extended_type({DT, AcceptableValue}, Value) ->
    case transform_supported_type(DT, Value) of
        {ok, AcceptableValue} ->
            {ok, AcceptableValue};
        {ok, _NewValue} ->
            {error, {transform_type_unacceptable, {Value, AcceptableValue}}};
        {error, Term} ->
            {error, Term}
    end.
%% Ok, this is tricky
%% There are three scenarios we have to deal with:
%% 1. The mapping is there! -> return mapping
%% 2. The mapping is not there -> error
%% 3. The mapping is there, but the key in the schema contains a $.
%%      (fuzzy match)
find_mapping([H|_]=Variable, Mappings) when is_list(H) ->
    {HardMappings, FuzzyMappings} =  lists:foldl(
        fun(Mapping, {HM, FM}) ->
            VariableDef = cuttlefish_mapping:variable(Mapping),
            case {Variable =:= VariableDef, cuttlefish_variable:is_fuzzy_match(Variable, VariableDef)} of
                {true, _} -> {[Mapping|HM], FM};
                {_, true} -> {HM, [Mapping|FM]};
                _ -> {HM, FM}
            end
        end,
        {[], []},
        Mappings),

    %% The input to this function is massaged enough that you'll never see a hard mapping count > 1
    %% You might see more than one fuzzy match, there's really nothing to stop that.
    FVariable = cuttlefish_variable:format(Variable),
    case {length(HardMappings), length(FuzzyMappings)} of
        {1, _} -> hd(HardMappings);
        {0, 1} -> hd(FuzzyMappings);
        {0, 0} -> {error, {mapping_not_found, FVariable}};
        {X, Y} -> {error, {mapping_multiple, {FVariable, {X, Y}}}}
    end;
find_mapping(Variable, Mappings) ->
    find_mapping(cuttlefish_variable:tokenize(Variable), Mappings).

-spec run_validations(cuttlefish_schema:schema(), cuttlefish_conf:conf())
    -> boolean()|list(cuttlefish_error:error()).
run_validations({_, Mappings, Validators}, Conf) ->
    Validations = lists:flatten([ begin
        Vs = cuttlefish_mapping:validators(M, Validators),
        Value = proplists:get_value(cuttlefish_mapping:variable(M), Conf),
        [ begin
            Validator = cuttlefish_validator:func(V),
            case {Value, Validator(Value)} of
                {undefined, _} -> true;
                {_, true} ->
                    true;
                _ ->
                    Error = {validation, { cuttlefish_variable:format(
                                             cuttlefish_mapping:variable(M)),
                                           cuttlefish_validator:description(V)
                                         }},
                    _ = ?LOG_ERROR(cuttlefish_error:xlate(Error)),
                    {error, Error}
            end
        end || V <- Vs]

     end || M <- Mappings,
            cuttlefish_mapping:validators(M) =/= [],
            cuttlefish_mapping:default(M) =/= undefined orelse proplists:is_defined(cuttlefish_mapping:variable(M), Conf)
            ]),
    case lists:all(fun(X) -> X =:= true end, Validations) of
        true -> true;
        _ -> Validations
    end.


%% @doc Calls Fun on each element of the list until it returns {ok,
%% term()}, otherwise accumulates {error, term()} into a list,
%% wrapping in {error, _} at the end.
-spec foldm_either(fun((term()) ->
                           {ok, term()}  | cuttlefish_error:errorlist()),
                   list()) ->
                      {ok, term()}  | cuttlefish_error:errorlist().
foldm_either(Fun, List) ->
    foldm_either(Fun, List, []).

%% @doc Calls Fun on each element of the list until it returns {ok,
%% term()}, otherwise accumulates {error, term()} into a list,
%% wrapping in {errorlist, _} at the end.
-spec foldm_either(fun((term()) ->
                           {ok, term()}  | cuttlefish_error:error()),
                   list(), list()) ->
                      {ok, term()}  | cuttlefish_error:errorlist().
foldm_either(_Fun, [], Acc) -> {errorlist, lists:reverse(Acc)};
foldm_either(Fun, [H|T], Acc) ->
    case Fun(H) of
        {ok, Result} -> {ok, Result};
        {error, _}=Error ->
            foldm_either(Fun, T, [Error|Acc])
    end.

%% @doc Validates the configuration against the schema by running all pipeline
%% phases and collecting errors from each, rather than stopping at the first
%% failing phase. Does not write any files.
-spec validate(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
    ok | {error, cuttlefish_error:errorlist()}.
validate(Schema, Config) ->
    validate(Schema, Config, []).

-spec validate(cuttlefish_schema:schema(), cuttlefish_conf:conf(), [proplists:property()]) ->
    ok | {error, cuttlefish_error:errorlist()}.
validate({_, Mappings, _} = Schema, Config, ParsedArgs) ->
    AliasResolvedConfig = resolve_aliases(Config, Mappings),
    DConfig = add_defaults(AliasResolvedConfig, Mappings),
    %% Separate errors injected by add_defaults from valid config entries so
    %% subsequent phases can still run.
    {DefaultErrors, CleanDConfig} = lists:partition(fun cuttlefish_error:is_error/1, DConfig),
    {SubbedConfig, SubErrors0} = value_sub(CleanDConfig),
    SubErrors = enrich_alias_sub_errors(SubErrors0, Mappings),
    %% transform_datatypes already returns {GoodConf, Errors}; in map/3 the
    %% error path discards GoodConf, but here we use both.
    {TypedConf, TypeErrors} = transform_datatypes(SubbedConfig, Mappings, ParsedArgs),
    ValidationErrors = case cuttlefish_error:errorlist_maybe(run_validations(Schema, TypedConf)) of
        {errorlist, EList} -> EList;
        _ -> []
    end,
    TranslationErrors = collect_translation_errors(Schema, TypedConf),
    AllErrors = DefaultErrors ++ SubErrors ++ TypeErrors ++ ValidationErrors ++ TranslationErrors,
    case AllErrors of
        [] -> ok;
        _  -> {error, {errorlist, AllErrors}}
    end.

%% @doc Runs apply_mappings and apply_translations to collect translation
%% errors without caring about the resulting config.
%% Translation errors have no other logging path, so each is logged here.
-spec collect_translation_errors(cuttlefish_schema:schema(), cuttlefish_conf:conf()) ->
    [cuttlefish_error:error()].
collect_translation_errors(Schema, TypedConf) ->
    {DirectMappings, TranslationsToDrop} = apply_mappings(Schema, TypedConf),
    case apply_translations(Schema, TypedConf, DirectMappings, TranslationsToDrop) of
        {error, _, {errorlist, Errors}} ->
            _ = [?LOG_ERROR(cuttlefish_error:xlate(E)) || E <- Errors],
            Errors;
        _ -> []
    end.

-ifdef(TEST).

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

bad_conf_test() ->
    Conf = [
        {["integer_thing"], "thirty_two"},
        {["enum_thing"], bad_enum_value},
        {["ip_thing"], "not an IP address"}
    ],

    Mappings = [
        cuttlefish_mapping:parse({mapping, "integer_thing", "to.int", [
            {datatype, integer}
        ]}),
        cuttlefish_mapping:parse({mapping, "enum_thing", "to.enum", [
            {datatype, {enum, [on, off]}}
        ]}),
        cuttlefish_mapping:parse({mapping, "ip_thing", "to.ip", [
            {datatype, ip}
        ]})
    ],

    Translations = [
        cuttlefish_translation:parse({translation, "to.enum", fun(_ConfConf) -> whatev end})
    ],

    NewConfig = map({Translations, Mappings, []}, Conf),
    io:format("NewConf: ~tp~n", [NewConfig]),

    ?assertMatch({error, transform_datatypes, _}, NewConfig),
    ok.

add_defaults_test() ->
    Conf = [
        %%{["a","b","c"], "override"}, %% Specifically left out. Uncomment line to break test,
        {["a","c","d"], "override"},
        {["no","match"], "unchanged"},
        %%{"m.rk.x", "defined"}, %% since this is undefined no defaults should be created for "m",

        %% two matches on a name "ak" and "bk"
        {["n","ak","x"], "set_n_name_x"},
        {["n","bk","x"], "set_n_name_x2"},
        {["n","ck","y"], "set_n_name_y3"}
    ],

    Mappings = [
        %% First mapping, direct, not in .conf, will be default
        cuttlefish_mapping:parse({mapping, "a.b.c", "b.c", [
                {default, "q"}
            ]}),
        %% default is "l", but since "a.c.d" is in Conf, it will be "override"
        cuttlefish_mapping:parse({mapping, "a.c.d", "c.d", [
                {default, "l"}
            ]}),
        cuttlefish_mapping:parse({mapping, "m.$name.x", "some.proplist", [
                {default, "m_name_x"}
            ]}),
        cuttlefish_mapping:parse({mapping, "n.$name.x", "some.proplist", [
            {default, "n_name_x"}
        ]}),
        cuttlefish_mapping:parse({mapping, "n.$name.y", "some.proplist", [
            {default, "n_name_y"}
        ]}),
        cuttlefish_mapping:parse({mapping, "o.$name.z", "some.proplist", [
            {default, "o_name_z"},
            {include_default, "blue"}
        ]})
    ],

    DConf = add_defaults(Conf, Mappings),
    io:format("DConf: ~tp~n", [DConf]),
    ?assertEqual(9, length(DConf)),
    ?assertEqual("q",               proplists:get_value(["a","b","c"], DConf)),
    ?assertNotEqual("l",            proplists:get_value(["a","c","d"], DConf)),
    ?assertEqual("override",        proplists:get_value(["a","c","d"], DConf)),
    ?assertEqual("unchanged",       proplists:get_value(["no","match"], DConf)),
    ?assertEqual("set_n_name_x",    proplists:get_value(["n","ak","x"], DConf)),
    ?assertEqual("set_n_name_x2",   proplists:get_value(["n","bk","x"], DConf)),
    ?assertEqual("n_name_x",        proplists:get_value(["n","ck","x"], DConf)),
    ?assertEqual("n_name_y",        proplists:get_value(["n","ak","y"], DConf)),
    ?assertEqual("n_name_y",        proplists:get_value(["n","bk","y"], DConf)),
    ?assertEqual("set_n_name_y3",   proplists:get_value(["n","ck","y"], DConf)),
    ?assertEqual(undefined,         proplists:get_value(["o","blue","z"], DConf)),
    ok.

map_test() ->
    Schema = cuttlefish_schema:file("test/riak.schema"),

    Conf = conf_parse:file("test/riak.conf"),

    NewConfig = map(Schema, Conf),

    NewRingSize = proplists:get_value(ring_creation_size, proplists:get_value(riak_core, NewConfig)),
    ?assertEqual(32, NewRingSize),

    NewAAE = proplists:get_value(anti_entropy, proplists:get_value(riak_kv, NewConfig)),
    ?assertEqual({on,[debug]}, NewAAE),

    NewSASL = proplists:get_value(sasl_error_logger, proplists:get_value(sasl, NewConfig)),
    ?assertEqual(false, NewSASL),

    NewHTTP = proplists:get_value(http, proplists:get_value(riak_core, NewConfig)),
    ?assertEqual([{"10.0.0.1", 80}, {"127.0.0.1", 8098}], NewHTTP),

    NewPB = proplists:get_value(pb, proplists:get_value(riak_api, NewConfig)),
    ?assertEqual([], NewPB),

    NewHTTPS = proplists:get_value(https, proplists:get_value(riak_core, NewConfig)),
    ?assertEqual(undefined, NewHTTPS),
    ok.

minimal_map_test() ->
    Schema = cuttlefish_schema:file("test/riak.schema"),
    Conf = [{["ring_size"], "32"},
            {["anti_entropy"], "debug"}],
    NewConfig = minimal_map(Schema, Conf),
    ?assertEqual([{riak_core, [{ring_creation_size, 32}]},{riak_kv,[{anti_entropy, {on, [debug]}}]}],
                 lists:sort(NewConfig)).


apply_mappings_test() ->
    %% Two mappings, both alike in dignity,
    %% In fair unit test, where we lay our scene,
    %% From ancient failure break to new mutiny,
    %% Where civil overrides makes civil priority unclean.
    %% From forth the fatal loins of these two foes
    %% A pair of star-cross'd mappings write one app var;
    %% Whose misadventured piteous overthrows
    %% Do with their merge behave unexpectedly.

    %% Assume add_defaults has already run
    Conf = [
        {["conf", "key1"], "1"},
        {["conf", "key2"], "2"}
    ],
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key1",
            "erlang.key",
            [
                {default, "1"}
            ]
        }),
        cuttlefish_mapping:parse({
            mapping,
            "conf.key2",
            "erlang.key",
            [
                {default, "2"}
            ]
        })
    ],

    {DirectMappings, []} = apply_mappings({[], Mappings, []}, Conf),
    cuttlefish_unit:assert_config(DirectMappings, "erlang.key", "1"),
    ok.

find_mapping_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "variable.with.fixed.name", "", [{ default, 0}]}),
        cuttlefish_mapping:parse({mapping, "variable.with.$matched.name", "",  [{ default, 1}]})
    ],
    io:format("Mappings: ~tp~n", [Mappings]),

    ?assertEqual(
        ["variable","with","fixed","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","fixed","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","fixed","name"],
        cuttlefish_mapping:variable(find_mapping("variable.with.fixed.name", Mappings))
        ),

    ?assertEqual(
        0,
        cuttlefish_mapping:default(find_mapping(["variable","with","fixed","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","$matched","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","A","name"], Mappings))
        ),

    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","A","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","$matched","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","B","name"], Mappings))
        ),

    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","B","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","$matched","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","C","name"], Mappings))
        ),

    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","C","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","$matched","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","D","name"], Mappings))
        ),

    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","D","name"], Mappings))
        ),

    ?assertEqual(
        ["variable","with","$matched","name"],
        cuttlefish_mapping:variable(find_mapping(["variable","with","E","name"], Mappings))
        ),

    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","E","name"], Mappings))
        ),

    %% Test variable name with dot
    ?assertEqual(
       "variable.with.E.F.name not_found",
       ?XLATE(find_mapping(["variable","with","E","F","name"], Mappings))
        ),
    %% Test variable name with escaped dot
    ?assertEqual(
        1,
        cuttlefish_mapping:default(find_mapping(["variable","with","E.F","name"], Mappings))
        ),
    ok.

multiple_hard_match_test() ->
    %% In real life this should never happen, but if it does, I'd love a log message
    Mappings = [
        cuttlefish_mapping:parse({mapping, "variable.with.fixed.name", "", [{ default, 0}]}),
        cuttlefish_mapping:parse({mapping, "variable.with.fixed.name", "",  [{ default, 1}]})
    ],
    ?assertEqual(
       "2 hard mappings and 0 fuzzy mappings found for variable.with.fixed.name",
       ?XLATE(find_mapping(["variable","with","fixed","name"], Mappings))
        ),
    ok.

apply_mappings_translations_dropped_correctly_test() ->
    Fun = fun(X) -> X end,
    ?assertEqual(1, Fun(1)), %% coverage kludge

    Translations = [
        cuttlefish_translation:parse({
            translation,
            "mapping.name",
            Fun
            })
    ],
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key",
            "mapping.name",
            [{default, 6}]
            })
    ],
    %% So, we have a translation for the corresponding mapping, but that mapping has no default
    {_DirectMappings, TranslationsToDrop} = apply_mappings({Translations, Mappings, []}, []),
    ?assertEqual([], TranslationsToDrop),
    ok.

apply_mappings_translations_dropped_correctly_mixed_test() ->
    Fun = fun(X) -> X end,
    ?assertEqual(1, Fun(1)), %% coverage kludge

    Translations = [
        cuttlefish_translation:parse({
            translation,
            "mapping.name",
            Fun
            })
    ],
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key",
            "mapping.name",
            [{default, 6}]
            }),
        cuttlefish_mapping:parse({
            mapping,
            "conf.key2",
            "mapping.name",
            []
            })
    ],
    %% One valid mapping, and one that should be dropped (no default)
    {_DirectMappings, TranslationsToDrop} = apply_mappings({Translations, Mappings, []}, []),
    ?assertEqual([], TranslationsToDrop),
    ok.
apply_mappings_translations_dropped_correctly_mixed2_test() ->
    Fun = fun(X) -> X end,
    ?assertEqual(1, Fun(1)), %% coverage kludge

    Translations = [
        cuttlefish_translation:parse({
            translation,
            "mapping.name",
            Fun
            })
    ],
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key",
            "mapping.name",
            [{default, 6}]
            }),
        cuttlefish_mapping:parse({
            mapping,
            "conf.key2",
            "mapping.name",
            []
            }),
        cuttlefish_mapping:parse({
            mapping,
            "conf.key3",
            "mapping.name",
            []
            })
    ],
    %% One valid mapping and two that should be dropped (no default)
    {_DirectMappings, TranslationsToDrop} = apply_mappings({Translations, Mappings, []}, []),
    ?assertEqual([], TranslationsToDrop),
    ok.

transform_datatypes_not_found_test() ->
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key",
            "erlang.key",
            []
            })
    ],

    Conf = [
        {["conf", "other"], "string"}
    ],
    NewConf = transform_datatypes(Conf, Mappings, []),
    ?assertEqual({[], [{error, {unknown_variable, "conf.other"}}]}, NewConf),
    ok.

transform_datatypes_allowed_not_found_test() ->
    Mappings = [
        cuttlefish_mapping:parse({
            mapping,
            "conf.key",
            "erlang.key",
            []
            })
    ],

    Conf = [
        {["conf", "other"], "string"}
    ],
    NewConf = transform_datatypes(Conf, Mappings, [{allow_extra, true}]),
    ?assertEqual({[], []}, NewConf),
    ok.

validation_test() ->

    Pid = self(),

    Mappings = [cuttlefish_mapping:parse(
        {mapping, "a", "b.c", [{validators, ["a"]}, {datatype, {enum, [true, false]}}]}
        )
    ],

    Validators = [cuttlefish_validator:parse(
        {validator, "a", "error msg", fun(X) -> Pid ! X, true end}
        )
    ],

    Conf = [
        {["a"], true}
    ],

    AppConf = map({[], Mappings, Validators}, Conf),

    receive
        X ->
            ?assert(X)
    after
        1000 ->
            ?assert(false)
    end,

    ?assertEqual([{b, [{c, true}]}], AppConf),
    ok.

throw_unset_test() ->
    Mappings = [cuttlefish_mapping:parse({mapping, "a", "b.c", []})],
    Translations = [cuttlefish_translation:parse(
        {translation, "b.c",
        fun(_X) -> cuttlefish:unset() end})
    ],
    AppConf = map({Translations, Mappings, []}, []),
    ?assertEqual([], AppConf),
    ok.

bad_prefix_match_test() ->
    Prefixes = [
        {["prefix", "one"], ["one", "two"]},
        {["prefix", "two"], ["one", "two"]}
    ],
    Conf = [],
    Default = 8,
    VariableDef = ["prefix", "one", "other_thing", "$name"],

    ?assertEqual([], add_fuzzy_default(Prefixes, Conf, Default, VariableDef)),
    ok.

assert_extended_datatype(
  Datatype,
  Setting,
  Expected) ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "a.b", "e.k", [
            {datatype, Datatype}
        ]})
    ],
    Conf = [
        {["a","b"], Setting}
    ],

    Actual = map({[], Mappings, []}, Conf),

    case Expected of
        {error, Phase, EMsg} ->
            ?assertMatch({error, Phase, _}, Actual),
            ?assertEqual(EMsg, ?XLATE(hd(element(2, element(3, Actual)))));
        _ ->
            ?assertEqual([{e, [{k, Expected}]}], map({[], Mappings, []}, Conf))
    end,
    ok.

extended_datatypes_test() ->
    assert_extended_datatype([integer, {atom, never}], "1", 1),
    assert_extended_datatype([integer, {atom, never}], "never", never),
    assert_extended_datatype([integer, {atom, never}], "always", {error, transform_datatypes, "Error transforming datatype for: a.b"}),
    assert_extended_datatype([{duration, s}, {atom, never}], "never", never),
    assert_extended_datatype([{atom, never}, integer], "1", 1),
    assert_extended_datatype([{enum, [never, always]}, {duration, s}], "1s", 1),
    assert_extended_datatype([{atom, never}, {atom, always}], "foo", {error, transform_datatypes, "Error transforming datatype for: a.b"}),
    ok.

not_found_test() ->
    Mappings = [cuttlefish_mapping:parse({mapping, "a", "b.c", []})],
    Translations = [cuttlefish_translation:parse(
        {translation, "b.c",
        fun(Conf) -> cuttlefish:conf_get("d", Conf) end})
    ],
    AppConf = map({Translations, Mappings, []}, [{["a"], "foo"}]),
    ?assertEqual({error, apply_translations,
                  {errorlist,
                   [{error, {translation_missing_setting,
                             {"b.c", "d"}}}]}},
                 AppConf).

invalid_test() ->
    Mappings = [cuttlefish_mapping:parse({mapping, "a", "b.c", []})],
    Translations = [cuttlefish_translation:parse(
        {translation, "b.c",
        fun(_Conf) -> cuttlefish:invalid("review all files") end})
    ],
    AppConf = map({Translations, Mappings, []}, [{["a"], "foo"}]),
    ?assertEqual({error, apply_translations,
                  {errorlist,
                   [{error, {translation_invalid_configuration,
                             {"b.c", "review all files"}}}]}},
                 AppConf).

value_env_sub_test() ->
    os:putenv("ENV_TEST_VAL", "/a/b"),
    Conf = [
            {["a","b","c"], "$(ENV_TEST_VAL)/c"},
            {["a","b"], "/b/a"}
           ],
    {NewConf, Errors} = value_sub(Conf),
    os:unsetenv("ENV_TEST_VAL"),
    ?assertEqual([], Errors),
    ABC = proplists:get_value(["a","b","c"], NewConf),
    ?assertEqual("/a/b/c", ABC),
    ok.

value_sub_test() ->
    Conf = [
            {["a","b","c"], "$(a.b)/c"},
            {["a","b"], "/a/b"}
           ],
    {NewConf, Errors} = value_sub(Conf),
    ?assertEqual([], Errors),
    ABC = proplists:get_value(["a","b","c"], NewConf),
    ?assertEqual("/a/b/c", ABC),
    ok.

value_sub_infinite_loop_test() ->
    Conf = [
            {["a"], "$(c)/d"},
            {["b"], "$(a)/d"},
            {["c"], "$(b)/d"}
           ],
    {_NewConf, Errors} = value_sub(Conf),
    ?assertEqual(
       "Circular RHS substitutions: [[\"a\"],[\"b\"],[\"c\"],[\"a\"]]",
       ?XLATE(hd(Errors))
      ),
    ?assertEqual(
       "Circular RHS substitutions: [[\"b\"],[\"c\"],[\"a\"],[\"b\"]]",
       ?XLATE(hd(tl(Errors)))
      ),
    ?assertEqual(
       "Circular RHS substitutions: [[\"c\"],[\"a\"],[\"b\"],[\"c\"]]",
       ?XLATE(hd(tl(tl(Errors))))
      ),
    ok.

value_sub_not_found_test() ->
    Conf = [
            {["a"], "$(b)/c"}
           ],
    {_NewConf, Errors} = value_sub(Conf),
    ?assertEqual(
       "'a' substitution requires a config variable 'b' to be set",
       ?XLATE(hd(Errors))
      ),
    ok.

value_sub_whitespace_test() ->
    Conf = [
            {["a", "b", "c"], "/tyktorp"},
            {["a"], "$(a.b.c)/svagen"},
            {["b"], "$(  a.b.c)/svagen"},
            {["c"], "$(a.b.c  )/svagen"},
            {["d"], "$(  a.b.c )/svagen"}
           ],
    {NewConf, []} = value_sub(Conf),
    ?assertEqual("/tyktorp/svagen", proplists:get_value(["a"], NewConf)),
    ?assertEqual("/tyktorp/svagen", proplists:get_value(["b"], NewConf)),
    ?assertEqual("/tyktorp/svagen", proplists:get_value(["c"], NewConf)),
    ?assertEqual("/tyktorp/svagen", proplists:get_value(["d"], NewConf)),
    ok.

value_sub_multiple_sub_test() ->
    Conf = [
            {["a"], "/a"},
            {["b"], "/b"},
            {["c"], "$(a)$(b)"}
           ],
    {NewConf, []} = value_sub(Conf),
    ?assertEqual("/a/b", proplists:get_value(["c"], NewConf)),
    ok.

value_sub_error_in_second_sub_test() ->
    Conf = [
            {["a"], "$(b)/$(c)"},
            {["b"], "/b"},
            {["c"], "$(a)/c"}
           ],
    {_NewConf, Errors} = value_sub(Conf),
    ?assertEqual(
       "Circular RHS substitutions: [[\"a\"],[\"c\"],[\"a\"]]",
       ?XLATE(hd(Errors))
      ),
    ?assertEqual(
       "Circular RHS substitutions: [[\"c\"],[\"a\"],[\"c\"]]",
       ?XLATE(hd(tl(Errors)))
      ),
    ok.

value_sub_false_circle_test() ->
    Conf = [
            {["a"], "$(c)/$(c)"},
            {["c"], "C"}
           ],
    {NewConf, Errors} = value_sub(Conf),
    ?assertEqual([], Errors),
    ?assertEqual("C/C", proplists:get_value(["a"], NewConf)),
    ok.

value_sub_paren_test() ->
    Conf = [
            {["a"], "$(c)/$(c)"},
            {["c"], "C)"}
           ],
    {NewConf, Errors} = value_sub(Conf),
    ?assertEqual([], Errors),
    ?assertEqual("C)/C)", proplists:get_value(["a"], NewConf)),
    ok.

%% Alias resolution tests

resolve_alias_only_alias_set_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    Conf = [{["old", "key"], "42"}],
    Resolved = resolve_aliases(Conf, [Mapping]),
    ?assertEqual([{["new", "key"], "42"}], Resolved).

resolve_alias_canonical_wins_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    Conf = [{["new", "key"], "100"}, {["old", "key"], "42"}],
    Resolved = resolve_aliases(Conf, [Mapping]),
    ?assertEqual([{["new", "key"], "100"}], Resolved).

resolve_alias_neither_set_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {default, 7}, {aliases, ["old.key"]}
    ]}),
    ?assertEqual([], resolve_aliases([], [Mapping])).

resolve_alias_multiple_first_wins_test() ->
    %% Two aliases set, canonical not set. First alias wins,
    %% second sees canonical now exists and is dropped.
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key", "oldest.key"]}
    ]}),
    Conf = [{["old", "key"], "1"}, {["oldest", "key"], "2"}],
    Resolved = resolve_aliases(Conf, [Mapping]),
    ?assertEqual([{["new", "key"], "1"}], Resolved).

resolve_alias_empty_mappings_test() ->
    Conf = [{["a", "b"], "val"}],
    ?assertEqual(Conf, resolve_aliases(Conf, [])).

resolve_alias_no_aliases_passthrough_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "a.b", "app.x", [
        {datatype, string}
    ]}),
    Conf = [{["a", "b"], "val"}, {["c", "d"], "other"}],
    ?assertEqual(Conf, resolve_aliases(Conf, [Mapping])).

resolve_alias_preserves_unrelated_keys_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    Conf = [{["old", "key"], "42"}, {["unrelated", "key"], "hello"}],
    Resolved = resolve_aliases(Conf, [Mapping]),
    ?assertEqual("hello", proplists:get_value(["unrelated", "key"], Resolved)),
    ?assertEqual("42", proplists:get_value(["new", "key"], Resolved)),
    ?assertEqual(2, length(Resolved)).

resolve_alias_multiple_independent_mappings_test() ->
    %% Two mappings, each with their own alias. No interaction.
    M1 = cuttlefish_mapping:parse({mapping, "new.a", "app.x", [
        {datatype, integer}, {aliases, ["old.a"]}
    ]}),
    M2 = cuttlefish_mapping:parse({mapping, "new.b", "app.y", [
        {datatype, string}, {aliases, ["old.b"]}
    ]}),
    Conf = [{["old", "a"], "1"}, {["old", "b"], "hello"}],
    Resolved = resolve_aliases(Conf, [M1, M2]),
    ?assertEqual("1", proplists:get_value(["new", "a"], Resolved)),
    ?assertEqual("hello", proplists:get_value(["new", "b"], Resolved)),
    ?assertEqual(2, length(Resolved)).

resolve_alias_warns_on_deprecated_key_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    _ = resolve_aliases([{["old", "key"], "42"}], [Mapping]),
    [Log] = cuttlefish_test_logging:get_logs(),
    ?assertMatch({match, _},
        re:run(Log, "old\\.key was renamed to new\\.key")).

resolve_alias_warns_on_ignored_key_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    _ = resolve_aliases([{["new", "key"], "1"}, {["old", "key"], "2"}], [Mapping]),
    [Log] = cuttlefish_test_logging:get_logs(),
    ?assertMatch({match, _},
        re:run(Log, "old\\.key was renamed to new\\.key, both are set, using new\\.key")).

resolve_alias_multiple_all_warn_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key", "oldest.key"]}
    ]}),
    _ = resolve_aliases([{["old", "key"], "1"}, {["oldest", "key"], "2"}], [Mapping]),
    Logs = cuttlefish_test_logging:get_logs(),
    ?assertEqual(2, length(Logs)).

%% End-to-end tests through map/2

alias_end_to_end_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 10},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
    ]),
    Conf = [{["old", "key"], "42"}],
    Result = map(Schema, Conf),
    ?assertEqual(42, proplists:get_value(setting, proplists:get_value(app, Result))).

alias_with_translation_test() ->
    %% Translation sees the canonical key, never the alias.
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.a\", \"app.combined\", [\n"
        "  {datatype, integer},\n"
        "  {aliases, [\"old.a\"]}\n"
        "]}.\n"
        "{mapping, \"new.b\", \"app.combined\", [\n"
        "  {datatype, integer}\n"
        "]}.\n"
        "{translation, \"app.combined\", fun(Conf) ->\n"
        "    A = cuttlefish:conf_get(\"new.a\", Conf),\n"
        "    B = cuttlefish:conf_get(\"new.b\", Conf),\n"
        "    {A, B}\n"
        "end}.\n"
    ]),
    Conf = [{["old", "a"], "1"}, {["new", "b"], "2"}],
    Result = map(Schema, Conf),
    ?assertEqual({1, 2}, proplists:get_value(combined, proplists:get_value(app, Result))).

alias_default_applies_when_neither_set_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 99},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
    ]),
    Result = map(Schema, []),
    ?assertEqual(99, proplists:get_value(setting, proplists:get_value(app, Result))).

alias_validation_runs_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {aliases, [\"old.key\"]},\n"
        "  {validators, [\"positive\"]}\n"
        "]}.\n"
        "{validator, \"positive\", \"must be positive\",\n"
        "  fun(X) -> X > 0 end}.\n"
    ]),
    Conf = [{["old", "key"], "-5"}],
    Result = map(Schema, Conf),
    ?assertMatch({error, validation, _}, Result).

minimal_map_resolves_alias_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 10},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
    ]),
    Conf = [{["old", "key"], "42"}],
    Result = minimal_map(Schema, Conf),
    ?assertEqual(42, proplists:get_value(setting, proplists:get_value(app, Result))).

minimal_map_excludes_mapping_without_alias_or_canonical_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 10},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
    ]),
    %% Neither canonical nor alias in config: mapping should be excluded
    Result = minimal_map(Schema, []),
    ?assertEqual([], Result).

%% RHS substitutions reference the canonical key, not the alias.
%% $(old.key) won't resolve after alias rewriting: use $(new.key).
rhs_substitution_uses_canonical_not_alias_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.a\", [\n"
        "  {datatype, string},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
        "{mapping, \"other\", \"app.b\", [\n"
        "  {datatype, string}\n"
        "]}.\n"
    ]),
    %% $(new.key) works after alias resolution
    Conf = [{["old", "key"], "hello"}, {["other"], "$(new.key)/world"}],
    Result = map(Schema, Conf),
    ?assertEqual("hello/world", proplists:get_value(b, proplists:get_value(app, Result))).

rhs_substitution_of_alias_key_fails_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.a\", [\n"
        "  {datatype, string},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
        "{mapping, \"other\", \"app.b\", [\n"
        "  {datatype, string}\n"
        "]}.\n"
    ]),
    %% $(old.key) fails because the alias has been rewritten
    Conf = [{["old", "key"], "hello"}, {["other"], "$(old.key)/world"}],
    Result = map(Schema, Conf),
    ?assertMatch({error, rhs_subs, _}, Result).

rhs_substitution_of_alias_key_error_message_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.a\", [\n"
        "  {datatype, string},\n"
        "  {aliases, [\"old.key\"]}\n"
        "]}.\n"
        "{mapping, \"other\", \"app.b\", [\n"
        "  {datatype, string}\n"
        "]}.\n"
    ]),
    Conf = [{["old", "key"], "hello"}, {["other"], "$(old.key)/world"}],
    {error, rhs_subs, {errorlist, [{error, {substitution_alias_key, {_, MissingVar, Canonical}}}]}} =
        map(Schema, Conf),
    ?assertMatch({match, _}, re:run(cuttlefish_error:xlate({substitution_alias_key, {"other", MissingVar, Canonical}}),
                                    "alias")).

%% Alias message tests

resolve_alias_custom_msg_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer},
        {aliases, [{"old.key", "renamed in 4.0"}]}
    ]}),
    Resolved = resolve_aliases([{["old", "key"], "42"}], [Mapping]),
    ?assertEqual([{["new", "key"], "42"}], Resolved),
    [Log] = cuttlefish_test_logging:get_logs(),
    ?assertMatch({match, _}, re:run(Log, "old\\.key was renamed to new\\.key")),
    ?assertMatch({match, _}, re:run(Log, "\\(renamed in 4\\.0\\)")).

resolve_alias_custom_msg_both_set_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer},
        {aliases, [{"old.key", "removed in 4.0"}]}
    ]}),
    _ = resolve_aliases([{["new", "key"], "100"}, {["old", "key"], "42"}], [Mapping]),
    [Log] = cuttlefish_test_logging:get_logs(),
    ?assertMatch({match, _},
        re:run(Log, "both are set, using new\\.key")),
    ?assertMatch({match, _}, re:run(Log, "\\(removed in 4\\.0\\)")).

resolve_alias_default_msg_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {aliases, ["old.key"]}
    ]}),
    _ = resolve_aliases([{["old", "key"], "42"}], [Mapping]),
    [Log] = cuttlefish_test_logging:get_logs(),
    ?assertMatch({match, _}, re:run(Log, "old\\.key was renamed to new\\.key")),
    ?assertEqual(nomatch, re:run(Log, "\\(")).

resolve_alias_mixed_messages_test() ->
    _ = cuttlefish_test_logging:set_up(),
    _ = cuttlefish_test_logging:bounce(warning),
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer},
        {aliases, [{"old.key", "renamed in 4.0"}, "oldest.key"]}
    ]}),
    _ = resolve_aliases([{["old", "key"], "1"}, {["oldest", "key"], "2"}], [Mapping]),
    Logs = cuttlefish_test_logging:get_logs(),
    ?assertEqual(2, length(Logs)),
    ?assertMatch({match, _}, re:run(hd(Logs), "\\(renamed in 4\\.0\\)")),
    ?assertEqual(nomatch, re:run(lists:last(Logs), "\\(")).

resolve_alias_singular_with_msg_test() ->
    Mapping = cuttlefish_mapping:parse({mapping, "new.key", "app.setting", [
        {datatype, integer}, {alias, {"old.key", "consolidated in 4.0"}}
    ]}),
    Resolved = resolve_aliases([{["old", "key"], "42"}], [Mapping]),
    ?assertEqual([{["new", "key"], "42"}], Resolved),
    ?assertEqual([["old", "key"]], cuttlefish_mapping:aliases(Mapping)),
    ?assertEqual([{["old", "key"], "consolidated in 4.0"}],
                 cuttlefish_mapping:aliases_with_messages(Mapping)).

alias_msg_end_to_end_plural_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 10},\n"
        "  {aliases, [{\"old.key\", \"renamed in 4.0\"}]}\n"
        "]}.\n"
    ]),
    Result = map(Schema, [{["old", "key"], "42"}]),
    ?assertEqual(42, proplists:get_value(setting, proplists:get_value(app, Result))).

alias_msg_end_to_end_singular_test() ->
    Schema = cuttlefish_schema:strings([
        "{mapping, \"new.key\", \"app.setting\", [\n"
        "  {datatype, integer},\n"
        "  {default, 10},\n"
        "  {alias, {\"old.key\", \"renamed in 4.0\"}}\n"
        "]}.\n"
    ]),
    Result = map(Schema, [{["old", "key"], "42"}]),
    ?assertEqual(42, proplists:get_value(setting, proplists:get_value(app, Result))).

validate_ok_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "integer_thing", "to.int",
                                  [{datatype, integer}, {default, 32}]}),
        cuttlefish_mapping:parse({mapping, "enum_thing", "to.enum",
                                  [{datatype, {enum, [on, off]}}, {default, on}]})
    ],
    Conf = [{["integer_thing"], "32"}, {["enum_thing"], "on"}],
    ?assertEqual(ok, validate({[], Mappings, []}, Conf)),
    ok.

%% validate/3 must collect errors from all phases that can still run, not stop
%% at the first failing phase as map/3 does. This test has a type error on one
%% key and a translation error on a different key; both must appear in the
%% result even though map/3 would stop before reaching the translation phase.
validate_collects_errors_across_phases_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "bad_type", "to.int",
                                  [{datatype, integer}]}),
        cuttlefish_mapping:parse({mapping, "good_key", "to.str",
                                  [{datatype, string}]})
    ],
    Translations = [
        cuttlefish_translation:parse(
            {translation, "to.str", fun(_Conf) -> cuttlefish:invalid("deliberate") end})
    ],
    Conf = [{["bad_type"], "not_an_integer"}, {["good_key"], "hello"}],
    Schema = {Translations, Mappings, []},
    %% `map/3` stops at transform_datatypes and never reaches the translation
    ?assertMatch({error, transform_datatypes, _}, map(Schema, Conf)),
    %% `validate/3` collects errors from both phases
    Result = validate(Schema, Conf),
    ?assertMatch({error, {errorlist, [_|_]}}, Result),
    {error, {errorlist, Errors}} = Result,
    Tags = [element(1, element(2, E)) || E <- Errors],
    ?assert(lists:member(transform_type, Tags)),
    ?assert(lists:member(translation_invalid_configuration, Tags)),
    ok.

validate_unknown_variable_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "known_key", "to.int",
                                  [{datatype, integer}, {default, 0}]})
    ],
    Conf = [{["unknown_key"], "42"}],
    Result = validate({[], Mappings, []}, Conf),
    ?assertMatch({error, {errorlist, [{error, {unknown_variable, _}}|_]}}, Result),
    ok.

%% collect tests

collect_list_generator_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "listeners.tcp.$name", "rabbit.tcp_listeners", [
            {datatype, integer},
            {collect, list}
        ]})
    ],
    Conf = [
        {["listeners", "tcp", "default"], 5672},
        {["listeners", "tcp", "tls"],    5671}
    ],
    Result = map({[], Mappings, []}, Conf),
    ?assertMatch([{rabbit, [{tcp_listeners, _}]}], Result),
    [{rabbit, [{tcp_listeners, Listeners}]}] = Result,
    ?assertEqual([5671, 5672], lists:sort(Listeners)),
    ok.

collect_list_empty_generator_test() ->
    %% If there are no matches, set the key with the value of [].
    Mappings = [
        cuttlefish_mapping:parse({mapping, "listeners.tcp.$name", "rabbit.tcp_listeners", [
            {datatype, integer},
            {collect, list}
        ]})
    ],
    Conf = [],
    Result = map({[], Mappings, []}, Conf),
    ?assertEqual([{rabbit, [{tcp_listeners, []}]}], Result),
    ok.

collect_list_sort_generator_test() ->
    %% Values are sorted by the $name segment.
    Mappings = [
        cuttlefish_mapping:parse({mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
            {datatype, atom},
            {collect, list}
        ]})
    ],
    Conf = [
        {["auth_mechanisms", "plain"],     plain},
        {["auth_mechanisms", "amqplain"], amqplain},
        {["auth_mechanisms", "external"], external}
    ],
    Result = map({[], Mappings, []}, Conf),
    ?assertMatch([{rabbit, [{auth_mechanisms, _}]}], Result),
    [{rabbit, [{auth_mechanisms, Mechanisms}]}] = Result,
    ?assertEqual([amqplain, external, plain], Mechanisms),
    ok.

collect_map_atom_empty_generator_test() ->
    %% If there are no matches, set the key with the value of #{}.
    Mappings = [
        cuttlefish_mapping:parse({mapping, "deprecated_features.permit.$name",
                                  "rabbit.permit_deprecated_features", [
            {datatype, {enum, [true, false]}},
            {collect, {map, atom}}
        ]})
    ],
    Result = map({[], Mappings, []}, []),
    ?assertEqual([{rabbit, [{permit_deprecated_features, #{}}]}], Result),
    ok.

collect_map_binary_empty_generator_test() ->
    %% If there are no matches, set the key with the value of #{}.
    Mappings = [
        cuttlefish_mapping:parse({mapping, "tags.$name", "app.tags", [
            {datatype, string},
            {collect, {map, binary}}
        ]})
    ],
    Result = map({[], Mappings, []}, []),
    ?assertEqual([{app, [{tags, #{}}]}], Result),
    ok.

collect_map_atom_generator_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "deprecated_features.permit.$name",
                                  "rabbit.permit_deprecated_features", [
            {datatype, {enum, [true, false]}},
            {collect, {map, atom}}
        ]})
    ],
    Conf = [
        {["deprecated_features", "permit", "classic_queue_mirroring"], false},
        {["deprecated_features", "permit", "transient_nonexcl_queues"],  true}
    ],
    Result = map({[], Mappings, []}, Conf),
    ?assertMatch([{rabbit, [{permit_deprecated_features, _}]}], Result),
    [{rabbit, [{permit_deprecated_features, M}]}] = Result,
    ?assert(is_map(M)),
    ?assertEqual(false, maps:get(classic_queue_mirroring, M)),
    ?assertEqual(true,  maps:get(transient_nonexcl_queues, M)),
    ok.

collect_map_binary_generator_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "tags.$name", "app.tags", [
            {datatype, string},
            {collect, {map, binary}}
        ]})
    ],
    Conf = [
        {["tags", "env"],  "production"},
        {["tags", "team"], "platform"}
    ],
    Result = map({[], Mappings, []}, Conf),
    ?assertMatch([{app, [{tags, _}]}], Result),
    [{app, [{tags, M}]}] = Result,
    ?assert(is_map(M)),
    ?assertEqual("production", maps:get(<<"env">>,  M)),
    ?assertEqual("platform",   maps:get(<<"team">>, M)),
    ok.

collect_proplist_atom_generator_test() ->
    Mappings = [
        cuttlefish_mapping:parse({mapping, "feature.$name", "app.features", [
            {datatype, {enum, [true, false]}},
            {collect, {proplist, atom}}
        ]})
    ],
    Conf = [
        {["feature", "alpha"], true},
        {["feature", "beta"],  false}
    ],
    Result = map({[], Mappings, []}, Conf),
    ?assertMatch([{app, [{features, _}]}], Result),
    [{app, [{features, PL}]}] = Result,
    ?assertEqual(true,  proplists:get_value(alpha, PL)),
    ?assertEqual(false, proplists:get_value(beta,  PL)),
    ok.

collect_translation_takes_precedence_test() ->
    %% If a translation and a collect mapping target the same key, the translation wins.
    Mappings = [
        cuttlefish_mapping:parse({mapping, "listeners.tcp.$name", "rabbit.tcp_listeners", [
            {datatype, integer},
            {collect, list}
        ]})
    ],
    Translations = [
        cuttlefish_translation:parse({translation, "rabbit.tcp_listeners",
            fun(Conf) ->
                Settings = cuttlefish_variable:filter_by_prefix("listeners.tcp", Conf),
                [V * 2 || {_, V} <- Settings]
            end})
    ],
    Conf = [
        {["listeners", "tcp", "default"], 5672}
    ],
    Result = map({Translations, Mappings, []}, Conf),
    ?assertMatch([{rabbit, [{tcp_listeners, [11344]}]}], Result),
    ok.

-endif.
