#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <string>

#include "control/controlobject.h"
#include "engine/controls/bpmcontrol.h"
#include "engine/sync/synccontrol.h"
#include "mixer/basetrackplayer.h"
#include "preferences/usersettings.h"
#include "test/mixxxtest.h"
#include "test/mockedenginebackendtest.h"
#include "track/beatfactory.h"
#include "track/beatmap.h"
#include "util/memory.h"

namespace {
constexpr double kMaxFloatingPointErrorLowPrecision = 0.005;
constexpr double kMaxFloatingPointErrorHighPrecision = 0.0000000000000005;
constexpr double kMaxBeatDistanceEpsilon = 1e-9;
} // namespace

/// Tests for Master Sync.
/// The following manual tests should probably be performed:
/// * Quantize mode nudges tracks in sync, whether internal or deck master.
/// * Flinging tracks with the waveform should work.
/// * vinyl??
class EngineSyncTest : public MockedEngineBackendTest {
  public:
    QString getMasterGroup() {
        Syncable* pMasterSyncable = m_pEngineSync->getMasterSyncable();
        if (pMasterSyncable) {
            return pMasterSyncable->getGroup();
        }
        return QString();
    }
    bool isExplicitMaster(const QString& group) {
        return isMaster(group, true);
    }
    bool isSoftMaster(const QString& group) {
        return isMaster(group, false);
    }

    bool isFollower(const QString& group) {
        if (group == m_sInternalClockGroup) {
            return !ControlObject::getControl(ConfigKey(m_sInternalClockGroup,
                                                      "sync_master"))
                            ->toBool();
        }
        if (ControlObject::getControl(ConfigKey(group, "sync_mode"))->get() != SYNC_FOLLOWER) {
            return false;
        }
        if (!ControlObject::getControl(ConfigKey(group, "sync_enabled"))->toBool()) {
            return false;
        }
        if (ControlObject::getControl(ConfigKey(group, "sync_master"))->toBool()) {
            return false;
        }
        return true;
    }

    void assertSyncOff(const QString& group) {
        if (group == m_sInternalClockGroup) {
            ASSERT_EQ(0,
                    ControlObject::getControl(
                            ConfigKey(m_sInternalClockGroup, "sync_master"))
                            ->get());
        } else {
            ASSERT_EQ(SYNC_NONE,
                    ControlObject::getControl(ConfigKey(group, "sync_mode"))
                            ->get());
            ASSERT_EQ(0,
                    ControlObject::getControl(ConfigKey(group, "sync_enabled"))
                            ->get());
            ASSERT_EQ(0,
                    ControlObject::getControl(ConfigKey(group, "sync_master"))
                            ->get());
        }
    }

    void assertNoMaster() {
        ASSERT_EQ(NULL, m_pEngineSync->getMaster());
        ASSERT_EQ(NULL, m_pEngineSync->getMasterSyncable());
    }

  private:
    bool isMaster(const QString& group, bool explicitMaster) {
        if (group == m_sInternalClockGroup) {
            if (!ControlObject::getControl(ConfigKey(m_sInternalClockGroup,
                                                   "sync_master"))
                            ->toBool()) {
                return false;
            }
            if (m_pEngineSync->getMaster()) {
                return false;
            }
            if (m_sInternalClockGroup != getMasterGroup()) {
                return false;
            }
            // Internal Clock doesn't have explicit mode
            if (explicitMaster) {
                qWarning() << "test error, internal clock can never be explicit master";
                return false;
            }
            return true;
        }
        if (group == m_sGroup1) {
            if (m_pEngineSync->getMaster() != m_pChannel1) {
                return false;
            }
        } else if (group == m_sGroup2) {
            if (m_pEngineSync->getMaster() != m_pChannel2) {
                return false;
            }
        }
        if (getMasterGroup() != group) {
            return false;
        }

        if (explicitMaster) {
            if (ControlObject::getControl(ConfigKey(group, "sync_mode"))
                            ->get() != SYNC_MASTER_EXPLICIT) {
                return false;
            }
        } else {
            if (ControlObject::getControl(ConfigKey(group, "sync_mode"))
                            ->get() != SYNC_MASTER_SOFT) {
                return false;
            }
        }
        if (!ControlObject::getControl(ConfigKey(group, "sync_enabled"))->toBool()) {
            return false;
        }
        if (!ControlObject::getControl(ConfigKey(group, "sync_master"))->toBool()) {
            return false;
        }
        return true;
    }
};

TEST_F(EngineSyncTest, ControlObjectsExist) {
    // This isn't exhaustive, but certain COs have a habit of not being set up properly.
    ASSERT_TRUE(ControlObject::getControl(ConfigKey(m_sGroup1, "file_bpm")) !=
            NULL);
}

TEST_F(EngineSyncTest, SetMasterSuccess) {
    // If we set the first channel to master, EngineSync should get that message.

    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT);
    ProcessBuffer();

    // No tracks are playing and we have no beats, SYNC_MASTER_EXPLICIT state is in stand-by
    EXPECT_DOUBLE_EQ(
            0.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    // The master sync should now be channel 1.
    ASSERT_TRUE(isExplicitMaster(m_sGroup1));

    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->set(SYNC_FOLLOWER);
    ProcessBuffer();

    ASSERT_TRUE(isFollower(m_sGroup2));

    // Now set channel 2 to be master.
    pButtonMasterSync2->set(SYNC_MASTER_EXPLICIT);
    ProcessBuffer();

    // Now channel 2 should be master, and channel 1 should be a follower.
    ASSERT_TRUE(isExplicitMaster(m_sGroup2));
    ASSERT_TRUE(isFollower(m_sGroup1));

    // Now back again.
    pButtonMasterSync1->set(SYNC_MASTER_EXPLICIT);
    ProcessBuffer();

    // Now channel 1 should be master, and channel 2 should be a follower.
    ASSERT_TRUE(isExplicitMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Now set channel 1 to follower, no all are followers, waiting for a tempo to adopt.
    pButtonMasterSync1->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
}

TEST_F(EngineSyncTest, ExplicitMasterPersists) {
    // If we set an explicit master, enabling sync or pressing play on other decks
    // doesn't cause the master to move around.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 120, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 124, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT);
    ProcessBuffer();
    // The master sync should now be channel 1.
    ASSERT_TRUE(isExplicitMaster(m_sGroup1));

    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    pButtonMasterSync2->set(1.0);
    ProcessBuffer();
    ASSERT_TRUE(isExplicitMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Stop deck 2, and restart it, no change.
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(0.0);
    ProcessBuffer();
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ProcessBuffer();
    ASSERT_TRUE(isExplicitMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
}

TEST_F(EngineSyncTest, SetMasterWhilePlaying) {
    // Make sure we don't get two master lights if we change masters while playing.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 120, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 124, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    mixxx::BeatsPointer pBeats3 = BeatFactory::makeBeatGrid(m_pTrack3->getSampleRate(), 128, 0.0);
    m_pTrack3->trySetBeats(pBeats3);

    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->set(SYNC_MASTER_EXPLICIT);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->slotSet(SYNC_FOLLOWER);
    auto pButtonMasterSync3 =
            std::make_unique<ControlProxy>(m_sGroup3, "sync_mode");
    pButtonMasterSync3->slotSet(SYNC_FOLLOWER);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup3, "play"))->set(1.0);

    ProcessBuffer();

    pButtonMasterSync3->slotSet(SYNC_MASTER_EXPLICIT);

    ProcessBuffer();

    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isExplicitMaster(m_sGroup3));
}

TEST_F(EngineSyncTest, SetEnabledBecomesMaster) {
    // If we set the first channel with a valid tempo to follower, it should be master.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 80, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
}

TEST_F(EngineSyncTest, DisableInternalMasterWhilePlaying) {
    auto pButtonMasterSync = std::make_unique<ControlProxy>(
            m_sInternalClockGroup, "sync_master");
    pButtonMasterSync->slotSet(1.0);
    auto pButtonSyncMode1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonSyncMode1->slotSet(SYNC_FOLLOWER);
    auto pButtonSyncMode2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonSyncMode2->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    // The master sync should now be Internal.
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));

    // Make sure both decks are playing.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 80, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 80, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ProcessBuffer();

    // Now unset Internal master.
    pButtonMasterSync->slotSet(0.0);
    ProcessBuffer();

    // This is not allowed, Internal should still be master.
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_EQ(1, pButtonMasterSync->get());
}

TEST_F(EngineSyncTest, DisableSyncOnMaster) {
    // Channel 1 follower, channel 2 master.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    auto pButtonSyncMode1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonSyncMode1->slotSet(SYNC_FOLLOWER);

    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 130, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    auto pButtonSyncMaster2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_master");
    pButtonSyncMaster2->slotSet(1.0);

    // TODO(owilliams): explicit master is disabled, so regular master sync is
    // enabled instead.
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));

    // Unset enabled on channel2, it should work.
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->slotSet(0.0);

    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_EQ(0, ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->get());
    ASSERT_EQ(0, ControlObject::getControl(ConfigKey(m_sGroup2, "sync_master"))->get());
}

TEST_F(EngineSyncTest, InternalMasterSetFollowerSliderMoves) {
    // If internal is master, and we turn on a follower, the slider should move.
    auto pButtonMasterSyncInternal = std::make_unique<ControlProxy>(
            m_sInternalClockGroup, "sync_master");
    auto pMasterSyncSlider =
            std::make_unique<ControlProxy>(m_sInternalClockGroup, "bpm");

    pMasterSyncSlider->set(100.0);
    pButtonMasterSyncInternal->slotSet(1);

    // Set the file bpm of channel 1 to 80 bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 80, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(getRateSliderValue(1.25),
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get());
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
}

TEST_F(EngineSyncTest, AnySyncDeckSliderStays) {
    // If there exists a sync deck, even if it's not playing, don't change the
    // master BPM if a new deck enables sync.

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 80, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);

    // After setting up the first deck, the internal BPM should be 80.
    EXPECT_DOUBLE_EQ(80.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());

    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->set(1.0);

    // After the second one, though, the internal BPM should still be 80.
    EXPECT_DOUBLE_EQ(80.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
}

TEST_F(EngineSyncTest, InternalClockFollowsFirstPlayingDeck) {
    // Same as above, except we use the midi lights to change state.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");

    // Set up decks so they can be playing, and start deck 1.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 100, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 130, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 0.0);
    ProcessBuffer();

    // Set channel 1 to be enabled
    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    // The master sync should now be deck 1.
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Set channel 2 to be enabled.
    pButtonSyncEnabled2->set(1);
    ProcessBuffer();

    // channel 1 still master while 2 is not playing
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // The rate should not have changed -- deck 1 still matches deck 2.
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get());

    // Reset channel 2 rate, set channel 2 to play, and process a buffer.
    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();
    // Now internal clock is master
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Now disable sync on channel 1.
    pButtonSyncEnabled1->set(0);
    ProcessBuffer();

    // Master flips to deck 2
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));

    // Rate should now match channel 2.
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, SetExplicitMasterByLights) {
    // Same as above, except we use the midi lights to change state.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    auto pButtonSyncMaster1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_master");
    auto pButtonSyncMaster2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_master");

    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    // Set the file bpm of channel 2 to 150bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 150, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Set channel 1 to be explicit master.
    pButtonSyncMaster1->slotSet(1.0);
    ProcessBuffer();

    // The master sync should now be channel 1.
    // TODO(owilliams): Because explicit master is broken, currently this deck will just
    // be a soft master.  This is intended but will be fixed.
    ASSERT_TRUE(isSoftMaster(m_sGroup1));

    // Set channel 2 to be follower.
    pButtonSyncEnabled2->slotSet(1);

    ASSERT_TRUE(isFollower(m_sGroup2));

    // Now set channel 2 to be master.
    pButtonSyncMaster2->slotSet(1);

    // Now channel 2 should be master, and channel 1 should be a follower.
    // TODO(owilliams): Because explicit master is broken, these will both be followers.
    // This will be fixed
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));

    // Now back again.
    pButtonSyncMaster1->slotSet(1);

    // Now channel 1 should be master, and channel 2 should be a follower.
    // TODO(owilliams): See above
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));

    // Now set channel 1 to not-master, all will become follower.
    // handing over master to the internal clock
    pButtonSyncMaster1->slotSet(0);

    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
}

TEST_F(EngineSyncTest, SetExplicitMasterByLightsNoTracks) {
    // Same as above, except we use the midi lights to change state.
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    auto pButtonSyncMaster1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_master");

    // Now back again.
    pButtonSyncMaster1->slotSet(1);

    // Set channel 2 to be follower.
    pButtonSyncEnabled2->slotSet(1);

    // Now channel 1 should be master, and channel 2 should be a follower.
    // Because there are no bpms anywhere, internal clock is also follower.
    // TODO(owilliams): Because explicit master is broken, both tracks are followers.
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));

    // Now set channel 1 to not-master, all will be follower waiting for a valid bpm.
    pButtonSyncMaster1->slotSet(0);

    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
}

TEST_F(EngineSyncTest, RateChangeTest) {
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->set(SYNC_MASTER_EXPLICIT);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->set(SYNC_FOLLOWER);
    ProcessBuffer();

    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    EXPECT_DOUBLE_EQ(
            160.0, ControlObject::get(ConfigKey(m_sGroup1, "file_bpm")));
    ProcessBuffer();
    EXPECT_DOUBLE_EQ(
            160.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Set the rate of channel 1 to 1.2.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.2));
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.2),
            ControlObject::get(ConfigKey(m_sGroup1, "rate")));
    EXPECT_DOUBLE_EQ(192.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));

    // Internal master should also be 192.
    EXPECT_DOUBLE_EQ(
            192.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Set the file bpm of channel 2 to 120bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    EXPECT_DOUBLE_EQ(
            120.0, ControlObject::get(ConfigKey(m_sGroup2, "file_bpm")));

    // rate slider for channel 2 should now be 1.6 = 160 * 1.2 / 120.
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.6),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
    EXPECT_DOUBLE_EQ(192.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
}

TEST_F(EngineSyncTest, RateChangeTestWeirdOrder) {
    // This is like the test above, but the user loads the track after the slider has been tweaked.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    EXPECT_DOUBLE_EQ(
            160.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Set the file bpm of channel 2 to 120bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Set the rate slider of channel 1 to 1.2.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.2));

    // Rate slider for channel 2 should now be 1.6 = (160 * 1.2 / 120) - 1.0.
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.6),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
    EXPECT_DOUBLE_EQ(192.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Internal Master BPM should read the same.
    EXPECT_DOUBLE_EQ(
            192.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, RateChangeTestOrder3) {
    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    EXPECT_DOUBLE_EQ(
            160.0, ControlObject::get(ConfigKey(m_sGroup1, "file_bpm")));

    // Set the file bpm of channel 2 to 120bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    EXPECT_DOUBLE_EQ(
            120.0, ControlObject::get(ConfigKey(m_sGroup2, "file_bpm")));

    // Turn on Master and Follower.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->set(SYNC_MASTER_EXPLICIT);
    ProcessBuffer();

    ASSERT_TRUE(isExplicitMaster(m_sGroup1));

    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->set(SYNC_FOLLOWER);
    ProcessBuffer();

    // Follower should immediately set its slider.
    EXPECT_NEAR(getRateSliderValue(1.3333333333),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_DOUBLE_EQ(160.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(
            160.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, FollowerRateChange) {
    // Confirm that followers can change master sync rate as well.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->set(SYNC_MASTER_EXPLICIT);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->set(SYNC_FOLLOWER);
    ProcessBuffer();

    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    // Set the file bpm of channel 2 to 120bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Set the rate slider of channel 1 to 1.2.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.2));

    // Rate slider for channel 2 should now be 1.6 = (160 * 1.2 / 120).
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.6),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
    EXPECT_DOUBLE_EQ(192.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Try to twiddle the rate slider on channel 2.
    auto pSlider2 = std::make_unique<ControlProxy>(m_sGroup2, "rate");
    pSlider2->set(getRateSliderValue(0.8));
    ProcessBuffer();

    // Rates should still be changed even though it's a follower.
    EXPECT_DOUBLE_EQ(getRateSliderValue(0.8),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
    EXPECT_DOUBLE_EQ(96.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(getRateSliderValue(0.6),
            ControlObject::get(ConfigKey(m_sGroup1, "rate")));
    EXPECT_DOUBLE_EQ(96.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
}

TEST_F(EngineSyncTest, InternalRateChangeTest) {
    auto pButtonMasterSyncInternal = std::make_unique<ControlProxy>(
            m_sInternalClockGroup, "sync_master");
    pButtonMasterSyncInternal->set(SYNC_MASTER_SOFT);
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->set(SYNC_FOLLOWER);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->set(SYNC_FOLLOWER);
    ProcessBuffer();

    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Set the file bpm of channel 1 to 160bpm.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    EXPECT_DOUBLE_EQ(160.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "file_bpm"))->get());

    // Set the file bpm of channel 2 to 120bpm.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    EXPECT_DOUBLE_EQ(120.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "file_bpm"))->get());

    // Set the internal rate to 150.
    auto pMasterSyncSlider =
            std::make_unique<ControlProxy>(m_sInternalClockGroup, "bpm");
    pMasterSyncSlider->set(150.0);
    EXPECT_DOUBLE_EQ(150.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    // Set decks playing, and process a buffer to update all the COs.
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    ProcessBuffer();

    // Rate sliders for channels 1 and 2 should change appropriately.
    EXPECT_DOUBLE_EQ(getRateSliderValue(0.9375),
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get());
    EXPECT_DOUBLE_EQ(150.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.25),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get());
    EXPECT_DOUBLE_EQ(150.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    // Set the internal rate to 140.
    pMasterSyncSlider->set(140.0);

    // Update COs again.
    ProcessBuffer();

    // Rate sliders for channels 1 and 2 should change appropriately.
    EXPECT_DOUBLE_EQ(getRateSliderValue(.875),
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get());
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_NEAR(getRateSliderValue(1.16666667),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get(),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
}

TEST_F(EngineSyncTest, MasterStopSliderCheck) {
    // If the master is playing, and stop is pushed, the sliders should stay the same.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 120, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 128, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT);
    auto pButtonMasterSync2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_mode");
    pButtonMasterSync2->slotSet(SYNC_FOLLOWER);
    ProcessBuffer();

    //ASSERT_TRUE(isExplicitMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    auto pChannel1Play = std::make_unique<ControlProxy>(m_sGroup1, "play");
    pChannel1Play->set(1.0);
    auto pChannel2Play = std::make_unique<ControlProxy>(m_sGroup2, "play");
    pChannel2Play->set(1.0);

    ProcessBuffer();

    EXPECT_DOUBLE_EQ(120.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(getRateSliderValue(0.9375),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));

    pChannel1Play->set(0.0);

    ProcessBuffer();

    EXPECT_DOUBLE_EQ(120.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(getRateSliderValue(0.9375),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
}

TEST_F(EngineSyncTest, EnableOneDeckInitsMaster) {
    // If Internal is master, and we turn sync on a playing deck, the playing deck sets the
    // internal master and the beat distances are now aligned.
    ControlObject::set(ConfigKey(m_sInternalClockGroup, "bpm"), 124.0);
    ControlObject::set(ConfigKey(m_sInternalClockGroup, "beat_distance"), 0.5);
    ProcessBuffer();

    // Set up the deck to play.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));
    ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->set(0.2);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);

    // Enable Sync.  We have to call requestEnableSync directly
    // because calling ProcessBuffer() tries to advance the beat_distance values.
    m_pEngineSync->requestEnableSync(
            m_pEngineSync->getSyncableForGroup(m_sGroup1), true);

    // Internal is no longer master because there is exactly one playing deck.
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));

    // Internal clock rate and beat distance should match that deck.
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(
            0.2, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.2,
            ControlObject::get(
                    ConfigKey(m_sInternalClockGroup, "beat_distance")));

    // Enable second deck, bpm and beat distance should still match original setting.
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 140, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))
            ->set(getRateSliderValue(1.0));
    ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->set(0.2);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    m_pEngineSync->requestEnableSync(
            m_pEngineSync->getSyncableForGroup(m_sGroup2), true);
    // Now master should be Internal Clock because we have two playing decks.
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(
            0.2, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.2,
            ControlObject::get(
                    ConfigKey(m_sInternalClockGroup, "beat_distance")));
}

TEST_F(EngineSyncTest, EnableOneDeckInitializesMaster) {
    // Enabling sync on a deck causes it to be master, and sets bpm and clock.
    // Set the deck to play.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));
    ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->set(0.2);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);

    // Set the deck to follower.
    // As above, use direct call to avoid advancing beat distance.
    m_pEngineSync->requestEnableSync(
            m_pEngineSync->getSyncableForGroup(m_sGroup1), true);

    // That first deck is now master
    ASSERT_TRUE(isSoftMaster(m_sGroup1));

    // Internal clock rate should be set and beat distances reset.
    EXPECT_DOUBLE_EQ(130.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(130.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(0.2,
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());
    EXPECT_DOUBLE_EQ(0.2,
            ControlObject::getControl(
                    ConfigKey(m_sInternalClockGroup, "beat_distance"))
                    ->get());
}

TEST_F(EngineSyncTest, LoadTrackInitializesMaster) {
    // First eject the fake tracks that come with the testing framework.
    m_pChannel1->getEngineBuffer()->slotEjectTrack(1.0);
    m_pChannel2->getEngineBuffer()->slotEjectTrack(1.0);
    m_pChannel3->getEngineBuffer()->slotEjectTrack(1.0);

    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->slotSet(1.0);

    // No master because this deck has no track.
    EXPECT_EQ(NULL, m_pEngineSync->getMaster());
    EXPECT_DOUBLE_EQ(
            0.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    // The track load trigger a master change.
    m_pMixerDeck1->loadFakeTrack(false, 140.0);
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    // But as soon as we play, deck 1 is master
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);

    // If sync is on two decks and we load a track in only one of them, it will be
    // master.
    m_pChannel1->getEngineBuffer()->slotEjectTrack(1.0);
    ASSERT_TRUE(isFollower(m_sGroup1));
    // no relevant tempo available so internal clock is following
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));

    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->slotSet(1.0);

    m_pMixerDeck1->loadFakeTrack(false, 128.0);

    // Deck 2 is still empty so Deck 1 becomes master again
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    // If sync is on two decks and one deck is loaded but not playing, we should
    // initialize to that deck with internal clock master.
    m_pMixerDeck2->loadFakeTrack(false, 110.0);

    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
}

TEST_F(EngineSyncTest, LoadTrackResetTempoOption) {
    // Make sure playing decks with master sync enabled do not change tempo when
    // the "Reset Speed/Tempo" preference is set and a track is loaded to another
    // deck with master sync enabled.
    m_pConfig->set(ConfigKey("[Controls]", "SpeedAutoReset"),
            ConfigValue(BaseTrackPlayer::RESET_SPEED));

    // Enable sync on two stopped decks
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);

    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->set(1.0);

    // If sync is on and we load a track, that should initialize master.
    TrackPointer track1 = m_pMixerDeck1->loadFakeTrack(false, 140.0);

    EXPECT_DOUBLE_EQ(
            140.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));

    // If sync is on two decks and we load a track while one is playing,
    // that should not change the playing deck.
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);

    TrackPointer track2 = m_pMixerDeck2->loadFakeTrack(false, 128.0);

    EXPECT_DOUBLE_EQ(
            140.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Repeat with RESET_PITCH_AND_SPEED
    m_pConfig->set(ConfigKey("[Controls]", "SpeedAutoReset"),
            ConfigValue(BaseTrackPlayer::RESET_PITCH_AND_SPEED));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    track1 = m_pMixerDeck1->loadFakeTrack(false, 140.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    track2 = m_pMixerDeck2->loadFakeTrack(false, 128.0);
    EXPECT_DOUBLE_EQ(
            140.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Repeat with RESET_NONE
    m_pConfig->set(ConfigKey("[Controls]", "SpeedAutoReset"),
            ConfigValue(BaseTrackPlayer::RESET_NONE));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));
    track1 = m_pMixerDeck1->loadFakeTrack(false, 140.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    track2 = m_pMixerDeck2->loadFakeTrack(false, 128.0);
    EXPECT_DOUBLE_EQ(
            128.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(128.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(128.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Load two tracks with sync off and RESET_SPEED
    m_pConfig->set(ConfigKey("[Controls]", "SpeedAutoReset"),
            ConfigValue(BaseTrackPlayer::RESET_SPEED));
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.5));
    ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))
            ->set(getRateSliderValue(1.5));
    pButtonSyncEnabled1->set(0.0);
    pButtonSyncEnabled2->set(0.0);
    track1 = m_pMixerDeck1->loadFakeTrack(false, 140.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    track2 = m_pMixerDeck2->loadFakeTrack(false, 128.0);
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(128.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Load two tracks with sync off and RESET_PITCH_AND_SPEED
    m_pConfig->set(ConfigKey("[Controls]", "SpeedAutoReset"),
            ConfigValue(BaseTrackPlayer::RESET_PITCH_AND_SPEED));
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.5));
    ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))
            ->set(getRateSliderValue(1.5));
    pButtonSyncEnabled1->slotSet(0.0);
    pButtonSyncEnabled2->slotSet(0.0);
    track1 = m_pMixerDeck1->loadFakeTrack(false, 140.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    track2 = m_pMixerDeck2->loadFakeTrack(false, 128.0);
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
}

TEST_F(EngineSyncTest, EnableOneDeckSliderUpdates) {
    // If we enable a deck to be master, the internal slider should immediately update.
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));

    // Set the deck to sync enabled.
    pButtonSyncEnabled1->slotSet(1.0);
    ProcessBuffer();

    // Group 1 should now be master (only one sync deck).
    ASSERT_TRUE(isSoftMaster(m_sGroup1));

    // Internal clock rate should be set.
    EXPECT_DOUBLE_EQ(130.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
}

TEST_F(EngineSyncTest, SyncToNonSyncDeck) {
    // If deck 1 is playing, and deck 2 presses sync, deck 2 should sync to deck 1 even if
    // deck 1 is not a sync deck.

    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ProcessBuffer();
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))
            ->set(getRateSliderValue(1.0));

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();

    pButtonSyncEnabled2->set(1.0);
    pButtonSyncEnabled2->set(0.0);
    ProcessBuffer();

    // There should be no master, and deck2 should match rate of deck1.  Sync slider should be
    // updated with the value, however.
    assertNoMaster();
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    assertSyncOff(m_sGroup2);
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.3),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));

    // Reset the pitch of deck 2.
    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));

    // The same should work in reverse.
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();

    // There should be no master, and deck2 should match rate of deck1.
    EXPECT_EQ(0,
            ControlObject::get(
                    ConfigKey(m_sInternalClockGroup, "sync_master")));
    EXPECT_DOUBLE_EQ(
            100.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_EQ(NULL, m_pEngineSync->getMaster());
    EXPECT_EQ(NULL, m_pEngineSync->getMasterSyncable());
    EXPECT_EQ(SYNC_NONE, ControlObject::get(ConfigKey(m_sGroup1, "sync_mode")));
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup1, "sync_enabled")));
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup1, "sync_master")));
    EXPECT_DOUBLE_EQ(getRateSliderValue(100.0 / 130.0),
            ControlObject::get(ConfigKey(m_sGroup1, "rate")));

    // Reset again.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));

    // If deck 1 is not playing, however, deck 2 should stay at the same rate.
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);

    // The same should work in reverse.
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();

    // There should be no master, and deck2 should match rate of deck1.
    EXPECT_EQ(0,
            ControlObject::get(
                    ConfigKey(m_sInternalClockGroup, "sync_master")));
    EXPECT_DOUBLE_EQ(
            100.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_EQ(NULL, m_pEngineSync->getMaster());
    EXPECT_EQ(NULL, m_pEngineSync->getMasterSyncable());
    EXPECT_EQ(SYNC_NONE, ControlObject::get(ConfigKey(m_sGroup2, "sync_mode")));
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "sync_enabled")));
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "sync_master")));
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::get(ConfigKey(m_sGroup2, "rate")));
}

TEST_F(EngineSyncTest, MomentarySyncDependsOnPlayingStates) {
    // Like it says -- if the current deck is playing, and the target deck is
    // playing, they should sync even if there's no sync mode enabled.

    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");

    // Set up decks so they can be playing, and start deck 1.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 100, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 130, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();
    ProcessBuffer();

    // Set channel 1 to be enabled momentarily.
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();

    // The master sync should still be off and the speed should match deck 2.
    assertNoMaster();
    assertSyncOff(m_sGroup1);
    assertSyncOff(m_sGroup2);
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Also works if deck 1 is not playing.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();
    assertNoMaster();
    assertSyncOff(m_sGroup1);
    assertSyncOff(m_sGroup2);
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Also works if neither deck is playing.
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 0.0);
    ProcessBuffer();
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();
    assertNoMaster();
    assertSyncOff(m_sGroup1);
    assertSyncOff(m_sGroup2);
    EXPECT_DOUBLE_EQ(
            130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // But it doesn't work if deck 2 isn't playing and deck 1 is. (This would
    // cause deck1 to suddenly change bpm while playing back).
    ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0));
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 0.0);
    ProcessBuffer();
    pButtonSyncEnabled1->set(1.0);
    pButtonSyncEnabled1->set(0.0);
    ProcessBuffer();
    assertNoMaster();
    assertSyncOff(m_sGroup1);
    assertSyncOff(m_sGroup2);
    EXPECT_DOUBLE_EQ(
            100.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, EjectTrackSyncRemains) {
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    auto pButtonEject1 = std::make_unique<ControlProxy>(m_sGroup1, "eject");

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 120, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    // m_sGroup1 takes over
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    assertSyncOff(m_sGroup2);

    pButtonEject1->set(1.0);
    // When an eject happens, the bpm gets set to zero.
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(
            0.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    // No valid tempo available all are waiting as follower
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    assertSyncOff(m_sGroup2);

    m_pMixerDeck1->loadFakeTrack(false, 128.0);
    EXPECT_DOUBLE_EQ(128.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 135, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    pButtonSyncEnabled2->set(1.0);
    ProcessBuffer();

    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    pButtonEject1->set(1.0);
    m_pTrack1->trySetBeats(mixxx::BeatsPointer());
    ProcessBuffer();

    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
}

TEST_F(EngineSyncTest, FileBpmChangesDontAffectMaster) {
    // If filebpm changes, don't treat it like a rate change.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 100, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->set(1.0);
    ProcessBuffer();

    pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    EXPECT_DOUBLE_EQ(
            100.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, ExplicitMasterPostProcessed) {
    // Regression test thanks to a bug.  Make sure that an explicit master
    // channel gets post-processed.
    auto pButtonMasterSync1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_mode");
    pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT);
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 160, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ProcessBuffer();
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ProcessBuffer();

    EXPECT_NEAR(0.0023219956,
            m_pChannel1->getEngineBuffer()->getVisualPlayPos(),
            kMaxFloatingPointErrorLowPrecision);
}

TEST_F(EngineSyncTest, ZeroBPMRateAdjustIgnored) {
    // If a track isn't loaded (0 bpm), but the deck has sync enabled,
    // don't pay attention to rate changes.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 0, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));
    ProcessBuffer();

    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 120, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->set(1.0);
    ProcessBuffer();

    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.3));

    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get());

    // Also try with explicit master/follower setting
    pButtonSyncEnabled1->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_MASTER_EXPLICIT);

    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.4));
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get());

    pButtonSyncEnabled1->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);

    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(0.9));
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get());
}

TEST_F(EngineSyncTest, BeatDistanceBeforeStart) {
    // https://bugs.launchpad.net/mixxx/+bug/1930143
    // If the start position is before zero, we should still initialize the beat distance
    // correctly.  Unfortunately, this currently doesn't work.

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(
            m_pTrack1->getSampleRate(), 128, 0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), -.05);
    ProcessBuffer();
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ProcessBuffer();
    // This fraction is one buffer beyond the seek position -- indicating that
    // we seeked correctly.
    EXPECT_NEAR(0.49143461829176116,
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxBeatDistanceEpsilon);
}

TEST_F(EngineSyncTest, ZeroLatencyRateChangeNoQuant) {
    // Confirm that a rate change in an explicit master is instantly communicated
    // to followers.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 160, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Make Channel2 master to weed out any channel ordering issues.
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    // Exaggerate the effect with a high rate.
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 10.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    EXPECT_EQ(ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))
                      ->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());

    for (int i = 0; i < 50; ++i) {
        ProcessBuffer();
        // Keep messing with the rate
        double rate = i % 2 == 0 ? i / 10.0 : i / -10.0;
        ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), rate);

        // Buffers should be in sync.
        ASSERT_NEAR(
                ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
                ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
                kMaxBeatDistanceEpsilon);
    }

    // Make sure we're actually going somewhere!
    EXPECT_GT(
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            0);
    // Buffers should be in sync.
    EXPECT_NEAR(
            ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxBeatDistanceEpsilon);
}

TEST_F(EngineSyncTest, ZeroLatencyRateChangeQuant) {
    // Confirm that a rate change in an explicit master is instantly communicated
    // to followers.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 160, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);

    // Make Channel2 master to weed out any channel ordering issues.
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    // Exaggerate the effect with a high rate.
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 10.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    EXPECT_EQ(ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))
                      ->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());

    ProcessBuffer();

    for (int i = 0; i < 50; ++i) {
        ProcessBuffer();
        // Keep messing with the rate
        double rate = i % 2 == 0 ? i / 10.0 : i / -10.0;
        ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), rate);

        // Buffers should be in sync.
        ASSERT_NEAR(
                ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
                ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
                kMaxBeatDistanceEpsilon);
    }

    // Make sure we're actually going somewhere!
    EXPECT_GT(
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            0);
    // Buffers should be in sync.
    EXPECT_NEAR(
            ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxBeatDistanceEpsilon);
}

TEST_F(EngineSyncTest, ZeroLatencyRateDiffQuant) {
    // Confirm that a rate change in an explicit master is instantly communicated
    // to followers.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 160, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);

    // Make Channel2 master to weed out any channel ordering issues.
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    // Exaggerate the effect with a high rate.
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 10.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    EXPECT_EQ(ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))
                      ->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());

    ProcessBuffer();

    for (int i = 0; i < 50; ++i) {
        ProcessBuffer();
        // Keep messing with the rate
        double rate = i % 2 == 0 ? i / 10.0 : i / -10.0;
        ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), rate);

        // Buffers should be in sync.
        ASSERT_NEAR(
                ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
                ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
                kMaxBeatDistanceEpsilon);
    }

    // Make sure we're actually going somewhere!
    EXPECT_GT(
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            0);
    // Buffers should be in sync.
    EXPECT_NEAR(
            ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxBeatDistanceEpsilon);
}

// In this test, we set play *first* and then turn on master sync.
// This exercises a slightly different ordering of signals that we
// need to check. The Sync feature is unfortunately brittle.
// This test exercises https://bugs.launchpad.net/mixxx/+bug/1884324
TEST_F(EngineSyncTest, ActivatingSyncDoesNotCauseDrifting) {
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 150, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 150, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    ProcessBuffer();

    // make sure we aren't out-of-sync from the start
    EXPECT_EQ(ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))
                      ->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());

    // engage first sync-master
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);

    // engage second Sync-master
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);

    // Run for a number of buffers
    for (int i = 0; i < 25; ++i) {
        ProcessBuffer();
    }
    // Make sure we're actually going somewhere!
    EXPECT_GT(ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                      ->get(),
            0);

    // Buffers should be in sync.
    EXPECT_EQ(ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))
                      ->get(),
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());
}

TEST_F(EngineSyncTest, HalfDoubleBpmTest) {
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 70, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 140, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);

    // Mixxx will choose the first playing deck to be master.  Let's start deck 2 first.
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ProcessBuffer();

    EXPECT_EQ(0.5,
            m_pChannel1->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);
    EXPECT_EQ(1.0,
            m_pChannel2->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);
    EXPECT_DOUBLE_EQ(
            m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance());
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup1, "rate")));
    EXPECT_EQ(70, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "rate")));
    EXPECT_EQ(140, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // Do lots of processing to make sure we get over the 0.5 beat_distance barrier.
    for (int i = 0; i < 50; ++i) {
        ProcessBuffer();
        // The beat distances are NOT as simple as x2 or /2.  Use the built-in functions
        // to do the proper conversion.
        EXPECT_NEAR(
                m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
                m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
                kMaxFloatingPointErrorLowPrecision);
    }

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(0.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_NONE);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_NONE);

    EXPECT_EQ(1.0,
            m_pChannel1->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);
    EXPECT_EQ(1.0,
            m_pChannel2->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))
            ->set(SYNC_FOLLOWER);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));

    // Now start deck 1 first and check again.
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    ProcessBuffer();

    EXPECT_EQ(1.0,
            m_pChannel1->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);
    EXPECT_EQ(2.0,
            m_pChannel2->getEngineBuffer()
                    ->m_pSyncControl->m_masterBpmAdjustFactor);

    // Exaggerate the effect with a high rate.
    ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))
            ->set(getRateSliderValue(2.0));

    for (int i = 0; i < 50; ++i) {
        ProcessBuffer();
        EXPECT_DOUBLE_EQ(m_pChannel1->getEngineBuffer()
                                 ->m_pSyncControl->getBeatDistance(),
                m_pChannel2->getEngineBuffer()
                        ->m_pSyncControl->getBeatDistance());
    }
}

TEST_F(EngineSyncTest, HalfDoubleThenPlay) {
    // If a deck plays that had its multiplier set, we need to reset the
    // internal clock.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 80, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 175, 0.0);
    m_pTrack2->trySetBeats(pBeats2);
    ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))
            ->set(getRateSliderValue(1.0));

    ProcessBuffer();

    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->slotSet(1.0);
    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    pButtonSyncEnabled2->slotSet(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);

    // We Expect that m_sGroup1 has adjusted its own bpm to the second deck and becomes a single master.
    // When the second deck is synced the master bpm is adopted by the interna clock, which becomes now
    // the master
    EXPECT_DOUBLE_EQ(87.5,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(87.5,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(175.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
    EXPECT_DOUBLE_EQ(87.5 / 80,
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate_ratio"))
                    ->get());
    EXPECT_DOUBLE_EQ(1,
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate_ratio"))
                    ->get());
    EXPECT_DOUBLE_EQ(80,
            ControlObject::getControl(ConfigKey(m_sGroup1, "local_bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(175.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "local_bpm"))
                    ->get());

    ProcessBuffer();

    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);

    ProcessBuffer();

    EXPECT_DOUBLE_EQ(175.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());

    ProcessBuffer();
    ProcessBuffer();
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(
            m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance());

    // Now enable the other deck first.
    // Sync only cares about which deck plays first now, enable order is irrelevant.
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(0.0);
    pButtonSyncEnabled1->slotSet(0.0);
    pButtonSyncEnabled2->slotSet(0.0);
    ProcessBuffer();
    pButtonSyncEnabled2->slotSet(1.0);
    pButtonSyncEnabled1->slotSet(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    EXPECT_DOUBLE_EQ(87.5 / 80,
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate_ratio"))
                    ->get());
    EXPECT_DOUBLE_EQ(1,
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate_ratio"))
                    ->get());

    ProcessBuffer();

    EXPECT_NEAR(
            m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            kMaxFloatingPointErrorHighPrecision);

    ProcessBuffer();
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(87.5,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    EXPECT_NEAR(
            m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            kMaxFloatingPointErrorHighPrecision);

    ProcessBuffer();
    ProcessBuffer();
    ProcessBuffer();

    EXPECT_NEAR(
            m_pChannel1->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance(),
            kMaxFloatingPointErrorHighPrecision);
}

TEST_F(EngineSyncTest, HalfDoubleInternalClockTest) {
    // If we set the file_bpm CO's directly, the correct signals aren't fired.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 70, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 140, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);
    // Make Channel2 master to weed out any channel ordering issues.
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1);

    EXPECT_DOUBLE_EQ(70.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get());
    EXPECT_DOUBLE_EQ(getRateSliderValue(1.0),
            ControlObject::getControl(ConfigKey(m_sGroup2, "rate"))->get());
}

namespace {
QVector<double> createBeatVector(
        double first_beat, unsigned int num_beats, double beat_length) {
    QVector<double> beats;
    for (unsigned int i = 0; i < num_beats; ++i) {
        beats.append(first_beat + i * beat_length);
    }
    return beats;
}
} // namespace

TEST_F(EngineSyncTest, HalfDoubleConsistency) {
    // half-double matching should be consistent
    double beatLengthFrames = 60.0 * 44100 / 90.0;
    double startOffsetFrames = 0;
    const int numBeats = 100;
    QVector<double> beats1 =
            createBeatVector(startOffsetFrames, numBeats, beatLengthFrames);
    auto pBeats1 = mixxx::BeatMap::makeBeatMap(m_pTrack1->getSampleRate(), QString(), beats1);
    m_pTrack1->trySetBeats(pBeats1);

    beatLengthFrames = 60.0 * 44100 / 145.0;
    QVector<double> beats2 =
            createBeatVector(startOffsetFrames, numBeats, beatLengthFrames);
    auto pBeats2 = mixxx::BeatMap::makeBeatMap(m_pTrack2->getSampleRate(), QString(), beats2);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1);
    EXPECT_DOUBLE_EQ(180.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1);
    EXPECT_DOUBLE_EQ(90.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(180.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(0);
    ProcessBuffer();
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1);
    ProcessBuffer();
    EXPECT_DOUBLE_EQ(90.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(180.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
}

TEST_F(EngineSyncTest, HalfDoubleEachOther) {
    // Confirm that repeated sync with both decks leads to the same
    // Half/Double decision.
    // This test demonstrates https://bugs.launchpad.net/mixxx/+bug/1921962
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 144, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 105, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Threshold 1.414 sqrt(2);
    // 144 / 105 = 1.37
    // 105 / 72 = 1.46
    // expect 144 BPM

    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(0.0);

    EXPECT_DOUBLE_EQ(144.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(0.0);

    EXPECT_DOUBLE_EQ(144.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    // Threshold 1.414 sqrt(2);
    // 150 / 105 = 1.43
    // 105 / 75 = 1.40
    // expect 75 BPM

    mixxx::BeatsPointer pBeats1b = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 150, 0.0);
    m_pTrack1->trySetBeats(pBeats1b);

    EXPECT_DOUBLE_EQ(150.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());

    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(0.0);

    EXPECT_DOUBLE_EQ(75.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(0.0);

    EXPECT_DOUBLE_EQ(150.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
}

TEST_F(EngineSyncTest, SetFileBpmUpdatesLocalBpm) {
    ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->set(0.2);
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ASSERT_EQ(
            130.0, m_pEngineSync->getSyncableForGroup(m_sGroup1)->getBaseBpm());
}

TEST_F(EngineSyncTest, SyncPhaseToPlayingNonSyncDeck) {
    // If we press play on a sync deck, we will only sync phase to a non-sync
    // deck if there are no sync decks and the non-sync deck is playing.

    auto pButtonSyncEnabled1 =
            std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);

    auto pButtonSyncEnabled2 =
            std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 1.0);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    // Set the sync deck playing with nothing else active.
    // Next Deck becomes master and the Master clock is set to 100 BPM
    // The 130 BPM Track should be played at 100 BPM, rate = 0,769230769
    pButtonSyncEnabled1->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    // Beat length: 40707.692307692305
    // Buffer size is 1024 at 44.1 kHz at the given rate = 787.692307692 Samples
    // Expected beat_distance = 0.019349962
    ProcessBuffer();

    // Internal clock rate should be set and beat distance already updated
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_NEAR(0.019349962,
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);

    // The internal clock must also have been advanced to the same fraction of a beat.
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))->get());
    EXPECT_NEAR(0.019349962,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(0.0);

    ProcessBuffer();

    // we expect that Deck 1 distance has not changed and the internal clock has continued
    // with the same rate
    EXPECT_NEAR(0.019349962,
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.019349962,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);

    // Now make the second deck playing and see if it works.
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);

    // What seems to be happening is a seek is queued when a track is loaded, so all these tracks
    // seek to 0, thus resetting the beat distance.
    ProcessBuffer();

    // We expect that the second deck (master) has adjusted the "beat_distance" of the
    // internal clock and the fist deck is advanced slightly but slower to slowly get
    // rid of the additional buffer ahead
    // TODO: It does sounds odd to start the track 1 at a random position and adjust the
    // phase later. Seeking into phase is the best option even with quantize off.
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    // The adjustment is calculated here: BpmControl::calcSyncAdjustment
    EXPECT_GT(0.038699925, ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->get());
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());
    EXPECT_NEAR(
            0.019349962,
            ControlObject::getControl(ConfigKey(m_sGroup2, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_DOUBLE_EQ(100.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))->get());
    EXPECT_NEAR(
            0.038699925,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "beat_distance"))->get(),
            kMaxFloatingPointErrorLowPrecision);

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    pButtonSyncEnabled1->set(0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "rate_ratio"), 1.0);

    ControlObject::set(ConfigKey(m_sGroup2, "play"), 0.0);
    pButtonSyncEnabled2->set(0.0);
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 1.0);

    // But if there is a third deck that is sync-enabled, we match that.
    auto pButtonSyncEnabled3 =
            std::make_unique<ControlProxy>(m_sGroup3, "sync_enabled");
    ControlObject::set(ConfigKey(m_sGroup3, "beat_distance"), 0.6);
    ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 1.0);
    mixxx::BeatsPointer pBeats3 = BeatFactory::makeBeatGrid(m_pTrack3->getSampleRate(), 140, 0.0);
    m_pTrack3->trySetBeats(pBeats3);
    // This will sync to the first deck here and not the second (lp1784185)
    pButtonSyncEnabled3->set(1.0);
    ProcessBuffer();
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup3, "bpm")));
    // revert that
    ControlObject::set(ConfigKey(m_sGroup3, "rate_ratio"), 1.0);
    ProcessBuffer();
    EXPECT_DOUBLE_EQ(140.0, ControlObject::get(ConfigKey(m_sGroup3, "bpm")));
    // now we have Deck 3 with 140 bpm and sync enabled

    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    ControlObject::getControl(ConfigKey(m_sGroup3, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ProcessBuffer();

    // We expect Deck 1 is Deck 3 bpm
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))
                    ->get());
    EXPECT_DOUBLE_EQ(140.0,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    // The exact beat distance will be one buffer past .6, but this is good
    // enough to confirm that it worked.
    EXPECT_GT(0.7,
            ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                    ->get());
    EXPECT_GT(0.7,
            ControlObject::getControl(
                    ConfigKey(m_sInternalClockGroup, "beat_distance"))
                    ->get());
}

TEST_F(EngineSyncTest, UserTweakBeatDistance) {
    // If a deck has a user tweak, and another deck stops such that the first
    // is used to reseed the master beat distance, make sure the user offset
    // is reset.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 128, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "quantize"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    // Play some buffers with the wheel at 0 to show the sync works
    ControlObject::getControl(ConfigKey(m_sGroup1, "wheel"))->set(0);
    for (int i = 0; i < 10; ++i) {
        ProcessBuffer();
    }

    // Spin the wheel, causing the useroffset for group1 to get set.
    ControlObject::getControl(ConfigKey(m_sGroup1, "wheel"))->set(0.4);
    for (int i = 0; i < 10; ++i) {
        ProcessBuffer();
    }
    // Play some more buffers with the wheel at 0.
    ControlObject::getControl(ConfigKey(m_sGroup1, "wheel"))->set(0);
    for (int i = 0; i < 10; ++i) {
        ProcessBuffer();
    }

    // Stop the second deck.  This causes the master beat distance to get
    // seeded with the beat distance from deck 1.
    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(0.0);

    // Play a buffer, which is enough to see if the beat distances align.
    ProcessBuffer();

    EXPECT_NEAR(ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))
                        ->get(),
            ControlObject::getControl(
                    ConfigKey(m_sInternalClockGroup, "beat_distance"))
                    ->get(),
            .00001);

    EXPECT_DOUBLE_EQ(0.0,
            m_pChannel1->getEngineBuffer()
                    ->m_pBpmControl->m_dUserOffset.getValue());
}

TEST_F(EngineSyncTest, UserTweakPreservedInSeek) {
    // Ensure that when we do a seek during master sync, the user offset is maintained.

    // This is about 128 bpm, but results in nice round numbers of samples.
    const double kDivisibleBpm = 44100.0 / 344.0;
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(
            m_pTrack1->getSampleRate(), kDivisibleBpm, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 130, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1);
    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_enabled"))->set(1);
    ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "quantize"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);

    ProcessBuffer();
    EXPECT_DOUBLE_EQ(kDivisibleBpm,
            ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get());
    EXPECT_DOUBLE_EQ(kDivisibleBpm,
            ControlObject::getControl(ConfigKey(m_sGroup2, "bpm"))->get());

    EXPECT_NEAR(0.024806201,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.0023219956,
            ControlObject::get(ConfigKey(m_sGroup1, "playposition")),
            kMaxFloatingPointErrorLowPrecision);

    ControlObject::set(ConfigKey(m_sGroup2, "playposition"), 0.2);
    ProcessBuffer();
    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), 0.2);
    ProcessBuffer();

    // We are a few buffers past the seeked position.  It's less than 0.2 because
    // of quantizing.  The playpositions are different because the tracks are at
    // different bpms.
    EXPECT_NEAR(0.19417687,
            ControlObject::get(ConfigKey(m_sGroup1, "playposition")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.19148479,
            ControlObject::get(ConfigKey(m_sGroup2, "playposition")),
            kMaxFloatingPointErrorLowPrecision);
    // The beat distances are identical though.
    EXPECT_NEAR(0.074418604,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.074418604,
            ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")),
            kMaxFloatingPointErrorLowPrecision);

    // Apply user tweak offset.
    m_pChannel1->getEngineBuffer()
            ->m_pBpmControl->m_dUserOffset.setValue(0.3);

    // Seek back to 0.2
    ControlObject::set(ConfigKey(m_sGroup2, "playposition"), 0.2);
    ProcessBuffer();
    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), 0.2);
    ProcessBuffer();

    // Now, the locations are much more different - but the beat distance appears
    // to be the same because beat distance CO hides the user offset.
    EXPECT_NEAR(0.2269025,
            ControlObject::get(ConfigKey(m_sGroup1, "playposition")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.1960644,
            ControlObject::get(ConfigKey(m_sGroup2, "playposition")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.12403101,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")),
            kMaxFloatingPointErrorLowPrecision);
    EXPECT_NEAR(0.12403101,
            ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")),
            kMaxFloatingPointErrorLowPrecision);
}

TEST_F(EngineSyncTest, MasterBpmNeverZero) {
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    auto pButtonSyncEnabled1 = std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);

    m_pTrack1->trySetBeats(mixxx::BeatsPointer());
    EXPECT_EQ(128.0,
              ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))->get());
}

TEST_F(EngineSyncTest, ZeroBpmNaturalRate) {
    // If a track has a zero bpm and a bad beatgrid, make sure the rate
    // doesn't end up something crazy when sync is enabled..
    // Maybe the beatgrid ended up at zero also.
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 0.0, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    auto pButtonSyncEnabled1 = std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    pButtonSyncEnabled1->set(1.0);

    ProcessBuffer();

    // 0 bpm is what we want, the sync code will play the track back at rate
    // 1.0.
    EXPECT_EQ(0.0,
              ControlObject::getControl(ConfigKey(m_sGroup1, "local_bpm"))->get());
}

TEST_F(EngineSyncTest, QuantizeImpliesSyncPhase) {
    auto pButtonSyncEnabled1 = std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonBeatsync1 = std::make_unique<ControlProxy>(m_sGroup1, "beatsync");
    auto pButtonBeatsyncPhase1 = std::make_unique<ControlProxy>(m_sGroup1, "beatsync_phase");

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0));
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();

    // Beat length: 40707.692307692; Buffer size is 1024
    // Expected beat_distance = 1024/40707 = 0.025155
    EXPECT_DOUBLE_EQ(130, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(0.025154950869236584, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));

    // Beat length: 52920; Buffer size is 1024
    // Expected beat_distance = 0.01935
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(0.019349962207105064, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    // first test without quantization
    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    // Deck 1 continues with 100 bpm
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(0.044504913076341648, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));

    // Deck 2 continues normally
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(0.038699924414210128, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    pButtonSyncEnabled1->set(0.0);

    ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0);
    ProcessBuffer();

    // Quantize only has no effect here, both decks are running freely.

    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(0.063854875283446716, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));

    // Deck 2 continues normally
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(0.058049886621315196, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));
    pButtonSyncEnabled1->set(1.0);
    ProcessBuffer();

    // normally the deck would be at 0.083204837, due to quantize it has been seeked
    // in phase of deck 2
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    EXPECT_NEAR(
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")),
            ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")),
            1e-15);

    // Reset Deck 1 Tempo
    pButtonSyncEnabled1->set(0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "rate_ratio"), 1.0);
    ProcessBuffer();

    // Now the deck should be running normally
    EXPECT_DOUBLE_EQ(130, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_NEAR(
            0.10255479969765675,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")),
            1e-15);

    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(0.096749811035525324, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    pButtonBeatsyncPhase1->set(1.0);
    ProcessBuffer();

    // check if the next beat will be aligned:

    EXPECT_DOUBLE_EQ(130, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // we align here to the past beat, because beat_distance < 1.0/8
    EXPECT_NEAR(
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")) / 130 * 100,
            ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")),
            1e-15);
}

TEST_F(EngineSyncTest, SeekStayInPhase) {
    ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0);

    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(0.025154950869236584, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.0023219954648526077, ControlObject::get(ConfigKey(m_sGroup1, "playposition")));

    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), 0.2);
    ProcessBuffer();

    // We expect to be two buffers ahead in a beat near 0.2
    EXPECT_DOUBLE_EQ(0.050309901738473162,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.18925937554508981, ControlObject::get(ConfigKey(m_sGroup1, "playposition")));

    // The same again with a stopped track loaded in Channel 2
    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);
    ProcessBuffer();

    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(0.025154950869236584,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.0023219954648526077,
            ControlObject::get(ConfigKey(m_sGroup1, "playposition")));

    ControlObject::set(ConfigKey(m_sGroup1, "playposition"), 0.2);
    ProcessBuffer();

    // We expect to be two buffers ahead in a beat near 0.2
    EXPECT_DOUBLE_EQ(0.050309901738473162,
            ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
    EXPECT_DOUBLE_EQ(0.18925937554508981, ControlObject::get(ConfigKey(m_sGroup1, "playposition")));
}

TEST_F(EngineSyncTest, SyncWithoutBeatgrid) {
    // this tests bug lp1783020, notresetting rate when other deck has no beatgrid
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 128, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    m_pTrack2->trySetBeats(mixxx::BeatsPointer());

    ControlObject::set(ConfigKey(m_sGroup1, "rate"), 0.5);

    // Play a buffer, which is enough to see if the beat distances align.
    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup1, "sync_enabled"), 1);

    ProcessBuffer();

    ASSERT_DOUBLE_EQ(0.5,
              ControlObject::get(ConfigKey(m_sGroup1, "rate")));

}

TEST_F(EngineSyncTest, QuantizeHotCueActivate) {
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    auto pHotCue2Activate = std::make_unique<ControlProxy>(m_sGroup2, "hotcue_1_activate");
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0);

    m_pTrack2->trySetBeats(pBeats2);

    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);

    // store a hot cue at 0:00
    pHotCue2Activate->set(1.0);
    ProcessBuffer();
    pHotCue2Activate->set(0.0);
    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup2, "quantize"), 0.0);
    // preview a hot cue without quantize
    pHotCue2Activate->set(1.0);
    ProcessBuffer();

    // Expect that the track has advanced by one buffer starting from the hot cue at 0:00
    EXPECT_DOUBLE_EQ(0.019349962207105064, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    pHotCue2Activate->set(0.0);
    ProcessBuffer();

    // Expect that the track was set back to 0:00
    EXPECT_DOUBLE_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    // Expect that the first track has advanced 4 buffers in the mean time
    EXPECT_DOUBLE_EQ(0.10061980347694634, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));

    ControlObject::set(ConfigKey(m_sGroup2, "quantize"), 1.0);
    // preview a hot cue with quantize seeks back to 0:00 and adjust beat distance to match the first track
    pHotCue2Activate->set(1.0);
    ProcessBuffer();

    // Beat_distance is the distance to the previous beat wich has already passed.
    // We compare here the distance to the next beat (1 - beat_distance) and
    // scale it by the different tempos.
    EXPECT_NEAR(
            (1 - ControlObject::get(ConfigKey(m_sGroup1, "beat_distance"))) / 130 * 100,
            (1 - ControlObject::get(ConfigKey(m_sGroup2, "beat_distance"))),
            1e-15);

    pHotCue2Activate->set(0.0);
    ProcessBuffer();

    // Expect that the track is back to 0:00
    EXPECT_DOUBLE_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")));

    // Expect that the first track has advanced 6 buffers in the mean time
    EXPECT_DOUBLE_EQ(0.15092970521541951, ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")));
}

TEST_F(EngineSyncTest, ChangeBeatGrid) {
    // https://bugs.launchpad.net/mixxx/+bug/1808698

    auto pButtonSyncEnabled1 = std::make_unique<ControlProxy>(m_sGroup1, "sync_enabled");
    auto pButtonSyncEnabled2 = std::make_unique<ControlProxy>(m_sGroup2, "sync_enabled");

    // set beatgrid
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 130, 0.0);
    m_pTrack1->trySetBeats(pBeats1);
    pButtonSyncEnabled1->set(1.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);

    ProcessBuffer();

    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // sync 0 bpm track to the first one
    pButtonSyncEnabled2->set(1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);

    ProcessBuffer();

    // expect no change in Deck 1
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0);

    ProcessBuffer();
    // Group1 remains master because it is the only one with a tempo,
    ASSERT_TRUE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Load a new beatgrid during playing, this happens when the analyser is finished
    mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 140, 0.0);
    m_pTrack2->trySetBeats(pBeats2);

    ProcessBuffer();

    // we expect that the new beatgrid is aligned to the other playing track
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sInternalClockGroup));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));

    // Play Both Tracks.
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ProcessBuffer();

    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));

    // Load a new beatgrid again, this happens when the user adjusts the beatgrid
    mixxx::BeatsPointer pBeats2n = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 75, 0.0);
    m_pTrack2->trySetBeats(pBeats2n);

    ProcessBuffer();

    // we expect that the new beatgrid is aligned to the other playing track
    // Not the case before fixing lp1808698
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    // Expect to sync on half beats
    EXPECT_DOUBLE_EQ(65.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    EXPECT_DOUBLE_EQ(130.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm")));
}

TEST_F(EngineSyncTest, BeatMapQantizePlay) {
    // This test demonstates https://bugs.launchpad.net/mixxx/+bug/1874918
    mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(m_pTrack1->getSampleRate(), 120, 0.0);
    m_pTrack1->trySetBeats(pBeats1);

    constexpr auto kSampleRate = mixxx::audio::SampleRate(44100);

    auto pBeats2 = mixxx::BeatMap::makeBeatMap(kSampleRate,
            QString(),
            // Add two beats at 120 Bpm
            QVector<double>({static_cast<double>(kSampleRate) / 2,
                    static_cast<double>(kSampleRate)}));
    m_pTrack2->trySetBeats(pBeats2);

    ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "quantize"), 1.0);

    ControlObject::getControl(ConfigKey(m_sGroup1, "sync_mode"))->set(SYNC_MASTER_EXPLICIT);
    ControlObject::getControl(ConfigKey(m_sGroup2, "sync_mode"))->set(SYNC_FOLLOWER);

    ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0);

    ProcessBuffer();

    ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0);

    ProcessBuffer();

    // Beat Distance shall be still 0, because we are before the first beat.
    // This was fixed in https://bugs.launchpad.net/mixxx/+bug/1920084
    EXPECT_DOUBLE_EQ(m_pChannel2->getEngineBuffer()->m_pSyncControl->getBeatDistance(), 0);
    EXPECT_DOUBLE_EQ(
            ControlObject::get(ConfigKey(m_sGroup1, "playposition")),
            ControlObject::get(ConfigKey(m_sGroup2, "playposition")));
}

TEST_F(EngineSyncTest, BpmAdjustFactor) {
    // Failing test case from https://github.com/mixxxdj/mixxx/pull/2376

    m_pMixerDeck1->loadFakeTrack(false, 40.0);
    m_pMixerDeck2->loadFakeTrack(false, 150.0);
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(40.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(150.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup2, "sync_enabled"), 1.0);
    ProcessBuffer();

    // group 2 should be synced to the first playing deck and becomes master
    EXPECT_DOUBLE_EQ(40.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(80.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
    assertSyncOff(m_sGroup1);

    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
    // Pretend a changing beatgrid
    static_cast<SyncControl*>(m_pEngineSync->getMasterSyncable())->setLocalBpm(152);
    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup1, "sync_enabled"), 1.0);
    ProcessBuffer();

    // Group 1 should be already in sync, it must not change due to sync
    // and Group 2 must also remain.
    EXPECT_DOUBLE_EQ(40.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(80.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    ASSERT_TRUE(isFollower(m_sGroup1));
    ASSERT_TRUE(isFollower(m_sGroup2));
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
}

TEST_F(EngineSyncTest, ImpliciteMasterToInternalClock) {
    m_pMixerDeck1->loadFakeTrack(false, 40.0);
    m_pMixerDeck2->loadFakeTrack(false, 150.0);
    ProcessBuffer();

    EXPECT_DOUBLE_EQ(40.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(150.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));

    // During cue-ing volume is 0.0
    ControlObject::set(ConfigKey(m_sGroup1, "volume"), 0.0);
    ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0);
    ProcessBuffer();

    ControlObject::set(ConfigKey(m_sGroup1, "sync_enabled"), 1.0);
    ControlObject::set(ConfigKey(m_sGroup2, "sync_enabled"), 1.0);
    ProcessBuffer();

    // group 2 should be synced to the first playing deck and becomes master
    EXPECT_DOUBLE_EQ(75.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm")));
    EXPECT_DOUBLE_EQ(150.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm")));
    ASSERT_FALSE(isSoftMaster(m_sGroup1));
    ASSERT_TRUE(isSoftMaster(m_sGroup2));
    ASSERT_FALSE(isSoftMaster(m_sInternalClockGroup));
    ProcessBuffer();

    // Drop Track
    ControlObject::set(ConfigKey(m_sGroup1, "volume"), 1.0);
    ProcessBuffer();
    ProcessBuffer();

    ASSERT_FALSE(isSoftMaster(m_sGroup1));
    ASSERT_FALSE(isSoftMaster(m_sGroup2));
    ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup));
}
