"""Tests for functions defined in sohstationviewer.controller.plottingData"""

from unittest import TestCase

from obspy import UTCDateTime

from sohstationviewer.controller.plottingData import (
    getMassposValueColors,
    formatTime,
    getTitle,
    getGaps,
    getTimeTicks,
    getDayTicks,
    getUnitBitweight
)


class TestGetGaps(TestCase):
    """Test suite for getGaps."""
    def test_mixed_gap_sizes(self):
        """
        Test getGaps - the given list of gaps contain both gaps that are too
        short and gaps that are long enough.
        """
        gaps = [(0, 60), (60, 180), (180, 360)]
        min_gap = 3
        self.assertListEqual(getGaps(gaps, min_gap), [(180, 360)])

    def test_empty_gap_list(self):
        """
        Test getGaps - the given list of gaps is empty.
        """
        gaps = []
        min_gap = 3
        self.assertListEqual(getGaps(gaps, min_gap), [])

    def test_all_gaps_are_too_short(self):
        """
        Test getGaps - the given list of gaps only contain gaps that are too
        short.
        """
        gaps = [(0, 60), (60, 180)]
        min_gap = 3
        self.assertListEqual(getGaps(gaps, min_gap), [])

    def test_all_gaps_are_long_enough(self):
        """
        Test getGaps - the given list of gaps only contain gaps that are long
        enough.
        """
        gaps = [(0, 180), (180, 360)]
        min_gap = 3
        self.assertListEqual(getGaps(gaps, min_gap), [(0, 180), (180, 360)])


class TestGetDayTicks(TestCase):
    """Test suite for getDayTicks."""
    def test_get_day_ticks(self):
        """Test getDayTicks."""
        expected = (
            [12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 168, 180,
             192, 204, 216, 228, 240, 252, 264, 276],
            [48, 96, 144, 192, 240],
            ['04', '08', '12', '16', '20']
        )
        self.assertTupleEqual(getDayTicks(), expected)


class TestGetMassposValue(TestCase):
    """Test suite for getMasspossValue"""
    def test_string_output(self):
        """
        Test basic functionality of getMassposValueColors - the range option
        and color mode are correct, and the output is a string.
        """
        expected_input_output_pairs = {
            ('regular', 'B'): '0.5:C|2.0:G|4.0:Y|7.0:R|7.0:+M',
            ('regular', 'W'): '0.5:B|2.0:B|4.0:B|7.0:B|7.0:+B',
            ('trillium', 'B'): '0.5:C|1.8:G|2.4:Y|3.5:R|3.5:+M',
            ('trillium', 'W'): '0.5:B|1.8:B|2.4:B|3.5:B|3.5:+B',
        }
        test_names = (
            'test_regular_B',
            'test_regular_W',
            'test_trillium_B',
            'test_trillium_W',
        )
        idx = 0
        for input_val in expected_input_output_pairs:
            with self.subTest(test_names[idx]):
                self.assertEqual(
                    getMassposValueColors(input_val[0], '', input_val[1], []),
                    expected_input_output_pairs[input_val]
                )
            idx += 1

    def test_list_output(self):
        """
        Test basic functionality of getMassposValueColors - the range option
        and color mode are correct, and the output is a list.
        """
        expected_input_output_pairs = {
            ('regular', 'B'):
                [(0.5, 'C'), (2.0, 'G'), (4.0, 'Y'), (7.0, 'R'), (7.0, 'M')],
            ('regular', 'W'):
                [(0.5, 'B'), (2.0, 'B'), (4.0, 'B'), (7.0, 'B'), (7.0, 'B')],
            ('trillium', 'B'):
                [(0.5, 'C'), (1.8, 'G'), (2.4, 'Y'), (3.5, 'R'), (3.5, 'M')],
            ('trillium', 'W'):
                [(0.5, 'B'), (1.8, 'B'), (2.4, 'B'), (3.5, 'B'), (3.5, 'B')],
        }
        test_names = (
            'test_regular_B',
            'test_regular_W',
            'test_trillium_B',
            'test_trillium_W',
        )
        for i, input_val in enumerate(expected_input_output_pairs):
            with self.subTest(test_names[i]):
                self.assertListEqual(
                    getMassposValueColors(
                        input_val[0], '', input_val[1], [], retType=''),
                    expected_input_output_pairs[input_val]
                )

    def test_range_option_not_supported(self):
        """
        Test basic functionality of getMassposValueColors - the range option
        is not supported.
        """
        errors = []
        empty_color_option = ''
        self.assertIsNone(
            getMassposValueColors(empty_color_option, '', 'B', errors))
        self.assertGreater(len(errors), 0)

        errors = []
        bad_color_option = 'unsupported'
        self.assertIsNone(
            getMassposValueColors(bad_color_option, '', 'B', errors))
        self.assertGreater(len(errors), 0)

    def test_color_mode_not_supported(self):
        """
        Test basic functionality of getMassposValueColors - the color mode is
        not supported.
        """
        errors = []
        empty_color_mode = ''
        with self.assertRaises(KeyError):
            getMassposValueColors('regular', '', empty_color_mode, errors)

        errors = []
        bad_color_mode = 'unsupported'
        with self.assertRaises(KeyError):
            getMassposValueColors('regular', '', bad_color_mode, errors)


class TestGetTimeTicks(TestCase):
    """Test suite for getTimeTicks."""
    def setUp(self) -> None:
        """Set up text fixtures."""
        self.label_cnt = 5
        self.date_fmt = 'YYYYMMDD'

    def test_expected_time_range(self):
        """
        Test basic functionality of getTimeTicks - the given time range is
        expected in the data.
        """

        with self.subTest('test_less_than_a_minute'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 0, 0, 5).timestamp
            expected = (
                [1.0, 2.0, 3.0, 4.0],
                [1.0, 2.0, 3.0, 4.0],
                ['19700101 00:00:01', '19700101 00:00:02',
                 '19700101 00:00:03', '19700101 00:00:04']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

        with self.subTest('test_at_least_a_minute'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 0, 5, 53).timestamp
            expected = (
                [60.0, 120.0, 180.0, 240.0, 300.0],
                [60.0, 120.0, 180.0, 240.0, 300.0],
                ['19700101 00:01:00', '19700101 00:02:00', '19700101 00:03:00',
                 '19700101 00:04:00', '19700101 00:05:00']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

        with self.subTest('test_at_least_an_hour'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 7, 45, 51).timestamp
            expected = (
                [3600.0, 7200.0, 10800.0, 14400.0, 18000.0, 21600.0, 25200.0],
                [3600.0, 10800.0, 18000.0, 25200.0],
                ['19700101 01:00', '19700101 03:00', '19700101 05:00',
                 '19700101 07:00']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

        with self.subTest('test_at_least_ten_days'):
            # Exactly 10 days
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 11, 0, 0, 0).timestamp
            expected = (
                [86400.0, 172800.0, 259200.0, 345600.0, 432000.0,
                 518400.0, 604800.0, 691200.0, 777600.0],
                [86400.0, 259200.0, 432000.0, 604800.0, 777600.0],
                ['19700102', '19700104', '19700106', '19700108', '19700110']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

            # More than 10 days but fewer than 30 days
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 14, 3, 5, 21).timestamp
            expected = (
                [86400.0, 172800.0, 259200.0, 345600.0, 432000.0,
                 518400.0, 604800.0, 691200.0, 777600.0, 864000.0,
                 950400.0, 1036800.0, 1123200.0],
                [86400.0, 345600.0, 604800.0, 864000.0, 1123200.0],
                ['19700102', '19700105', '19700108', '19700111', '19700114']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

        with self.subTest('test_at_least_thirty_days'):
            # Exactly 30 days
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 31, 0, 0, 0).timestamp
            expected = ([864000.0, 1728000.0],
                        [864000.0, 1728000.0],
                        ['19700111', '19700121'])
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

            # More than 30 days
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 3, 21, 21, 43, 53).timestamp
            expected = (
                [864000.0, 1728000.0, 2592000.0, 3456000.0, 4320000.0,
                 5184000.0, 6048000.0],
                [864000.0, 2592000.0, 4320000.0, 6048000.0],
                ['19700111', '19700131', '19700220', '19700312']
            )
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

    def test_boundary_time_range(self):
        """
        Test basic functionality of getTimeTicks - the given time range is
        exactly 1 second, 1 minute, or 1 hour. Test the behavior where these
        time ranges make getTimeTicks returns a tuple that contains empty
        lists.
        """
        expected = ([], [], [])
        with self.subTest('test_exactly_one_second'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 0, 0, 1).timestamp
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )
        with self.subTest('test_exactly_one_minute'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 0, 1, 0).timestamp
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )
        with self.subTest('test_exactly_one_hour'):
            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
            latest = UTCDateTime(1970, 1, 1, 1, 0, 0).timestamp
            self.assertTupleEqual(
                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
                expected
            )

    def test_earliest_time_later_than_latest_time(self):
        """
        Test basic functionality of getTimeTicks - the given latest time is
        earlier than the earliest time.
        """
        self.assertTupleEqual(
            getTimeTicks(100, 0, self.date_fmt, self.label_cnt),
            ([], [], [])
        )

    def test_time_range_is_zero(self):
        """
        Test basic functionality of getTimeTicks - the given time range is 0.
        """
        self.assertTupleEqual(
            getTimeTicks(0, 0, self.date_fmt, self.label_cnt),
            ([], [], [])
        )

    def test_get_time_ticks_no_label_displayed(self):
        """
        Test basic functionality of getTimeTicks - no time label is requested
        to be displayed.
        """
        zero_label_cnt = 0
        with self.assertRaises(ZeroDivisionError):
            getTimeTicks(0, 1000, self.date_fmt, zero_label_cnt)


class TestFormatTimeAndGetTitle(TestCase):
    """Test suite for formatTime and getTitle"""
    def setUp(self) -> None:
        """Set up text fixtures."""
        self.positive_epoch_time = 67567567
        self.positive_formatted_dates = {
            'YYYY-MM-DD': '1972-02-22',
            'YYYYMMDD': '19720222',
            'YYYY:DOY': '1972:053',
        }
        self.positive_formatted_time = '00:46:07'

        self.negative_epoch_time = -67567567
        self.negative_formatted_dates = {
            'YYYY-MM-DD': '1967-11-10',
            'YYYYMMDD': '19671110',
            'YYYY:DOY': '1967:314',
        }
        self.negative_formatted_time = '23:13:53'

    def test_format_time_epoch_time_date_only(self):
        """
        Test basic functionality of formatTime - given time is epoch time and
        uses only a date format. Tests three cases for each date format: epoch
        time is positive, negative, and zero.
        """
        # formatter:off
        test_name_to_date_mode_map = {
            'test_year_month_day_format':         'YYYY-MM-DD',
            'test_year_month_day_format_no_dash': 'YYYYMMDD',
            'test_day_of_year_format':            'YYYY:DOY'
        }
        zero_epoch_formatted = {
            'test_year_month_day_format':         '1970-01-01',
            'test_year_month_day_format_no_dash': '19700101',
            'test_day_of_year_format':            '1970:001',
        }
        # formatter:on
        for test_name, date_mode in test_name_to_date_mode_map.items():
            with self.subTest(test_name):
                self.assertEqual(
                    formatTime(self.positive_epoch_time, date_mode),
                    self.positive_formatted_dates[date_mode])
                self.assertEqual(
                    formatTime(self.negative_epoch_time, date_mode),
                    self.negative_formatted_dates[date_mode])
                self.assertEqual(
                    formatTime(0, date_mode),
                    zero_epoch_formatted[test_name])

    def test_format_time_epoch_time_date_and_time(self):
        """
        Test basic functionality of formatTime - given time is epoch time and
        both a time and a date format are used. Tests three cases for each date
        format: epoch time is positive, negative, and zero.
        """
        # formatter:off
        test_name_to_date_mode_map = {
            'test_year_month_day_format':         'YYYY-MM-DD',
            'test_year_month_day_format_no_dash': 'YYYYMMDD',
            'test_day_of_year_format':            'YYYY:DOY',
        }
        zero_epoch_formatted = {
            'test_year_month_day_format':         '1970-01-01 00:00:00',
            'test_year_month_day_format_no_dash': '19700101 00:00:00',
            'test_day_of_year_format':            '1970:001 00:00:00',
        }
        # formatter:on
        for test_name, date_mode in test_name_to_date_mode_map.items():
            with self.subTest(test_name):
                positive_time_expected = (
                    f'{self.positive_formatted_dates[date_mode]} '
                    f'{self.positive_formatted_time}'
                )
                negative_time_expected = (
                    f'{self.negative_formatted_dates[date_mode]} '
                    f'{self.negative_formatted_time}'
                )

                self.assertEqual(
                    formatTime(self.positive_epoch_time, date_mode,
                               'HH:MM:SS'),
                    positive_time_expected
                )
                self.assertEqual(
                    formatTime(self.negative_epoch_time, date_mode,
                               'HH:MM:SS'),
                    negative_time_expected
                )
                self.assertEqual(formatTime(0, date_mode, 'HH:MM:SS'),
                                 zero_epoch_formatted[test_name])

    def test_format_time_UTCDateTime_date_only(self):
        """
        Test basic functionality of formatTime - given time is an UTCDateTime
        instance and uses only a date format.
        """
        test_name_to_date_mode_map = {
            'test_year_month_day_format': 'YYYY-MM-DD',
            'test_year_month_day_format_no_dash': 'YYYYMMDD',
            'test_day_of_year_format': 'YYYY:DOY',
        }
        utc_date_time = UTCDateTime(self.positive_epoch_time)
        expected_dates = self.positive_formatted_dates
        for test_name, date_mode in test_name_to_date_mode_map.items():
            with self.subTest(test_name):
                self.assertEqual(formatTime(utc_date_time, date_mode),
                                 expected_dates[date_mode])

    def test_format_time_UTCDateTime_date_and_time(self):
        """
        Test basic functionality of formatTime - given time is an UTCDateTime
        instance and both a time and a date format are used.
        """
        test_name_to_date_mode_map = {
            'test_year_month_day_format': 'YYYY-MM-DD',
            'test_year_month_day_format_no_dash': 'YYYYMMDD',
            'test_day_of_year_format': 'YYYY:DOY',
        }
        test_time = UTCDateTime(self.positive_epoch_time)
        expected_dates = self.positive_formatted_dates
        expected_time = self.positive_formatted_time
        for test_name, date_mode in test_name_to_date_mode_map.items():
            with self.subTest('test_year_month_day_format'):
                self.assertEqual(
                    formatTime(test_time, date_mode, 'HH:MM:SS'),
                    f'{expected_dates[date_mode]} {expected_time}'
                )

    def test_format_time_unsupported_date_format(self):
        """
        Test basic functionality of formatTime - given date format is not
        supported.
        """
        test_time = self.positive_epoch_time
        empty_format = ''
        bad_format = 'bad_format'

        with self.subTest('test_without_time_format'):
            expected = ''
            self.assertEqual(formatTime(test_time, empty_format),
                             expected)
            self.assertEqual(formatTime(test_time, bad_format),
                             expected)

        with self.subTest('test_with_time_format'):
            expected = f' {self.positive_formatted_time}'
            self.assertEqual(formatTime(test_time, empty_format, 'HH:MM:SS'),
                             expected)
            self.assertEqual(formatTime(test_time, bad_format, 'HH:MM:SS'),
                             expected)

    def test_format_time_unsupported_time_format(self):
        """
        Test basic functionality of formatTime - given time format is not
        supported.
        """
        test_time = self.positive_epoch_time
        date_format = 'YYYYMMDD'
        empty_format = ''
        bad_format = 'bad_format'

        expected = self.positive_formatted_dates[date_format]
        self.assertEqual(formatTime(test_time, date_format, empty_format),
                         expected)
        self.assertEqual(formatTime(test_time, date_format, bad_format),
                         expected)

    def test_format_time_unsupported_date_and_time_format(self):
        """
        Test basic functionality of formatTime - both given date and time
        format are unsupported.
        """
        test_time = self.positive_epoch_time
        expected = ''
        bad_date_formats = ['', 'bad_date_format']
        bad_time_format = ['', 'bad_time_format']
        for date_format in bad_date_formats:
            for time_format in bad_time_format:
                self.assertEqual(
                    formatTime(test_time, date_format, time_format),
                    expected
                )

    def test_get_title(self):
        """Test basic functionality of getTitle."""
        date_mode = 'YYYYMMDD'
        min_time = 0
        max_time = self.positive_epoch_time
        formatted_max_time = (f'{self.positive_formatted_dates[date_mode]}'
                              f' {self.positive_formatted_time}')
        with self.subTest('test_mseed'):
            key = '3734'
            expected = (f'3734  19700101 00:00:00  to  '
                        f'{formatted_max_time}  (18768.77)')
            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
                             expected)
        with self.subTest('test_rt130'):
            key = ('92EB', 25)
            expected = (f"('92EB', 25)  19700101 00:00:00  to  "
                        f"{formatted_max_time}  (18768.77)")
            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
                             expected)

    def test_get_title_max_time_earlier_than_min_time(self):
        """
        Test basic functionality of getTitle - the given maximum time is
        chronologically earlier than the given minimum time.
        """
        date_mode = 'YYYYMMDD'
        min_time = self.positive_epoch_time
        max_time = 0
        formatted_max_time = (f'{self.positive_formatted_dates[date_mode]}'
                              f' {self.positive_formatted_time}')
        with self.subTest('test_mseed'):
            key = '3734'
            expected = (f'3734  {formatted_max_time}  to  '
                        f'19700101 00:00:00  (-18768.77)')
            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
                             expected)
        with self.subTest('test_rt130'):
            key = ('92EB', 25)
            expected = (f"('92EB', 25)  {formatted_max_time}  to  "
                        f"19700101 00:00:00  (-18768.77)")
            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
                             expected)


class TestGetUnitBitweight(TestCase):
    """Test suite for getUnitBitweight."""

    def setUp(self) -> None:
        """Set up test fixtures."""
        self.chan_info = {
            'channel': 'Test Channel Name',
            'fixPoint': 0,
            'plotType': 'test_plot_type',
            'unit': 'test_unit'
        }
        # In most cases, we do not care about the value of bitweightOpt. So, we
        # give it a default value unless needed.
        self.default_bitweight_opt = 'low'

    def test_soh_channel_linesDots_linesSRate_linesMasspos_plot_type(self):
        """
        Test basic functionality of getUnitBitweight - the given plot type is
        linesDots, linesSRate, or linesMassposs.
        """
        self.chan_info['plotType'] = 'linesDots'
        with self.subTest('test_no_fix_point'):
            self.assertEqual(
                getUnitBitweight(self.chan_info, self.default_bitweight_opt),
                '{}test_unit'
            )
        with self.subTest('test_have_fix_point'):
            self.chan_info['fixPoint'] = 1
            self.assertEqual(
                getUnitBitweight(self.chan_info, self.default_bitweight_opt),
                '{:.1f}test_unit'
            )

    def test_soh_channel_other_plot_type(self):
        """
        Test basic functionality of getUnitBitweight - the given plot type is
        not linesDots, linesSRate, or linesMassposs and the channel is not a
        seismic data channel.
        """
        self.assertEqual(
            getUnitBitweight(self.chan_info, self.default_bitweight_opt),
            ''
        )

    def test_seismic_channel_have_fix_point(self):
        """
        Test basic functionality of getUnitBitweight - the given plot type is
        not linesDots, linesSRate, or linesMassposs, the channel is a
        seismic data channel, and there is a fix point.
        """
        self.chan_info['channel'] = 'SEISMIC'
        self.chan_info['fixPoint'] = 1
        self.assertEqual(
            getUnitBitweight(self.chan_info, self.default_bitweight_opt),
            '{:.1f}test_unit'
        )

    def test_seismic_channel_no_fix_point(self):
        """
        Test basic functionality of getUnitBitweight - the given plot type is
        not linesDots, linesSRate, or linesMassposs, the channel is a
        seismic data channel, and there is no fix point.
        """
        self.chan_info['channel'] = 'SEISMIC'
        with self.subTest('test_no_bitweight_option'):
            self.assertEqual(getUnitBitweight(self.chan_info, 'high'), '{}V')
        with self.subTest('test_have_bitweight_option'):
            self.assertEqual(getUnitBitweight(self.chan_info, ''),
                             '{}test_unit')

    def test_no_fix_point(self):
        """
        Test basic functionality of getUnitBitweight - the given channel info
        does not contain a value for fixPoint.
        """
        del self.chan_info['fixPoint']
        # Asserts that an error is not raised. TestCase does not have a method
        # to check that an exception was not raised so we have to use this
        # workaround. For more discussion on this topic, see these
        # stackoverflow questions and the answers
        # https://stackoverflow.com/questions/6181555/pass-a-python-unittest-if-an-exception-isnt-raised?noredirect=1&lq=1 # noqa
        # https://stackoverflow.com/questions/4319825/python-unittest-opposite-of-assertraises/4319870#4319870 # noqa:E501

        # Some context for this code: in getUnitBitWeight, if chanDB does not
        # have 'fix_point' as a key, then the value of fix_point defaults to 0.
        # This is implemented by getting the value of 'fix_point' in chanDB and
        # catching the resulting KeyError. So, in order to test that
        # getUnitBitweight handles this case correctly, we assert that no
        # exception was raised.
        try:
            getUnitBitweight(self.chan_info, '')
        except KeyError:
            self.fail()