#include <stdint.h>

#include "test/mixxxtest.h"
#include "controllers/learningutils.h"

std::ostream& operator<<(std::ostream& stream, const MidiInputMapping& mapping) {
    stream << mapping.key.key << mapping.options.all;
    return stream;
}

class LearningUtilsTest : public MixxxTest {
  protected:
    void addMessage(unsigned char status, unsigned char control, unsigned char value) {
        m_messages.append(qMakePair(MidiKey(status, control), value));
    }

    QList<QPair<MidiKey, unsigned char> > m_messages;
};

TEST_F(LearningUtilsTest, NoteOnButton) {
    // Status: 0x91, Control: 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.first());
}

TEST_F(LearningUtilsTest, NoteOnNoteOffButton) {
    // Status: 0x91, Control: 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
    addMessage(MIDI_NOTE_OFF | 0x01, 0x10, 0x00);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(2, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(1));
}

TEST_F(LearningUtilsTest, CC7BitKnob) {
    // Standard CC 7-bit knobs show up as a MIDI_CC message, single channel,
    // single control and a variety of values in the range of 0x00 to 0x7F.
    // Status: 0x81 Control: 0x01
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitKnob_CenterPointButton_NoteOn) {
    // This style of 7-bit CC knob is found on controllers like the
    // VCI-100. They do not emit a center-point CC message with value 0x40 and
    // instead emit a NOTE_ON (0x7F, 0x00) pair for the center point. This
    // causes Mixxx to confuse these knobs for buttons and also cause them to
    // never truly reach their center point. We map the button to the
    // _set_default control if it exists.

    // Starting outside the center point.
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);

    ConfigKey control("[Test]", "SomeControl");
    // This message recognizer checks for the existence of a _reset control.
    ConfigKey resetControl("[Test]", "SomeControl_set_default");
    QScopedPointer<ControlObject> pResetControl(new ControlObject(resetControl));
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(2, mappings.size());
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                                                   MidiOptions(), control)));
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0xE0),
                                                   MidiOptions(), resetControl)));

    m_messages.clear();

    // Starting on the center point the VCI-100 only sends a NOTE_ON with value
    // 0x00 to indicate we started moving but are leaving the center point.
    addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x35);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x25);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x15);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x05);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);

    ASSERT_EQ(2, mappings.size());
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                                                   MidiOptions(), control)));
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0xE0),
                                                   MidiOptions(), resetControl)));
}

TEST_F(LearningUtilsTest, CC14BitKnob_MSBFirst) {
    // A CC 14-bit knob with the MSB as the first message. We treat streams of
    // single-channel, CC opcode, 2 control, equal size message streams as
    // 14-bit CC knobs. The order of the LSB and MSB does not matter to us.

    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(2, mappings.size());
    MidiOptions lsb_option;
    lsb_option.fourteen_bit_lsb = true;
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                                                   lsb_option, control)));
    MidiOptions msb_option;
    msb_option.fourteen_bit_msb = true;
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x11),
                                                   msb_option, control)));
}

TEST_F(LearningUtilsTest, CC14BitKnob_LSBFirst) {
    // A CC 14-bit knob with the LSB as the first message. We treat streams of
    // single-channel, CC opcode, 2 control, equal size message streams as
    // 14-bit CC knobs. The order of the LSB and MSB does not matter to us.

    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x11, 0x03);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(2, mappings.size());
    MidiOptions lsb_option;
    lsb_option.fourteen_bit_lsb = true;
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                                                   lsb_option, control)));
    MidiOptions msb_option;
    msb_option.fourteen_bit_msb = true;
    EXPECT_TRUE(mappings.contains(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x11),
                                                   msb_option, control)));
}

TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker_Zeroes) {
    // Make sure that 0x00 is a tell that we are not a 2's complement select
    // knob.
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    MidiOptions options;
    options.selectknob = true;
    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10), options, control),
              mappings.at(0));

    m_messages.clear();

    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);

    mappings = LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker_ZeroIncluded) {
    // Moving a CC knob through its range multiple times is confusable for
    // select knobs in some cases. More than 8 distinct values tell us this is
    // not a select knob.
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    // Zero tells us this is not a select knob.
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker) {
    // Moving a CC knob through its range multiple times is confusable for
    // select knobs when a 0x7F is repeated. More than 8 distinct values tells
    // us this is not a select knob.
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x75);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);
    addMessage(MIDI_CC | 0x01, 0x10, 0x65);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x55);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x45);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x75);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForSpread64Ticker_StartAndStopOn41) {
    // Moving a CC knob through its range multiple times is confusable for
    // Spread64 select knobs when a 0x41 or 0x3F is repeated. If we start and
    // stop on 0x41 (and don't pass through 0x40) then this can set off Spread64
    // detection. We require <8 distinct values for Spread64 detection to
    // prevent this.
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_CC | 0x01, 0x10, 0x45);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x55);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x65);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);
    addMessage(MIDI_CC | 0x01, 0x10, 0x75);
    addMessage(MIDI_CC | 0x01, 0x10, 0x62);
    addMessage(MIDI_CC | 0x01, 0x10, 0x52);
    addMessage(MIDI_CC | 0x01, 0x10, 0x48);
    addMessage(MIDI_CC | 0x01, 0x10, 0x46);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForSpread64Ticker_0x40Included) {
    // Moving a CC knob through its range multiple times is confusable for
    // Spread64 select knobs when a 0x41 or 0x3F is repeated.
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);
    addMessage(MIDI_CC | 0x01, 0x10, 0x10);
    addMessage(MIDI_CC | 0x01, 0x10, 0x20);
    addMessage(MIDI_CC | 0x01, 0x10, 0x30);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    // 0x40 tells us this is not a Spread64 select knob.
    addMessage(MIDI_CC | 0x01, 0x10, 0x40);
    addMessage(MIDI_CC | 0x01, 0x10, 0x50);
    addMessage(MIDI_CC | 0x01, 0x10, 0x60);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitTicker) {
    // A CC 7-bit ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
    // message, single channel, single control and a variety of values in two's
    // complement. We detect this by looking at jumps across the boundary of
    // 0x7F to 0x00 so the user has to go forward and backward.
    // Status: 0x81 Control: 0x10
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x05);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7E);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7C);
    addMessage(MIDI_CC | 0x01, 0x10, 0x70);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    MidiOptions options;
    options.selectknob = true;
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, Spread64Ticker) {
    // A Spread64 ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
    // message, single channel, single control and a variety of values centered
    // around 0x40 but never including that value (since 0x40 means "not moving")
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_CC | 0x01, 0x10, 0x42);
    addMessage(MIDI_CC | 0x01, 0x10, 0x41);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x3F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    MidiOptions options;
    options.spread64 = true;
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, CC7BitTicker_SingleDirection) {
    // A CC 7-bit ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
    // message, single channel, single control and a variety of values in two's
    // complement. It's harder to detect when the user does not change direction
    // because we do not see 0x00 boundary crossings that are tell-tale signs of
    // two's complement. If we see repeat 0x01 or 0x7F values then we interpret
    // those as being a ticker as well.

    // Status: 0x81 Control: 0x10
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x02);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);
    addMessage(MIDI_CC | 0x01, 0x10, 0x03);
    addMessage(MIDI_CC | 0x01, 0x10, 0x01);

    MidiOptions options;
    options.selectknob = true;

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);
    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));

    m_messages.clear();

    // Status: 0x81 Control: 0x10
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7E);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
    addMessage(MIDI_CC | 0x01, 0x10, 0x7C);

    mappings = LearningUtils::guessMidiInputMappings(control, m_messages);
    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, SingleMessageSwitchMode_NoteOn) {
    // If we only get one NOTE_ON message that is either 0x7F or 0x00 then we
    // assume this is a binary toggle switch.

    // Status: 0x91 Control: 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    MidiOptions options;
    options.sw = true;
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               options, control),
              mappings.at(0));

    m_messages.clear();

    // Status: 0x91 Control: 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);

    mappings = LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               options, control),
              mappings.at(0));
}

TEST_F(LearningUtilsTest, SingleMessageSwitchMode_CC) {
    // If we only get one CC message that is either 0x7F or 0x00 then we
    // assume this is a binary toggle switch.

    // Status: 0xB1 Control: 0x10
    addMessage(MIDI_CC | 0x01, 0x10, 0x7F);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    MidiOptions options;
    options.sw = true;
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));

    m_messages.clear();

    // Status: 0x91 Control: 0x10
    addMessage(MIDI_CC | 0x01, 0x10, 0x00);

    mappings = LearningUtils::guessMidiInputMappings(control, m_messages);

    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
                               options, control),
              mappings.at(0));
}


TEST_F(LearningUtilsTest, MultipleControlsUnrecognized_BindsFirst) {
    // Status 0x91, Control 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);

    // Status 0x91, Control 0x11
    addMessage(MIDI_NOTE_ON | 0x01, 0x11, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x01, 0x11, 0x00);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);
    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.first());
}

TEST_F(LearningUtilsTest, MultipleChannelsUnrecognized_BindsFirst) {
    // Status 0x91, Control 0x10
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);

    // Status 0x91, Control 0x11
    addMessage(MIDI_NOTE_ON | 0x02, 0x10, 0x7F);
    addMessage(MIDI_NOTE_ON | 0x02, 0x10, 0x00);

    ConfigKey control("[Test]", "SomeControl");
    MidiInputMappings mappings =
            LearningUtils::guessMidiInputMappings(control, m_messages);
    ASSERT_EQ(1, mappings.size());
    EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
                               MidiOptions(), control),
              mappings.first());
}
