
/**************************************************************************
 *                                                                        *
 *  Regina - A Normal Surface Theory Calculator                           *
 *  Test Suite                                                            *
 *                                                                        *
 *  Copyright (c) 1999-2025, Ben Burton                                   *
 *  For further details contact Ben Burton (bab@debian.org).              *
 *                                                                        *
 *  This program 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 2 of the    *
 *  License, or (at your option) any later version.                       *
 *                                                                        *
 *  As an exception, when this program is distributed through (i) the     *
 *  App Store by Apple Inc.; (ii) the Mac App Store by Apple Inc.; or     *
 *  (iii) Google Play by Google Inc., then that store may impose any      *
 *  digital rights management, device limits and/or redistribution        *
 *  restrictions that are required by its terms of service.               *
 *                                                                        *
 *  This program 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 this program. If not, see <https://www.gnu.org/licenses/>. *
 *                                                                        *
 **************************************************************************/

#include "algebra/abeliangroup.h"
#include "algebra/grouppresentation.h"
#include "packet/container.h"
#include "triangulation/example.h"
#include "triangulation/isosigtype.h"
#include "triangulation/detail/isosig-impl.h"
#include "utilities/typeutils.h"

#include "utilities/tightencodingtest.h"

using regina::Isomorphism;
using regina::Perm;
using regina::Simplex;
using regina::Triangulation;
using regina::Vertex;

// A size above which we will omit homology checks, in settings where
// operations are performed many times and speed is becoming a problem.
static constexpr size_t HOMOLOGY_THRESHOLD = 40;

/**
 * Clears all computed properties of the given triangulation.
 *
 * We allow the triangulation to be const, since the intent of this operation
 * is to not change the triangulation, but just to force it to forget its
 * cached properties.
 */
template <int dim>
static void clearProperties(const Triangulation<dim>& tri) {
    // Make and undo a trivial modification that will cause all
    // computed properties to be flushed.
    const_cast<Triangulation<dim>&>(tri).newSimplex();
    const_cast<Triangulation<dim>&>(tri).removeSimplexAt(tri.size()-1);
}

/**
 * Implements several tests for triangulations in dimension \a dim.
 *
 * Test fixtures in each dimension should use TriangulationTest<dim>
 * as a base class, since this base class provides example triangulations
 * that can be shared between tests.
 */
template <int dim>
class TriangulationTest : public testing::Test {
    public:
        struct TestCase {
            Triangulation<dim> tri;
            const char* name;
        };

        /**
         * We make detailed skeletal tests public because we may wish to
         * access them from test suites in other dimensions.
         */
        static void verifyComponentsDetail(const Triangulation<dim>& tri) {
            bool allOrbl = true;
            size_t totSize = 0;
            size_t totBdry = 0;
            size_t totBdryFacets = 0;
            for (auto c : tri.components()) {
                bool allOrblInComponent = true;
                bool allValidInComponent = true;
                size_t boundaryFacets = 0;
                size_t doubleDualTree = 0;

                totSize += c->size();
                for (auto s : c->simplices()) {
                    EXPECT_EQ(s->component(), c);
                    EXPECT_EQ(std::abs(s->orientation()), 1);
                    for (int i = 0; i <= dim; ++i) {
                        if (auto adj = s->adjacentSimplex(i)) {
                            if (s->adjacentGluing(i).sign() > 0) {
                                if (adj->orientation() != - s->orientation())
                                    allOrbl = allOrblInComponent = false;
                            } else {
                                if (adj->orientation() != s->orientation())
                                    allOrbl = allOrblInComponent = false;
                            }
                            if (s->facetInMaximalForest(i)) {
                                ++doubleDualTree;
                                EXPECT_TRUE(adj->facetInMaximalForest(
                                    s->adjacentFacet(i)));
                            }
                        } else {
                            ++boundaryFacets;
                            ++totBdryFacets;
                            EXPECT_FALSE(s->facetInMaximalForest(i));
                        }
                    }
                }

                totBdry += c->countBoundaryComponents();
                for (auto b : c->boundaryComponents())
                    EXPECT_EQ(b->component(), c);

                // See if this component contains any invalid faces.
                regina::for_constexpr<0, dim>([c, &allValidInComponent](
                        auto subdim) {
                    if constexpr (regina::standardDim(dim)) {
                        // Access faces directly from the component.
                        for (auto f : c->template faces<subdim>())
                            if (! f->isValid()) {
                                allValidInComponent = false;
                                return; // from lambda
                            }
                    } else {
                        // Access faces via the top-dimensional simplices.
                        for (auto s : c->simplices())
                            for (size_t j = 0;
                                    j < regina::Face<dim, subdim>::nFaces; ++j)
                                if (! s->template face<subdim>(j)->isValid()) {
                                    allValidInComponent = false;
                                    return; // from lambda
                                }
                    }
                });

                EXPECT_EQ(c->isOrientable(), allOrblInComponent);
                EXPECT_EQ(c->isValid(), allValidInComponent);
                EXPECT_EQ(c->countBoundaryFacets(), boundaryFacets);
                EXPECT_EQ(doubleDualTree, 2 * (c->size() - 1));
            }
            EXPECT_EQ(tri.isOrientable(), allOrbl);
            EXPECT_EQ(tri.size(), totSize);
            EXPECT_EQ(tri.countBoundaryComponents(), totBdry);
            EXPECT_EQ(tri.countBoundaryFacets(), totBdryFacets);
        }

        static void verifyBoundaryComponentsDetail(
                const Triangulation<dim>& tri) {
            size_t totBdryFacets = 0;
            for (auto b : tri.boundaryComponents()) {
                totBdryFacets += b->size();
                EXPECT_EQ(b->size() * dim, b->countRidges() * 2);

                size_t builtSize = 0;
                if (b->isReal()) {
                    builtSize = b->size();

                    for (auto f : b->facets())
                        EXPECT_EQ(f->boundaryComponent(), b);

                    // NOTE: Below we test whether face->boundaryComponent()
                    // matches the boundary component containing face.
                    // This test could fail for legitimate reasons if the face
                    // is pinched between two different boundary components.
                    // However, none of our test cases have this property,
                    // and so we leave the tests as they are for now.
                    if constexpr (regina::BoundaryComponent<dim>::allFaces) {
                        // Access faces directly from the boundary component.
                        regina::for_constexpr<0, dim-1>([b](auto subdim) {
                            for (auto f : b->template faces<subdim>()) {
                                EXPECT_EQ(f->boundaryComponent(), b);
                            }
                        });
                    } else {
                        // Access faces via the boundary facets.
                        for (auto f : b->facets()) {
                            regina::for_constexpr<0, dim-1>(
                                    [b, f](auto subdim) {
                                // Check all subdim-faces of f.
                                for (size_t j = 0;
                                        j < regina::Face<dim-1, subdim>::nFaces;
                                        ++j) {
                                    auto sub = f->template face<subdim>(j);
                                    EXPECT_EQ(sub->boundaryComponent(), b);
                                }
                            });
                        }
                    }
                } else {
                    if constexpr (regina::BoundaryComponent<dim>::allowVertex) {
                        EXPECT_EQ(b->countVertices(), 1);
                        regina::for_constexpr<1, dim>([b](auto subdim) {
                            EXPECT_EQ(b->template countFaces<subdim>(), 0);
                        });

                        auto v = b->vertex(0);
                        builtSize = v->degree();
                        // NOTE: This next test could fail for legitimate
                        // reasons if the vertex is pinched between two
                        // different boundary components.  See above for
                        // further explanation.
                        EXPECT_EQ(v->boundaryComponent(), b);
                    } else {
                        ADD_FAILURE() << "Vertex-only boundary component "
                            "not allowed in this dimension";
                    }
                }

                if constexpr (dim > 2) {
                    Triangulation<dim - 1> built = b->build();
                    EXPECT_EQ(built.size(), builtSize);
                    EXPECT_EQ(built.isOrientable(), b->isOrientable());
                }
            }
            EXPECT_EQ(tri.countBoundaryFacets(), totBdryFacets);
        }

        static void verifyFacesDetail(const Triangulation<dim>& tri) {
            bool allValid = true;
            regina::for_constexpr<0, dim>([&tri, &allValid](auto subdim) {
                size_t bdry = 0;
                size_t degreeSum = 0;
                for (auto f : tri.template faces<subdim>()) {
                    SCOPED_TRACE_NUMERIC(subdim);

                    if (! f->isValid())
                        allValid = false;
                    if (f->isBoundary())
                        ++bdry;
                    degreeSum += f->degree();

                    EXPECT_EQ(f->component(),
                        f->front().simplex()->component());

                    if (! f->hasBadIdentification()) {
                        // We already test link orientability more precisely
                        // for dim == 3,4 further below (we compare the
                        // cached link orientability to the
                        // orientability of the full triangulated link).
                        // Therefore the tests here only need to be things
                        // that are relevant in higher dimensions.
                        if (f->isLinkOrientable()) {
                            // What is there that's sensible to test here?
                        } else {
                            EXPECT_FALSE(f->component()->isOrientable());
                        }
                    }

                    size_t embIndex = 0;
                    for (auto emb : f->embeddings()) {
                        auto s = emb.simplex();
                        auto v = emb.vertices();
                        auto which = regina::Face<dim, subdim>::faceNumber(v);
                        EXPECT_EQ(s->template face<subdim>(which), f);
                        EXPECT_EQ(s->template faceMapping<subdim>(which), v);

                        if (s->component()->isOrientable()) {
                            if constexpr (subdim < dim - 1) {
                                EXPECT_EQ(v.sign(), s->orientation());
                            } else {
                                if (embIndex == 0) {
                                    EXPECT_EQ(v.sign(), s->orientation());
                                } else {
                                    EXPECT_EQ(v.sign(), - s->orientation());
                                }
                            }
                        }

                        for (int i = subdim + 1; i <= dim; ++i) {
                            if (auto adj = s->adjacentSimplex(v[i])) {
                                Perm adjMap = s->adjacentGluing(v[i]) * v;
                                Perm crossMap =
                                    adj->template faceMapping<subdim>(
                                    regina::Face<dim, subdim>::faceNumber(
                                    adjMap)).inverse() * adjMap;
                                // The permutation crossMap is essentially
                                // a "gluing map" for the implicit vertices
                                // of f and the implicit vertices of the
                                // face opposite f when we step across
                                // facet v[i] of simplex s.

                                if constexpr (subdim < dim - 1) {
                                    if (f->isLinkOrientable()) {
                                        // The faces opposite f should have
                                        // consistent implicit orientations.
                                        // Here we need to ignore the images
                                        // of i ≤ subdim.
                                        SCOPED_TRACE("Gluings for faces "
                                            "opposite f");
                                        Perm reverse = Perm<dim+1>().reverse();
                                        Perm p = reverse * crossMap * reverse;
                                        p.clear(dim - subdim);
                                        EXPECT_EQ(p.sign(), -1);
                                    }
                                }

                                if (! f->hasBadIdentification()) {
                                    // For the vertices of f itself, this map
                                    // should be the identity.  Here we need
                                    // to ignore the images of i ≥ subdim+1.
                                    SCOPED_TRACE("Gluings for f");
                                    Perm p = crossMap;
                                    p.clear(subdim + 1);
                                    EXPECT_TRUE(p.isIdentity());
                                }
                            }
                        }

                        ++embIndex;
                    }

                    if constexpr (subdim == dim - 2) {
                        // For codimension 2, the order of embeddings is
                        // specified precisely.
                        for (size_t i = 1; i < f->degree(); ++i) {
                            auto prev = f->embedding(i - 1);
                            auto next = f->embedding(i);

                            int facet = prev.vertices()[dim - 1];
                            auto adj = prev.simplex()->adjacentSimplex(facet);
                            EXPECT_EQ(next.simplex(), adj);
                            if (adj) {
                                EXPECT_EQ(next.vertices(),
                                    prev.simplex()->adjacentGluing(facet) *
                                    prev.vertices() * Perm<dim+1>(dim-1, dim));
                            }
                        }
                    }
                }
                constexpr size_t nFaces = regina::Face<dim, subdim>::nFaces;
                EXPECT_EQ(bdry, tri.template countBoundaryFaces<subdim>());
                EXPECT_EQ(degreeSum, tri.size() * nFaces);
            });
            EXPECT_EQ(tri.isValid(), allValid);

            if constexpr (dim == 3) {
                // All triangle types should, at this point, be not yet
                // determined.
                for (auto t : tri.triangles()) {
                    int sub = t->triangleSubtype();
                    switch (t->triangleType()) {
                        case regina::TriangleType::Triangle:
                        case regina::TriangleType::Parachute:
                        case regina::TriangleType::L31:
                            EXPECT_EQ(sub, -1);
                            break;

                        case regina::TriangleType::Scarf:
                        case regina::TriangleType::Cone:
                        case regina::TriangleType::Mobius:
                        case regina::TriangleType::Horn:
                        case regina::TriangleType::DunceHat:
                            EXPECT_GE(sub, 0);
                            EXPECT_LE(sub, 2);
                            break;

                        default:
                            ADD_FAILURE() << "Unexpected triangle type";
                            break;
                    }
                }
            }
        }

        static void verifySkeletonDetail(const Triangulation<dim>& tri) {
            verifyComponentsDetail(tri);
            verifyBoundaryComponentsDetail(tri);
            verifyFacesDetail(tri);

            // Additional skeletal data for low dimensions:
            if constexpr (regina::standardDim(dim)) {
                regina::for_constexpr<0, dim>([&tri](auto subdim) {
                    size_t count = 0;
                    for (auto c : tri.components()) {
                        for (auto f : c->template faces<subdim>()) {
                            EXPECT_EQ(f->component(), c);
                            ++count;
                        }
                    }
                    EXPECT_EQ(count, tri.template countFaces<subdim>());
                });
            }
            if constexpr (dim == 3 || dim == 4) {
                bool foundIdeal = false;
                bool allStandard = true;
                for (auto c : tri.components()) {
                    bool foundIdealInComponent = false;
                    for (auto v : c->vertices()) {
                        if (v->isIdeal())
                            foundIdeal = foundIdealInComponent = true;

                        const auto& link = v->buildLink();
                        EXPECT_EQ(v->isLinkOrientable(), link.isOrientable());

                        if (link.isSphere()) {
                            EXPECT_TRUE(v->isValid());
                            EXPECT_FALSE(v->isIdeal());
                            if constexpr (dim == 3)
                                EXPECT_EQ(v->linkType(),
                                    Vertex<dim>::Link::Sphere);
                        } else if (link.isBall()) {
                            EXPECT_TRUE(v->isValid());
                            EXPECT_FALSE(v->isIdeal());
                            if constexpr (dim == 3)
                                EXPECT_EQ(v->linkType(),
                                    Vertex<dim>::Link::Disc);
                        } else if (link.isValid() && link.isClosed()) {
                            EXPECT_TRUE(v->isValid());
                            EXPECT_TRUE(v->isIdeal());
                            if constexpr (dim == 3) {
                                if (link.eulerCharTri() == 0) {
                                    if (link.isOrientable())
                                        EXPECT_EQ(v->linkType(),
                                            Vertex<dim>::Link::Torus);
                                    else
                                        EXPECT_EQ(v->linkType(),
                                            Vertex<dim>::Link::KleinBottle);
                                } else {
                                    allStandard = false;
                                    EXPECT_EQ(v->linkType(),
                                        Vertex<dim>::Link::NonStandardCusp);
                                }
                            }
                        } else {
                            allStandard = false;
                            EXPECT_FALSE(v->isValid());
                            EXPECT_FALSE(v->isIdeal());
                            if constexpr (dim == 3)
                                EXPECT_EQ(v->linkType(),
                                    Vertex<dim>::Link::Invalid);
                        }

                        if constexpr (dim == 3)
                            EXPECT_EQ(link.eulerCharTri(), v->linkEulerChar());
                    }
                    if constexpr (dim == 4) {
                        for (auto e : c->edges()) {
                            const auto& link = e->buildLink();
                            EXPECT_EQ(e->isLinkOrientable(),
                                link.isOrientable());
                            EXPECT_EQ(e->hasBadLink(),
                                ! (link.isSphere() || link.isBall()));
                        }
                    }
                    EXPECT_EQ(c->isIdeal(), foundIdealInComponent);
                }
                if constexpr (dim == 4) {
                    // In 4-D, we restrict the notion of "ideal triangulations"
                    // to only include valid triangulations.
                    // See Triangulation<4>::isIdeal() for why.
                    if (tri.isValid())
                        EXPECT_EQ(tri.isIdeal(), foundIdeal);
                    else
                        EXPECT_FALSE(tri.isIdeal());
                } else /* dim == 3 */ {
                    EXPECT_EQ(tri.isIdeal(), foundIdeal);
                    EXPECT_EQ(tri.isStandard(), allStandard);
                }
            }
        }

    protected:
        // Trivial case:
        TestCase empty { {}, "Empty" };

        // Closed orientable triangulations:
        TestCase sphere { regina::Example<dim>::sphere(), "Sphere" };
        TestCase simpSphere { regina::Example<dim>::simplicialSphere(),
            "Simplicial sphere" };
        TestCase sphereBundle { regina::Example<dim>::sphereBundle(),
            "Sphere bundle" };

        // Closed non-orientable triangulations:
        TestCase twistedSphereBundle {
            regina::Example<dim>::twistedSphereBundle(),
            "Twisted sphere bundle" };

        // Triangulations with real boundary:
        TestCase ball { regina::Example<dim>::ball(), "Ball" };
        TestCase ballBundle { regina::Example<dim>::ballBundle(),
            "Ball bundle" };
        TestCase twistedBallBundle { regina::Example<dim>::twistedBallBundle(),
            "Twisted ball bundle" };

        /**
         * Run the given test over all of the example triangulations stored in
         * this generic test fixture.
         */
        void testGenericCases(
                void (*f)(const regina::Triangulation<dim>&, const char*)) {
            f(empty.tri, empty.name);
            f(sphere.tri, sphere.name);
            f(simpSphere.tri, simpSphere.name);
            f(sphereBundle.tri, sphereBundle.name);
            f(twistedSphereBundle.tri, twistedSphereBundle.name);
            f(ball.tri, ball.name);
            f(ballBundle.tri, ballBundle.name);
            f(twistedBallBundle.tri, twistedBallBundle.name);
        }

        static void verifyValid(const TestCase& test) {
            SCOPED_TRACE_CSTRING(test.name);

            EXPECT_TRUE(test.tri.isValid());

            regina::for_constexpr<0, dim>([&test](auto subdim) {
                SCOPED_TRACE_NUMERIC(subdim);
                for (size_t i = 0; i < test.tri.template countFaces<subdim>();
                        ++i) {
                    SCOPED_TRACE_NAMED_NUMERIC("face", i);
                    auto f = test.tri.template face<subdim>(i);

                    EXPECT_TRUE(f->isValid());
                    EXPECT_FALSE(f->hasBadIdentification());
                    if constexpr (regina::standardDim(dim))
                        EXPECT_FALSE(f->hasBadLink());
                }
            });
        }

        void validityGenericCases() {
            verifyValid(empty);
            verifyValid(sphere);
            verifyValid(simpSphere);
            verifyValid(sphereBundle);
            verifyValid(twistedSphereBundle);
            verifyValid(ball);
            verifyValid(ballBundle);
            verifyValid(twistedBallBundle);
        }

        void connectivityGenericCases() {
            EXPECT_TRUE(empty.tri.isConnected());
            EXPECT_TRUE(sphere.tri.isConnected());
            EXPECT_TRUE(simpSphere.tri.isConnected());
            EXPECT_TRUE(sphereBundle.tri.isConnected());
            EXPECT_TRUE(twistedSphereBundle.tri.isConnected());
            EXPECT_TRUE(ball.tri.isConnected());
            EXPECT_TRUE(ballBundle.tri.isConnected());
            EXPECT_TRUE(twistedBallBundle.tri.isConnected());
        }

        void orientabilityGenericCases() {
            EXPECT_TRUE(empty.tri.isOrientable());
            EXPECT_TRUE(sphere.tri.isOrientable());
            EXPECT_TRUE(simpSphere.tri.isOrientable());
            EXPECT_TRUE(sphereBundle.tri.isOrientable());
            EXPECT_FALSE(twistedSphereBundle.tri.isOrientable());
            EXPECT_TRUE(ball.tri.isOrientable());
            EXPECT_TRUE(ballBundle.tri.isOrientable());
            EXPECT_FALSE(twistedBallBundle.tri.isOrientable());
        }

        void eulerCharGenericCases() {
            EXPECT_EQ(empty.tri.eulerCharTri(), 0);
            EXPECT_EQ(sphere.tri.eulerCharTri(), (dim % 2 ? 0 : 2));
            EXPECT_EQ(simpSphere.tri.eulerCharTri(), (dim % 2 ? 0 : 2));
            EXPECT_EQ(sphereBundle.tri.eulerCharTri(), 0);
            EXPECT_EQ(twistedSphereBundle.tri.eulerCharTri(), 0);
            EXPECT_EQ(ball.tri.eulerCharTri(), 1);
            EXPECT_EQ(ballBundle.tri.eulerCharTri(), 0);
            EXPECT_EQ(twistedBallBundle.tri.eulerCharTri(), 0);

            if constexpr (regina::standardDim(dim) && dim > 2) {
                // In these dimensions, Regina understands ideal triangulations
                // and thus offers a separate function eulerCharManifold().
                EXPECT_EQ(empty.tri.eulerCharManifold(), 0);
                EXPECT_EQ(sphere.tri.eulerCharManifold(), (dim % 2 ? 0 : 2));
                EXPECT_EQ(simpSphere.tri.eulerCharManifold(),
                    (dim % 2 ? 0 : 2));
                EXPECT_EQ(sphereBundle.tri.eulerCharManifold(), 0);
                EXPECT_EQ(twistedSphereBundle.tri.eulerCharManifold(), 0);
                EXPECT_EQ(ball.tri.eulerCharManifold(), 1);
                EXPECT_EQ(ballBundle.tri.eulerCharManifold(), 0);
                EXPECT_EQ(twistedBallBundle.tri.eulerCharManifold(), 0);
            }
        }

        static void verifyBoundaryBasic(const TestCase& test,
                std::initializer_list<long> expectReal,
                std::initializer_list<long> expectIdeal,
                std::initializer_list<long> expectInvalid) {
            // Verifies boundary counts, types, and (where boundary face
            // counts are available) Euler characteristics.
            SCOPED_TRACE_CSTRING(test.name);

            EXPECT_EQ(test.tri.countBoundaryComponents(),
                expectReal.size() + expectIdeal.size() + expectInvalid.size());
            EXPECT_EQ(test.tri.hasBoundaryFacets(), (expectReal.size() > 0));

            if constexpr (regina::standardDim(dim)) {
                // These dimensions offer functions to query closedness and
                // ideal boundary components.
                EXPECT_EQ(test.tri.isClosed(), (expectReal.size() +
                    expectIdeal.size() + expectInvalid.size() == 0));
                if constexpr (dim < 4) {
                    // Ideal invalid triangulations are allowed.
                    EXPECT_EQ(test.tri.isIdeal(), expectIdeal.size() > 0);
                } else {
                    // To be considered ideal, a triangulation _must_ be valid.
                    EXPECT_EQ(test.tri.isIdeal(),
                        (test.tri.isValid() && expectIdeal.size() > 0));
                }
                if constexpr (dim == 2)
                    EXPECT_EQ(expectIdeal.size(), 0);
                if constexpr (dim <= 3)
                    EXPECT_EQ(expectInvalid.size(), 0);
            } else {
                // These dimensions only support real boundary components.
                EXPECT_EQ(expectIdeal.size(), 0);
                EXPECT_EQ(expectInvalid.size(), 0);
            }

            auto nextReal = expectReal.begin();
            auto nextIdeal = expectIdeal.begin();
            auto nextInvalid = expectInvalid.begin();

            for (auto b : test.tri.boundaryComponents()) {
                if (b->isIdeal()) {
                    EXPECT_FALSE(b->isReal());
                    EXPECT_FALSE(b->isInvalidVertex());

                    if (nextIdeal == expectIdeal.end())
                        ADD_FAILURE() << "Too many ideal boundary components";
                    else {
                        if constexpr (regina::BoundaryComponent<dim>::allFaces)
                            EXPECT_EQ(b->eulerChar(), *nextIdeal);
                        ++nextIdeal;
                    }
                } else if (b->isInvalidVertex()) {
                    EXPECT_FALSE(b->isReal());
                    EXPECT_FALSE(b->isIdeal());

                    if (nextInvalid == expectInvalid.end())
                        ADD_FAILURE() << "Too many invalid boundary components";
                    else {
                        if constexpr (regina::BoundaryComponent<dim>::allFaces)
                            EXPECT_EQ(b->eulerChar(), *nextInvalid);
                        ++nextInvalid;
                    }
                } else {
                    EXPECT_TRUE(b->isReal());
                    EXPECT_FALSE(b->isIdeal());
                    EXPECT_FALSE(b->isInvalidVertex());

                    if (nextReal == expectReal.end())
                        ADD_FAILURE() << "Too many real boundary components";
                    else {
                        if constexpr (regina::BoundaryComponent<dim>::allFaces)
                            EXPECT_EQ(b->eulerChar(), *nextReal);
                        ++nextReal;
                    }
                }
            }

            EXPECT_EQ(nextReal, expectReal.end());
            EXPECT_EQ(nextIdeal, expectIdeal.end());
            EXPECT_EQ(nextInvalid, expectInvalid.end());
        }

        void boundaryBasicGenericCases() {
            verifyBoundaryBasic(empty, {}, {}, {});
            verifyBoundaryBasic(sphere, {}, {}, {});
            verifyBoundaryBasic(simpSphere, {}, {}, {});
            verifyBoundaryBasic(sphereBundle, {}, {}, {});
            verifyBoundaryBasic(twistedSphereBundle, {}, {}, {});
            verifyBoundaryBasic(ball, {dim % 2 ? 2 : 0}, {}, {});
            if constexpr (dim == 2) {
                verifyBoundaryBasic(ballBundle, {0, 0}, {}, {});
            } else {
                verifyBoundaryBasic(ballBundle, {0}, {}, {});
            }
            verifyBoundaryBasic(twistedBallBundle, {0}, {}, {});
        }

        static void verifyBoundaryPinching(const Triangulation<dim>& tri,
                const char* name) {
            static_assert(dim > 2 && regina::BoundaryComponent<dim>::allFaces);

            for (auto* bc : tri.boundaryComponents()) {
                if (bc->size() == 0)
                    continue;

                // We have boundary facets.  Look for pinched faces.
                long adjEuler = 0;
                regina::for_constexpr<0, dim - 2>([bc, &adjEuler](auto subdim) {
                    for (auto f : bc->template faces<subdim>())
                        if (! f->isValid()) {
                            // Beware: face links themselves can have both real
                            // and ideal boundary components.
                            size_t realBdries = 0;
                            for (auto c : f->buildLink().boundaryComponents())
                                if (c->size() > 0)
                                    ++realBdries;
                            if (realBdries > 1) {
                                if constexpr (subdim % 2 == 0)
                                    adjEuler -= (realBdries - 1);
                                else
                                    adjEuler += (realBdries - 1);
                            }
                        }
                });

                EXPECT_EQ(bc->eulerChar(),
                    bc->build().eulerCharTri() + adjEuler);
            }
        }

        static void verifyVertexLinksBasic(const TestCase& test,
                size_t expectSphere, size_t expectBall,
                size_t expectIdeal = 0, size_t expectInvalid = 0) {
            // In higher (non-standard) dimensions regina cannot recognise
            // ideal vertices, and so we treat expectSphere / expectBall as
            // simply "not on real boundary" / "on real boundary".
            SCOPED_TRACE_CSTRING(test.name);

            using regina::Vertex;

            size_t foundSphere = 0, foundBall = 0;
            size_t foundIdeal = 0, foundInvalid = 0;
            for (auto v : test.tri.vertices()) {
                if constexpr (dim > 2 && regina::standardDim(dim)) {
                    if (! v->isValid()) {
                        ++foundInvalid;
                        if constexpr (dim == 3) {
                            EXPECT_TRUE(v->isBoundary());
                            EXPECT_FALSE(v->isLinkClosed());
                            EXPECT_FALSE(v->isStandard());
                            EXPECT_FALSE(v->isIdeal());
                            EXPECT_EQ(v->linkType(),
                                Vertex<dim>::Link::Invalid);
                        }
                    } else if (v->isIdeal()) {
                        ++foundIdeal;
                        if constexpr (dim == 3) {
                            using Link = typename Vertex<dim>::Link;
                            EXPECT_TRUE(v->isBoundary());
                            EXPECT_TRUE(v->isLinkClosed());
                            if (v->isStandard()) {
                                EXPECT_TRUE(
                                    v->linkType() == Link::Torus ||
                                    v->linkType() == Link::KleinBottle);
                                EXPECT_EQ(v->linkEulerChar(), 0);
                                EXPECT_EQ(v->isLinkOrientable(),
                                    v->linkType() == Link::Torus);
                            } else {
                                EXPECT_EQ(v->linkType(), Link::NonStandardCusp);
                                EXPECT_NE(v->linkEulerChar(), 2);
                                EXPECT_NE(v->linkEulerChar(), 0);
                            }
                        }
                    } else if (v->isBoundary()) {
                        ++foundBall;
                        if constexpr (dim == 3) {
                            EXPECT_FALSE(v->isLinkClosed());
                            EXPECT_TRUE(v->isLinkOrientable());
                            EXPECT_TRUE(v->isStandard());
                            EXPECT_EQ(v->linkType(), Vertex<dim>::Link::Disc);
                            EXPECT_EQ(v->linkEulerChar(), 1);
                        }
                    } else {
                        ++foundSphere;
                        if constexpr (dim == 3) {
                            EXPECT_TRUE(v->isLinkClosed());
                            EXPECT_TRUE(v->isLinkOrientable());
                            EXPECT_TRUE(v->isStandard());
                            EXPECT_EQ(v->linkType(), Vertex<dim>::Link::Sphere);
                            EXPECT_EQ(v->linkEulerChar(), 2);
                        }
                    }
                } else {
                    if (v->isBoundary())
                        ++foundBall;
                    else
                        ++foundSphere;
                }
            }

            EXPECT_EQ(foundSphere, expectSphere);
            EXPECT_EQ(foundBall, expectBall);
            EXPECT_EQ(foundIdeal, expectIdeal);
            EXPECT_EQ(foundInvalid, expectInvalid);
        }

        void vertexLinksBasicGenericCases() {
            verifyVertexLinksBasic(empty, 0, 0);
            verifyVertexLinksBasic(sphere, dim + 1, 0);
            verifyVertexLinksBasic(simpSphere, dim + 2, 0);
            verifyVertexLinksBasic(sphereBundle, 1, 0);
            verifyVertexLinksBasic(twistedSphereBundle, 1, 0);
            verifyVertexLinksBasic(ball, 0, dim + 1);
            verifyVertexLinksBasic(ballBundle, 0, dim % 2 ? 1 : 2);
            verifyVertexLinksBasic(twistedBallBundle, 0, dim % 2 ? 2 : 1);
        }

        static void verifyOrient(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            static constexpr int trials = 10;

            {
                // Test a direct copy:
                Triangulation<dim> oriented(tri, false, true);
                oriented.orient();
                clearProperties(oriented); // forget the cached orientability

                EXPECT_EQ(tri.isOrientable(), oriented.isOrientable());
                EXPECT_TRUE(oriented.isIsomorphicTo(tri));
                if (tri.isOrientable())
                    EXPECT_TRUE(oriented.isOriented());
            }

            for (int i = 0; i < trials; ++i) {
                // Test an isomorphic copy:
                Triangulation<dim> oriented =
                    Isomorphism<dim>::random(tri.size())(tri);
                oriented.orient();
                clearProperties(oriented); // forget the cached orientability

                EXPECT_EQ(tri.isOrientable(), oriented.isOrientable());
                EXPECT_TRUE(oriented.isIsomorphicTo(tri));
                if (tri.isOrientable())
                    EXPECT_TRUE(oriented.isOriented());
            }
        }

        static void verifySkeleton(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            verifySkeletonDetail(tri);

            // A side-effect of the above is that tri's skeleton is computed.
            // Now test that the skeleton is cloned correctly.
            SCOPED_TRACE("Clone");
            Triangulation<dim> copy(tri);
            verifySkeletonDetail(copy);
        }

        static void verifyBoundaryLabellingDetail(
                const regina::BoundaryComponent<dim>* bc,
                const regina::Triangulation<dim - 1>& built,
                const char* context) {
            static_assert(dim > 2);
            static_assert(regina::BoundaryComponent<dim>::allFaces);

            SCOPED_TRACE_CSTRING(context);

            regina::for_constexpr<0, dim-1>([bc, &built](auto subdim) {
                SCOPED_TRACE_NUMERIC(subdim);

                // Before doing anything else, check the consistency of the
                // skeleton (which includes the face mapping permutations).
                TriangulationTest<dim-1>::verifySkeletonDetail(built);
                EXPECT_EQ(built.countBoundaryFacets(), 0);

                // The labelling and ordering of subdim-faces is only
                // guaranteed if no subdim-face is pinched.  Conversely, if
                // some subdim-face *is* pinched then that face will appear
                // multiple times in the triangulated boundary, and so such a
                // labelling / ordering will be impossible.

                bool hasPinched = false;
                if constexpr (subdim <= dim - 3) {
                    for (auto f : bc->template faces<subdim>()) {
                        const auto& link = f->buildLink();
                        size_t realBdry = 0;
                        for (auto sub : link.boundaryComponents())
                            if (sub->isReal())
                                ++realBdry;
                        if (realBdry > 1) {
                            hasPinched = true;
                            break;
                        }
                    }
                }

                if (hasPinched) {
                    // We cannot check the labelling / ordering, but we should
                    // still ensure that the triangulated boundary component
                    // has strictly more subdim-faces.
                    EXPECT_LT(bc->template countFaces<subdim>(),
                            built.template countFaces<subdim>());
                    return; // from lambda
                }

                // There are no pinched faces; go ahead and verify the full
                // labelling / ordering.
                ASSERT_EQ(bc->template countFaces<subdim>(),
                        built.template countFaces<subdim>());

                for (size_t i = 0; i < bc->size(); ++i) {
                    const auto* innerSimp = built.simplex(i);
                    const auto* outerSimp = bc->template face<dim-1>(i);

                    for (size_t j = 0; j < regina::Face<dim-1, subdim>::nFaces;
                            ++j) {
                        auto* innerFace = innerSimp->template face<subdim>(j);
                        auto* outerFace = outerSimp->template face<subdim>(j);
                        EXPECT_EQ(
                            bc->template face<subdim>(innerFace->index()),
                            outerFace);

                        Perm<dim> innerPerm = innerSimp->template
                            faceMapping<subdim>(j);
                        Perm<dim+1> outerPerm = outerSimp->template
                            faceMapping<subdim>(j);
                        EXPECT_EQ(
                            innerPerm.trunc(subdim+1),
                            outerPerm.trunc(subdim+1));
                    }
                }
            });
        }

        static void verifyBoundaryLabelling(const Triangulation<dim>& tri,
                const char* name) {
            // This test verifies that a triangulated boundary component
            // has the correct number of faces of each dimension and these
            // faces are ordered and labelled correctly.
            //
            // Currently we define "correctly" as "matches the
            // ordering/labelling of the original boundary component", which
            // means we can only use this test in dimensions where boundary
            // components store all of their lower-dimensional faces.
            //
            static_assert(dim > 2);
            static_assert(regina::BoundaryComponent<dim>::allFaces);

            SCOPED_TRACE_CSTRING(name);

            for (auto bc : tri.boundaryComponents())
                if (bc->isReal()) {
                    // We have a real boundary component.
                    SCOPED_TRACE_NAMED_NUMERIC("index", bc->index());
                    const Triangulation<dim-1>& built = bc->build();

                    verifyBoundaryLabellingDetail(bc, built, "built");

                    // Try this again with copies of the triangulated boundary
                    // (instead of the reference to the cached property of tri).
                    // This allows us to test that deep copies preserve the
                    // numbering/labelling of lower-dimensional faces.
                    {
                        // Make a deep copy of the triangulated boundary.
                        Triangulation<dim-1> clone(built);
                        verifyBoundaryLabellingDetail(bc, clone, "clone");
                    }
                    {
                        // This time make a "light" deep copy that does not
                        // clone properties (but should still clone the
                        // skeleton).
                        Triangulation<dim-1> clone(built, false, false);
                        verifyBoundaryLabellingDetail(bc, clone, "light clone");
                    }
                    {
                        Triangulation<dim-1> assigned;
                        assigned.newSimplex(); // junk for assignment to replace
                        assigned = built;
                        verifyBoundaryLabellingDetail(bc, assigned, "assigned");
                    }

                    // Verify the gluings between (dim-2)-faces.
                    ASSERT_EQ(bc->size(), built.size());
                    for (size_t i = 0; i < bc->size(); ++i) {
                        const auto* innerSimp = built.simplex(i);
                        const auto* outerSimp = bc->template face<dim-1>(i);

                        for (int j = 0; j < dim; ++j) {
                            auto innerAdj = innerSimp->adjacentSimplex(j);
                            ASSERT_NE(innerAdj, nullptr);
                            auto outerAdj = bc->template face<dim-1>(
                                innerAdj->index());
                            EXPECT_EQ(outerAdj->template face<dim-2>(
                                    innerSimp->adjacentFacet(j)),
                                outerSimp->template face<dim-2>(j));
                        }
                    }
                }
        }

        void edgeAccess() {
            // Ensure that Simplex<dim>::edge(i, j) returns the correct edge.

            // Find ourselves a top-dimensional simplex with all edges distinct.
            auto s = ball.tri.simplex(0);

            for (int i = 0; i <= dim; ++i)
                for (int j = 0; j <= dim; ++j) {
                    if (i == j)
                        continue;

                    // Build a permutation that maps (0,1) -> (i,j).
                    Perm<dim+1> p;
                    if (j == 0)
                        p = Perm<dim+1>(1, i) * Perm<dim+1>(0, 1);
                    else
                        p = Perm<dim+1>(0, i) * Perm<dim+1>(1, j);

                    EXPECT_EQ(s->edge(i, j),
                        s->edge(regina::Edge<dim>::faceNumber(p)));
                }
        }

        static void verifyReordering(const Triangulation<dim>& t,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            // Note: we explicitly don't clear properties after reorderBFS().
            // The reorder operation _preserves_ the skeleton despite
            // reordering top-dimensional simplices, and we should work with
            // this preserved skeleton because this is more likely to cause
            // problems than a freshly computed skeleton.

            // Reordering the original:
            {
                Triangulation<dim> a(t);
                a.reorderBFS();
                EXPECT_TRUE(t.isIsomorphicTo(a));
            }
            {
                Triangulation<dim> b(t);
                b.reorderBFS(true);
                EXPECT_TRUE(t.isIsomorphicTo(b));
            }

            // Reordering a random relabelling of the original:
            Triangulation<dim> relabel = Isomorphism<dim>::random(t.size())(t);
            clearProperties(relabel); // recompute the skeleton here
            EXPECT_TRUE(t.isIsomorphicTo(relabel));
            {
                Triangulation<dim> d(relabel);
                d.reorderBFS();
                EXPECT_TRUE(t.isIsomorphicTo(d));
            }
            {
                Triangulation<dim> e(relabel);
                e.reorderBFS(true);
                EXPECT_TRUE(t.isIsomorphicTo(e));
            }
        }

        static void verifyDoubleCover(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            // The tests below assumed that tri has ≤ 1 connected component.
            if (! tri.isConnected())
                return;

            Triangulation<dim> cover = tri.doubleCover();

            if (tri.isEmpty()) {
                EXPECT_TRUE(cover.isEmpty());
                return;
            }

            // We have a non-empty connected triangulation.
            if (tri.isOrientable()) {
                // We should simply come away with two identical copies of tri.
                auto components = cover.triangulateComponents();
                EXPECT_EQ(components.size(), 2);
                for (const Triangulation<dim>& c : components)
                    EXPECT_TRUE(tri.isIsomorphicTo(c));
            } else {
                // We should come away with a proper connected double cover.
                EXPECT_EQ(cover.countComponents(), 1);
                EXPECT_TRUE(cover.isOrientable());
                EXPECT_EQ(cover.size(), 2 * tri.size());

                // Verify that the face counts double in each facial dimension.
                EXPECT_EQ(cover.template countFaces<dim-1>(),
                    2 * tri.template countFaces<dim-1>());
                if (tri.isValid()) {
                    // There are legitimate reasons for (0..dim-2)-face counts
                    // to *not* double in invalid triangulations.
                    // Likewise for vertex counts in ideal triangulations.
                    regina::for_constexpr<0, dim-1>([&tri, &cover](
                            auto subdim) {
                        if constexpr (dim == 3 || dim == 4) {
                            // These dimensions support ideal triangulations.
                            if constexpr (subdim == 0) {
                                if (tri.isIdeal())
                                    return; // from lambda
                            }
                        }
                        EXPECT_EQ(cover.template countFaces<subdim>(),
                            2 * tri.template countFaces<subdim>());
                    });
                }

                /**
                 * Commenting out this claim about homology groups,
                 * which is nonsense.
                // We expect the first homology group to be identical,
                // or to be missing a copy of Z_2.
                if (tri.isValid() && (tri.homology() != cover.homology())) {
                    regina::AbelianGroup hCover(cover.homology());
                    hCover.addTorsion(2);
                    EXPECT_EQ(tri.homology(), hCover);
                }
                */
            }
        }

        static void verifyDoubleOverBoundary(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            Triangulation<dim> dbl = tri.doubleOverBoundary();

            if (tri.isEmpty()) {
                EXPECT_TRUE(dbl.isEmpty());
                return;
            }

            if (! tri.hasBoundaryFacets()) {
                // We should simply come away with two identical copies of tri.
                auto components = dbl.triangulateComponents();
                EXPECT_EQ(components.size(), 2 * tri.countComponents());
                if (tri.isConnected())
                    for (const Triangulation<dim>& c : components)
                        EXPECT_TRUE(tri.isIsomorphicTo(c));
                return;
            }

            if (tri.isConnected())
                EXPECT_EQ(dbl.countComponents(), 1);
            else {
                EXPECT_GE(dbl.countComponents(), 1);
                EXPECT_LT(dbl.countComponents(), 2 * tri.countComponents());
            }
            EXPECT_EQ(dbl.isOrientable(), tri.isOrientable());
            EXPECT_FALSE(dbl.isOriented());
            EXPECT_FALSE(dbl.hasBoundaryFacets());
            EXPECT_EQ(dbl.size(), 2 * tri.size());

            if constexpr (regina::standardDim(dim)) {
                if (tri.isValid()) {
                    size_t nIdeal = 0;
                    long bdryEuler = 0;
                    for (auto b : tri.boundaryComponents()) {
                        if (b->isReal())
                            bdryEuler += b->eulerChar();
                        else
                            ++nIdeal;
                    }
                    EXPECT_EQ(dbl.countBoundaryComponents(), 2 * nIdeal);
                    EXPECT_EQ(dbl.eulerCharTri(),
                        2 * tri.eulerCharTri() - bdryEuler);
                    if constexpr (dim > 2) {
                        EXPECT_EQ(dbl.eulerCharManifold(),
                            2 * tri.eulerCharManifold() - bdryEuler);
                    }
                }
            }

            // Note: invalid vertices will become ideal vertices.
            // Any other invalid face will remain invalid.
            if (tri.isValid())
                EXPECT_TRUE(dbl.isValid());
            else {
                bool expectValid = true;
                regina::for_constexpr<1, dim-1>([&tri, &expectValid](
                        auto subdim) {
                    if (! expectValid)
                        return;
                    for (auto f : tri.template faces<subdim>())
                        if (! f->isValid()) {
                            expectValid = false;
                            break;
                        }
                });
                EXPECT_EQ(dbl.isValid(), expectValid);
            }
        }

        static void verifyMakeCanonical(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            // Currently makeCanonical() insists on connected triangulations.
            if (! tri.isConnected())
                return;

            static constexpr int trials = 5;

            Triangulation<dim> canonical(tri);
            canonical.makeCanonical();
            clearProperties(canonical); // recompute skeleton for detail()
            EXPECT_TRUE(canonical.isIsomorphicTo(tri));

            for (int i = 0; i < trials; ++i) {
                Triangulation<dim> t =
                    Isomorphism<dim>::random(tri.size())(tri);

                t.makeCanonical();
                clearProperties(t); // recompute skeleton for detail()

                EXPECT_EQ(t, canonical);
                EXPECT_EQ(t.detail(), canonical.detail());
            }
        }

        template <template <int> class Type>
        static void verifyIsomorphismSignatureUsing(
                const Triangulation<dim>& tri) {
            SCOPED_TRACE_TYPE(Type<dim>);

            std::string sig = tri.template isoSig<Type<dim>>();
            SCOPED_TRACE_STDSTRING(sig);

            ASSERT_FALSE(sig.empty());

            size_t sigSize = Triangulation<dim>::isoSigComponentSize(sig);
            if (tri.isEmpty()) {
                EXPECT_EQ(sigSize, 0);
            } else {
                size_t c;
                for (c = 0; c < tri.countComponents(); ++c)
                    if (sigSize == tri.component(c)->size())
                        break;
                if (c == tri.countComponents())
                    ADD_FAILURE() << "isoSigComponentSize() does not "
                        "match any component";
            }

            ASSERT_NO_THROW({
                EXPECT_TRUE(regina::Triangulation<dim>::fromIsoSig(sig).
                    isIsomorphicTo(tri));
            });

            // Does rebuilding still work if the signature has whitespace?
            EXPECT_NO_THROW({
                EXPECT_TRUE(regina::Triangulation<dim>::fromIsoSig(
                    std::string("\t " + sig + "\t \n")).isIsomorphicTo(tri));
            });

            if (tri.isEmpty())
                return;

            static constexpr int trials = 5;

            for (int i = 0; i < trials; ++i) {
                auto other = Isomorphism<dim>::random(tri.size())(tri);
                EXPECT_EQ(other.template isoSig<Type<dim>>(), sig);
            }

            if (tri.countComponents() == 1) {
                auto detail = tri.template isoSigDetail<Type<dim>>();

                EXPECT_EQ(detail.first, sig);

                auto relabelled = detail.second(tri);
                auto reconstructed =
                    Triangulation<dim>::fromIsoSig(detail.first);

                EXPECT_EQ(relabelled, reconstructed);

                EXPECT_EQ(tri.hasLocks(), reconstructed.hasLocks());
                for (size_t i = 0;
                        i < relabelled.size() && i < reconstructed.size(); ++i)
                    EXPECT_EQ(relabelled.simplex(i)->lockMask(),
                        reconstructed.simplex(i)->lockMask());
            }

            std::string lockFree = tri.template isoSig<Type<dim>,
                regina::IsoSigPrintableLockFree<dim>>();
            if (tri.hasLocks())
                EXPECT_NE(lockFree, sig);
            else
                EXPECT_EQ(lockFree, sig);
        }

        static void verifyIsomorphismSignature(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            verifyIsomorphismSignatureUsing<regina::IsoSigClassic>(tri);
            verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(tri);
        }

        static void verifyIsomorphismSignatureWithLocks(
                const Triangulation<dim>& tri, const char* name) {
            SCOPED_TRACE_CSTRING(name);

            Triangulation<dim> t = tri;

            // Lock one simplex.
            for (auto s : t.simplices()) {
                s->lock();
                verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
                s->unlock();
            }

            // Lock all simplices.
            for (auto s : t.simplices())
                s->lock();
            verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
            for (auto s : t.simplices())
                s->unlock();

            // Lock one facet.
            for (auto f : t.template faces<dim - 1>()) {
                f->lock();
                verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
                f->unlock();
            }

            // Lock all facets.
            for (auto f : t.template faces<dim - 1>())
                f->lock();
            verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
            for (auto f : t.template faces<dim - 1>())
                f->unlock();

            // Lock one simplex and one facet.
            for (auto s : t.simplices()) {
                s->lock();
                for (auto f : t.template faces<dim - 1>()) {
                    f->lock();
                    verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
                    f->unlock();
                }
                s->unlock();
            }

            // Lock all simplices and facets.
            for (auto s : t.simplices())
                s->lock();
            for (auto f : t.template faces<dim - 1>())
                f->lock();
            verifyIsomorphismSignatureUsing<regina::IsoSigEdgeDegrees>(t);
            for (auto f : t.template faces<dim - 1>())
                f->unlock();
            for (auto s : t.simplices())
                s->unlock();
        }

        static void verifyLockPropagation(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            static constexpr int trials = 10;

            // This test uses isosigs, so only do it for small triangulations.
            // Also, locks are not relevant for the empty triangulation.
            if (tri.isEmpty() || tri.size() > 10)
                return;

            Triangulation<dim> locked(tri, false, false);
            locked.simplex(0)->lock();
            locked.simplex(0)->lockFacet(dim);
            locked.simplex(locked.size() - 1)->lockFacet(dim - 1);
            std::string lockedSig = locked.sig();
            EXPECT_NE(lockedSig.find_first_of('.'), std::string::npos);

            {
                SCOPED_TRACE("Propagating through direct copy");
                Triangulation<dim> clone(locked, false, true);
                EXPECT_EQ(clone, locked);
                EXPECT_EQ(clone, tri); // a == b should ignore locks
                EXPECT_EQ(clone.sig(), lockedSig);

                SCOPED_TRACE("Propagating through move assignment");
                Triangulation<dim> alt = regina::Example<dim>::sphere();
                alt = std::move(clone);
                EXPECT_EQ(alt, locked);
                EXPECT_EQ(alt, tri); // a == b should ignore locks
                EXPECT_EQ(alt.sig(), lockedSig);
            }

            {
                SCOPED_TRACE("Propagating through copy assignment");
                Triangulation<dim> alt = regina::Example<dim>::sphere();
                alt = locked;
                EXPECT_EQ(alt, locked);
                EXPECT_EQ(alt, tri); // a == b should ignore locks
                EXPECT_EQ(alt.sig(), lockedSig);
            }

            for (int i = 0; i < trials; ++i) {
                SCOPED_TRACE("Propagating through isomorphic copy");
                Triangulation<dim> relabelled =
                    Isomorphism<dim>::random(locked.size())(locked);
                EXPECT_EQ(relabelled.sig(), lockedSig);

                {
                    SCOPED_TRACE("Propagating through reorderBFS()");
                    for (int j = 0; j < 2; ++j) {
                        Triangulation<dim> reordered(relabelled, false, true);
                        reordered.reorderBFS(j == 0);
                        EXPECT_EQ(reordered.sig(), lockedSig);
                    }
                }

                {
                    SCOPED_TRACE("Propagating through orient()");
                    relabelled.orient();
                    EXPECT_EQ(tri.isOrientable(), relabelled.isOrientable());
                    if (tri.isOrientable())
                        EXPECT_TRUE(relabelled.isOriented());
                    EXPECT_EQ(relabelled.sig(), lockedSig);
                }

                {
                    SCOPED_TRACE("Propagating through reflect()");
                    relabelled.reflect();
                    EXPECT_EQ(tri.isOrientable(), relabelled.isOrientable());
                    if (tri.isOrientable())
                        EXPECT_TRUE(relabelled.isOriented());
                    EXPECT_EQ(relabelled.sig(), lockedSig);
                }

                {
                    SCOPED_TRACE("Propagating through doubleCover()");
                    Triangulation<dim> dbl = tri.doubleCover();
                    for (size_t i = 0; i < tri.size(); ++i) {
                        EXPECT_EQ(tri.simplex(i)->lockMask(),
                            dbl.simplex(i)->lockMask());
                        EXPECT_EQ(tri.simplex(i)->lockMask(),
                            dbl.simplex(tri.size() + i)->lockMask());
                    }
                }
                {
                    SCOPED_TRACE("Propagating through doubleOverBoundary()");
                    Triangulation<dim> dbl = tri.doubleOverBoundary();
                    for (size_t i = 0; i < tri.size(); ++i) {
                        EXPECT_EQ(tri.simplex(i)->lockMask(),
                            dbl.simplex(i)->lockMask());
                        EXPECT_EQ(tri.simplex(i)->lockMask(),
                            dbl.simplex(tri.size() + i)->lockMask());
                    }
                }

                if constexpr (dim == 3) {
                    // Test the result of order() for small triangulations
                    // only, since the order() algorithm could be slow:
                    if (tri.size() <= 5 && tri.isValid()) {
                        SCOPED_TRACE("Propagating through order()");
                        for (int i = 0; i < 2; ++i) {
                            Triangulation<dim> alt(locked, false, true);
                            alt.order(i == 0);
                            EXPECT_EQ(alt.sig(), lockedSig);
                        }
                    }
                }
            }

            // In 3-D, summands() explicitly does _not_ propagate locks.
            // We test this elsewhere (in the connected sum decomposition
            // tests), so no need to test it again here.

            // TODO: Some other things that would be nice to check here:
            // triangulateComponents(), insertTriangulation(), makeCanonical(),
            // application of Cut.
            //
            // Also, in dimension 3: connectedSumWith().
            // Also, in dimension 4: I-bundles, S1-bundles, bundles with
            // monodromy.
        }

        static void verifyLockEnforcement(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            // Locks are not relevant for the empty triangulation.
            if (tri.isEmpty())
                return;

            if constexpr (regina::standardDim(dim)) {
                SCOPED_TRACE("Trying subdivide()");
                Triangulation<dim> alt(tri, false, false);

                alt.simplex(alt.size() - 1)->lock();
                EXPECT_THROW({ alt.subdivide(); }, regina::LockViolation);
                alt.unlockAll();
                alt.simplex(0)->lockFacet(dim - 1);
                EXPECT_THROW({ alt.subdivide(); }, regina::LockViolation);

                // Check that no subdivisions were performed.
                EXPECT_EQ(alt, tri);
            }

            if constexpr (regina::standardDim(dim) && dim > 2) {
                SCOPED_TRACE("Trying truncateIdeal()");
                Triangulation<dim> alt(tri, false, false);

                bool hasIdealOrInvalid = false;
                for (auto v : alt.vertices())
                    if (v->isIdeal() || ! v->isValid()) {
                        hasIdealOrInvalid = true;
                        break;
                    }

                if (hasIdealOrInvalid) {
                    // There is something to truncate.
                    alt.simplex(alt.size() - 1)->lock();
                    EXPECT_THROW({ alt.truncateIdeal(); },
                        regina::LockViolation);
                    alt.unlockAll();
                    alt.simplex(0)->lockFacet(dim - 1);
                    EXPECT_THROW({ alt.truncateIdeal(); },
                        regina::LockViolation);

                    // Check that no subdivisions were performed.
                    EXPECT_EQ(alt, tri);
                } else {
                    // There is nothing to truncate.
                    bool result;
                    alt.simplex(alt.size() - 1)->lock();
                    EXPECT_NO_THROW({ result = alt.truncateIdeal(); });
                    EXPECT_FALSE(result);
                    EXPECT_EQ(alt, tri);
                    EXPECT_TRUE(alt.hasLocks());
                    alt.unlockAll();
                    alt.simplex(0)->lockFacet(dim - 1);
                    EXPECT_NO_THROW({ result = alt.truncateIdeal(); });
                    EXPECT_FALSE(result);
                    EXPECT_EQ(alt, tri);
                    EXPECT_TRUE(alt.hasLocks());
                }
            }

            if constexpr (dim == 3) {
                SCOPED_TRACE("Trying truncate()");
                Triangulation<dim> alt(tri, false, false);

                alt.simplex(alt.size() - 1)->lock();
                EXPECT_THROW({ alt.truncate(alt.vertex(0)); },
                    regina::LockViolation);
                alt.unlockAll();
                alt.simplex(0)->lockFacet(dim - 1);
                EXPECT_THROW({ alt.truncate(alt.vertex(0)); },
                    regina::LockViolation);

                // Check that no subdivisions were performed.
                EXPECT_EQ(alt, tri);
            }

            {
                SCOPED_TRACE("Trying makeIdeal()");
                {
                    Triangulation<dim> alt(tri, false, false);
                    for (auto f : alt.template faces<dim - 1>())
                        if (f->isBoundary()) {
                            f->lock();
                            EXPECT_THROW({ alt.makeIdeal(); },
                                regina::LockViolation);
                            f->unlock();

                            // Check that the operation was not performed.
                            EXPECT_EQ(alt, tri);
                            EXPECT_FALSE(alt.hasLocks());
                        }
                }
                {
                    // Locked simplices should not be a problem.
                    // Nor should locked internal facets.
                    Triangulation<dim> alt(tri, false, false);
                    for (auto s : alt.simplices())
                        s->lock();
                    for (auto f : alt.template faces<dim - 1>())
                        if (! f->isBoundary())
                            f->lock();
                    EXPECT_NO_THROW({ alt.makeIdeal(); });

                    // Check that the operation was actually performed.
                    if (tri.hasBoundaryFacets())
                        EXPECT_NE(alt, tri);
                    else
                        EXPECT_EQ(alt, tri);
                }
            }

            // TODO: We should be testing all of our local moves here.
            // We should also test simple operations such as adding/removing
            // simplices, or gluing/ungluing them.
            //
            // Also, in dimensions 3,4: simplification, improveTreewidth(),
            // more local moves
            //
            // Also, in dimension 3 only: truncateIdeal(), truncate(),
            // layerOn(), puncture(), drillEdge()
        }

        /**
         * Tests all potential local moves of the form tri.move(f), where f is
         * a subdim-face of the triangulation tri.
         *
         * These tests are of a general nature that can be used with any type
         * of move - essentialy they verify that, if the move was performed,
         * it did not change the topology.
         *
         * More specific tests can be included through the optional preTest and
         * postTest arguments.  Specifically, when attempting to perform the
         * move on the ith face:
         *
         * - preTest(tri, i) will be called to determine in advance whether
         *   the move should be legal.  This returns a std::optional<bool>,
         *   which is true if the move should be legal, false if the move
         *   should be illegal, or has no value if the legality is not known
         *   in advance.
         *
         * - If the move was performed, postTest(pre_move_tri, post_move_tri, i)
         *   will be called.  This may run any additional tests (e.g,. verifying
         *   the combinatorics of the resulting triangulation).
         *
         * It should surely be possible to deduce subdim automatically, and even
         * to make move and sizeChange template parameters (so that verifyMove
         * can be plugged into exhaustive testing code), but I am struggling
         * to work out how to do this with pointers to member functions.
         * I think the fact that Triangulation is templated is not helping.
         */
        template <int subdim>
        static void verifyMove(const Triangulation<dim>& tri,
                const char* name,
                bool(Triangulation<dim>::*move)(regina::Face<dim, subdim>*),
                int sizeChange,
                std::optional<bool>(*preTest)(const Triangulation<dim>&,
                    size_t) = nullptr,
                void(*postTest)(const Triangulation<dim>&,
                    const Triangulation<dim>&, size_t) = nullptr) {
            static_assert(0 <= subdim && subdim <= dim);
            SCOPED_TRACE_CSTRING(name);
            SCOPED_TRACE_NAMED_NUMERIC("subdim", subdim);

            Triangulation<dim> oriented(tri);
            if (tri.isOrientable())
                oriented.orient();

            for (size_t i = 0; i < tri.template countFaces<subdim>(); ++i) {
                SCOPED_TRACE_NAMED_NUMERIC("face", i);

                Triangulation<dim> result(oriented);

                // Perform the move (if we can).
                std::optional<bool> expect =
                    (preTest ? preTest(tri, i) : std::nullopt);
                bool performed;
                if constexpr (subdim == dim)
                    performed = (result.*move)(result.simplex(i));
                else
                    performed = (result.*move)(result.template face<subdim>(i));
                if (expect.has_value())
                    EXPECT_EQ(performed, *expect);

                // Ensure that properties we are about to verify have been
                // explicitly recomputed.
                clearProperties(result);

                if (! performed) {
                    // Verify that the move was indeed not performed.
                    EXPECT_EQ(result, oriented);
                    continue;
                }

                // The move was performed.

                EXPECT_EQ(result.size(), tri.size() + sizeChange);
                EXPECT_EQ(result.isValid(), tri.isValid());
                EXPECT_EQ(result.isIdeal(), tri.isIdeal());
                EXPECT_EQ(result.isOrientable(), tri.isOrientable());
                if (tri.isOrientable())
                    EXPECT_TRUE(result.isOriented());
                EXPECT_EQ(result.countBoundaryComponents(),
                    tri.countBoundaryComponents());
                EXPECT_EQ(result.eulerCharTri(), tri.eulerCharTri());

                // Closedness can only be tested in standard dimensions.
                if constexpr (regina::standardDim(dim))
                    EXPECT_EQ(result.isClosed(), tri.isClosed());

                // Homology can only be tested for valid triangulations.
                if (tri.size() <= HOMOLOGY_THRESHOLD && tri.isValid()) {
                    EXPECT_EQ(result.homology(), tri.homology());
                    // We only test H2 in small dimensions, since for higher
                    // dimensions this becomes too slow.
                    if constexpr (dim == 3 || dim == 4)
                        EXPECT_EQ(result.template homology<2>(),
                            tri.template homology<2>());
                }

                if (postTest)
                    postTest(oriented, result, i);
            }
        }

        template <int k>
        static void verifyPachnerDetail(const Triangulation<dim>& tri,
                bool standardSimplex) {
            // Tests Pachner moves on k-faces, and their inverses.
            static_assert(0 <= k && k <= dim);
            SCOPED_TRACE_NAMED_NUMERIC("subdim", k);

            Triangulation<dim> oriented(tri);
            if (tri.isOrientable())
                oriented.orient();

            for (size_t i = 0; i < tri.template countFaces<k>(); ++i) {
                SCOPED_TRACE_NAMED_NUMERIC("face", i);

                Triangulation<dim> result(oriented);

                // Perform the move (if we can).
                bool performed;
                if constexpr (k == dim) {
                    // Moves on top-dimensional simplices are always allowed.
                    performed = result.pachner(result.simplex(i));
                    EXPECT_TRUE(performed);
                } else {
                    // For the simplicial sphere, all k-faces can be used.
                    // Otherwise, the legality of the move is still easy to
                    // know in advance for k == dim - 1.
                    auto face = result.template face<k>(i);
                    if constexpr (k == dim - 1) {
                        bool expected = ! (face->isBoundary() ||
                            face->embedding(0).simplex() ==
                            face->embedding(1).simplex());
                        performed = result.pachner(face); // destroys face
                        EXPECT_EQ(performed, expected);
                    } else {
                        performed = result.pachner(face); // destroys face
                    }
                    if (standardSimplex)
                        EXPECT_TRUE(performed);
                }
                // Ensure that properties we are about to verify have been
                // explicitly recomputed.
                clearProperties(result);

                if (! performed) {
                    // Verify that the move was indeed not performed.
                    EXPECT_EQ(result, oriented);
                    continue;
                }

                // The move was performed.

                EXPECT_EQ(result.size(), tri.size() + 2 * k - dim);
                EXPECT_EQ(result.isValid(), tri.isValid());
                EXPECT_EQ(result.isOrientable(), tri.isOrientable());
                if (tri.isOrientable())
                    EXPECT_TRUE(result.isOriented());
                EXPECT_EQ(result.countBoundaryComponents(),
                    tri.countBoundaryComponents());
                EXPECT_EQ(result.eulerCharTri(), tri.eulerCharTri());

                // Closedness can only be tested in standard dimensions.
                if constexpr (regina::standardDim(dim))
                    EXPECT_EQ(result.isClosed(), tri.isClosed());

                // Homology can only be tested for valid triangulations.
                if (tri.size() <= HOMOLOGY_THRESHOLD && tri.isValid()) {
                    EXPECT_EQ(result.homology(), tri.homology());
                    // We only test H2 in small dimensions, since for higher
                    // dimensions this becomes too slow.
                    if constexpr (dim == 3 || dim == 4)
                        EXPECT_EQ(result.template homology<2>(),
                            tri.template homology<2>());
                }

                // Randomly relabel the simplices, but preserve orientation.
                Isomorphism<dim> iso = result.randomiseLabelling(true);

                if constexpr (k == dim && (dim == 3 || dim == 4)) {
                    // For k == dim, we can undo the Pacher move with an edge
                    // collapse (which is supported for dimensions 3 and 4).
                    regina::Triangulation<dim> inv(result);

                    EXPECT_TRUE(inv.collapseEdge(
                        inv.simplex(iso.simpImage(tri.size() + dim - 1))->
                            edge(regina::Edge<dim>::edgeNumber
                                [iso.facetPerm(tri.size() + dim - 1)[0]]
                                [iso.facetPerm(tri.size() + dim - 1)[dim]])));

                    // Don't clear properties from inv, since what we're about
                    // to test does not rely on computed topological properties.
                    EXPECT_TRUE(inv.isIsomorphicTo(tri));
                    if (tri.isOrientable())
                        EXPECT_TRUE(inv.isOriented());
                }

                // For all k, we can undo the original Pachner move by
                // performing the inverse Pachner move.
                regina::Triangulation<dim> inv(result);
                if constexpr (k == 0) {
                    performed = inv.pachner(
                        inv.simplex(iso.simpImage(result.size() - 1)));
                } else {
                    auto face = inv.simplex(iso.simpImage(result.size() - 1))->
                        template face<dim - k>(
                            regina::Face<dim, dim - k>::faceNumber(
                                iso.facetPerm(result.size() - 1)));
                    performed = inv.pachner(face);
                }
                EXPECT_TRUE(performed);

                // Don't clear properties from inv, since what we're about to
                // test does not rely on computed topological properties.
                EXPECT_TRUE(inv.isIsomorphicTo(tri));
                if (tri.isOrientable())
                    EXPECT_TRUE(inv.isOriented());
            }
        }

        static void verifyPachner(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            regina::for_constexpr<0, dim + 1>([&tri](auto subdim) {
                verifyPachnerDetail<subdim>(tri, false);
            });
        }

        void verifyPachnerSimplicial() {
            SCOPED_TRACE("Simplicial sphere");
            regina::for_constexpr<0, dim + 1>([this](auto subdim) {
                verifyPachnerDetail<subdim>(simpSphere.tri, true);
            });
        }

        static void verify20Vertex(const Triangulation<dim>& tri,
                const char* name) {
            verifyMove<0>(tri, name,
                &Triangulation<dim>::template move20<0>,
                -2,
                nullptr /* preTest */,
                [](const Triangulation<dim>& pre,
                        const Triangulation<dim>& post, size_t i) {
                    // Verify that the vertex link was correct, and that
                    // the move did the right thing.
                    // Here the "right thing" is a 2-dim Pachner move followed
                    // by a (dim+1)-1 Pachner move.
                    Triangulation<dim> alt(pre);

                    Vertex<dim>* v = alt.vertex(i);
                    auto emb0 = v->front();
                    auto emb1 = v->back();
                    if (! emb0.simplex()->adjacentSimplex(emb0.face()))
                        std::swap(emb0, emb1);

                    auto glue = emb0.simplex()->adjacentGluing(
                        emb0.vertices()[dim]);
                    for (int j = 1; j <= dim; ++j) {
                        int f = emb0.vertices()[j];
                        EXPECT_EQ(emb0.simplex()->adjacentSimplex(f),
                            emb1.simplex());
                        EXPECT_EQ(emb0.simplex()->adjacentGluing(f), glue);
                    }

                    EXPECT_TRUE(alt.pachner(
                        emb0.simplex()->template face<dim-1>(emb0.face())));
                    EXPECT_TRUE(alt.pachner(
                        emb1.simplex()->vertex(emb1.face())));
                    EXPECT_TRUE(alt.isIsomorphicTo(post));
                });
        }

        static void verify20Edge(const Triangulation<dim>& tri,
                const char* name) {
            verifyMove<1>(tri, name,
                &Triangulation<dim>::template move20<1>,
                -2);
        }

        static void verify20Triangle(const Triangulation<dim>& tri,
                const char* name) {
            verifyMove<2>(tri, name,
                &Triangulation<dim>::template move20<2>,
                -2);
        }

        static void verifyShellBoundary(const Triangulation<dim>& tri,
                const char* name) {
            verifyMove<dim>(tri, name, &Triangulation<dim>::shellBoundary, -1);
        }

        static void verifyBarycentricSubdivision(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            Triangulation<dim> subdiv(tri);
            if (subdiv.isOrientable())
                subdiv.orient();

            subdiv.subdivide();
            // Ensure that properties we are about to verify have been
            // explicitly recomputed.
            clearProperties(subdiv);

            EXPECT_EQ(tri.hasBoundaryFacets(), subdiv.hasBoundaryFacets());
            EXPECT_EQ(tri.isOrientable(), subdiv.isOrientable());
            if (tri.isOrientable())
                EXPECT_TRUE(subdiv.isOriented());
            EXPECT_EQ(tri.isConnected(), subdiv.isConnected());
            EXPECT_EQ(tri.countComponents(), subdiv.countComponents());

            // Subdivisions can turn invalid triangulations into valid
            // triangulations (specifically when there are bad face
            // identifications involved).  These wreaks havoc on several tests
            // in cases where the incoming triangulation is not valid.
            if (tri.isValid()) {
                EXPECT_TRUE(subdiv.isValid());
                EXPECT_EQ(tri.isClosed(), subdiv.isClosed());
                EXPECT_EQ(tri.isIdeal(), subdiv.isIdeal());
                EXPECT_EQ(tri.countBoundaryComponents(),
                    subdiv.countBoundaryComponents());
                EXPECT_EQ(tri.eulerCharTri(), subdiv.eulerCharTri());
                if constexpr (regina::standardDim(dim) && dim > 2)
                    EXPECT_EQ(tri.eulerCharManifold(),
                        subdiv.eulerCharManifold());
            } else {
                // Subdivision can _create_ ideal vertices, but cannot remove
                // them.
                if (! tri.isClosed())
                    EXPECT_FALSE(subdiv.isClosed());
                if (tri.isIdeal())
                    EXPECT_TRUE(subdiv.isIdeal());
                EXPECT_LE(tri.countBoundaryComponents(),
                    subdiv.countBoundaryComponents());
            }

            // Some tests that are better done _after_ simplification:
            if constexpr (dim > 2) {
                subdiv.simplify();
                // While we're here: simplification shouldn't break orientation.
                if (tri.isOrientable())
                    EXPECT_TRUE(subdiv.isOriented());
            }

            // Note: homology<k>() requires a valid triangulation for k > 1,
            // and even with k == 1, bad face identifications can mess with
            // the comparison (since these become ideal vertices after
            // subdivision).
            if (tri.isValid()) {
                regina::for_constexpr<1, dim/2 + 1>([&tri, &subdiv](auto k) {
                    EXPECT_EQ(tri.template homology<k>(),
                        subdiv.template homology<k>());
                });
            }
        }

        static void verifyTightEncoding(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            TightEncodingTest<Triangulation<dim>>::verifyTightEncoding(tri);
        }

        void homologyH1GenericCases() {
            using regina::AbelianGroup;

            EXPECT_EQ(empty.tri.template homology<1>(), AbelianGroup());
            EXPECT_EQ(sphere.tri.template homology<1>(), AbelianGroup());
            EXPECT_EQ(simpSphere.tri.template homology<1>(), AbelianGroup());
            if constexpr (dim == 2) {
                EXPECT_EQ(sphereBundle.tri.template homology<1>(),
                    AbelianGroup(2));
                EXPECT_EQ(twistedSphereBundle.tri.template homology<1>(),
                    AbelianGroup(1, {2}));
            } else {
                EXPECT_EQ(sphereBundle.tri.template homology<1>(),
                    AbelianGroup(1));
                EXPECT_EQ(twistedSphereBundle.tri.template homology<1>(),
                    AbelianGroup(1));
            }
            EXPECT_EQ(ball.tri.template homology<1>(), AbelianGroup());
            EXPECT_EQ(ballBundle.tri.template homology<1>(), AbelianGroup(1));
            EXPECT_EQ(twistedBallBundle.tri.template homology<1>(),
                AbelianGroup(1));
        }

        void homologyH2GenericCases() {
            static_assert(dim > 2);
            using regina::AbelianGroup;

            // It's a pity that almost all of these examples have trivial H2.
            // We need some more interesting generic constructions.

            EXPECT_EQ(empty.tri.template homology<2>(), AbelianGroup());
            EXPECT_EQ(sphere.tri.template homology<2>(), AbelianGroup());
            EXPECT_EQ(simpSphere.tri.template homology<2>(), AbelianGroup());
            if constexpr (dim == 3) {
                EXPECT_EQ(sphereBundle.tri.template homology<2>(),
                    AbelianGroup(1));
                EXPECT_EQ(twistedSphereBundle.tri.template homology<2>(),
                    AbelianGroup(0, {2}));
            } else {
                EXPECT_EQ(sphereBundle.tri.template homology<2>(),
                    AbelianGroup());
                EXPECT_EQ(twistedSphereBundle.tri.template homology<2>(),
                    AbelianGroup());
            }
            EXPECT_EQ(ball.tri.template homology<2>(), AbelianGroup());
            EXPECT_EQ(ballBundle.tri.template homology<2>(), AbelianGroup());
            EXPECT_EQ(twistedBallBundle.tri.template homology<2>(),
                AbelianGroup());

            if constexpr (dim == 5) {
                using Example = regina::Example<dim>;
                using LowDimExample = regina::Example<dim - 1>;
                EXPECT_EQ(Example::singleCone(LowDimExample::sphereBundle()).
                    template homology<2>(), AbelianGroup());
                EXPECT_EQ(Example::doubleCone(LowDimExample::sphereBundle()).
                    template homology<2>(), AbelianGroup());

                EXPECT_EQ(
                    Example::singleCone(LowDimExample::twistedSphereBundle()).
                    template homology<2>(), AbelianGroup());
                EXPECT_EQ(
                    Example::doubleCone(LowDimExample::twistedSphereBundle()).
                    template homology<2>(), AbelianGroup());

                EXPECT_EQ(
                    Example::singleCone(LowDimExample::s2xs2()).
                    template homology<2>(), AbelianGroup(2));
                EXPECT_EQ(
                    Example::doubleCone(LowDimExample::s2xs2()).
                    template homology<2>(), AbelianGroup(2));
            }
        }

        void homologyH3GenericCases() {
            static_assert(dim >= 4);
            using regina::AbelianGroup;

            // It's a pity that almost all of these examples have trivial H3.
            // We need some more interesting generic constructions.

            EXPECT_EQ(empty.tri.template homology<3>(), AbelianGroup());
            EXPECT_EQ(sphere.tri.template homology<3>(), AbelianGroup());
            EXPECT_EQ(simpSphere.tri.template homology<3>(), AbelianGroup());
            if constexpr (dim == 4) {
                EXPECT_EQ(sphereBundle.tri.template homology<3>(),
                    AbelianGroup(1));
                EXPECT_EQ(twistedSphereBundle.tri.template homology<3>(),
                    AbelianGroup(0, {2}));
            } else {
                EXPECT_EQ(sphereBundle.tri.template homology<3>(),
                    AbelianGroup());
                EXPECT_EQ(twistedSphereBundle.tri.template homology<3>(),
                    AbelianGroup());
            }
            EXPECT_EQ(ball.tri.template homology<3>(), AbelianGroup());
            EXPECT_EQ(ballBundle.tri.template homology<3>(), AbelianGroup());
            EXPECT_EQ(twistedBallBundle.tri.template homology<3>(),
                AbelianGroup());

            if constexpr (dim == 5) {
                using Example = regina::Example<dim>;
                using LowDimExample = regina::Example<dim - 1>;
                EXPECT_EQ(Example::singleCone(LowDimExample::sphereBundle()).
                    template homology<3>(), AbelianGroup(1));
                EXPECT_EQ(Example::doubleCone(LowDimExample::sphereBundle()).
                    template homology<3>(), AbelianGroup(1));

                EXPECT_EQ(
                    Example::singleCone(LowDimExample::twistedSphereBundle()).
                    template homology<3>(), AbelianGroup(0, {2}));
                EXPECT_EQ(
                    Example::doubleCone(LowDimExample::twistedSphereBundle()).
                    template homology<3>(), AbelianGroup(0, {2}));

                EXPECT_EQ(Example::singleCone(LowDimExample::s2xs2()).
                    template homology<3>(), AbelianGroup());
                EXPECT_EQ(Example::doubleCone(LowDimExample::s2xs2()).
                    template homology<3>(), AbelianGroup());
            }
        }

        static void verifyBoundaryH1(const TestCase& test,
                size_t whichBdry, const regina::AbelianGroup& expect) {
            static_assert(dim > 2);

            SCOPED_TRACE_CSTRING(test.name);
            ASSERT_LT(whichBdry, test.tri.countBoundaryComponents());

            // Calling homology() does not truncate ideal boundaries
            // at the centroids of invalid (dim-3)-faces that are
            // self-identified under a non-trivial map.
            //
            // This problem only appears in dimension dim ≥ 4.
            // Unfortunately, to fix it we need to do a barycentric
            // subdivision, which is currently only available in
            // dimension dim ≤ 5 (i.e., where the boundary triangulation
            // has dimension ≤ 4).
            //
            // So: for the time being, we perform this subdivision for
            // the cases dim ≤ 5 only.
            auto t = test.tri.boundaryComponent(whichBdry)->build();
            if constexpr (dim >= 4 && regina::standardDim(dim - 1)) {
                t.subdivide();
                t.simplify();
            }
            EXPECT_EQ(t.homology(), expect);
        }

        void boundaryHomologyGenericCases() {
            static_assert(dim > 2);

            verifyBoundaryH1(ball, 0, {});
            if constexpr (dim == 3) {
                verifyBoundaryH1(ballBundle, 0, {2});
                verifyBoundaryH1(twistedBallBundle, 0, {1, {2}});
            } else {
                verifyBoundaryH1(ballBundle, 0, {1});
                verifyBoundaryH1(twistedBallBundle, 0, {1});
            }
        }

        void fundGroupGenericCases() {
            EXPECT_EQ(empty.tri.group().recogniseGroup(), "0");
            EXPECT_EQ(sphere.tri.group().recogniseGroup(), "0");
            EXPECT_EQ(simpSphere.tri.group().recogniseGroup(), "0");
            if constexpr (dim == 2) {
                EXPECT_EQ(sphereBundle.tri.group().recogniseGroup(), "2 Z");
                EXPECT_EQ(twistedSphereBundle.tri.group().recogniseGroup(),
                    "Z~Z w/monodromy a ↦ a^-1");
            } else {
                EXPECT_EQ(sphereBundle.tri.group().recogniseGroup(), "Z");
                EXPECT_EQ(twistedSphereBundle.tri.group().recogniseGroup(),
                    "Z");
            }
            EXPECT_EQ(ball.tri.group().recogniseGroup(), "0");
            EXPECT_EQ(ballBundle.tri.group().recogniseGroup(), "Z");
            EXPECT_EQ(twistedBallBundle.tri.group().recogniseGroup(), "Z");
        }

        template <int k>
        static void verifyChainComplexDetail(const Triangulation<dim>& tri) {
            static_assert(0 < k && k < dim);
            SCOPED_TRACE_NUMERIC(k);

            // These tests use homology on the skeleton: invalid or empty
            // triangulations are explicitly disallowed, and ideal
            // triangluations will give wrong answers (since the ideal
            // vertices will not be considered as truncated).
            if (tri.isEmpty() || ! tri.isValid())
                return;
            if (tri.isIdeal())
                return;

            regina::MatrixInt m = tri.template boundaryMap<k>();
            regina::MatrixInt n = tri.template boundaryMap<k + 1>();

            ASSERT_EQ(m.columns(), n.rows());
            ASSERT_TRUE((m * n).isZero());

            // Verify that homology with Z coefficients is correct:
            regina::AbelianGroup g1(m, n);
            regina::MarkedAbelianGroup g2 = tri.template markedHomology<k>();
            ASSERT_EQ(g1, g2.unmarked());
            EXPECT_EQ(tri.template homology<k>(), g1);

            // Verify that homology with Z_2 coefficients looks believable:
            regina::AbelianGroup g1z2(m, n, 2);
            regina::MarkedAbelianGroup g2z2(m, n, 2);
            EXPECT_EQ(g1z2, g2z2.unmarked());
            EXPECT_EQ(g1z2.rank(), 0);
            size_t z2rank = g1z2.countInvariantFactors();
            for (size_t i = 0; i < z2rank; ++i)
                EXPECT_EQ(g1z2.invariantFactor(i), 2);
            if constexpr (k == 2 && dim == 3) {
                // For this special case, we can verify the group precisely.
                EXPECT_EQ(tri.homologyH2Z2(), z2rank);
            }
        }

        static void verifyChainComplex(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            regina::for_constexpr<1, dim>([&tri](auto k) {
                verifyChainComplexDetail<k>(tri);
            });
        }

        template <int k>
        static void verifyDualChainComplexDetail(
                const Triangulation<dim>& tri) {
            static_assert(0 < k && k < dim);
            SCOPED_TRACE_NUMERIC(k);

            // These tests use homology on the dual skeleton: invalid or
            // empty triangulations are explicitly disallowed, but ideal
            // triangulations are fine.
            if (tri.isEmpty() || ! tri.isValid())
                return;

            regina::MatrixInt m = tri.template dualBoundaryMap<k>();
            regina::MatrixInt n = tri.template dualBoundaryMap<k + 1>();

            ASSERT_EQ(m.columns(), n.rows());
            ASSERT_TRUE((m * n).isZero());

            // Verify that homology with Z coefficients is correct:
            regina::AbelianGroup g1(m, n);
            EXPECT_EQ(tri.template homology<k>(), g1);

            // Verify that homology with Z_2 coefficients looks believable:
            regina::AbelianGroup g1z2(m, n, 2);
            EXPECT_EQ(g1z2.rank(), 0);
            size_t z2rank = g1z2.countInvariantFactors();
            for (size_t i = 0; i < z2rank; ++i)
                EXPECT_EQ(g1z2.invariantFactor(i), 2);
            if constexpr (k == 2 && dim == 3) {
                // For this special case, we can verify the group precisely.
                EXPECT_EQ(tri.homologyH2Z2(), z2rank);
            }
        }

        static void verifyDualChainComplex(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            regina::for_constexpr<1, dim>([&tri](auto k) {
                verifyDualChainComplexDetail<k>(tri);
            });
        }

        template <int k>
        static void verifyDualToPrimalDetail(const Triangulation<dim>& tri) {
            static_assert(0 <= k && k < dim);
            SCOPED_TRACE_NUMERIC(k);

            // Do not try to work with triangulations that fail the
            // preconditions for dualToPrimal().
            if (tri.isEmpty() || ! tri.isValid())
                return;
            if constexpr (regina::standardDim(dim))
                if (tri.isIdeal())
                    return;

            regina::MatrixInt map = tri.template dualToPrimal<k>();

            // This map sends homologous cycles to homologous cycles;
            // in particular, this means it must send boundaries to boundaries.
            //
            // Also, the map should describe an isomorphism between the dual
            // and primal homology groups.

            // Start with what is easy to test.

            if constexpr (regina::standardDim(dim) || k + 1 < dim) {
                auto dualBoundariesAsPrimal =
                    map * tri.template dualBoundaryMap<k + 1>();

                if constexpr (0 < k)
                    EXPECT_TRUE((tri.template boundaryMap<k>() *
                            dualBoundariesAsPrimal).isZero());

                if (! dualBoundariesAsPrimal.isZero()) {
                    // Test whether the column space for dualBoundariesAsPrimal
                    // lives within the column space for boundaryMap<k + 1>.
                    auto b = tri.template boundaryMap<k + 1>();
                    auto rank = b.columnEchelonForm();

                    regina::MatrixInt comb(b.rows(),
                        b.columns() + dualBoundariesAsPrimal.columns());
                    for (size_t row = 0; row < b.rows(); ++row) {
                        for (size_t col = 0; col < b.columns(); ++col)
                            comb.entry(row, col) = b.entry(row, col);
                        for (size_t col = 0;
                                col < dualBoundariesAsPrimal.columns(); ++col)
                            comb.entry(row, b.columns() + col) =
                                dualBoundariesAsPrimal.entry(row, col);
                    }

                    EXPECT_EQ(rank, std::move(comb).rank());
                }

                if constexpr (0 < k) {
                    // We can use HomMarkedAbelianGroup to verify that
                    // this is indeed an isomorphism between homology groups.
                    regina::MarkedAbelianGroup homDual(
                        tri.template dualBoundaryMap<k>(),
                        tri.template dualBoundaryMap<k+1>());
                    regina::MarkedAbelianGroup homPrimal(
                        tri.template boundaryMap<k>(),
                        tri.template boundaryMap<k+1>());
                    regina::HomMarkedAbelianGroup hom(homDual, homPrimal, map);

                    EXPECT_TRUE(hom.isCycleMap());
                    EXPECT_TRUE(hom.isEpic());
                    EXPECT_TRUE(hom.isMonic());
                }
            }
        }

        static void verifyDualToPrimal(const Triangulation<dim>& tri,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);
            regina::for_constexpr<0, dim>([&tri](auto k) {
                verifyDualToPrimalDetail<k>(tri);
            });
        }

        static bool looksIdentical(const Triangulation<dim>& a,
                const Triangulation<dim>& b) {
            if (a.size() != b.size())
                return false;
            if (a.countComponents() != b.countComponents())
                return false;
            if (a != b)
                return false;

            // Test isosigs only in smaller dimensions, since the
            // running time grows with (dim!).
            if constexpr (dim <= 6)
                if (a.isoSig() != b.isoSig())
                    return false;

            return true;
        }

        static void verifyCopyMove(const Triangulation<dim>& t,
                const char* name) {
            SCOPED_TRACE_CSTRING(name);

            if (t.isEmpty()) {
                EXPECT_EQ(t.size(), 0);

                Triangulation<dim> copy(t);
                EXPECT_TRUE(copy.isEmpty());
                EXPECT_TRUE(looksIdentical(copy, t));

                Triangulation<dim> move(std::move(copy));
                EXPECT_TRUE(move.isEmpty());
                EXPECT_TRUE(looksIdentical(move, t));

                Triangulation<dim> copyAss;
                copyAss.newSimplex(); // Give it something to overwrite.
                copyAss = t;
                EXPECT_TRUE(copyAss.isEmpty());
                EXPECT_TRUE(looksIdentical(copyAss, t));

                Triangulation<dim> moveAss;
                moveAss.newSimplex(); // Give it something to overwrite.
                moveAss = std::move(copyAss);
                EXPECT_TRUE(moveAss.isEmpty());
                EXPECT_TRUE(looksIdentical(moveAss, t));
            } else {
                ASSERT_GT(t.size(), 0);

                ASSERT_GT(t.countVertices(), 0);
                Vertex<dim>* v0 = t.vertex(0);

                Triangulation<dim> copy(t);
                EXPECT_TRUE(looksIdentical(copy, t));

                // Copy construction should use different vertices.
                ASSERT_GT(copy.countVertices(), 0);
                Vertex<dim>* v1 = copy.vertex(0);
                EXPECT_NE(v1, v0);

                Triangulation<dim> move(std::move(copy));
                EXPECT_TRUE(looksIdentical(move, t));

                // Move construction should use the same vertices.
                ASSERT_GT(move.countVertices(), 0);
                Vertex<dim>* v2 = move.vertex(0);
                EXPECT_EQ(v2, v1);

                Triangulation<dim> copyAss;
                copyAss.newSimplex(); // Give it something to overwrite.
                copyAss = t;
                EXPECT_TRUE(looksIdentical(copyAss, t));

                // Copy assignment should use different vertices.
                ASSERT_GT(copyAss.countVertices(), 0);
                Vertex<dim>* v3 = copyAss.vertex(0);
                EXPECT_NE(v3, v0);

                Triangulation<dim> moveAss;
                moveAss.newSimplex(); // Give it something to overwrite.
                moveAss = std::move(copyAss);
                EXPECT_TRUE(looksIdentical(moveAss, t));

                // Move assignment should use the same vertices.
                ASSERT_GT(moveAss.countVertices(), 0);
                Vertex<dim>* v4 = moveAss.vertex(0);
                EXPECT_EQ(v4, v3);
            }
        }
};
