import itertools
import math
import unittest
from contract import THRESHOLDS, observation, display_state, score_text


def row(scores=None, status=None):
    return dict(sample=3, source_frame=10, seconds=0.3,
                items=[] if scores is None else [dict(scores=scores)], status=status)


class ContractTests(unittest.TestCase):
    def test_all_combinations(self):
        for mask in itertools.product((False, True), repeat=4):
            scores = [0.] * 60
            for key, yes in zip(THRESHOLDS, mask):
                scores[key] = THRESHOLDS[key][1] if yes else 0.
            d = observation(row(scores))
            self.assertEqual(d['count'], sum(mask))
            self.assertEqual(d['ge2'], sum(mask) >= 2)
            self.assertEqual(d['ge3'], sum(mask) >= 3)
            self.assertFalse(d['ge3'] and not d['ge2'])

    def test_boundaries_and_rounding(self):
        for key, levels in THRESHOLDS.items():
            for i, level in enumerate(levels):
                for value, expected in [(math.nextafter(level, 0), False), (level, True),
                                        (math.nextafter(level, 1), True)]:
                    scores = [0.] * 60; scores[key] = value
                    self.assertEqual(observation(row(scores))['classes'][str(key)][('low','basic','high')[i]], expected)

    def test_count_once_basic_only(self):
        scores = [0.] * 60; scores[23] = 1.
        self.assertEqual(observation(row(scores))['count'], 1)
        for key, levels in THRESHOLDS.items(): scores[key] = levels[0]
        self.assertEqual(observation(row(scores))['count'], 0)

    def test_missing_states(self):
        for status, expected in [('INSUFFICIENT_WINDOW','INPUT_INSUFFICIENT'), (None,'NO_SCORES')]:
            d = observation(row(status=status)); self.assertEqual(d['state'], expected)
            self.assertIsNone(d['count']); self.assertIsNone(d['ge2'])
        r = row(); r['items'] = [dict(status='NO_VALID_SKELETON')]
        self.assertEqual(observation(r)['state'], 'NO_VALID_SKELETON')

    def test_no_temporal_hold(self):
        self.assertTrue(observation(row([1.] * 60))['ge3'])
        self.assertFalse(observation(row([0.] * 60))['ge2'])
        self.assertIsNone(observation(row())['ge2'])

    def test_other_classes_do_not_filter(self):
        scores = [1.] * 60
        for key, levels in THRESHOLDS.items(): scores[key] = levels[1]
        self.assertEqual(observation(row(scores))['count'], 4)

    def test_invalid_vector_rejected(self):
        for bad in ([0.] * 59, [float('nan')] * 60, [float('inf')] * 60, [-1.] * 60, [1.1] * 60):
            with self.assertRaises(ValueError): observation(row(bad))
        r = row([0.] * 60); r['items'] *= 2
        with self.assertRaises(ValueError): observation(r)

    def test_display_and_precision(self):
        d = observation(row([0.] * 60))
        self.assertEqual(display_state(d, 'signals_3levels'), 'SIGNALS')
        self.assertEqual(display_state(d, 'filtered_ge2'), 'NO_MATCH')
        self.assertEqual(display_state(observation(row()), 'filtered_ge3'), 'NO_SCORES')
        self.assertNotEqual(score_text(1e-12), '0.000000000')


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