"""Unit tests for the osc_types module."""
import unittest

from pythonosc.parsing import ntp
from pythonosc.parsing import osc_types

from datetime import datetime


class TestString(unittest.TestCase):
    def test_get_string(self):
        cases = {
            b"A\x00\x00\x00": ("A", 4),
            b"AB\x00\x00": ("AB", 4),
            b"ABC\x00": ("ABC", 4),
            b"ABCD\x00\x00\x00\x00": ("ABCD", 8),

            b"ABCD\x00\x00\x00\x00GARBAGE": ("ABCD", 8),
            b'\x00\x00\x00\x00': ("", 4),
        }

        for dgram, expected in cases.items():
            self.assertEqual(expected, osc_types.get_string(dgram, 0))

    def test_get_string_raises_on_wrong_dgram(self):
        cases = [
            b'blablaba',
            b'',
            b'\x00',
            b'\x00\x00',
            True,
        ]

        for case in cases:
            self.assertRaises(
                osc_types.ParseError, osc_types.get_string, case, 0)

    def test_get_string_raises_when_datagram_too_short(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_string, b'abc\x00', 1)

    def test_get_string_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_string, b'abc\x00', -1)


class TestInteger(unittest.TestCase):
    def test_get_integer(self):
        cases = {
            b"\x00\x00\x00\x00": (0, 4),
            b"\x00\x00\x00\x01": (1, 4),
            b"\x00\x00\x00\x02": (2, 4),
            b"\x00\x00\x00\x03": (3, 4),

            b"\x00\x00\x01\x00": (256, 4),
            b"\x00\x01\x00\x00": (65536, 4),
            b"\x01\x00\x00\x00": (16777216, 4),

            b"\x00\x00\x00\x01GARBAGE": (1, 4),
        }

        for dgram, expected in cases.items():
            self.assertEqual(
                expected, osc_types.get_int(dgram, 0))

    def test_get_integer_raises_on_type_error(self):
        cases = [b'', True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_int, case, 0)

    def test_get_integer_raises_on_wrong_start_index(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_int, b'\x00\x00\x00\x11', 1)

    def test_get_integer_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_int, b'\x00\x00\x00\x00', -1)

    def test_datagram_too_short(self):
        dgram = b'\x00' * 3
        self.assertRaises(osc_types.ParseError, osc_types.get_int, dgram, 2)


class TestRGBA(unittest.TestCase):
    def test_get_rgba(self):
        cases = {
            b"\x00\x00\x00\x00": (0, 4),
            b"\x00\x00\x00\x01": (1, 4),
            b"\x00\x00\x00\x02": (2, 4),
            b"\x00\x00\x00\x03": (3, 4),

            b"\xFF\x00\x00\x00": (4278190080, 4),
            b"\x00\xFF\x00\x00": (16711680, 4),
            b"\x00\x00\xFF\x00": (65280, 4),
            b"\x00\x00\x00\xFF": (255, 4),

            b"\x00\x00\x00\x01GARBAGE": (1, 4),
        }

        for dgram, expected in cases.items():
            self.assertEqual(
                expected, osc_types.get_rgba(dgram, 0))

    def test_get_rgba_raises_on_type_error(self):
        cases = [b'', True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_rgba, case, 0)

    def test_get_rgba_raises_on_wrong_start_index(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_rgba, b'\x00\x00\x00\x11', 1)

    def test_get_rgba_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_rgba, b'\x00\x00\x00\x00', -1)

    def test_datagram_too_short(self):
        dgram = b'\x00' * 3
        self.assertRaises(osc_types.ParseError, osc_types.get_rgba, dgram, 2)


class TestMidi(unittest.TestCase):
    def test_get_midi(self):
        cases = {
            b"\x00\x00\x00\x00": ((0, 0, 0, 0), 4),
            b"\x00\x00\x00\x02": ((0, 0, 0, 1), 4),
            b"\x00\x00\x00\x02": ((0, 0, 0, 2), 4),
            b"\x00\x00\x00\x03": ((0, 0, 0, 3), 4),

            b"\x00\x00\x01\x00": ((0, 0, 1, 0), 4),
            b"\x00\x01\x00\x00": ((0, 1, 0, 0), 4),
            b"\x01\x00\x00\x00": ((1, 0, 0, 0), 4),

            b"\x00\x00\x00\x01GARBAGE": ((0, 0, 0, 1), 4),
        }

        for dgram, expected in cases.items():
            self.assertEqual(
                expected, osc_types.get_midi(dgram, 0))

    def test_get_midi_raises_on_type_error(self):
        cases = [b'', True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_midi, case, 0)

    def test_get_midi_raises_on_wrong_start_index(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x11', 1)

    def test_get_midi_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x00', -1)

    def test_datagram_too_short(self):
        dgram = b'\x00' * 3
        self.assertRaises(osc_types.ParseError, osc_types.get_midi, dgram, 2)


class TestDate(unittest.TestCase):
    def test_get_timetag(self):
        cases = {
            b"\xde\x9c\x91\xbf\x00\x01\x00\x00": ((datetime(2018, 5, 8, 21, 14, 39), 65536), 8),  # NOTE: fraction is expresed as 32bit OSC.
            b"\x00\x00\x00\x00\x00\x00\x00\x00": ((datetime(1900, 1, 1, 0, 0, 0), 0), 8),
            b"\x83\xaa\x7E\x80\x0A\x00\xB0\x0C": ((datetime(1970, 1, 1, 0, 0, 0), 167817228), 8)
        }

        for dgram, expected in cases.items():
            self.assertEqual(expected, osc_types.get_timetag(dgram, 0))

    def test_get_timetag_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_timetag, b'\x00\x00\x00\x00\x00\x00\x00\x00', -1)

    def test_get_timetag_raises_on_type_error(self):
        cases = [b'', True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_timetag, case, 0)

    def test_get_timetag_raises_on_wrong_start_index(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_date, b'\x00\x00\x00\x11\x00\x00\x00\x11', 1)

    def test_ttag_datagram_too_short(self):
        dgram = b'\x00' * 7
        self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 6)

        dgram = b'\x00' * 2
        self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 1)

        dgram = b'\x00' * 5
        self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 4)

        dgram = b'\x00' * 1
        self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 0)


class TestFloat(unittest.TestCase):
    def test_get_float(self):
        cases = {
            b"\x00\x00\x00\x00": (0.0, 4),
            b"?\x80\x00\x00'": (1.0, 4),
            b'@\x00\x00\x00': (2.0, 4),

            b"\x00\x00\x00\x00GARBAGE": (0.0, 4),
        }

        for dgram, expected in cases.items():
            self.assertAlmostEqual(expected, osc_types.get_float(dgram, 0))

    def test_get_float_raises_on_wrong_dgram(self):
        cases = [True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)

    def test_get_float_raises_on_type_error(self):
        cases = [None]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)

    def test_datagram_too_short_pads(self):
        dgram = b'\x00' * 2
        self.assertEqual((0, 4), osc_types.get_float(dgram, 0))


class TestDouble(unittest.TestCase):
    def test_get_double(self):
        cases = {
            b'\x00\x00\x00\x00\x00\x00\x00\x00': (0.0, 8),
            b'?\xf0\x00\x00\x00\x00\x00\x00': (1.0, 8),
            b'@\x00\x00\x00\x00\x00\x00\x00': (2.0, 8),
            b'\xbf\xf0\x00\x00\x00\x00\x00\x00': (-1.0, 8),
            b'\xc0\x00\x00\x00\x00\x00\x00\x00': (-2.0, 8),

            b"\x00\x00\x00\x00\x00\x00\x00\x00GARBAGE": (0.0, 8),
        }

        for dgram, expected in cases.items():
            self.assertAlmostEqual(expected, osc_types.get_double(dgram, 0))

    def test_get_double_raises_on_wrong_dgram(self):
        cases = [True]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)

    def test_get_double_raises_on_type_error(self):
        cases = [None]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)

    def test_datagram_too_short_pads(self):
        dgram = b'\x00' * 2
        self.assertRaises(osc_types.ParseError, osc_types.get_double, dgram, 0)


class TestBlob(unittest.TestCase):
    def test_get_blob(self):
        cases = {
            b"\x00\x00\x00\x00": (b"", 4),
            b"\x00\x00\x00\x08stuff\x00\x00\x00": (b"stuff\x00\x00\x00", 12),
            b"\x00\x00\x00\x04\x00\x00\x00\x00": (b"\x00\x00\x00\x00", 8),
            b"\x00\x00\x00\x02\x00\x00\x00\x00": (b"\x00\x00", 8),

            b"\x00\x00\x00\x08stuff\x00\x00\x00datagramcontinues": (
                b"stuff\x00\x00\x00", 12),
        }

        for dgram, expected in cases.items():
            self.assertEqual(expected, osc_types.get_blob(dgram, 0))

    def test_get_blob_raises_on_wrong_dgram(self):
        cases = [b'', True, b"\x00\x00\x00\x08"]

        for case in cases:
            self.assertRaises(osc_types.ParseError, osc_types.get_blob, case, 0)

    def test_get_blob_raises_on_wrong_start_index(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_blob, b'\x00\x00\x00\x11', 1)

    def test_get_blob_raises_too_short_buffer(self):
        self.assertRaises(
            osc_types.ParseError,
            osc_types.get_blob,
            b'\x00\x00\x00\x11\x00\x00', 1)

    def test_get_blog_raises_on_wrong_start_index_negative(self):
        self.assertRaises(
            osc_types.ParseError, osc_types.get_blob, b'\x00\x00\x00\x00', -1)


class TestNTPTimestamp(unittest.TestCase):
    def test_immediately_dgram(self):
        dgram = ntp.IMMEDIATELY
        self.assertEqual(osc_types.IMMEDIATELY, osc_types.get_date(dgram, 0)[0])

    def test_origin_of_time(self):
        dgram = b'\x00' * 8
        self.assertGreater(0, osc_types.get_date(dgram, 0)[0])

    def test_datagram_too_short(self):
        dgram = b'\x00' * 8
        self.assertRaises(osc_types.ParseError, osc_types.get_date, dgram, 2)

    def test_write_date(self):
        time = 1569899476.167749  # known round(time.time(), 6)
        timetag = b'\xe1=BT*\xf1\x98\x00'
        self.assertEqual(timetag, osc_types.write_date(time))


class TestBuildMethods(unittest.TestCase):
    def test_string(self):
        self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_string(''))
        self.assertEqual(b'A\x00\x00\x00', osc_types.write_string('A'))
        self.assertEqual(b'AB\x00\x00', osc_types.write_string('AB'))
        self.assertEqual(b'ABC\x00', osc_types.write_string('ABC'))
        self.assertEqual(b'ABCD\x00\x00\x00\x00', osc_types.write_string('ABCD'))

    def test_string_raises(self):
        self.assertRaises(osc_types.BuildError, osc_types.write_string, 123)

    def test_int(self):
        self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_int(0))
        self.assertEqual(b'\x00\x00\x00\x01', osc_types.write_int(1))

    def test_int_raises(self):
        self.assertRaises(osc_types.BuildError, osc_types.write_int, 'no int')

    def test_float(self):
        self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_float(0.0))
        self.assertEqual(b'?\x00\x00\x00', osc_types.write_float(0.5))
        self.assertEqual(b'?\x80\x00\x00', osc_types.write_float(1.0))
        self.assertEqual(b'?\x80\x00\x00', osc_types.write_float(1))

    def test_float_raises(self):
        self.assertRaises(osc_types.BuildError, osc_types.write_float, 'no float')

    def test_blob(self):
        self.assertEqual(
            b'\x00\x00\x00\x02\x00\x01\x00\x00',
            osc_types.write_blob(b'\x00\x01'))
        self.assertEqual(
            b'\x00\x00\x00\x04\x00\x01\x02\x03',
            osc_types.write_blob(b'\x00\x01\x02\x03'))

    def test_blob_raises(self):
        self.assertRaises(osc_types.BuildError, osc_types.write_blob, b'')


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