import functools
import logging
import os
import unittest

from chirp import chirp_common
from chirp import bandplan_na

LOG = logging.getLogger(__name__)


class DriverTest(unittest.TestCase):
    RADIO_CLASS = None
    SUB_DEVICE = None
    TEST_IMAGE = None

    def setUp(self):
        super().setUp()
        self.patches = []

        if ('CHIRP_TEST_BITWISE_STRICT_BYTES' in os.environ and
                not self.RADIO_CLASS.NEEDS_COMPAT_SERIAL):
            self.use_patch(unittest.mock.patch(
                'chirp.bitwise.DataElement._compat_bytes',
                side_effect=self._strict_bytes))
            self.use_patch(unittest.mock.patch(
                'chirp.bitwise.string_straight_encode',
                side_effect=AssertionError(
                    'string_straight_encode not allowed in strict mode')))

        self.parent = self.RADIO_CLASS(self.TEST_IMAGE)
        self.parent_rf = self.parent.get_features()
        # If SUB_DEVICE is set to an index, then the actual radio we are
        # to test is get_sub_devices()[SUB_DEVICE]. Otherwise, it's the
        # actual class we were handed.
        if self.SUB_DEVICE is not None:
            self.radio = self.parent.get_sub_devices()[self.SUB_DEVICE]
            self.rf = self.radio.get_features()
        else:
            self.radio = self.parent
            self.rf = self.parent_rf

    def use_patch(self, patch):
        self.patches.append(patch)
        patch.start()

    def tearDown(self):
        for patch in self.patches:
            patch.stop()

    def _strict_bytes(self, bs, asbytes):
        """Enforce strict get_raw() behavior returning bytes()"""
        assert asbytes, 'asbytes must be True in strict mode'
        assert isinstance(bs, bytes), 'Type should be bytes here'
        return bs

    def get_mem(self):
        """Attempt to build a suitable memory for testing"""
        # Check to see if memory #1 has immutable fields, and if so,
        # use that as our template instead of constructing a memory ourselves
        try:
            m = self.radio.get_memory(1)
            # Pre-filter the name so it will match what we expect back
            if 'name' not in m.immutable:
                m.name = self.radio.filter_name(m.name)
            # Disable duplex in case it's set because this will cause some
            # weirdness if we much with other values, like offset.
            if 'duplex' not in m.immutable:
                m.duplex = ''
            if m.immutable:
                return m
        except Exception:
            pass

        m = chirp_common.Memory()

        # Some of the exposed bands may not be transmit-enabled, so
        # iterate them all
        attempt = 0
        for band_lo, band_hi in self.rf.valid_bands:
            m.freq = band_lo
            if chirp_common.is_airband(m.freq):
                # Air band requires special step handling.
                # First see if we can pick a 2m frequency to avoid airband
                # details.
                if chirp_common.in_range(146520000, self.rf.valid_bands):
                    m.freq = 146520000
                else:
                    # Nope, so go 25kHz up from the bottom of the band to
                    # avoid 8.33kHz issues
                    m.freq += 25000
            if self.rf.valid_tuning_steps:
                # If we have valid tuning steps, go one step above the
                # bottom of the band. Select a different tuning_step each
                # time, as some radios have various requirements for which
                # steps work in each band, mode, etc.
                steps = [x for x in self.rf.valid_tuning_steps
                         if x > 1]
                step_index = attempt % len(steps)
                m.tuning_step = steps[step_index]
                m.freq += int(m.tuning_step * 1000)
            elif m.freq + 1000000 < band_hi:
                # Otherwise just pick 1MHz above the bottom, which has been
                # our test basis for a long time, unless that extends past
                # the end of the band.
                m.freq += 1000000

            if m.freq < 30000000 and "AM" in self.rf.valid_modes:
                m.mode = "AM"
            else:
                try:
                    m.mode = self.rf.valid_modes[0]
                except IndexError:
                    pass

            for i in range(*self.rf.memory_bounds)[:10]:
                m.number = i
                msgs = self.radio.validate_memory(m)
                warnings, errors = chirp_common.split_validation_msgs(msgs)
                if warnings and not errors:
                    # If we got some warnings and no errors, then we know the
                    # memory is almost good enough. Set it and pull it back
                    # to let the radio squash whatever was "almost correct"
                    # and then use that.
                    self.radio.set_memory(m)
                    m = self.radio.get_memory(m.number)
                    try:
                        del m.extra
                    except AttributeError:
                        pass
                    return m
                # If we got no warnings, or we have only one band, then
                # no errors means we found our candidate.
                elif not errors:
                    return m

                attempt += 1

        self.fail("No mutable memory locations found - unable to run this "
                  "test because I don't have a memory to test with")

    def assertEqualMem(self, a, b, ignore=None):
        if a.tmode == "Cross":
            tx_mode, rx_mode = a.cross_mode.split("->")
        if ignore is None:
            ignore = []

        if a.duplex == b.duplex == 'off':
            # If we're asking for duplex=off, we should not obsess over the
            # driver keeping track of our offset, as it may use the offset
            # field to manage the TX behavior
            ignore.append('offset')

        a_vals = {}
        b_vals = {}

        for k, v in list(a.__dict__.items()):
            if ignore and k in ignore:
                continue
            if k == "power":
                continue  # FIXME
            elif k == "extra":
                continue
            elif k == "immutable":
                continue
            elif k == "name":
                if not self.rf.has_name:
                    continue  # Don't complain about name, if not supported
                else:
                    # Name mismatch fair if filter_name() is right
                    v = self.radio.filter_name(v).rstrip()
            elif k == "tuning_step" and not self.rf.has_tuning_step:
                continue
            elif k == "rtone" and not (
                a.tmode == "Tone" or
                (a.tmode == "TSQL" and not self.rf.has_ctone) or
                (a.tmode == "Cross" and tx_mode == "Tone") or
                (a.tmode == "Cross" and rx_mode == "Tone" and
                 not self.rf.has_ctone)
            ):
                continue
            elif k == "ctone" and (not self.rf.has_ctone or
                                   not (a.tmode == "TSQL" or
                                        (a.tmode == "Cross" and
                                         rx_mode == "Tone"))):
                continue
            elif k == "dtcs" and (a.tmode != 'DTCS' or
                                  (a.tmode == 'Cross' and tx_mode != 'DTCS')):
                # If we are not in a tmode where a transmit DTCS code is
                # required, we do not care if the code is persisted.
                continue
            elif k == "rx_dtcs" and (not self.rf.has_rx_dtcs or
                                     not (a.tmode == "Cross" and
                                          rx_mode == "DTCS")):
                # If we are not in a tmode where a receive DTCS code is
                # required, we do not care if the code is persisted.
                continue
            elif k == "offset" and not a.duplex:
                continue
            elif k == "cross_mode" and a.tmode != "Cross":
                continue

            if (a.freq in bandplan_na.ALL_GMRS_FREQS and
                    k in a.immutable or k in b.immutable):
                # If the radio returned a field in immutable, it probably
                # means that it's a mandatory setting (i.e. power or duplex
                # in GMRS)
                continue

            a_vals[k] = v
            b_vals[k] = b.__dict__[k]

        self.assertEqual(a_vals, b_vals,
                         'Memories have unexpected differences')

        # Consider mem.extra as matching if the structure remains the same.
        # Since we don't know anything about model-specific things here we
        # can't really assert any more than that, but we can ensure that the
        # structure doesn't change due to the contents of the rest of the
        # memory.
        if a.extra and b.extra:
            self.assertEqual([x.get_name() for x in a.extra],
                             [x.get_name() for x in b.extra],
                             'Memories have different mem.extra keys')


def requires_feature(flag, equal=None):
    def inner(fn):
        @functools.wraps(fn)
        def wraps(self, *a, **k):
            val = getattr(self.rf, flag)
            if (val == equal if equal is not None else val):
                fn(self, *a, **k)
            else:
                self.skipTest('Feature %s not supported' % flag)
        return wraps
    return inner
