/*
 * Copyright © 2004 Ondra Kamenik
 * Copyright © 2019-2026 Dynare Team
 *
 * This file is part of Dynare.
 *
 * Dynare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Dynare is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dynare.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "SylvException.hh"
#include "factory.hh"
#include "gs_tensor.hh"
#include "monoms.hh"
#include "ps_tensor.hh"
#include "rfs_tensor.hh"
#include "stack_container.hh"
#include "t_container.hh"
#include "t_polynomial.hh"
#include "tl_exception.hh"
#include "tl_static.hh"

#include <cstdlib>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

class TestRunnable
{
public:
  const std::string name;
  int dim;  // dimension of the solved problem
  int nvar; // number of variable of the solved problem
  TestRunnable(std::string name_arg, int d, int nv) : name {std::move(name_arg)}, dim(d), nvar(nv)
  {
  }
  virtual ~TestRunnable() = default;
  [[nodiscard]] bool test() const;
  [[nodiscard]] virtual bool run() const = 0;

protected:
  template<class _Ttype>
  static bool index_forward(const Symmetry& s, const IntSequence& nvs);

  template<class _Ttype>
  static bool index_backward(const Symmetry& s, const IntSequence& nvs);

  template<class _Ttype>
  static bool index_offset(const Symmetry& s, const IntSequence& nvs);

  static bool fold_unfold(std::unique_ptr<FTensor> folded);
  static bool
  fs_fold_unfold(int r, int nv, int dim)
  {
    Factory f;
    return fold_unfold(f.make<FFSTensor>(r, nv, dim));
  }
  static bool
  r_fold_unfold(int r, int nv, int dim)
  {
    Factory f;
    return fold_unfold(f.make<FRTensor>(r, nv, dim));
  }
  static bool
  gs_fold_unfold(int r, const Symmetry& s, const IntSequence& nvs)
  {
    Factory f;
    return fold_unfold(f.make<FGSTensor>(r, s, nvs));
  }

  static bool dense_prod(const Symmetry& bsym, const IntSequence& bnvs, int hdim, int hnv,
                         int rows);

  static bool folded_monomial(int ng, int nx, int ny, int nu, int dim);

  static bool unfolded_monomial(int ng, int nx, int ny, int nu, int dim);

  static bool fold_zcont(int nf, int ny, int nu, int nup, int nbigg, int ng, int dim);

  static bool unfold_zcont(int nf, int ny, int nu, int nup, int nbigg, int ng, int dim);

  static bool folded_contraction(int r, int nv, int dim);

  static bool unfolded_contraction(int r, int nv, int dim);

  static bool poly_eval(int r, int nv, int maxdim);
};

bool
TestRunnable::test() const
{
  std::cout << "Running test <" << name << ">" << '\n';
  clock_t start = clock();
  bool passed = run();
  clock_t end = clock();
  std::cout << "CPU time " << static_cast<double>(end - start) / CLOCKS_PER_SEC
            << " (CPU seconds)..................";
  if (passed)
    std::cout << "passed\n\n";
  else
    std::cout << "FAILED\n\n";
  return passed;
}

/****************************************************/
/*     definition of TestRunnable static methods    */
/****************************************************/
template<class _Ttype>
bool
TestRunnable::index_forward(const Symmetry& s, const IntSequence& nvs)
{
  int fails = 0;
  int ndecr = 0;
  int nincr = 0;
  _Ttype dummy(0, TensorDimens(s, nvs));
  auto run = dummy.end();
  do
    {
      --run;
      ndecr++;
      auto run2 = dummy.begin();
      for (int i = 0; i < *run; i++)
        {
          ++run2;
          nincr++;
        }
      if (run != run2)
        fails++;
    }
  while (run != dummy.begin());

  std::cout << "\tnumber of columns    = " << dummy.ncols() << '\n'
            << "\tnumber of increments = " << nincr << '\n'
            << "\tnumber of decrements = " << ndecr << '\n'
            << "\tnumber of failures   = " << fails << '\n';

  return fails == 0;
}

template<class _Ttype>
bool
TestRunnable::index_backward(const Symmetry& s, const IntSequence& nvs)
{
  int fails = 0;
  int ndecr = 0;
  int nincr = 0;
  _Ttype dummy(0, TensorDimens(s, nvs));
  auto run = dummy.begin();
  while (run != dummy.end())
    {
      auto run2 = dummy.end();
      for (int i = 0; i < dummy.ncols() - *run; i++)
        {
          --run2;
          ndecr++;
        }
      if (run != run2)
        fails++;
      ++run;
      nincr++;
    }

  std::cout << "\tnumber of columns    = " << dummy.ncols() << '\n'
            << "\tnumber of increments = " << nincr << '\n'
            << "\tnumber of decrements = " << ndecr << '\n'
            << "\tnumber of failures   = " << fails << '\n';

  return fails == 0;
}

template<class _Ttype>
bool
TestRunnable::index_offset(const Symmetry& s, const IntSequence& nvs)
{
  int fails = 0;
  int nincr = 0;
  _Ttype dummy(0, TensorDimens(s, nvs));
  for (auto run = dummy.begin(); run != dummy.end(); ++run, nincr++)
    {
      typename _Ttype::index run2(dummy, run.getCoor());
      if (run != run2)
        fails++;
    }

  std::cout << "\tnumber of columns    = " << dummy.ncols() << '\n'
            << "\tnumber of increments = " << nincr << '\n'
            << "\tnumber of failures   = " << fails << '\n';

  return fails == 0;
}

bool
TestRunnable::fold_unfold(std::unique_ptr<FTensor> folded)
{
  auto unfolded = folded->unfold();
  auto folded2 = unfolded->fold();
  folded2->add(-1.0, *folded);
  double normInf = folded2->getNormInf();
  double norm1 = folded2->getNorm1();
  std::cout << "\tfolded size:       (" << folded->nrows() << ", " << folded->ncols() << ")\n"
            << "\tunfolded size:     (" << unfolded->nrows() << ", " << unfolded->ncols() << ")\n"
            << "\tdifference normInf: " << normInf << '\n'
            << "\tdifference norm1:   " << norm1 << '\n';

  return normInf < 1.0e-15;
}

bool
TestRunnable::dense_prod(const Symmetry& bsym, const IntSequence& bnvs, int hdim, int hnv, int rows)
{
  Factory f;
  FGSContainer cont {f.makeCont<FGSTensor, FGSContainer>(hnv, bnvs, bsym.dimen() - hdim + 1)};
  auto fh = f.make<FGSTensor>(rows, Symmetry {hdim}, IntSequence(1, hnv));
  UGSTensor uh(*fh);
  FGSTensor fb(rows, TensorDimens(bsym, bnvs));
  fb.getData().zeros();
  clock_t s1 = clock();
  cont.multAndAdd(uh, fb);
  clock_t s2 = clock();
  UGSContainer ucont(cont);
  clock_t s3 = clock();
  UGSTensor ub(rows, fb.getDims());
  ub.getData().zeros();
  clock_t s4 = clock();
  ucont.multAndAdd(uh, ub);
  clock_t s5 = clock();

  UGSTensor btmp(fb);
  btmp.add(-1, ub);
  double norm = btmp.getData().getMax();
  double norm1 = btmp.getNorm1();
  double normInf = btmp.getNormInf();

  std::cout << "\ttime for folded product:     " << static_cast<double>(s2 - s1) / CLOCKS_PER_SEC
            << '\n'
            << "\ttime for unfolded product:   " << static_cast<double>(s5 - s4) / CLOCKS_PER_SEC
            << '\n'
            << "\ttime for container convert:  " << static_cast<double>(s3 - s2) / CLOCKS_PER_SEC
            << '\n'
            << "\tunfolded difference normMax: " << norm << '\n'
            << "\tunfolded difference norm1:   " << norm1 << '\n'
            << "\tunfolded difference normInf: " << normInf << '\n';

  return norm < 1.e-13;
}

bool
TestRunnable::folded_monomial(int ng, int nx, int ny, int nu, int dim)
{
  clock_t gen_time = clock();
  DenseDerivGenerator gen(ng, nx, ny, nu, 5, 0.3, dim);
  gen_time = clock() - gen_time;
  std::cout << "\ttime for monom generation: " << static_cast<double>(gen_time) / CLOCKS_PER_SEC
            << '\n';
  IntSequence nvs {ny, nu};
  double maxnorm = 0;
  for (int ydim = 0; ydim <= dim; ydim++)
    {
      Symmetry s {ydim, dim - ydim};
      std::cout << "\tSymmetry: ";
      s.print();
      FGSTensor res(ng, TensorDimens(s, nvs));
      res.getData().zeros();
      clock_t stime = clock();
      for (int d = 1; d <= dim; d++)
        gen.xcont.multAndAdd(*(gen.ts[d - 1]), res);
      stime = clock() - stime;
      std::cout << "\t\ttime for symmetry: " << static_cast<double>(stime) / CLOCKS_PER_SEC << '\n';
      const FGSTensor& mres = gen.rcont.get(s);
      res.add(-1.0, mres);
      double normtmp = res.getData().getMax();
      std::cout << "\t\terror normMax:     " << normtmp << '\n';
      maxnorm = std::max(maxnorm, normtmp);
    }
  return maxnorm < 1.0e-10;
}

bool
TestRunnable::unfolded_monomial(int ng, int nx, int ny, int nu, int dim)
{
  clock_t gen_time = clock();
  DenseDerivGenerator gen(ng, nx, ny, nu, 5, 0.3, dim);
  gen_time = clock() - gen_time;
  std::cout << "\ttime for monom generation: " << static_cast<double>(gen_time) / CLOCKS_PER_SEC
            << '\n';
  clock_t u_time = clock();
  gen.unfold();
  u_time = clock() - u_time;
  std::cout << "\ttime for monom unfolding:  " << static_cast<double>(u_time) / CLOCKS_PER_SEC
            << '\n';
  IntSequence nvs {ny, nu};
  double maxnorm = 0;
  for (int ydim = 0; ydim <= dim; ydim++)
    {
      Symmetry s {ydim, dim - ydim};
      std::cout << "\tSymmetry: ";
      s.print();
      UGSTensor res(ng, TensorDimens(s, nvs));
      res.getData().zeros();
      clock_t stime = clock();
      for (int d = 1; d <= dim; d++)
        gen.uxcont.multAndAdd(*(gen.uts[d - 1]), res);
      stime = clock() - stime;
      std::cout << "\t\ttime for symmetry: " << static_cast<double>(stime) / CLOCKS_PER_SEC << '\n';
      const FGSTensor& mres = gen.rcont.get(s);
      FGSTensor foldres(res);
      foldres.add(-1.0, mres);
      double normtmp = foldres.getData().getMax();
      std::cout << "\t\terror normMax:     " << normtmp << '\n';
      maxnorm = std::max(maxnorm, normtmp);
    }
  return maxnorm < 1.0e-10;
}

bool
TestRunnable::fold_zcont(int nf, int ny, int nu, int nup, int nbigg, int ng, int dim)
{
  clock_t gen_time = clock();
  SparseDerivGenerator dg(nf, ny, nu, nup, nbigg, ng, 5, 0.55, dim);
  gen_time = clock() - gen_time;
  for (int d = 1; d <= dim; d++)
    std::cout << "\tfill of dim=" << d << " tensor:     " << std::setprecision(2) << std::fixed
              << std::setw(6) << 100 * dg.ts[d - 1].getFillFactor() << std::setprecision(6)
              << std::defaultfloat << " %\n";
  std::cout << "\ttime for monom generation: " << static_cast<double>(gen_time) / CLOCKS_PER_SEC
            << '\n';

  IntSequence nvs {ny, nu, nup, 1};
  double maxnorm = 0.0;

  // form ZContainer
  FoldedZContainer zc(&dg.bigg, nbigg, &dg.g, ng, ny, nu);

  for (int d = 2; d <= dim; d++)
    for (auto& si : SymmetrySet(d, 4))
      {
        std::cout << "\tSymmetry: ";
        si.print();
        FGSTensor res(nf, TensorDimens(si, nvs));
        res.getData().zeros();
        clock_t stime = clock();
        for (int l = 1; l <= si.dimen(); l++)
          zc.multAndAdd(dg.ts[l - 1], res);
        stime = clock() - stime;
        std::cout << "\t\ttime for symmetry: " << static_cast<double>(stime) / CLOCKS_PER_SEC
                  << '\n';
        const FGSTensor& mres = dg.rcont.get(si);
        res.add(-1.0, mres);
        double normtmp = res.getData().getMax();
        std::cout << "\t\terror normMax:     " << normtmp << '\n';
        maxnorm = std::max(maxnorm, normtmp);
      }

  return maxnorm < 1.0e-10;
}

bool
TestRunnable::unfold_zcont(int nf, int ny, int nu, int nup, int nbigg, int ng, int dim)
{
  clock_t gen_time = clock();
  SparseDerivGenerator dg(nf, ny, nu, nup, nbigg, ng, 5, 0.55, dim);
  gen_time = clock() - gen_time;
  for (int d = 1; d <= dim; d++)
    std::cout << "\tfill of dim=" << d << " tensor:     " << std::setprecision(2) << std::fixed
              << std::setw(6) << 100 * dg.ts[d - 1].getFillFactor() << std::setprecision(6)
              << std::defaultfloat << " %\n";
  std::cout << "\ttime for monom generation: " << static_cast<double>(gen_time) / CLOCKS_PER_SEC
            << '\n';

  clock_t con_time = clock();
  UGSContainer uG_cont(dg.bigg);
  UGSContainer ug_cont(dg.g);
  con_time = clock() - con_time;
  std::cout << "\ttime for container unfold: " << static_cast<double>(con_time) / CLOCKS_PER_SEC
            << '\n';

  IntSequence nvs {ny, nu, nup, 1};
  double maxnorm = 0.0;

  // Form ZContainer
  UnfoldedZContainer zc(&uG_cont, nbigg, &ug_cont, ng, ny, nu);

  for (int d = 2; d <= dim; d++)
    for (auto& si : SymmetrySet(d, 4))
      {
        std::cout << "\tSymmetry: ";
        si.print();
        UGSTensor res(nf, TensorDimens(si, nvs));
        res.getData().zeros();
        clock_t stime = clock();
        for (int l = 1; l <= si.dimen(); l++)
          zc.multAndAdd(dg.ts[l - 1], res);
        stime = clock() - stime;
        std::cout << "\t\ttime for symmetry: " << static_cast<double>(stime) / CLOCKS_PER_SEC
                  << '\n';
        FGSTensor fold_res(res);
        const FGSTensor& mres = dg.rcont.get(si);
        fold_res.add(-1.0, mres);
        double normtmp = fold_res.getData().getMax();
        std::cout << "\t\terror normMax:     " << normtmp << '\n';
        maxnorm = std::max(maxnorm, normtmp);
      }

  return maxnorm < 1.0e-10;
}

bool
TestRunnable::folded_contraction(int r, int nv, int dim)
{
  Factory fact;
  Vector x {fact.makeVector(nv)};

  auto forig = fact.make<FFSTensor>(r, nv, dim);
  auto f = std::make_unique<FFSTensor>(*forig);
  clock_t ctime = clock();
  for (int d = dim - 1; d > 0; d--)
    f = std::make_unique<FFSTensor>(*f, ConstVector(x));
  ctime = clock() - ctime;
  Vector res(forig->nrows());
  res.zeros();
  f->multaVec(res, x);

  UFSTensor u(*forig);
  clock_t utime = clock();
  URSingleTensor ux(x, dim);
  Vector v(u.nrows());
  v.zeros();
  u.multaVec(v, ux.getData());
  utime = clock() - utime;

  v.add(-1.0, res);
  std::cout << "\ttime for folded contraction: " << static_cast<double>(ctime) / CLOCKS_PER_SEC
            << '\n'
            << "\ttime for unfolded power:     " << static_cast<double>(utime) / CLOCKS_PER_SEC
            << '\n'
            << "\terror normMax:     " << v.getMax() << '\n'
            << "\terror norm1:       " << v.getNorm1() << '\n';

  return v.getMax() < 1.e-10;
}

bool
TestRunnable::unfolded_contraction(int r, int nv, int dim)
{
  Factory fact;
  Vector x {fact.makeVector(nv)};

  auto forig = fact.make<FFSTensor>(r, nv, dim);
  UFSTensor uorig(*forig);
  auto u = std::make_unique<UFSTensor>(uorig);
  clock_t ctime = clock();
  for (int d = dim - 1; d > 0; d--)
    u = std::make_unique<UFSTensor>(*u, ConstVector(x));
  ctime = clock() - ctime;
  Vector res(uorig.nrows());
  res.zeros();
  u->multaVec(res, x);

  clock_t utime = clock();
  URSingleTensor ux(x, dim);
  Vector v(uorig.nrows());
  v.zeros();
  uorig.multaVec(v, ux.getData());
  utime = clock() - utime;

  v.add(-1.0, res);
  std::cout << "\ttime for unfolded contraction: " << static_cast<double>(ctime) / CLOCKS_PER_SEC
            << '\n'
            << "\ttime for unfolded power:       " << static_cast<double>(utime) / CLOCKS_PER_SEC
            << '\n'
            << "\terror normMax:     " << v.getMax() << '\n'
            << "\terror norm1:       " << v.getNorm1() << '\n';

  return v.getMax() < 1.e-10;
}

bool
TestRunnable::poly_eval(int r, int nv, int maxdim)
{
  Factory fact;
  Vector x {fact.makeVector(nv)};

  Vector out_ft(r);
  out_ft.zeros();
  Vector out_fh(r);
  out_fh.zeros();
  Vector out_ut(r);
  out_ut.zeros();
  Vector out_uh(r);
  out_uh.zeros();

  FTensorPolynomial fp {fact.makePoly<FFSTensor, FTensorPolynomial>(r, nv, maxdim)};

  clock_t ft_cl = clock();
  fp.evalTrad(out_ft, x);
  ft_cl = clock() - ft_cl;
  std::cout << "\ttime for folded power eval:    " << static_cast<double>(ft_cl) / CLOCKS_PER_SEC
            << '\n';

  clock_t fh_cl = clock();
  fp.evalHorner(out_fh, x);
  fh_cl = clock() - fh_cl;
  std::cout << "\ttime for folded horner eval:   " << static_cast<double>(fh_cl) / CLOCKS_PER_SEC
            << '\n';

  UTensorPolynomial up {fp};

  clock_t ut_cl = clock();
  up.evalTrad(out_ut, x);
  ut_cl = clock() - ut_cl;
  std::cout << "\ttime for unfolded power eval:  " << static_cast<double>(ut_cl) / CLOCKS_PER_SEC
            << '\n';

  clock_t uh_cl = clock();
  up.evalHorner(out_uh, x);
  uh_cl = clock() - uh_cl;
  std::cout << "\ttime for unfolded horner eval: " << static_cast<double>(uh_cl) / CLOCKS_PER_SEC
            << '\n';

  out_ft.add(-1.0, out_ut);
  double max_ft = out_ft.getMax();
  out_fh.add(-1.0, out_ut);
  double max_fh = out_fh.getMax();
  out_uh.add(-1.0, out_ut);
  double max_uh = out_uh.getMax();

  std::cout << "\tfolded power error norm max:     " << max_ft << '\n'
            << "\tfolded horner error norm max:    " << max_fh << '\n'
            << "\tunfolded horner error norm max:  " << max_uh << '\n';

  return max_ft + max_fh + max_uh < 1.0e-10;
}

/****************************************************/
/*     definition of TestRunnable subclasses        */
/****************************************************/
class SmallIndexForwardFold : public TestRunnable
{
public:
  SmallIndexForwardFold() : TestRunnable("small index forward for fold (44)(222)", 5, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3};
    IntSequence nvs {4, 2};
    return index_forward<FGSTensor>(s, nvs);
  }
};

class SmallIndexForwardUnfold : public TestRunnable
{
public:
  SmallIndexForwardUnfold() : TestRunnable("small index forward for unfold (44)(222)", 5, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3};
    IntSequence nvs {4, 2};
    return index_forward<UGSTensor>(s, nvs);
  }
};

class IndexForwardFold : public TestRunnable
{
public:
  IndexForwardFold() : TestRunnable("index forward for fold (55)(222)(22)", 7, 5)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {5, 2, 2};
    return index_forward<FGSTensor>(s, nvs);
  }
};

class IndexForwardUnfold : public TestRunnable
{
public:
  IndexForwardUnfold() : TestRunnable("index forward for unfold (55)(222)(22)", 7, 5)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {5, 2, 2};
    return index_forward<UGSTensor>(s, nvs);
  }
};

class SmallIndexBackwardFold : public TestRunnable
{
public:
  SmallIndexBackwardFold() : TestRunnable("small index backward for fold (3)(3)(222)", 5, 3)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {1, 1, 3};
    IntSequence nvs {3, 3, 2};
    return index_backward<FGSTensor>(s, nvs);
  }
};

class IndexBackwardFold : public TestRunnable
{
public:
  IndexBackwardFold() : TestRunnable("index backward for fold (44)(222)(44)", 7, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {4, 2, 4};
    return index_backward<FGSTensor>(s, nvs);
  }
};

class SmallIndexBackwardUnfold : public TestRunnable
{
public:
  SmallIndexBackwardUnfold() : TestRunnable("small index backward for unfold (3)(3)(222)", 5, 3)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {1, 1, 3};
    IntSequence nvs {3, 3, 2};
    return index_backward<UGSTensor>(s, nvs);
  }
};

class IndexBackwardUnfold : public TestRunnable
{
public:
  IndexBackwardUnfold() : TestRunnable("index backward for unfold (44)(222)(44)", 7, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {4, 2, 4};
    return index_backward<UGSTensor>(s, nvs);
  }
};

class SmallIndexOffsetFold : public TestRunnable
{
public:
  SmallIndexOffsetFold() : TestRunnable("small index offset for fold (44)(222)", 5, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3};
    IntSequence nvs {4, 2};
    return index_offset<FGSTensor>(s, nvs);
  }
};

class SmallIndexOffsetUnfold : public TestRunnable
{
public:
  SmallIndexOffsetUnfold() : TestRunnable("small index offset for unfold (44)(222)", 5, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3};
    IntSequence nvs {4, 2};
    return index_offset<UGSTensor>(s, nvs);
  }
};

class IndexOffsetFold : public TestRunnable
{
public:
  IndexOffsetFold() : TestRunnable("index offset for fold (55)(222)(22)", 5, 5)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {5, 2, 2};
    return index_offset<FGSTensor>(s, nvs);
  }
};

class IndexOffsetUnfold : public TestRunnable
{
public:
  IndexOffsetUnfold() : TestRunnable("index offset for unfold (55)(222)(22)", 7, 5)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 3, 2};
    IntSequence nvs {5, 2, 2};
    return index_offset<UGSTensor>(s, nvs);
  }
};

class SmallFoldUnfoldFS : public TestRunnable
{
public:
  SmallFoldUnfoldFS() : TestRunnable("small fold-unfold for full symmetry (444)", 3, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return fs_fold_unfold(5, 4, 3);
  }
};

class SmallFoldUnfoldGS : public TestRunnable
{
public:
  SmallFoldUnfoldGS() : TestRunnable("small fold-unfold for gen symmetry (3)(33)(22)", 5, 3)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {1, 2, 2};
    IntSequence nvs {3, 3, 2};
    return gs_fold_unfold(5, s, nvs);
  }
};

class FoldUnfoldFS : public TestRunnable
{
public:
  FoldUnfoldFS() : TestRunnable("fold-unfold for full symmetry (9999)", 4, 9)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return fs_fold_unfold(5, 9, 4);
  }
};

class FoldUnfoldGS : public TestRunnable
{
public:
  FoldUnfoldGS() : TestRunnable("fold-unfold for gen symmetry (66)(2)(66)", 5, 6)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    Symmetry s {2, 1, 2};
    IntSequence nvs {6, 2, 6};
    return gs_fold_unfold(5, s, nvs);
  }
};

class SmallFoldUnfoldR : public TestRunnable
{
public:
  SmallFoldUnfoldR() : TestRunnable("small fold-unfold for row full symmetry (333)", 3, 3)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return r_fold_unfold(5, 3, 3);
  }
};

class FoldUnfoldR : public TestRunnable
{
public:
  FoldUnfoldR() : TestRunnable("fold-unfold for row full symmetry (66666)", 5, 6)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return r_fold_unfold(5, 6, 5);
  }
};

class SmallDenseProd : public TestRunnable
{
public:
  SmallDenseProd() : TestRunnable("small dense prod bsym=1-2,nvs=3-2,h=2-3,r=2", 3, 3)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    IntSequence bnvs {3, 2};
    return dense_prod(Symmetry {1, 2}, bnvs, 2, 3, 2);
  }
};

class DenseProd : public TestRunnable
{
public:
  DenseProd() : TestRunnable("dense prod bsym=2-3,nvs=10-7,h=3-15,r=10", 5, 15)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    IntSequence bnvs {10, 7};
    return dense_prod(Symmetry {2, 3}, bnvs, 3, 15, 10);
  }
};

class BigDenseProd : public TestRunnable
{
public:
  BigDenseProd() : TestRunnable("dense prod bsym=3-2,nvs=13-11,h=3-20,r=20", 6, 20)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    IntSequence bnvs {13, 11};
    return dense_prod(Symmetry {3, 2}, bnvs, 3, 20, 20);
  }
};

class SmallFoldedMonomial : public TestRunnable
{
public:
  SmallFoldedMonomial() : TestRunnable("folded vrs. monoms (g,x,y,u)=(10,4,5,3), dim=4", 4, 8)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return folded_monomial(10, 4, 5, 3, 4);
  }
};

class FoldedMonomial : public TestRunnable
{
public:
  FoldedMonomial() : TestRunnable("folded vrs. monoms (g,x,y,u)=(20,12,10,5), dim=4", 4, 15)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return folded_monomial(20, 12, 10, 5, 4);
  }
};

class SmallUnfoldedMonomial : public TestRunnable
{
public:
  SmallUnfoldedMonomial() : TestRunnable("unfolded vrs. monoms (g,x,y,u)=(10,4,5,3), dim=4", 4, 8)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfolded_monomial(10, 4, 5, 3, 4);
  }
};

class UnfoldedMonomial : public TestRunnable
{
public:
  UnfoldedMonomial() : TestRunnable("unfolded vrs. monoms (g,x,y,u)=(20,12,10,5), dim=4", 4, 15)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfolded_monomial(20, 12, 10, 5, 4);
  }
};

class FoldedContractionSmall : public TestRunnable
{
public:
  FoldedContractionSmall() : TestRunnable("folded contraction small (r=5, nv=4, dim=3)", 3, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return folded_contraction(5, 4, 3);
  }
};

class FoldedContractionBig : public TestRunnable
{
public:
  FoldedContractionBig() : TestRunnable("folded contraction big (r=20, nv=12, dim=5)", 5, 12)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return folded_contraction(20, 12, 5);
  }
};

class UnfoldedContractionSmall : public TestRunnable
{
public:
  UnfoldedContractionSmall() : TestRunnable("unfolded contraction small (r=5, nv=4, dim=3)", 3, 4)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfolded_contraction(5, 4, 3);
  }
};

class UnfoldedContractionBig : public TestRunnable
{
public:
  UnfoldedContractionBig() : TestRunnable("unfolded contraction big (r=20, nv=12, dim=5)", 5, 12)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfolded_contraction(20, 12, 5);
  }
};

class PolyEvalSmall : public TestRunnable
{
public:
  PolyEvalSmall() : TestRunnable("polynomial evaluation small (r=4, nv=5, maxdim=4)", 4, 5)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return poly_eval(4, 5, 4);
  }
};

class PolyEvalBig : public TestRunnable
{
public:
  PolyEvalBig() : TestRunnable("polynomial evaluation big (r=244, nv=97, maxdim=2)", 2, 97)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return poly_eval(244, 97, 2);
  }
};

class FoldZContSmall : public TestRunnable
{
public:
  FoldZContSmall() : TestRunnable("folded Z container (r=3,ny=2,nu=2,nup=1,G=2,g=2,dim=3)", 3, 8)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return fold_zcont(3, 2, 2, 1, 2, 2, 3);
  }
};

class FoldZCont : public TestRunnable
{
public:
  FoldZCont() : TestRunnable("folded Z container (r=13,ny=5,nu=7,nup=4,G=6,g=7,dim=4)", 4, 25)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return fold_zcont(13, 5, 7, 4, 6, 7, 4);
  }
};

class UnfoldZContSmall : public TestRunnable
{
public:
  UnfoldZContSmall() :
      TestRunnable("unfolded Z container (r=3,ny=2,nu=2,nup=1,G=2,g=2,dim=3)", 3, 8)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfold_zcont(3, 2, 2, 1, 2, 2, 3);
  }
};

class UnfoldZCont : public TestRunnable
{
public:
  UnfoldZCont() : TestRunnable("unfolded Z container (r=13,ny=5,nu=7,nup=4,G=6,g=7,dim=4", 4, 25)
  {
  }
  [[nodiscard]] bool
  run() const override
  {
    return unfold_zcont(13, 5, 7, 4, 6, 7, 4);
  }
};

int
main()
{
  std::vector<std::unique_ptr<TestRunnable>> all_tests;
  // fill in vector of all tests
  all_tests.push_back(std::make_unique<SmallIndexForwardFold>());
  all_tests.push_back(std::make_unique<SmallIndexForwardUnfold>());
  all_tests.push_back(std::make_unique<IndexForwardFold>());
  all_tests.push_back(std::make_unique<IndexForwardUnfold>());
  all_tests.push_back(std::make_unique<SmallIndexBackwardFold>());
  all_tests.push_back(std::make_unique<IndexBackwardFold>());
  all_tests.push_back(std::make_unique<SmallIndexBackwardUnfold>());
  all_tests.push_back(std::make_unique<IndexBackwardUnfold>());
  all_tests.push_back(std::make_unique<SmallIndexOffsetFold>());
  all_tests.push_back(std::make_unique<SmallIndexOffsetUnfold>());
  all_tests.push_back(std::make_unique<IndexOffsetFold>());
  all_tests.push_back(std::make_unique<IndexOffsetUnfold>());
  all_tests.push_back(std::make_unique<SmallFoldUnfoldFS>());
  all_tests.push_back(std::make_unique<SmallFoldUnfoldGS>());
  all_tests.push_back(std::make_unique<FoldUnfoldFS>());
  all_tests.push_back(std::make_unique<FoldUnfoldGS>());
  all_tests.push_back(std::make_unique<SmallFoldUnfoldR>());
  all_tests.push_back(std::make_unique<FoldUnfoldR>());
  all_tests.push_back(std::make_unique<SmallDenseProd>());
  all_tests.push_back(std::make_unique<DenseProd>());
  all_tests.push_back(std::make_unique<BigDenseProd>());
  all_tests.push_back(std::make_unique<SmallFoldedMonomial>());
  all_tests.push_back(std::make_unique<FoldedMonomial>());
  all_tests.push_back(std::make_unique<SmallUnfoldedMonomial>());
  all_tests.push_back(std::make_unique<UnfoldedMonomial>());
  all_tests.push_back(std::make_unique<FoldedContractionSmall>());
  all_tests.push_back(std::make_unique<FoldedContractionBig>());
  all_tests.push_back(std::make_unique<UnfoldedContractionSmall>());
  all_tests.push_back(std::make_unique<UnfoldedContractionBig>());
  all_tests.push_back(std::make_unique<PolyEvalSmall>());
  all_tests.push_back(std::make_unique<PolyEvalBig>());
  all_tests.push_back(std::make_unique<FoldZContSmall>());
  all_tests.push_back(std::make_unique<FoldZCont>());
  all_tests.push_back(std::make_unique<UnfoldZContSmall>());
  all_tests.push_back(std::make_unique<UnfoldZCont>());

  /* Initialize library. We do it for each test individually instead of
     computing the maximum dimension and number of variables, because otherwise
     it does not pass the check on maximum problem size. */
  for (const auto& test : all_tests)
    TLStatic::init(test->dim, test->nvar);

  thread_pool::initialize();

  // Launch the tests
  int success = 0;
  for (const auto& test : all_tests)
    {
      try
        {
          if (test->test())
            success++;
        }
      catch (const TLException& e)
        {
          std::cout << "Caught TL exception in <" << test->name << ">:\n";
          e.print();
        }
      catch (SylvException& e)
        {
          std::cout << "Caught Sylv exception in <" << test->name << ">:\n";
          e.printMessage();
        }
    }

  int nfailed = all_tests.size() - success;
  std::cout << "There were " << nfailed << " tests that failed out of " << all_tests.size()
            << " tests run." << '\n';

  if (nfailed)
    return EXIT_FAILURE;
  else
    return EXIT_SUCCESS;
}
