class KNNAnomalyClassifierRegionTest(unittest.TestCase):
    """KNNAnomalyClassifierRegion unit tests."""
    def setUp(self):

        self.params = dict(trainRecords=10,
                           anomalyThreshold=1.1,
                           cacheSize=10000,
                           k=1,
                           distanceMethod='rawOverlap',
                           distanceNorm=1,
                           doBinarization=1,
                           replaceDuplicates=0,
                           maxStoredPatterns=1000)

        self.helper = KNNAnomalyClassifierRegion(**self.params)

    def testInit(self):

        params = dict(trainRecords=100,
                      anomalyThreshold=101,
                      cacheSize=102,
                      classificationVectorType=1,
                      k=1,
                      distanceMethod='rawOverlap',
                      distanceNorm=1,
                      doBinarization=1,
                      replaceDuplicates=0,
                      maxStoredPatterns=1000)

        helper = KNNAnomalyClassifierRegion(**params)

        self.assertEqual(helper.trainRecords, params['trainRecords'])
        self.assertEqual(helper.anomalyThreshold, params['anomalyThreshold'])
        self.assertEqual(helper.cacheSize, params['cacheSize'])
        self.assertEqual(helper.classificationVectorType,
                         params['classificationVectorType'])

    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    @patch.object(KNNAnomalyClassifierRegion, 'getParameter')
    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    def testCompute(self, constructRecord, getParam, classifyState):
        params = {'trainRecords': 0}
        getParam.side_effect = params.get
        state = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": [1, 4, 5],
            "anomalyLabel": "Label"
        }
        record = _CLAClassificationRecord(**state)
        constructRecord.return_value = record
        self.helper.compute(dict(), dict())
        classifyState.assert_called_once_with(record)
        self.assertEqual(self.helper.labelResults, state['anomalyLabel'])

    def testGetLabels(self):
        # No _recordsCache
        self.helper._recordsCache = []
        self.assertEqual(self.helper.getLabels(), \
          {'isProcessing': False, 'recordLabels': []})

        # Invalid ranges
        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.getLabels,
                          start=100,
                          end=100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.getLabels,
                          start=-100,
                          end=-100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.getLabels,
                          start=100,
                          end=-100)

        # Valid no threshold labels
        values = {
            'categoryRecencyList': [4, 5, 7],
        }
        self.helper.saved_categories = ['TestCategory']
        categoryList = [1, 1, 1]
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn._categoryList = categoryList

        results = self.helper.getLabels()
        self.assertTrue('isProcessing' in results)
        self.assertTrue('recordLabels' in results)
        self.assertEqual(len(results['recordLabels']),
                         len(values['categoryRecencyList']))
        for record in results['recordLabels']:
            self.assertTrue(record['ROWID'] in values['categoryRecencyList'])
            self.assertEqual(record['labels'], self.helper.saved_categories)

    @patch.object(KNNAnomalyClassifierRegion, '_getStateAnomalyVector')
    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    def testAddLabel(self, classifyState, constructVector, getVector):
        # Setup Mocks
        getVector.return_value = numpy.array([0, 0, 0, 1, 0, 0, 1])
        knn = self.helper._knnclassifier._knn
        knn.learn = Mock()

        # Invalid ranges
        self.helper._recordsCache = []
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.addLabel,
                          start=100,
                          end=100,
                          labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.addLabel,
                          start=100,
                          end=100,
                          labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.addLabel,
                          start=-100,
                          end=-100,
                          labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.addLabel,
                          start=100,
                          end=-100,
                          labelName="test")

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=[], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True)
        ]
        results = self.helper.addLabel(11, 12, "Added")

        # Verifies records were updated
        self.assertEqual(results, None)
        self.assertTrue('Added' in self.helper._recordsCache[1].anomalyLabel)
        self.assertTrue(self.helper._recordsCache[1].setByUser)

        # Verifies record added to KNN classifier
        knn.learn.assert_called_once_with(ANY, ANY, rowID=11)

        # Verifies records after added label is recomputed
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    def testRemoveLabel(self, classifyState, constructClassificationRecord):
        knn = self.helper._knnclassifier._knn
        knn._numPatterns = 3
        knn._categoryRecencyList = [10, 11, 12]
        knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        self.helper._recordsCache = []
        self.assertRaises(
            CLAModelInvalidRangeError,
            self.helper.removeLabels,
        )

        # Invalid ranges
        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.removeLabels,
                          start=100,
                          end=100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.removeLabels,
                          start=-100,
                          end=-100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError,
                          self.helper.removeLabels,
                          start=100,
                          end=-100)

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True)
        ]
        results = self.helper.removeLabels(11, 12, "Test")

        self.assertEqual(results, None)
        self.assertTrue(
            'Test' not in self.helper._recordsCache[1].anomalyLabel)

        # Verifies records removed from KNN classifier
        self.assertEqual(knn.removeIds.mock_calls, [call([11]), call([])])

        # Verifies records after removed record are updated
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    def testRemoveLabelNoFilter(self, classifyState,
                                constructClassificationRecord):
        knn = self.helper._knnclassifier._knn
        knn._numPatterns = 3
        knn._categoryRecencyList = [10, 11, 12]
        knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True)
        ]
        results = self.helper.removeLabels(11, 12)

        self.assertEqual(results, None)
        self.assertTrue(
            'Test' not in self.helper._recordsCache[1].anomalyLabel)

        # Verifies records removed from KNN classifier
        self.assertEqual(knn.removeIds.mock_calls, [call([11]), call([])])

        # Verifies records after removed record are updated
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    def testSetGetThreshold(self, classifyState):
        self.helper._recordsCache = [Mock(), Mock(), Mock()]

        self.helper.setParameter('anomalyThreshold', None, 1.0)

        self.assertAlmostEqual(self.helper.anomalyThreshold, 1.0)
        self.assertEqual(len(classifyState.mock_calls),
                         len(self.helper._recordsCache))

        self.assertAlmostEqual(self.helper.getParameter('anomalyThreshold'),
                               1.0)

        self.assertRaises(Exception, self.helper.setParameter,
                          'anomalyThreshold', None, 'invalid')

    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    def testSetGetWaitRecords(self, classifyState):
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True)
        ]

        self.helper.setParameter('trainRecords', None, 20)

        self.assertEqual(self.helper.trainRecords, 20)
        self.assertEqual(len(classifyState.mock_calls),
                         len(self.helper._recordsCache))

        self.assertEqual(self.helper.getParameter('trainRecords'), 20)

        # Test invalid parameter type
        self.assertRaises(Exception, self.helper.setParameter, 'trainRecords',
                          None, 'invalid')

        # Test invalid value before first record ROWID in cache
        state = {
            "ROWID": 1000,
            "anomalyScore": 1.0,
            "anomalyVector": [1, 4, 5],
            "anomalyLabel": "Label"
        }
        record = _CLAClassificationRecord(**state)
        self.helper._recordsCache = [state]
        self.assertRaises(Exception, self.helper.setParameter, 'trainRecords',
                          None, 0)

    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    def testSetGetWaitRecordsRecalculate(self, getRecord):
        """
    This test ensures that records in classifier are removed when they are no
    longer being used when the trainRecords is set.
    """
        self.helper.cacheSize = 5
        self.helper.anomalyThreshold = 0.8

        self.helper._anomalyVectorLength = 20
        records = [
            Mock(ROWID=10,
                 anomalyLabel=["Test"],
                 anomalyScore=1,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 4])),
            Mock(ROWID=11,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 2])),
            Mock(ROWID=12,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 4])),
            Mock(ROWID=13,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 2, 6, 7])),
            Mock(ROWID=14,
                 anomalyLabel=["Test"],
                 anomalyScore=1,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 10])),
            Mock(ROWID=15,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 3])),
            Mock(ROWID=16,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 4])),
            Mock(ROWID=17,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([10])),
            Mock(ROWID=18,
                 anomalyLabel=["Test"],
                 anomalyScore=0,
                 setByUser=False,
                 anomalyVector=numpy.array([1, 4]))
        ]

        getRecord.side_effect = records

        for i in records:
            self.helper.compute(dict(), dict())

        self.assertEqual(self.helper._knnclassifier._knn._numPatterns, 6)
        self.assertEqual(
            self.helper._knnclassifier.getParameter('categoryRecencyList'),
            [10, 12, 14, 16, 17, 18],
            "Classifier incorrectly classified test records.")

        # Now set trainRecords and should remove the labels outside of cache
        # and relabel points.
        self.helper.setParameter('trainRecords', None, 14)

        self.assertEqual(self.helper._knnclassifier._knn._numPatterns, 2)
        self.assertEqual(
            self.helper._knnclassifier.getParameter('categoryRecencyList'),
            [14, 17],
            "Classifier incorrectly reclassified test records after setting "
            "trainRecords")

    @patch.object(KNNAnomalyClassifierRegion, '_addRecordToKNN')
    @patch.object(KNNAnomalyClassifierRegion, '_deleteRecordsFromKNN')
    @patch.object(KNNAnomalyClassifierRegion, '_recomputeRecordFromKNN')
    @patch.object(KNNAnomalyClassifierRegion, '_categoryToLabelList')
    def testUpdateState(self, toLabelList, recompute, deleteRecord, addRecord):
        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "",
            "anomalyLabel": ["Label"],
            "setByUser": False
        }

        # Test record not labeled and not above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 0
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [])
        deleteRecord.assert_called_once_with([state])

        # Test record not labeled and above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 0.5
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)

        self.assertEqual(state.anomalyLabel, \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL])
        addRecord.assert_called_once_with(state)

        # Test record not labeled and above threshold during wait period
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 10
        self.helper.anomalyThreshold = 0.5
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)

        self.assertEqual(state.anomalyLabel, [])
        self.assertTrue(not addRecord.called)

        # Test record labeled and not above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 0
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = ["Label"]
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, ["Label"])
        self.assertTrue(not addRecord.called)

        # Test setByUser
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = ["Label 2"]
        recordCopy = copy.deepcopy(record)
        recordCopy['setByUser'] = True
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(
            state.anomalyLabel,
            [recordCopy["anomalyLabel"][0], toLabelList.return_value[0]])
        addRecord.assert_called_once_with(state)

        # Test removal of above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = []
        recordCopy = copy.deepcopy(record)
        recordCopy['setByUser'] = True
        recordCopy['anomalyLabel'] = \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL,
           KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + \
            KNNAnomalyClassifierRegion.AUTO_TAG]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [])

        # Auto classified threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        recordCopy = copy.deepcopy(record)
        recordCopy['setByUser'] = True
        recordCopy['anomalyLabel'] = \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel,
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + \
            KNNAnomalyClassifierRegion.AUTO_TAG])
        addRecord.assert_called_once_with(state)

        # Test precedence of threshold label above auto threshold label
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 0.8
        toLabelList.return_value = \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL,
            KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + \
            KNNAnomalyClassifierRegion.AUTO_TAG]
        recordCopy = copy.deepcopy(record)
        recordCopy['setByUser'] = True
        recordCopy['anomalyLabel'] = \
          [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(
            state.anomalyLabel,
            [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL])
        addRecord.assert_called_once_with(state)

    @patch.object(KNNAnomalyClassifierRegion, '_getStateAnomalyVector')
    def testAddRecordToKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = numpy.array([0, 1, 0, 0, 1, 0, 1, 1])
        values = {'categoryRecencyList': [1, 2, 3]}
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn.learn = Mock()
        classifier._knn.prototypeSetCategory = Mock()
        state = {
            "ROWID": 5,
            "anomalyScore": 1.0,
            "anomalyVector": numpy.array([1, 5, 7, 8]),
            "anomalyLabel": ["Label"],
            "setByUser": False
        }
        record = _CLAClassificationRecord(**state)

        # Test with record not already in KNN
        self.helper._addRecordToKNN(record)
        classifier._knn.learn.assert_called_once_with(
            getAnomalyVector.return_value, ANY, rowID=state['ROWID'])
        self.assertTrue(not classifier._knn.prototypeSetCategory.called)
        classifier._knn.learn.reset_mock()

        # Test with record already in KNN
        values = {'categoryRecencyList': [1, 2, 3, 5]}
        classifier.getParameter.side_effect = values.get
        self.helper._addRecordToKNN(record)
        classifier._knn.prototypeSetCategory.assert_called_once_with(
            state['ROWID'], ANY)
        self.assertTrue(not classifier._knn.learn.called)

    @patch.object(KNNAnomalyClassifierRegion, '_getStateAnomalyVector')
    def testDeleteRangeFromKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = "Vector"
        values = {'categoryRecencyList': [1, 2, 3]}
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn._numPatterns = len(values['categoryRecencyList'])
        classifier._knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        # Test with record not already in KNN
        self.helper._deleteRangeFromKNN(start=1, end=3)
        classifier._knn.removeIds.assert_called_once_with([1, 2])
        classifier._knn.removeIds.reset_mock()

        # Test with record already in KNN
        values = {'categoryRecencyList': [1, 2, 3, 5]}
        classifier.getParameter.side_effect = values.get
        self.helper._deleteRangeFromKNN(start=1)
        classifier._knn.removeIds.assert_called_once_with([1, 2, 3, 5])

    @patch.object(KNNAnomalyClassifierRegion, '_getStateAnomalyVector')
    def testRecomputeRecordFromKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = "Vector"
        self.helper.trainRecords = 0
        values = {
            'categoryRecencyList': [1, 2, 3, 5, 6, 7, 8, 9],
            'latestDists': numpy.array([0.7, 0.2, 0.5, 1, 0.3, 0.2, 0.1]),
            'categories': ['A', 'B', 'C', 'D', 'E', 'F', 'G']
        }
        classifier = self.helper._knnclassifier
        classifier.getLatestDistances = Mock(
            return_value=values['latestDists'])
        classifier.getCategoryList = Mock(return_value=values['categories'])
        classifier.getParameter = Mock(side_effect=values.get)
        classifier.setParameter = Mock()
        classifier.compute = Mock()
        state = {
            "ROWID": 5,
            "anomalyScore": 1.0,
            "anomalyVector": "",
            "anomalyLabel": ["Label"],
            "setByUser": False
        }
        record = _CLAClassificationRecord(**state)

        # Test finding best category before record - exists
        self.helper._classificationMaxDist = 0.4
        self.helper._autoDetectWaitRecords = 0
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, 'B')

        # Test finding best category before record - does not exists
        self.helper._classificationMaxDist = 0.1
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, None)

        # Test finding best category before record - not record before
        record.ROWID = 0
        self.helper._classificationMaxDist = 0.1
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, None)

    def testConstructClassificationVector(self):
        modelParams = {'__numRunCalls': 0}
        spVals = {
            'params': {
                'activeOutputCount': 5
            },
            'output': {
                'bottomUpOut': numpy.array([1, 1, 0, 0, 1])
            }
        }
        tpVals = {
            'params': {
                'cellsPerColumn': 2,
                'columnCount': 2
            },
            'output': {
                'lrnActive': numpy.array([1, 0, 0, 1]),
                'topDownOut': numpy.array([1, 0, 0, 0, 1])
            }
        }
        inputs = dict(spBottomUpOut=spVals['output']['bottomUpOut'],
                      tpTopDownOut=tpVals['output']['topDownOut'],
                      tpLrnActiveStateT=tpVals['output']['lrnActive'])

        self.helper._activeColumnCount = 5

        # Test TP Cell vector
        self.helper.classificationVectorType = 1
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector,
                         tpVals['output']['lrnActive'].nonzero()[0].tolist())

        # Test SP and TP Column Error vector
        self.helper.classificationVectorType = 2
        self.helper._prevPredictedColumns = numpy.array([1, 0, 0, 0,
                                                         1]).nonzero()[0]
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector, [0, 1, 4])

        self.helper._prevPredictedColumns = numpy.array([1, 0, 1, 0,
                                                         0]).nonzero()[0]
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector, [0, 1, 4, 7])

        self.helper.classificationVectorType = 3
        self.assertRaises(TypeError, self.helper.constructClassificationRecord,
                          inputs)

    @patch.object(KNNAnomalyClassifierRegion, 'classifyState')
    @patch.object(KNNAnomalyClassifierRegion, 'constructClassificationRecord')
    def testCompute(self, createRecord, updateState):
        state = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": numpy.array([1, 0, 0, 0, 1]),
            "anomalyLabel": "Label"
        }
        record = _CLAClassificationRecord(**state)
        createRecord.return_value = record

        inputs = dict()
        outputs = dict()

        # Test add first record
        self.helper.cacheSize = 10
        self.helper.trainRecords = 0
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        updateState.assert_called_once_with(self.helper._recordsCache[-1])

        # Test add record before wait records
        updateState.reset_mock()
        self.helper.cacheSize = 10
        self.helper.trainRecords = 10
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 2)
        self.assertTrue(not updateState.called)

        # Test exceeded cache length
        updateState.reset_mock()
        self.helper.cacheSize = 1
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.assertTrue(not updateState.called)

    def testCategoryToList(self):
        result = self.helper._categoryToLabelList(None)
        self.assertEqual(result, [])

        self.helper.saved_categories = ['A', 'B', 'C']
        result = self.helper._categoryToLabelList(1)
        self.assertEqual(result, ['A'])

        result = self.helper._categoryToLabelList(4)
        self.assertEqual(result, ['C'])

        result = self.helper._categoryToLabelList(5)
        self.assertEqual(result, ['A', 'C'])

    def testGetAnomalyVector(self):
        state = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": [1, 4, 5],
            "anomalyLabel": "Label"
        }
        record = _CLAClassificationRecord(**state)
        self.helper._anomalyVectorLength = 10
        vector = self.helper._getStateAnomalyVector(record)

        self.assertEqual(len(vector), self.helper._anomalyVectorLength)
        self.assertEqual(vector.nonzero()[0].tolist(), record.anomalyVector)

    # Tests for configuration
    # ===========================================================================

    def testSetState(self):
        # No Version set
        state = dict(_classificationDelay=100)
        state['_knnclassifierProps'] = self.params
        self.helper._vectorType = None
        self.helper.__setstate__(state)
        self.assertEqual(self.helper.classificationVectorType, 1)
        self.assertEqual(self.helper._version,
                         KNNAnomalyClassifierRegion.__VERSION__)

        # Version 1
        state = dict(_version=1, _classificationDelay=100)
        state['_knnclassifierProps'] = self.params
        self.helper.__setstate__(state)
        self.assertEqual(self.helper._version,
                         KNNAnomalyClassifierRegion.__VERSION__)

        # Invalid Version
        state = dict(_version="invalid")
        state['_knnclassifierProps'] = self.params
        self.assertRaises(Exception, self.helper.__setstate__, state)

    # Tests for _CLAClassificationRecord class
    # ===========================================================================

    def testCLAClassificationRecord(self):
        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label"
        }

        state = _CLAClassificationRecord(**record)
        self.assertEqual(state.ROWID, record['ROWID'])
        self.assertEqual(state.anomalyScore, record['anomalyScore'])
        self.assertEqual(state.anomalyVector, record['anomalyVector'])
        self.assertEqual(state.anomalyLabel, record['anomalyLabel'])
        self.assertEqual(state.setByUser, False)

        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": True
        }

        state = _CLAClassificationRecord(**record)
        self.assertEqual(state.ROWID, record['ROWID'])
        self.assertEqual(state.anomalyScore, record['anomalyScore'])
        self.assertEqual(state.anomalyVector, record['anomalyVector'])
        self.assertEqual(state.anomalyLabel, record['anomalyLabel'])
        self.assertEqual(state.setByUser, record['setByUser'])

    def testCLAClassificationRecordGetState(self):
        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": False
        }

        state = _CLAClassificationRecord(**record)

        self.assertEqual(state.__getstate__(), record)

    def testCLAClassificationRecordSetState(self):
        record = {
            "ROWID": None,
            "anomalyScore": None,
            "anomalyVector": None,
            "anomalyLabel": None,
            "setByUser": None
        }

        state = _CLAClassificationRecord(**record)

        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": False
        }

        state.__setstate__(record)

        self.assertEqual(state.ROWID, record['ROWID'])
        self.assertEqual(state.anomalyScore, record['anomalyScore'])
        self.assertEqual(state.anomalyVector, record['anomalyVector'])
        self.assertEqual(state.anomalyLabel, record['anomalyLabel'])
        self.assertEqual(state.setByUser, record['setByUser'])

    def mockRemoveIds(self, ids):
        self.helper._knnclassifier._knn._numPatterns -= len(ids)
        knnClassifier = self.helper._knnclassifier
        for idx in ids:
            if idx in self.helper._knnclassifier.getParameter(
                    'categoryRecencyList'):
                knnClassifier.getParameter('categoryRecencyList').remove(idx)
class KNNAnomalyClassifierRegionTest(unittest.TestCase):
    """KNNAnomalyClassifierRegion unit tests."""

    def setUp(self):

        self.params = dict(
            trainRecords=10,
            anomalyThreshold=1.1,
            cacheSize=10000,
            k=1,
            distanceMethod="rawOverlap",
            distanceNorm=1,
            doBinarization=1,
            replaceDuplicates=0,
            maxStoredPatterns=1000,
        )

        self.helper = KNNAnomalyClassifierRegion(**self.params)

    def testInit(self):

        params = dict(
            trainRecords=100,
            anomalyThreshold=101,
            cacheSize=102,
            classificationVectorType=1,
            k=1,
            distanceMethod="rawOverlap",
            distanceNorm=1,
            doBinarization=1,
            replaceDuplicates=0,
            maxStoredPatterns=1000,
        )

        helper = KNNAnomalyClassifierRegion(**params)

        self.assertEqual(helper.trainRecords, params["trainRecords"])
        self.assertEqual(helper.anomalyThreshold, params["anomalyThreshold"])
        self.assertEqual(helper.cacheSize, params["cacheSize"])
        self.assertEqual(helper.classificationVectorType, params["classificationVectorType"])

    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    @patch.object(KNNAnomalyClassifierRegion, "getParameter")
    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    def testCompute(self, constructRecord, getParam, classifyState):
        params = {"trainRecords": 0}
        getParam.side_effect = params.get
        state = {"ROWID": 0, "anomalyScore": 1.0, "anomalyVector": [1, 4, 5], "anomalyLabel": "Label"}
        record = _CLAClassificationRecord(**state)
        constructRecord.return_value = record
        self.helper.compute(dict(), dict())
        classifyState.assert_called_once_with(record)
        self.assertEqual(self.helper.labelResults, state["anomalyLabel"])

    def testGetLabels(self):
        # No _recordsCache
        self.helper._recordsCache = []
        self.assertEqual(self.helper.getLabels(), {"isProcessing": False, "recordLabels": []})

        # Invalid ranges
        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.getLabels, start=100, end=100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.getLabels, start=-100, end=-100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.getLabels, start=100, end=-100)

        # Valid no threshold labels
        values = {"categoryRecencyList": [4, 5, 7]}
        self.helper.saved_categories = ["TestCategory"]
        categoryList = [1, 1, 1]
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn._categoryList = categoryList

        results = self.helper.getLabels()
        self.assertTrue("isProcessing" in results)
        self.assertTrue("recordLabels" in results)
        self.assertEqual(len(results["recordLabels"]), len(values["categoryRecencyList"]))
        for record in results["recordLabels"]:
            self.assertTrue(record["ROWID"] in values["categoryRecencyList"])
            self.assertEqual(record["labels"], self.helper.saved_categories)

    @patch.object(KNNAnomalyClassifierRegion, "_getStateAnomalyVector")
    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    def testAddLabel(self, classifyState, constructVector, getVector):
        # Setup Mocks
        getVector.return_value = numpy.array([0, 0, 0, 1, 0, 0, 1])
        knn = self.helper._knnclassifier._knn
        knn.learn = Mock()

        # Invalid ranges
        self.helper._recordsCache = []
        self.assertRaises(CLAModelInvalidRangeError, self.helper.addLabel, start=100, end=100, labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.addLabel, start=100, end=100, labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.addLabel, start=-100, end=-100, labelName="test")

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.addLabel, start=100, end=-100, labelName="test")

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=[], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True),
        ]
        results = self.helper.addLabel(11, 12, "Added")

        # Verifies records were udpated
        self.assertEqual(results, None)
        self.assertTrue("Added" in self.helper._recordsCache[1].anomalyLabel)
        self.assertTrue(self.helper._recordsCache[1].setByUser)

        # Verifies record added to KNN classifier
        knn.learn.assert_called_once_with(ANY, ANY, rowID=11)

        # Verifies records after added label is recomputed
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    def testRemoveLabel(self, classifyState, constructClassificationRecord):
        knn = self.helper._knnclassifier._knn
        knn._numPatterns = 3
        knn._categoryRecencyList = [10, 11, 12]
        knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        self.helper._recordsCache = []
        self.assertRaises(CLAModelInvalidRangeError, self.helper.removeLabels)

        # Invalid ranges
        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.removeLabels, start=100, end=100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.removeLabels, start=-100, end=-100)

        self.helper._recordsCache = [Mock(ROWID=10)]
        self.assertRaises(CLAModelInvalidRangeError, self.helper.removeLabels, start=100, end=-100)

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True),
        ]
        results = self.helper.removeLabels(11, 12, "Test")

        self.assertEqual(results, None)
        self.assertTrue("Test" not in self.helper._recordsCache[1].anomalyLabel)

        # Verifies records removed from KNN classifier
        self.assertEqual(knn.removeIds.mock_calls, [call([11]), call([])])

        # Verifies records after removed record are updated
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    def testRemoveLabelNoFilter(self, classifyState, constructClassificationRecord):
        knn = self.helper._knnclassifier._knn
        knn._numPatterns = 3
        knn._categoryRecencyList = [10, 11, 12]
        knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        # Valid no threshold labels
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True),
        ]
        results = self.helper.removeLabels(11, 12)

        self.assertEqual(results, None)
        self.assertTrue("Test" not in self.helper._recordsCache[1].anomalyLabel)

        # Verifies records removed from KNN classifier
        self.assertEqual(knn.removeIds.mock_calls, [call([11]), call([])])

        # Verifies records after removed record are updated
        classifyState.assert_called_once_with(self.helper._recordsCache[2])

    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    def testSetGetThreshold(self, classifyState):
        self.helper._recordsCache = [Mock(), Mock(), Mock()]

        self.helper.setParameter("anomalyThreshold", None, 1.0)

        self.assertAlmostEqual(self.helper.anomalyThreshold, 1.0)
        self.assertEqual(len(classifyState.mock_calls), len(self.helper._recordsCache))

        self.assertAlmostEqual(self.helper.getParameter("anomalyThreshold"), 1.0)

        self.assertRaises(Exception, self.helper.setParameter, "anomalyThreshold", None, "invalid")

    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    def testSetGetWaitRecords(self, classifyState):
        self.helper._recordsCache = [
            Mock(ROWID=10, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=11, anomalyLabel=["Test"], setByUser=False),
            Mock(ROWID=12, anomalyLabel=["Test"], setByUser=True),
        ]

        self.helper.setParameter("trainRecords", None, 20)

        self.assertEqual(self.helper.trainRecords, 20)
        self.assertEqual(len(classifyState.mock_calls), len(self.helper._recordsCache))

        self.assertEqual(self.helper.getParameter("trainRecords"), 20)

        # Test invalid parameter type
        self.assertRaises(Exception, self.helper.setParameter, "trainRecords", None, "invalid")

        # Test invalid value before first record ROWID in cache
        state = {"ROWID": 1000, "anomalyScore": 1.0, "anomalyVector": [1, 4, 5], "anomalyLabel": "Label"}
        record = _CLAClassificationRecord(**state)
        self.helper._recordsCache = [state]
        self.assertRaises(Exception, self.helper.setParameter, "trainRecords", None, 0)

    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    def testSetGetWaitRecordsRecalculate(self, getRecord):
        """
    This test ensures that records in classifier are removed when they are no
    longer being used when the trainRecords is set.
    """
        self.helper.cacheSize = 5
        self.helper.anomalyThreshold = 0.8

        self.helper._anomalyVectorLength = 20
        records = [
            Mock(ROWID=10, anomalyLabel=["Test"], anomalyScore=1, setByUser=False, anomalyVector=numpy.array([1, 4])),
            Mock(ROWID=11, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([1, 2])),
            Mock(ROWID=12, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([1, 4])),
            Mock(
                ROWID=13,
                anomalyLabel=["Test"],
                anomalyScore=0,
                setByUser=False,
                anomalyVector=numpy.array([1, 2, 6, 7]),
            ),
            Mock(ROWID=14, anomalyLabel=["Test"], anomalyScore=1, setByUser=False, anomalyVector=numpy.array([1, 10])),
            Mock(ROWID=15, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([1, 3])),
            Mock(ROWID=16, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([1, 4])),
            Mock(ROWID=17, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([10])),
            Mock(ROWID=18, anomalyLabel=["Test"], anomalyScore=0, setByUser=False, anomalyVector=numpy.array([1, 4])),
        ]

        getRecord.side_effect = records

        for i in records:
            self.helper.compute(dict(), dict())

        self.assertEqual(self.helper._knnclassifier._knn._numPatterns, 6)
        self.assertEqual(
            self.helper._knnclassifier.getParameter("categoryRecencyList"),
            [10, 12, 14, 16, 17, 18],
            "Classifier incorrectly classified test records.",
        )

        # Now set trainRecords and should remove the labels outside of cache
        # and relabel points.
        self.helper.setParameter("trainRecords", None, 14)

        self.assertEqual(self.helper._knnclassifier._knn._numPatterns, 2)
        self.assertEqual(
            self.helper._knnclassifier.getParameter("categoryRecencyList"),
            [14, 17],
            "Classifier incorrectly reclassified test records after setting " "trainRecords",
        )

    @patch.object(KNNAnomalyClassifierRegion, "_addRecordToKNN")
    @patch.object(KNNAnomalyClassifierRegion, "_deleteRecordsFromKNN")
    @patch.object(KNNAnomalyClassifierRegion, "_recomputeRecordFromKNN")
    @patch.object(KNNAnomalyClassifierRegion, "_categoryToLabelList")
    def testUpdateState(self, toLabelList, recompute, deleteRecord, addRecord):
        record = {"ROWID": 0, "anomalyScore": 1.0, "anomalyVector": "", "anomalyLabel": ["Label"], "setByUser": False}

        # Test record not labeled and not above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 0
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [])
        deleteRecord.assert_called_once_with([state])

        # Test record not labeled and above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 0.5
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)

        self.assertEqual(state.anomalyLabel, [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL])
        addRecord.assert_called_once_with(state)

        # Test record not labeled and above threshold during wait period
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 10
        self.helper.anomalyThreshold = 0.5
        toLabelList.return_value = []
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)

        self.assertEqual(state.anomalyLabel, [])
        self.assertTrue(not addRecord.called)

        # Test record labeled and not above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.trainRecords = 0
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = ["Label"]
        state = _CLAClassificationRecord(**record)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, ["Label"])
        self.assertTrue(not addRecord.called)

        # Test setByUser
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = ["Label 2"]
        recordCopy = copy.deepcopy(record)
        recordCopy["setByUser"] = True
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [recordCopy["anomalyLabel"][0], toLabelList.return_value[0]])
        addRecord.assert_called_once_with(state)

        # Test removal of above threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = []
        recordCopy = copy.deepcopy(record)
        recordCopy["setByUser"] = True
        recordCopy["anomalyLabel"] = [
            KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL,
            KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + KNNAnomalyClassifierRegion.AUTO_TAG,
        ]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [])

        # Auto classified threshold
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 1.1
        toLabelList.return_value = [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        recordCopy = copy.deepcopy(record)
        recordCopy["setByUser"] = True
        recordCopy["anomalyLabel"] = [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(
            state.anomalyLabel,
            [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + KNNAnomalyClassifierRegion.AUTO_TAG],
        )
        addRecord.assert_called_once_with(state)

        # Test precedence of threshold label above auto threshold label
        deleteRecord.reset_mock()
        addRecord.reset_mock()
        self.helper.anomalyThreshold = 0.8
        toLabelList.return_value = [
            KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL,
            KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL + KNNAnomalyClassifierRegion.AUTO_TAG,
        ]
        recordCopy = copy.deepcopy(record)
        recordCopy["setByUser"] = True
        recordCopy["anomalyLabel"] = [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL]
        state = _CLAClassificationRecord(**recordCopy)
        self.helper.classifyState(state)
        self.assertEqual(state.anomalyLabel, [KNNAnomalyClassifierRegion.AUTO_THRESHOLD_CLASSIFIED_LABEL])
        addRecord.assert_called_once_with(state)

    @patch.object(KNNAnomalyClassifierRegion, "_getStateAnomalyVector")
    def testAddRecordToKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = numpy.array([0, 1, 0, 0, 1, 0, 1, 1])
        values = {"categoryRecencyList": [1, 2, 3]}
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn.learn = Mock()
        classifier._knn.prototypeSetCategory = Mock()
        state = {
            "ROWID": 5,
            "anomalyScore": 1.0,
            "anomalyVector": numpy.array([1, 5, 7, 8]),
            "anomalyLabel": ["Label"],
            "setByUser": False,
        }
        record = _CLAClassificationRecord(**state)

        # Test with record not already in KNN
        self.helper._addRecordToKNN(record)
        classifier._knn.learn.assert_called_once_with(getAnomalyVector.return_value, ANY, rowID=state["ROWID"])
        self.assertTrue(not classifier._knn.prototypeSetCategory.called)
        classifier._knn.learn.reset_mock()

        # Test with record already in KNN
        values = {"categoryRecencyList": [1, 2, 3, 5]}
        classifier.getParameter.side_effect = values.get
        self.helper._addRecordToKNN(record)
        classifier._knn.prototypeSetCategory.assert_called_once_with(state["ROWID"], ANY)
        self.assertTrue(not classifier._knn.learn.called)

    @patch.object(KNNAnomalyClassifierRegion, "_getStateAnomalyVector")
    def testDeleteRangeFromKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = "Vector"
        values = {"categoryRecencyList": [1, 2, 3]}
        classifier = self.helper._knnclassifier
        classifier.getParameter = Mock(side_effect=values.get)
        classifier._knn._numPatterns = len(values["categoryRecencyList"])
        classifier._knn.removeIds = Mock(side_effect=self.mockRemoveIds)

        # Test with record not already in KNN
        self.helper._deleteRangeFromKNN(start=1, end=3)
        classifier._knn.removeIds.assert_called_once_with([1, 2])
        classifier._knn.removeIds.reset_mock()

        # Test with record already in KNN
        values = {"categoryRecencyList": [1, 2, 3, 5]}
        classifier.getParameter.side_effect = values.get
        self.helper._deleteRangeFromKNN(start=1)
        classifier._knn.removeIds.assert_called_once_with([1, 2, 3, 5])

    @patch.object(KNNAnomalyClassifierRegion, "_getStateAnomalyVector")
    def testRecomputeRecordFromKNN(self, getAnomalyVector):
        getAnomalyVector.return_value = "Vector"
        self.helper.trainRecords = 0
        values = {
            "categoryRecencyList": [1, 2, 3, 5, 6, 7, 8, 9],
            "latestDists": numpy.array([0.7, 0.2, 0.5, 1, 0.3, 0.2, 0.1]),
            "categories": ["A", "B", "C", "D", "E", "F", "G"],
        }
        classifier = self.helper._knnclassifier
        classifier.getLatestDistances = Mock(return_value=values["latestDists"])
        classifier.getCategoryList = Mock(return_value=values["categories"])
        classifier.getParameter = Mock(side_effect=values.get)
        classifier.setParameter = Mock()
        classifier.compute = Mock()
        state = {"ROWID": 5, "anomalyScore": 1.0, "anomalyVector": "", "anomalyLabel": ["Label"], "setByUser": False}
        record = _CLAClassificationRecord(**state)

        # Test finding best category before record - exists
        self.helper._classificationMaxDist = 0.4
        self.helper._autoDetectWaitRecords = 0
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, "B")

        # Test finding best category before record - does not exists
        self.helper._classificationMaxDist = 0.1
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, None)

        # Test finding best category before record - not record before
        record.ROWID = 0
        self.helper._classificationMaxDist = 0.1
        result = self.helper._recomputeRecordFromKNN(record)
        self.assertEqual(result, None)

    def testConstructClassificationVector(self):
        modelParams = {"__numRunCalls": 0}
        spVals = {"params": {"activeOutputCount": 5}, "output": {"bottomUpOut": numpy.array([1, 1, 0, 0, 1])}}
        tpVals = {
            "params": {"cellsPerColumn": 2, "columnCount": 2},
            "output": {"lrnActive": numpy.array([1, 0, 0, 1]), "topDownOut": numpy.array([1, 0, 0, 0, 1])},
        }
        inputs = dict(
            spBottomUpOut=spVals["output"]["bottomUpOut"],
            tpTopDownOut=tpVals["output"]["topDownOut"],
            tpLrnActiveStateT=tpVals["output"]["lrnActive"],
        )

        self.helper._activeColumnCount = 5

        # Test TP Cell vector
        self.helper.classificationVectorType = 1
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector, tpVals["output"]["lrnActive"].nonzero()[0].tolist())

        # Test SP and TP Column Error vector
        self.helper.classificationVectorType = 2
        self.helper._prevPredictedColumns = numpy.array([1, 0, 0, 0, 1]).nonzero()[0]
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector, [0, 1, 4])

        self.helper._prevPredictedColumns = numpy.array([1, 0, 1, 0, 0]).nonzero()[0]
        vector = self.helper.constructClassificationRecord(inputs)
        self.assertEqual(vector.anomalyVector, [0, 1, 4, 7])

        self.helper.classificationVectorType = 3
        self.assertRaises(TypeError, self.helper.constructClassificationRecord, inputs)

    @patch.object(KNNAnomalyClassifierRegion, "classifyState")
    @patch.object(KNNAnomalyClassifierRegion, "constructClassificationRecord")
    def testCompute(self, createRecord, updateState):
        state = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": numpy.array([1, 0, 0, 0, 1]),
            "anomalyLabel": "Label",
        }
        record = _CLAClassificationRecord(**state)
        createRecord.return_value = record

        inputs = dict()
        outputs = dict()

        # Test add first record
        self.helper.cacheSize = 10
        self.helper.trainRecords = 0
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        updateState.assert_called_once_with(self.helper._recordsCache[-1])

        # Test add record before wait records
        updateState.reset_mock()
        self.helper.cacheSize = 10
        self.helper.trainRecords = 10
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 2)
        self.assertTrue(not updateState.called)

        # Test exceeded cache length
        updateState.reset_mock()
        self.helper.cacheSize = 1
        self.helper._recordsCache = []
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.helper.compute(inputs, outputs)
        self.assertEqual(self.helper._recordsCache[-1], record)
        self.assertEqual(len(self.helper._recordsCache), 1)
        self.assertTrue(not updateState.called)

    def testCategoryToList(self):
        result = self.helper._categoryToLabelList(None)
        self.assertEqual(result, [])

        self.helper.saved_categories = ["A", "B", "C"]
        result = self.helper._categoryToLabelList(1)
        self.assertEqual(result, ["A"])

        result = self.helper._categoryToLabelList(4)
        self.assertEqual(result, ["C"])

        result = self.helper._categoryToLabelList(5)
        self.assertEqual(result, ["A", "C"])

    def testGetAnomalyVector(self):
        state = {"ROWID": 0, "anomalyScore": 1.0, "anomalyVector": [1, 4, 5], "anomalyLabel": "Label"}
        record = _CLAClassificationRecord(**state)
        self.helper._anomalyVectorLength = 10
        vector = self.helper._getStateAnomalyVector(record)

        self.assertEqual(len(vector), self.helper._anomalyVectorLength)
        self.assertEqual(vector.nonzero()[0].tolist(), record.anomalyVector)

    # Tests for configuration
    #############################################################################
    def testSetState(self):
        # No Version set
        state = dict(_classificationDelay=100)
        state["_knnclassifierProps"] = self.params
        self.helper._vectorType = None
        self.helper.__setstate__(state)
        self.assertEqual(self.helper.classificationVectorType, 1)
        self.assertEqual(self.helper._version, KNNAnomalyClassifierRegion.__VERSION__)

        # Version 1
        state = dict(_version=1, _classificationDelay=100)
        state["_knnclassifierProps"] = self.params
        self.helper.__setstate__(state)
        self.assertEqual(self.helper._version, KNNAnomalyClassifierRegion.__VERSION__)

        # Invalid Version
        state = dict(_version="invalid")
        state["_knnclassifierProps"] = self.params
        self.assertRaises(Exception, self.helper.__setstate__, state)

    # Tests for _CLAClassificationRecord class
    #############################################################################
    def testCLAClassificationRecord(self):
        record = {"ROWID": 0, "anomalyScore": 1.0, "anomalyVector": "Vector", "anomalyLabel": "Label"}

        state = _CLAClassificationRecord(**record)
        self.assertEqual(state.ROWID, record["ROWID"])
        self.assertEqual(state.anomalyScore, record["anomalyScore"])
        self.assertEqual(state.anomalyVector, record["anomalyVector"])
        self.assertEqual(state.anomalyLabel, record["anomalyLabel"])
        self.assertEqual(state.setByUser, False)

        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": True,
        }

        state = _CLAClassificationRecord(**record)
        self.assertEqual(state.ROWID, record["ROWID"])
        self.assertEqual(state.anomalyScore, record["anomalyScore"])
        self.assertEqual(state.anomalyVector, record["anomalyVector"])
        self.assertEqual(state.anomalyLabel, record["anomalyLabel"])
        self.assertEqual(state.setByUser, record["setByUser"])

    def testCLAClassificationRecordGetState(self):
        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": False,
        }

        state = _CLAClassificationRecord(**record)

        self.assertEqual(state.__getstate__(), record)

    def testCLAClassificationRecordSetState(self):
        record = {"ROWID": None, "anomalyScore": None, "anomalyVector": None, "anomalyLabel": None, "setByUser": None}

        state = _CLAClassificationRecord(**record)

        record = {
            "ROWID": 0,
            "anomalyScore": 1.0,
            "anomalyVector": "Vector",
            "anomalyLabel": "Label",
            "setByUser": False,
        }

        state.__setstate__(record)

        self.assertEqual(state.ROWID, record["ROWID"])
        self.assertEqual(state.anomalyScore, record["anomalyScore"])
        self.assertEqual(state.anomalyVector, record["anomalyVector"])
        self.assertEqual(state.anomalyLabel, record["anomalyLabel"])
        self.assertEqual(state.setByUser, record["setByUser"])

    def mockRemoveIds(self, ids):
        self.helper._knnclassifier._knn._numPatterns -= len(ids)
        knnClassifier = self.helper._knnclassifier
        for idx in ids:
            if idx in self.helper._knnclassifier.getParameter("categoryRecencyList"):
                knnClassifier.getParameter("categoryRecencyList").remove(idx)