import io
from unittest import TestCase, mock

from bme280pi.sensor import Sensor, I2CException


class FakeSMBus:
    def __init__(self, value):
        """
        This is a fake bus class (to replace SMBus).
        It records the value it is initialized with, and makes
        it accessible for checks. It also reports its chip ID and
        version as fake.
        """
        self.value = value

    @staticmethod
    def read_i2c_block_data(address, register, length, force=None):
        return "fake_chip_id", "fake_version"


class TestInitializeBus(TestCase):
    def test(self):
        # this test requires us to override the processor type and smbbus
        known_revisions = {'0002': 0,
                           '0003': 0,
                           '0004': 1,
                           '0005': 1,
                           '0006': 1,
                           '0007': 0,
                           '0008': 0,
                           '0009': 0,
                           '000d': 1,
                           '000e': 1,
                           '000f': 1,
                           '0010': 0,
                           '0011': 1,
                           '0012': 0,
                           'a01041': 1,
                           'a21041': 1,
                           '900092': 1,
                           '900093': 1,
                           'a02082': 1,
                           'a22082': 1,
                           '9000c1': 1,
                           'c03111': 1,
                           'abcdef': 1,
                           '0000': 1}

        for revision in known_revisions:
            m = mock.mock_open(read_data="\nRevision:" + revision + "\n")
            with mock.patch('builtins.open', m):
                with mock.patch('smbus.SMBus', FakeSMBus):
                    sensor = Sensor()
                    self.assertEqual(sensor.bus.value,
                                     known_revisions[revision])


class FakeDataBus:
    def __init__(self, starting_point=-1):
        """
        A further fake bus class (to replace SMBus).
        This version returns data that is a realistic representation
        of actual data
        """
        self.i_read = starting_point
        self.data = [['fake_chip_id', 'fake_version'],
                     [96, 110, 203, 104, 50, 0, 29, 145, 59, 215, 208, 11,
                      232, 38, 42, 255, 249, 255, 172, 38, 10, 216, 189, 16],
                     [75],
                     [129, 1, 0, 16, 44, 3, 30],
                     [76, 60, 128, 129, 49, 128, 94, 120]]

    def write_byte_data(self, address, register, value, force=None):
        pass

    def read_i2c_block_data(self, address, register, length, force=None):
        self.i_read += 1
        return self.data[self.i_read]


class FileNotFoundSMBus:
    def __init__(self, bus_address):
        """
        This fake bus class raises a FileNotFoundError, because that's what
        SMBus can do in case I2C is not configured.
        """
        raise FileNotFoundError("No such file or directory")


def initialize_fake_bus(*args, **kwargs):
    return FakeDataBus()


class TestSensor(TestCase):
    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_data(self):
        sensor = Sensor()
        self.assertEqual(sensor.chip_id, "fake_chip_id")
        self.assertEqual(sensor.chip_version, "fake_version")

        data = sensor.get_data()
        self.assertLess(abs(data['temperature'] - 24.65), 1e-4)
        self.assertLess(abs(data['pressure'] - 969.1056565652227), 1e-4)
        self.assertLess(abs(data['humidity'] - 41.07329061361983), 1e-4)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_temperature(self):
        sensor = Sensor()
        temperature = sensor.get_temperature()
        self.assertLess(abs(temperature - 24.65), 1e-4)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_pressure(self):
        sensor = Sensor()
        pressure = sensor.get_pressure()
        self.assertLess(abs(pressure - 969.1056565652227), 1e-4)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_pressure_above_sea_level(self):
        sensor = Sensor()
        pressure = sensor.get_pressure(height_above_sea_level=440,
                                       as_pressure_at_sea_level=True)
        self.assertLess(abs(pressure - 1019.0420210), 1e-4)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_pressure_without_height_above_sea_level(self):
        sensor = Sensor()
        with self.assertRaises(ValueError):
            sensor.get_pressure(height_above_sea_level=None,
                                as_pressure_at_sea_level=True)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_get_humidity(self):
        sensor = Sensor()
        humidity = sensor.get_humidity()
        self.assertLess(abs(humidity - 41.07329061361983), 1e-4)

        sensor = Sensor()
        humidity = sensor.get_humidity(relative=False)
        self.assertLess(abs(humidity - 0.009291279797753835), 1e-4)

    def test_unconfigured_i2c(self):
        with mock.patch('smbus.SMBus', FileNotFoundSMBus):
            with self.assertRaises(I2CException):
                Sensor()


class TestPrintSensor(TestCase):
    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test(self):
        sensor = Sensor()
        self.assertEqual(sensor.chip_id, "fake_chip_id")
        self.assertEqual(sensor.chip_version, "fake_version")

        ref_message = "Temperature:  24.65 C\n" + \
                      "Humidity:     41.07 %\n" + \
                      "Pressure:     969.1 hPa\n"

        with mock.patch("sys.stdout", new_callable=io.StringIO) as fake_out:
            sensor.print_data()
            self.assertEqual(fake_out.getvalue(), ref_message)

    @mock.patch("bme280pi.Sensor._initialize_bus", initialize_fake_bus)
    def test_print_with_absolute_humidity(self):
        sensor = Sensor()
        ref_message = "Temperature:  24.65 C\n" + \
                      "Humidity:     0.009291 kg / m^3\n" + \
                      "Pressure:     969.1 hPa\n"

        with mock.patch("sys.stdout", new_callable=io.StringIO) as fake_out:
            sensor.print_data(relative_humidity=False)
            self.assertEqual(fake_out.getvalue(), ref_message)
