/*
   Test file for HfstTransducer member functions:
   - compare
   - compose
   - convert
   - extract_paths and extract_paths_fd
   - insert_freely
   - is_cyclic
   - is_lookup_infinitely_ambiguous, lookup and lookup_fd
   - n_best
   - push_weights
   - set_final_weights
   - substitute
   - transform_weights

   Functions:
      concatenate
      determinize
      disjunct
      input_project
      intersect
      invert
      minimize
      optionalize
      output_project
      remove_epsilons
      repeat_n
      repeat_n_minus
      repeat_n_plus
      repeat_n_to_k
      repeat_plus
      repeat_star
      reverse
      subtract
   are tested in command line tool tests
*/

#include "HfstTransducer.h"
#include "auxiliary_functions.cc"

using namespace hfst;

using hfst::implementations::HfstBasicTransducer;
using hfst::implementations::HfstBasicTransition;

/* Used by the tests. */
bool
compare_alphabets(const HfstTransducer &t1, const HfstTransducer &t2)
{
    StringSet alpha1 = t1.get_alphabet();
    StringSet alpha2 = t2.get_alphabet();
    bool retval = (alpha1 == alpha2);

    // DEBUG
    if (not retval)
    {
        for (StringSet::const_iterator it = alpha1.begin(); it != alpha1.end();
             it++)
        {
            std::cerr << *it << std::endl;
        }
        std::cerr << "--" << std::endl;
        for (StringSet::const_iterator it = alpha2.begin(); it != alpha1.end();
             it++)
        {
            std::cerr << *it << std::endl;
        }
        std::cerr << "----" << std::endl;
    }

    return retval;
}

/* Used by function 'do_hfst_lookup_paths_contain'. */
bool
compare_string_vectors(const StringVector &v1, const StringVector &v2,
                       bool test_strings = false)
{
    /* Test the generated strings. */
    if (test_strings)
    {
        std::string v1_string;
        std::string v2_string;

        for (unsigned int i = 0; i < v1.size(); i++)
        {
            v1_string.append(v1[i]);
        }
        for (unsigned int i = 0; i < v2.size(); i++)
        {
            v2_string.append(v2[i]);
        }
        // fprintf(stderr, "####### comparing strings '%s' and '%s'...\n",
        // v1_string.c_str(), v2_string.c_str() ); // debug
        return (v1_string.compare(v2_string) == 0);
    }

    /* Test the exact alignments. */
    if (v1.size() != v2.size())
        return false;
    for (unsigned int i = 0; i < v1.size(); i++)
    {
        if (v1[i].compare(v2[i]) != 0)
            return false;
    }
    return true;
}

/* Does \a results contain \a expected_path. \a test_path_weight defines
   whether the weight of the path found in \a results must be equal to
   \a path_weight (a deviation of +/- 0.01 is allowed). */
bool
do_hfst_lookup_paths_contain(const HfstOneLevelPaths &results,
                             const StringVector &expected_path,
                             float path_weight = 0,
                             bool test_path_weight = false)
{
    bool found = false;
    float weight = 0;
    for (HfstOneLevelPaths::const_iterator it = results.begin();
         it != results.end(); it++)
    {
        if (compare_string_vectors(it->second, expected_path, true))
        {
            found = true;
            weight = it->first;
        }
    }
    if (found == false)
    {
        // fprintf(stderr, "######## returning false...\n"); // debug
        return false;
    }
    if (not test_path_weight)
        return true;

    if (weight > (path_weight - 0.01) && weight < (path_weight + 0.01))
        return true;
    fprintf(stderr, "FAIL: The path weight is %f, %f expected.\n", weight,
            path_weight);
    return false;
}

bool
do_results_contain(const HfstTwoLevelPaths &paths, const std::string &istring,
                   const std::string &ostring, float weight = 0,
                   bool test_path_weight = false)
{
    for (HfstTwoLevelPaths::const_iterator it = paths.begin();
         it != paths.end(); it++)
    {
        std::string path_istring;
        std::string path_ostring;
        for (StringPairVector::const_iterator IT = it->second.begin();
             IT != it->second.end(); IT++)
        {
            if (IT->first.compare("@_EPSILON_SYMBOL_@") != 0)
                path_istring.append(IT->first);
            if (IT->second.compare("@_EPSILON_SYMBOL_@") != 0)
                path_ostring.append(IT->second);
        }

        if (path_istring.compare(istring) == 0
            && path_ostring.compare(ostring) == 0)
        {
            if (not test_path_weight)
                return true;
            if (it->first > (weight - 0.01) && it->first < (weight + 0.01))
                return true;
        }
    }
    return false;
}

/*void print_weighted_paths(const WeightedPaths<float>::Set &paths)
{
  for (WeightedPaths<float>::Set::const_iterator it = paths.begin();
       it != paths.end(); it++)
    {
      fprintf(stderr, "%s:%s\t%f\n",
          it->istring.c_str(), it->ostring.c_str(), it->weight);
    }
    }*/

void
print_string_vector(const StringVector &sv)
{
    for (StringVector::const_iterator it = sv.begin(); it != sv.end(); it++)
    {
        fprintf(stderr, "\"%s\" ", it->c_str());
    }
}

void
print_lookup_path(const HfstOneLevelPath &path)
{
    print_string_vector(path.second);
    fprintf(stderr, "\t%f", path.first);
}

void
print_lookup_paths(const HfstOneLevelPaths &paths)
{
    for (HfstOneLevelPaths::const_iterator it = paths.begin();
         it != paths.end(); it++)
    {
        print_lookup_path(*it);
        fprintf(stderr, "\n");
    }
}

/* Used in testing function 'transform_weights'. */
float
modify_weights(float f)
{
    return f / 2;
}

/* Used in testing function 'substitute'. */
bool
modify_transitions(const StringPair &sp, StringPairSet &sps)
{
    if (sp.first.compare(sp.second) == 0)
    {
        sps.insert(StringPair("<ID>", "<ID>"));
        return true;
    }
    return false;
}

int
main(int argc, char **argv)
{

    const unsigned int TYPES_SIZE = 3;
    const ImplementationType types[] = { SFST_TYPE, TROPICAL_OPENFST_TYPE,
                                         /*LOG_OPENFST_TYPE,*/
                                         FOMA_TYPE };

    /* For all transducer implementation types, perform the following tests: */
    for (unsigned int i = 0; i < TYPES_SIZE; i++)
    {

        if (not HfstTransducer::is_implementation_type_available(types[i]))
            continue;

        try
        {

            /* Function compare. */
            {
                verbose_print("function compare", types[i]);

                HfstTransducer t1("foo", "bar", types[i]);
                HfstTransducer t2("foo", "@_EPSILON_SYMBOL_@", types[i]);
                HfstTransducer t3("@_EPSILON_SYMBOL_@", "bar", types[i]);
                t2.concatenate(t3);
                t2.minimize();
                /* Alignments must be the same. */
                assert(not t1.compare(t2));

                HfstTransducer t4("foo", "bar", types[i]);
                HfstTransducer t5("@_EPSILON_SYMBOL_@", types[i]);
                t4.concatenate(t5);
                /* One transducer is minimal, another is not. */
                assert(t1.compare(t4));

                /* Weights. */
                if (types[i] == TROPICAL_OPENFST_TYPE
                    || types[i] == LOG_OPENFST_TYPE)
                {
                    HfstTransducer t6("foo", "bar", types[i]);
                    t6.set_final_weights(0.3);
                    HfstTransducer t7("foo", "bar", types[i]);
                    t7.set_final_weights(0.1);

                    /* Weights differ. */
                    assert(not t6.compare(t7));

                    HfstTransducer t8("@_EPSILON_SYMBOL_@", types[i]);
                    t8.set_final_weights(0.2);
                    t7.concatenate(t8);
                    /* Weights are the same on each path. */
                    assert(t6.compare(t7));
                }
            }

            /* Function compose. */
            {
                verbose_print("function compose", types[i]);

                HfstTransducer t1("foo", "bar", types[i]);
                t1.set_final_weights(2);
                HfstTransducer t2("bar", "baz", types[i]);
                t2.set_final_weights(3);
                HfstTransducer t3("foo", "baz", types[i]);
                t3.set_final_weights(5);
                t1.compose(t2);
                assert(t1.compare(t3));
            }

            /* Function shuffle. */
            {
                verbose_print("function shuffle", types[i]);

                hfst::HfstTokenizer tok;
                HfstTransducer t1("abc", "abc", tok, types[i]);
                HfstTransducer t1_ = t1;
                HfstTransducer t2("cde", "cde", tok, types[i]);
                t1.shuffle(t2);

                HfstTransducer t3("abc", "abC", tok, types[i]);
                try
                {
                    t3.shuffle(t2);
                    assert(false);
                }
                catch (const TransducersAreNotAutomataException &e)
                {
                    assert(true);
                }
            }

            /* Function convert. */
            {
                verbose_print("function convert", types[i]);

                HfstTransducer t1("foo", "bar", types[i]);
                HfstTransducer t2("foo", "bar", types[i]);
                /* Go through all implementation formats
                   and get back to the original one.*/
                for (unsigned int j = 0; j <= TYPES_SIZE; j++)
                {
                    unsigned int index = (i + j) % TYPES_SIZE;
                    if (HfstTransducer::is_implementation_type_available(
                            types[index]))
                        t1.convert(types[index]);
                    assert(compare_alphabets(t1, t2));
                }
                assert(t1.compare(t2));
                assert(compare_alphabets(t1, t2));
            }

            /* Functions extract_paths and extract_paths_fd. */
            {
                verbose_print("function extract_paths(_fd)", types[i]);

                using hfst::HfstTokenizer;
                using hfst::StringPair;
                using hfst::StringPairSet;
                // using hfst::WeightedPaths;

                /* Create a transducer that contains animals. */
                HfstTokenizer tok;
                HfstTransducer cat("cat", "cats", tok, types[i]);
                cat.set_final_weights(3);
                HfstTransducer dog("dog", "dogs", tok, types[i]);
                dog.set_final_weights(2.5);
                HfstTransducer mouse("mouse", "mice", tok, types[i]);
                mouse.set_final_weights(1.7);
                HfstTransducer animals(types[i]);
                animals.disjunct(cat);
                animals.disjunct(dog);
                animals.disjunct(mouse);
                animals.minimize();

                /* What we expect to get from the animal transducer. */
                StringPairSet expected_results;
                expected_results.insert(StringPair("cat", "cats"));
                expected_results.insert(StringPair("dog", "dogs"));
                expected_results.insert(StringPair("mouse", "mice"));

                HfstTwoLevelPaths results;
                animals.extract_paths(results, 3, /* max_num */
                                      0 /* cycles */);

                /* Test that results are as expected. */
                assert(results.size() == 3);
                for (HfstTwoLevelPaths::const_iterator it = results.begin();
                     it != results.end(); it++)
                {
                    std::string istring;
                    std::string ostring;
                    for (StringPairVector::const_iterator IT
                         = it->second.begin();
                         IT != it->second.end(); IT++)
                    {
                        if (IT->first.compare("@_EPSILON_SYMBOL_@") != 0)
                            istring.append(IT->first);
                        if (IT->second.compare("@_EPSILON_SYMBOL_@") != 0)
                            ostring.append(IT->second);
                    }
                    StringPair sp(istring, ostring);

                    assert(expected_results.find(sp)
                           != expected_results.end());
                    /* Test weights. */
                    if (types[i] == TROPICAL_OPENFST_TYPE
                        || types[i] == LOG_OPENFST_TYPE)
                    {
                        /* Rounding can affect precision. */
                        if (istring.compare("cat") == 0)
                            assert(it->first > 2.99 && it->first < 3.01);
                        else if (istring.compare("dog") == 0)
                            assert(it->first > 2.49 && it->first < 2.51);
                        else if (istring.compare("mouse") == 0)
                            assert(it->first > 1.69 && it->first < 1.71);
                        else
                            assert(false);
                    }
                }

                /* More tests... */

                /* Functions is_lookup_infinitely_ambiguous, lookup and
                 * lookup_fd. */
                verbose_print("functions is_lookup_infinitely_ambiguous "
                              "and lookup(_fd)",
                              types[i]);

                /* add an animal with two possible plural forms */
                // if type is LOG_OPENFST_TYPE:
                // FATAL: EncodeMapper: Weight-encoded arc has non-trivial
                // weight
                if (types[i] != LOG_OPENFST_TYPE)
                {
                    HfstTransducer hippopotamus1("hippopotamus", "hippopotami",
                                                 tok, types[i]);
                    hippopotamus1.set_final_weights(1.2);
                    HfstTransducer hippopotamus2(
                        "hippopotamus", "hippopotamuses", tok, types[i]);
                    hippopotamus2.set_final_weights(1.4);
                    animals.disjunct(hippopotamus1);
                    animals.disjunct(hippopotamus2);
                    animals.minimize();
                }

                /* convert to optimized lookup format */
                HfstTransducer animals_ol(animals);

                // debug
                // std::cerr << "####### animals_ol:" << std::endl;
                // std::cerr << animals_ol << std::endl;

                if (types[i] == TROPICAL_OPENFST_TYPE
                    || types[i] == LOG_OPENFST_TYPE)
                {
                    animals_ol.convert(HFST_OLW_TYPE);
                }
                else
                {
                    animals_ol.convert(HFST_OL_TYPE);
                }

                /* no limit to the number of lookup results */
                ssize_t limit = -1;

                /* strings to lookup */
                StringVector lookup_cat = tok.tokenize_one_level("cat", false);
                StringVector lookup_dog = tok.tokenize_one_level("dog", false);
                StringVector lookup_mouse
                    = tok.tokenize_one_level("mouse", false);
                StringVector lookup_hippopotamus
                    = tok.tokenize_one_level("hippopotamus", false);

                /* where results of lookup are stored */
                HfstOneLevelPaths *results_cat;
                HfstOneLevelPaths *results_dog;
                HfstOneLevelPaths *results_mouse;
                HfstOneLevelPaths *results_hippopotamus;

                /* check that lookups are not infinitely ambiguous */
                assert(
                    not animals_ol.is_lookup_infinitely_ambiguous(lookup_cat));
                assert(
                    not animals_ol.is_lookup_infinitely_ambiguous(lookup_dog));
                assert(not animals_ol.is_lookup_infinitely_ambiguous(
                    lookup_mouse));
                assert(not animals_ol.is_lookup_infinitely_ambiguous(
                    lookup_hippopotamus));

                // todo: more is_lookup_infinitely_ambiguous tests...

                /* perform lookups */
                results_cat = animals_ol.lookup(lookup_cat, limit);
                results_dog = animals_ol.lookup(lookup_dog, limit);
                results_mouse = animals_ol.lookup(lookup_mouse, limit);
                results_hippopotamus
                    = animals_ol.lookup(lookup_hippopotamus, limit);

                /* check that the number of results is correct */
                assert(results_cat->size() == 1);
                assert(results_dog->size() == 1);
                assert(results_mouse->size() == 1);

                if (types[i] != LOG_OPENFST_TYPE)
                    assert(results_hippopotamus->size() == 2);

                bool test_weight = false;
                if (types[i] == TROPICAL_OPENFST_TYPE
                    || types[i] == LOG_OPENFST_TYPE)
                {
                    test_weight = true;
                }

                /* check that the results are correct */
                StringVector expected_path
                    = tok.tokenize_one_level("cats", false);

                assert(do_hfst_lookup_paths_contain(
                    *results_cat, expected_path, 3, test_weight));

                expected_path = tok.tokenize_one_level("dogs", false);
                assert(do_hfst_lookup_paths_contain(
                    *results_dog, expected_path, 2.5, test_weight));

                expected_path = tok.tokenize_one_level("mice", false);
                assert(do_hfst_lookup_paths_contain(
                    *results_mouse, expected_path, 1.7, test_weight));

                expected_path = tok.tokenize_one_level("hippopotami", false);
                if (types[i] != LOG_OPENFST_TYPE)
                    assert(do_hfst_lookup_paths_contain(*results_hippopotamus,
                                                        expected_path, 1.2,
                                                        test_weight));

                expected_path
                    = tok.tokenize_one_level("hippopotamuses", false);
                if (types[i] != LOG_OPENFST_TYPE)
                    assert(do_hfst_lookup_paths_contain(*results_hippopotamus,
                                                        expected_path, 1.4,
                                                        test_weight));

                // if type is LOG_OPENFST_TYPE:
                // FATAL: SingleShortestPath: Weight needs to have the path
                // property and be right distributive: log
                if (types[i] != LOG_OPENFST_TYPE)
                {

                    /* Function n_best. */
                    verbose_print("function n_best", types[i]);

                    bool weighted = false;
                    if (types[i] == TROPICAL_OPENFST_TYPE
                        || types[i] == LOG_OPENFST_TYPE)
                        weighted = true;

                    HfstTransducer animals1(animals);
                    animals1.n_best(1);
                    HfstTwoLevelPaths results1;
                    animals1.extract_paths(results1);
                    assert(results1.size() == 1);
                    if (weighted)
                    {
                        assert(do_results_contain(results1, "hippopotamus",
                                                  "hippopotami", 1.2, true));
                    }

                    HfstTransducer animals2(animals);
                    animals2.n_best(2);
                    HfstTwoLevelPaths results2;
                    animals2.extract_paths(results2);
                    assert(results2.size() == 2);
                    if (weighted)
                    {
                        assert(do_results_contain(results2, "hippopotamus",
                                                  "hippopotami", 1.2, true)
                               && do_results_contain(results2, "hippopotamus",
                                                     "hippopotamuses", 1.4,
                                                     true));
                    }

                    HfstTransducer animals3(animals);
                    animals3.n_best(3);
                    HfstTwoLevelPaths results3;
                    animals3.extract_paths(results3);
                    assert(results3.size() == 3);
                    if (weighted)
                    {
                        assert(do_results_contain(results3, "hippopotamus",
                                                  "hippopotami", 1.2, true)
                               && do_results_contain(results3, "hippopotamus",
                                                     "hippopotamuses", 1.4,
                                                     true)
                               && do_results_contain(results3, "mouse", "mice",
                                                     1.7, true));
                    }

                    HfstTransducer animals4(animals);
                    animals4.n_best(4);
                    HfstTwoLevelPaths results4;
                    animals4.extract_paths(results4);
                    assert(results4.size() == 4);
                    if (weighted)
                    {
                        assert(do_results_contain(results4, "hippopotamus",
                                                  "hippopotami", 1.2, true)
                               && do_results_contain(results4, "hippopotamus",
                                                     "hippopotamuses", 1.4,
                                                     true)
                               && do_results_contain(results4, "mouse", "mice",
                                                     1.7, true)
                               && do_results_contain(results4, "dog", "dogs",
                                                     2.5, true));
                    }

                    HfstTransducer animals5(animals);
                    animals5.n_best(5);
                    HfstTwoLevelPaths results5;
                    animals5.extract_paths(results5);
                    assert(results5.size() == 5);
                    if (weighted)
                    {
                        assert(do_results_contain(results5, "hippopotamus",
                                                  "hippopotami", 1.2, true)
                               && do_results_contain(results5, "hippopotamus",
                                                     "hippopotamuses", 1.4,
                                                     true)
                               && do_results_contain(results5, "mouse", "mice",
                                                     1.7, true)
                               && do_results_contain(results5, "dog", "dogs",
                                                     2.5, true)
                               && do_results_contain(results5, "cat", "cats",
                                                     3.0, true));
                    }
                }

                delete results_cat;
                delete results_dog;
                delete results_mouse;
                delete results_hippopotamus;
            }

            /* Functions insert_freely. */
            {
                verbose_print("functions insert_freely", types[i]);

                HfstTransducer t1("a", "b", types[i]);
                t1.insert_freely(StringPair("c", "d"));

                HfstTransducer t2("a", "b", types[i]);
                HfstTransducer tr("c", "d", types[i]);
                t2.insert_freely(tr); // FAILS HERE
                assert(t1.compare(t2));

                HfstTransducer cd_star("c", "d", types[i]);
                cd_star.repeat_star();
                HfstTransducer ab("a", "b", types[i]);
                HfstTransducer test(types[i]);
                test = cd_star;
                test.concatenate(ab);
                test.concatenate(cd_star);

                assert(t1.compare(test));
                assert(t2.compare(test));

                HfstTransducer unk2unk("@_UNKNOWN_SYMBOL_@",
                                       "@_UNKNOWN_SYMBOL_@", types[i]);
                unk2unk.insert_freely(StringPair("c", "d"));
                HfstTransducer dc("d", "c", types[i]);

                std::cerr << unk2unk << "--\n" << dc << std::endl;

                HfstTransducer empty(types[i]);
                assert(not unk2unk.intersect(dc).compare(empty));

                HfstTransducer unk2unk_("@_UNKNOWN_SYMBOL_@",
                                        "@_UNKNOWN_SYMBOL_@", types[i]);
                HfstTransducer cd_("c", "d", types[i]);
                unk2unk_.insert_freely(cd_);

                HfstTransducer dc_("d", "c", types[i]);
                HfstTransducer empty_(types[i]);
                assert(not unk2unk_.intersect(dc_).compare(empty));
            }

            /* Function is_cyclic. */
            {
                verbose_print("function is_cyclic", types[i]);

                HfstTransducer t1("a", "b", types[i]);
                assert(not t1.is_cyclic());
                t1.repeat_star();
                assert(t1.is_cyclic());
            }
        }
        catch (const HfstException &e)
        {
            std::cerr << e() << std::endl;
        }

        /* Function push_weights. */
        {
            if (types[i] == TROPICAL_OPENFST_TYPE)
            {
                verbose_print("function push_weights", types[i]);

                /* Create an HFST basic transducer [a:b] with transition
                   weight 0.3 and final weight 0.5. */
                HfstBasicTransducer t;
                t.add_state(1);
                t.add_transition(0, HfstBasicTransition(1, "a", "b", 0.3));
                t.set_final_weight(1, 0.5);

                /* Convert to tropical OpenFst format and push weights
                   toward final and initial states. */
                HfstTransducer T_final(t, TROPICAL_OPENFST_TYPE);
                T_final.push_weights(TO_FINAL_STATE);
                HfstTransducer T_initial(t, TROPICAL_OPENFST_TYPE);
                T_initial.push_weights(TO_INITIAL_STATE);

                /* Convert back to HFST basic transducer. */
                HfstBasicTransducer t_final(T_final);
                HfstBasicTransducer t_initial(T_initial);

                /* Test the final weight. */
                try
                {
                    /* Rounding can affect the precision. */
                    assert(0.79 < t_final.get_final_weight(1)
                           && t_final.get_final_weight(1) < 0.81);
                }
                /* If the state does not exist or is not final */
                // catch (hfst::exceptions::HfstArgumentException e) {
                catch (const HfstException e)
                {
                    assert(false);
                }

                /* Test the transition weight. */
                try
                {
                    hfst::implementations::HfstBasicTransitions transitions
                        = t_initial[0];
                    assert(transitions.size() == 1);
                    float weight = transitions.begin()->get_weight();
                    /* Rounding can affect the precision. */
                    assert(0.79 < weight && weight < 0.81);
                }
                /* If the state does not exist or is not final */
                // catch (hfst::exceptions::HfstArgumentException e) {
                catch (const HfstException e)
                {
                    assert(false);
                }
            }
        }

        /* Functions set_final_weights and transform_weights. */
        {
            if (types[i] == TROPICAL_OPENFST_TYPE
                || types[i] == LOG_OPENFST_TYPE)
            {
                verbose_print("functions set_final_weights and "
                              "transform_weights",
                              types[i]);

                /* Create an HFST basic transducer [a:b] with transition
                   weight 0.3 and final weight 0.5. */
                HfstBasicTransducer t;
                t.add_state(1);
                t.add_transition(0, HfstBasicTransition(1, "a", "b", 0.3));
                t.set_final_weight(1, 0.5);

                /* Modify weights. */
                HfstTransducer T(t, types[i]);
                T.set_final_weights(0.2);
                T.transform_weights(&modify_weights);
                T.push_weights(TO_FINAL_STATE);

                /* Convert back to HFST basic transducer and test the weight.
                 */
                HfstBasicTransducer tc(T);
                try
                {
                    assert(0.24 < tc.get_final_weight(1)
                           && tc.get_final_weight(1) < 0.26);
                }
                /* If the state does not exist or is not final */
                // catch (hfst::exceptions::HfstArgumentException e) {
                catch (const HfstException e)
                {
                    assert(false);
                }
            }
        }

        /* Functions substitute. */
        {
            verbose_print("functions substitute", types[i]);

            HfstTokenizer tok;
            tok.add_multichar_symbol("<eps>");
            HfstTransducer t("cat", "cats", tok, types[i]);

            /* String with String */
            HfstTransducer t1(t);
            t1.substitute("c", "C", true, false);
            t1.substitute("t", "T", false, true);
            t1.substitute("@_EPSILON_SYMBOL_@", "<eps>");
            t1.substitute("a", "A");
            t1.substitute("T", "T");     // special
            t1.substitute("foo", "bar"); // cases
            HfstTransducer t1_("CAt<eps>", "cATs", tok, types[i]);
            assert(t1.compare(t1_));

            /* StringPair with StringPair */
            HfstTransducer t2(t);
            t2.substitute(StringPair("c", "c"), StringPair("C", "c"));
            t2.substitute(StringPair("C", "c"), StringPair("H", "h"));
            t2.substitute(StringPair("a", "a"),
                          StringPair("a", "a")); // special
            t2.substitute(StringPair("foo", "bar"),
                          StringPair("f", "b")); // cases
            HfstTransducer t2_("Hat", "hats", tok, types[i]);
            assert(t2.compare(t2_));

            /* StringPair with StringPairSet */
            HfstTransducer t3(t);
            StringPairSet sps;
            sps.insert(StringPair("c", "c"));
            sps.insert(StringPair("C", "C"));
            sps.insert(StringPair("h", "h"));
            sps.insert(StringPair("H", "H"));
            t3.substitute(StringPair("c", "c"), sps);
            HfstTransducer t3_("cat", "cats", tok, types[i]);
            HfstTransducer t3_1("Cat", "Cats", tok, types[i]);
            HfstTransducer t3_2("hat", "hats", tok, types[i]);
            HfstTransducer t3_3("Hat", "Hats", tok, types[i]);
            t3_.disjunct(t3_1);
            t3_.disjunct(t3_2);
            t3_.disjunct(t3_3);
            t3_.minimize();
            assert(t3.compare(t3_));

            /* StringPair with HfstTransducer */
            HfstTransducer t4(t);
            HfstTransducer subs("ch", tok, types[i]);
            t4.substitute(StringPair("c", "c"), subs);
            HfstTransducer t4_("chat", "chats", tok, types[i]);
            assert(t4.compare(t4_));

            /* Substitute with function */
            HfstTransducer t5(t);
            t5.substitute(&modify_transitions);
            tok.add_multichar_symbol("<ID>");
            HfstTransducer t5_("<ID><ID><ID>", "<ID><ID><ID>s", tok, types[i]);
            assert(t5.compare(t5_));

            /* Perform multiple string-to-string substitutions. */
            HfstTransducer t6(t);
            HfstSymbolSubstitutions subs_symbol;
            subs_symbol["c"] = "C";
            subs_symbol["a"] = "A";
            subs_symbol["t"] = "T";
            subs_symbol["s"] = "S";
            t6.substitute(subs_symbol);
            HfstTransducer t6_("CAT", "CATS", tok, types[i]);
            assert(t6.compare(t6_));

            /* Perform multiple string pair-to-string pair substitutions. */
            HfstTransducer t7(t);
            HfstSymbolPairSubstitutions subs_pair;
            subs_pair[StringPair("a", "a")] = StringPair("A", "a");
            subs_pair[StringPair("s", "s")] = StringPair("S", "S");
            subs_pair[StringPair("t", "t")] = StringPair("t", "T");
            t7.substitute(subs_pair);
            HfstTransducer t7_("cAt", "caTs", tok, types[i]);
            assert(t7.compare(t7_));
        }

        {
            verbose_print("alphabets", types[i]);

            HfstTransducer a2unk("a", "@_UNKNOWN_SYMBOL_@", types[i]);
            assert(a2unk.get_alphabet().size() == 4);
            a2unk.insert_to_alphabet("FOO");
            assert(a2unk.get_alphabet().size() == 5);
            a2unk.remove_from_alphabet("FOO");
            assert(a2unk.get_alphabet().size() == 4);
            StringSet alpha = a2unk.get_alphabet();
            assert(alpha.find("FOO") == alpha.end());
        }

        /* Test that binary operations do not change the transducer argument.
         */

        {
            verbose_print("binary operations", types[i]);

            HfstTransducer id2id(internal_identity, internal_identity,
                                 types[i]);
            HfstTransducer a2b("a", "b", types[i]);

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.concatenate(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.disjunct(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.intersect(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.subtract(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.compose(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            {
                HfstTransducer a2b_copy(a2b);
                HfstTransducer id2id_copy(id2id);
                HfstTransducer id2id_copy2(id2id);

                a2b_copy.insert_freely(id2id_copy);
                assert(id2id_copy.compare(id2id_copy2));
                assert(id2id_copy.get_alphabet()
                       == id2id_copy2.get_alphabet());
            }

            /* Test that binary functions work when the argument and the
               calling object are the same. */
            {
                HfstTransducer foo("foo", types[i]);
                foo.concatenate(foo);
                HfstTransducer foofoo("foo", types[i]);
                HfstTransducer foo2("foo", types[i]);
                foofoo.concatenate(foo2);
                assert(foo.compare(foofoo));
            }
        }
    }

    // A special case..

    if (HfstTransducer::is_implementation_type_available(SFST_TYPE)
        && HfstTransducer::is_implementation_type_available(
            TROPICAL_OPENFST_TYPE)
        && HfstTransducer::is_implementation_type_available(FOMA_TYPE))
    {
        HfstTransducer t_sfst("c", "c", SFST_TYPE);
        t_sfst.convert(TROPICAL_OPENFST_TYPE);
        HfstTransducer t_foma("a", "b", FOMA_TYPE);
        t_foma.substitute(StringPair("a", "b"), StringPair("b", "a"));
        StringSet alphabet = t_foma.get_alphabet();
        assert(alphabet.find("c") == alphabet.end());
    }
}
