import unittest
import sys
for path in ['../', './']:
    sys.path.insert(1, path)

# python -m unittest discover
from processing.clustering.clusterer import Clusterer
import numpy as np

class TestClusterer(unittest.TestCase):
    clusterer:Clusterer = None

    def setUp(self):
        self.clusterer = Clusterer(min_points=2)

    #region _extract_features

    def test_extract_features_emptyDataset_noResults(self):
        features = self.clusterer._extract_features(dataset=[], features=['test'])
        np.testing.assert_equal(np.asarray([]), features)

    def test_extract_features_emptyFeatures_singleEmptyResult(self):
        features = self.clusterer._extract_features(dataset=[{'a':1, 'b':2}], features=[])
        np.testing.assert_equal(np.asarray([[]]), features)

    def test_extract_features_singleFeature_Projection(self):
        features = self.clusterer._extract_features(dataset=[{'a':1, 'b':2}], features=['a'])
        np.testing.assert_equal(np.asarray([[1]]), features)

    def test_extract_features_singleFeature_Projection_2(self):
        features = self.clusterer._extract_features(dataset=[{'a':1, 'b':2}, {'a':3, 'b':4}], features=['a'])
        np.testing.assert_equal(np.asarray([[1], [3]]), features)
        
    def test_extract_features_multFeature_Projection(self):
        features = self.clusterer._extract_features(dataset=[{'a':0, 'b':2, 'c':4}, {'a':1, 'b':3, 'c':5}], features=['a','c'])
        np.testing.assert_equal(np.asarray([[0,4], [1,5]]), features)
    
    #endregion _extract_features

    #region create_labels

    def test_create_labels_noneInput_noneOutput(self):
        labels = self.clusterer.create_labels(None)
        self.assertEqual(None, labels)

    def test_create_labels_emptyInput_emptyOutput(self):
        labels = self.clusterer.create_labels([])
        self.assertEqual([], labels)
                
    def test_create_labels_singleInput_noise(self):
        clusterer = Clusterer(min_points=1)

        features =  clusterer._extract_features(dataset=[self.location(1,2)], features=self.get_location_features())
        labels = clusterer.create_labels(features)

        self.assertEqual(1, len(labels))
        self.assertEqual(-1, labels[0])

    def test_create_labels_tooSmallInputForMinPtsHyperparameter_error(self):
        clusterer = Clusterer(min_points=3) 

        features =  clusterer._extract_features(dataset=[self.location(1,2), self.location(1,2)], features=self.get_location_features())
        with self.assertRaises(ValueError):
            # Fails because (min_pts > |input elements|)
            clusterer.create_labels(features)

    def test_create_labels_nearInputs_singleCluster(self):
        locations = [self.location(1,2), self.location(2,2)]

        features = self.clusterer._extract_features(dataset=locations, features=self.get_location_features())
        labels = self.clusterer.create_labels(features)

        self.assertEqual(2, len(labels))
        self.assertEqual(labels[0], labels[1])

    def test_create_labels_nearInputs_twoClusters(self):
        locations = [self.location(1,2), self.location(2,2), self.location(20,20), self.location(20,23)]

        features = self.clusterer._extract_features(dataset=locations, features=self.get_location_features())
        labels = self.clusterer.create_labels(features)

        self.assertEqual(4, len(labels))
        self.assertEqual(labels[0], labels[1])
        self.assertEqual(labels[2], labels[3])
        self.assertNotEqual(labels[0], labels[2])

    #endregion create_labels

    #region label_dataset

    def test_label_dataset_NoneLocations_NoException(self):
        self.clusterer.label_dataset(None, [])

    def test_label_dataset_NoneLabels_NoException(self):
        self.clusterer.label_dataset([], None)

    def test_label_dataset_emptyInput_emptyOutput(self):
        locations = []
        self.clusterer.label_dataset(locations, [])
        self.assertEqual(0, len(locations))

    def test_label_dataset_diffInputLengths_ValueError_1(self):
        with self.assertRaises(ValueError):
            self.clusterer.label_dataset([], [1])

    def test_label_dataset_diffInputLengths_ValueError_2(self):
        with self.assertRaises(ValueError):
            self.clusterer.label_dataset([self.location(1,2)], [])

    def test_label_dataset_multInput_correctlyLabeled(self):
        locations = [self.location(1,2), self.location(2,2), self.location(20,20)]
        labels = [17,2,20]

        self.clusterer.label_dataset(locations, labels)

        self.assertEqual(3, len(locations))
        self.assertHaveLabelsAsNewKey(locations, labels)

    #endregion label_dataset

    #region cluster_dataset

    def test_cluster_dataset_locationsMultInput_correctlyLabeled(self):
        locations = [self.location(1,2), self.location(2,2), self.location(20,20), self.location(20,21)]
        labels = [0,0,1,1]
        exp_res = {0:locations[0:2], 1:locations[2:4]}

        res = self.clusterer.cluster_dataset(locations, self.get_location_features())

        self.assertHaveLabelsAsNewKey(locations, labels)
        self.assertClusteringResult(exp_res, res)

    def test_cluster_dataset_timesMultInput_correctlyLabeled(self):
        times = [self.time(123), self.time(128), self.time(223), self.time(225)]
        labels = [0,0,1,1]
        exp_res = {0:times[0:2], 1:times[2:4]}

        res = self.clusterer.cluster_dataset(times, self.get_time_features())
        
        self.assertHaveLabelsAsNewKey(times, labels)
        self.assertClusteringResult(exp_res, res)

    def test_cluster_dataset_locationsMultInput_correctlyLabeled_2(self):
        clusterer = Clusterer(3)
        locations = [
                    self.location(1,2), self.location(2,2), self.location(2,2), 
                    self.location(20,20), self.location(20,21),  self.location(20,20), 
                    self.location(50,50), 
                    self.location(50,1), self.location(50,2), self.location(50,-1)
                    ]
        labels = [0,0,0,1,1,1,-1,2,2,2]
        exp_res = {0:locations[0:3], 1:locations[3:6], -1:locations[6:7], 2:locations[7:10]}

        res = clusterer.cluster_dataset(locations, self.get_location_features())

        self.assertHaveLabelsAsNewKey(locations, labels)
        self.assertClusteringResult(exp_res, res)

    #endregion cluster_dataset

    #region helper methods

    def location(self, lat, long_) -> dict:
        return {'latitude': lat, 'longitude':long_}

    def get_location_features(self):
        return ['latitude', 'longitude']

    def time(self, ts) -> dict:
        return {'timestamp': ts}

    def get_time_features(self):
        return ['timestamp']

    def assertHaveLabelsAsNewKey(self, locations, labels):
        self.assertEqual(len(labels), len(locations))

        for i in range(len(locations)):
            self.assertEqual(labels[i], locations[i]['cluster_label'])

    def assertClusteringResult(self, expected, actual):
        self.assertEqual(len(expected), len(actual))

        for k in expected.keys():
            if k not in actual:
                self.fail(f"Cluster key ({k}, {type(k)}) not in result.")
            
            self.assertListEqual(expected[k], actual[k])
   
    #endregion helper methods

if __name__ == '__main__':
    unittest.main()
