import logging
import pytest
from unittest.mock import Mock

import numpy as np

from ophyd import (Device, Component, FormattedComponent)
from ophyd.signal import (Signal, AttributeSignal, ArrayAttributeSignal)
from ophyd.utils import ExceptionBundle
from .conftest import AssertTools

logger = logging.getLogger(__name__)


class FakeSignal(Signal):
    def __init__(self, read_pv, *, name=None, parent=None, **kwargs):
        self.read_pv = read_pv
        super().__init__(name=name, parent=parent, **kwargs)

    def get(self):
        return self.name

    def describe_configuration(self):
        return {self.name + '_conf': {'source': 'SIM:test'}}

    def read_configuration(self):
        return {self.name + '_conf': {'value': 0}}


def setUpModule():
    pass


def tearDownModule():
    logger.debug('Cleaning up')


def test_device_state():
    d = Device('test', name='test')

    d.stage()
    old, new = d.configure({})
    assert old == new
    d.unstage()


class TestDevice(AssertTools):
    def test_attrs(self):
        class MyDevice(Device):
            cpt1 = Component(FakeSignal, '1')
            cpt2 = Component(FakeSignal, '2')
            cpt3 = Component(FakeSignal, '3')

        d = MyDevice('prefix', read_attrs=['cpt1'],
                     configuration_attrs=['cpt2'],
                     name='test'
                     )

        d.read()
        self.assertEqual(d.read_attrs, ['cpt1'])
        self.assertEqual(d.configuration_attrs, ['cpt2'])

        self.assertEqual(list(d.read().keys()), [d.cpt1.name])
        self.assertEqual(set(d.read_configuration().keys()),
                         {d.cpt2.name + '_conf'})

        self.assertEqual(list(d.describe().keys()), [d.cpt1.name])
        self.assertEqual(set(d.describe_configuration().keys()),
                         {d.cpt2.name + '_conf', })

    def test_complexdevice(self):
        class SubDevice(Device):
            cpt1 = Component(FakeSignal, '1')
            cpt2 = Component(FakeSignal, '2')
            cpt3 = Component(FakeSignal, '3')

        class SubSubDevice(SubDevice):
            cpt4 = Component(FakeSignal, '4')

        class MyDevice(Device):
            sub1 = Component(SubDevice, '1')
            subsub2 = Component(SubSubDevice, '2')
            cpt3 = Component(FakeSignal, '3')

        device = MyDevice('prefix', name='dev')
        device.configuration_attrs = ['sub1',
                                      'subsub2.cpt2',
                                      'subsub2.cpt4',
                                      'cpt3']
        device.sub1.read_attrs = ['cpt2']
        device.sub1.configuration_attrs = ['cpt1']

        self.assertIs(device.sub1.parent, device)
        self.assertIs(device.subsub2.parent, device)
        self.assertIs(device.cpt3.parent, device)

        self.assertEquals(device.sub1.component_names,
                          ('cpt1', 'cpt2', 'cpt3'))
        self.assertEquals(device.subsub2.component_names,
                          ('cpt1', 'cpt2', 'cpt3', 'cpt4'))

        with pytest.raises(AttributeError):
            device.component_names.remove('sub1')

        conf_keys = {'dev_sub1_cpt1_conf',    # from sub1.*
                     # 'dev_sub1_cpt2_conf',  # not in sub1.config_attrs
                     'dev_subsub2_cpt2_conf', # from subsub2.cpt2
                     'dev_subsub2_cpt4_conf', # from subsub2.cpt4
                     'dev_cpt3_conf',         # from cpt3


                     }

        self.assertEquals(set(device.describe_configuration().keys()),
                          conf_keys)
        self.assertEquals(set(device.read_configuration().keys()),
                          conf_keys)

    def test_complexdevice_stop(self):
        class SubSubDevice(Device):
            cpt4 = Component(FakeSignal, '4')

            def stop(self, *, success=False):
                self.stop_called = True
                self.success = success
                if self.prefix.endswith('_raises_'):
                    raise Exception('stop failed for some reason')

        class SubDevice(Device):
            cpt1 = Component(FakeSignal, '1')
            cpt2 = Component(FakeSignal, '2')
            cpt3 = Component(FakeSignal, '3')
            subsub = Component(SubSubDevice, '')

            def stop(self, *, success=False):
                self.stop_called = True
                self.success = success
                super().stop(success=success)

        class MyDevice(Device):
            sub1 = Component(SubDevice, '1')
            sub2 = Component(SubDevice, '_raises_')
            sub3 = Component(SubDevice, '_raises_')
            cpt3 = Component(FakeSignal, '3')

        dev = MyDevice('', name='mydev')
        with pytest.raises(ExceptionBundle) as cm:
            dev.stop()

        ex = cm.value
        self.assertEquals(len(ex.exceptions), 2)
        self.assertTrue(dev.sub1.stop_called)
        self.assertTrue(dev.sub2.stop_called)
        self.assertTrue(dev.sub3.stop_called)
        self.assertFalse(dev.sub1.success)
        self.assertFalse(dev.sub2.success)
        self.assertFalse(dev.sub3.success)

        self.assertTrue(dev.sub1.subsub.stop_called)
        self.assertTrue(dev.sub2.subsub.stop_called)
        self.assertTrue(dev.sub3.subsub.stop_called)
        self.assertFalse(dev.sub1.subsub.success)
        self.assertFalse(dev.sub2.subsub.success)
        self.assertFalse(dev.sub3.subsub.success)

        dev = MyDevice('', name='mydev')
        with pytest.raises(ExceptionBundle) as cm:
            dev.stop(success=True)

        ex = cm.value
        self.assertEquals(len(ex.exceptions), 2)
        self.assertTrue(dev.sub1.stop_called)
        self.assertTrue(dev.sub2.stop_called)
        self.assertTrue(dev.sub3.stop_called)
        self.assertTrue(dev.sub1.success)
        self.assertTrue(dev.sub2.success)
        self.assertTrue(dev.sub3.success)

        self.assertTrue(dev.sub1.subsub.stop_called)
        self.assertTrue(dev.sub2.subsub.stop_called)
        self.assertTrue(dev.sub3.subsub.stop_called)
        self.assertTrue(dev.sub1.subsub.success)
        self.assertTrue(dev.sub2.subsub.success)
        self.assertTrue(dev.sub3.subsub.success)

    def test_name_shadowing(self):
        RESERVED_ATTRS = ['name', 'parent', 'component_names', '_signals',
                          '_sig_attrs',
                          '_sub_devices']

        type('a', (Device,), {'a': None})  # legal class definition
        # Illegal class definitions:
        for attr in RESERVED_ATTRS:
            self.assertRaises(TypeError, type, 'a', (Device,), {attr: None})

    def test_formatted_component(self):
        FC = FormattedComponent

        class MyDevice(Device):
            cpt = Component(FakeSignal, 'suffix')
            ch = FC(FakeSignal, '{self.prefix}{self._ch}')

            def __init__(self, prefix, ch='a', **kwargs):
                self._ch = ch
                super().__init__(prefix, **kwargs)

        ch_value = '_test_'

        device = MyDevice('prefix:', ch=ch_value, name='test')
        self.assertIs(device.cpt.parent, device)
        self.assertIs(device.ch.parent, device)
        self.assertIs(device._ch, ch_value)
        self.assertEquals(device.ch.read_pv, device.prefix + ch_value)
        self.assertEquals(device.cpt.read_pv,
                          device.prefix + MyDevice.cpt.suffix)

    def test_root(self):
        class MyDevice(Device):
            cpt = Component(FakeSignal, 'suffix')

        d = MyDevice('', name='test')
        assert d.cpt.root == d
        assert d.root == d

    def test_hidden_component(self):
        class MyDevice(Device):
            _hidden_sig = Component(FakeSignal, 'suffix')
        d = MyDevice('', name='test')
        assert '_hidden_sig' in d.component_names
        assert not hasattr(d.get(), '_hidden_sig')


def test_attribute_signal():
    init_value = 33

    class SubDevice(Device):
        @property
        def prop(self):
            return init_value

        @prop.setter
        def prop(self, value):
            pass

    class MyDevice(Device):
        sub1 = Component(SubDevice, '1')
        attrsig = Component(AttributeSignal, 'prop')
        sub_attrsig = Component(AttributeSignal, 'sub1.prop')

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._value = init_value

        @property
        def prop(self):
            return self._value

        @prop.setter
        def prop(self, value):
            self._value = value

    dev = MyDevice('', name='mydev')
    assert (dev.describe()['mydev_attrsig']['source'] == 'PY:mydev.prop')
    assert (dev.describe()['mydev_sub_attrsig']['source'] == 'PY:mydev.sub1.prop')
    assert dev.attrsig.get() == init_value
    cb = Mock()
    dev.attrsig.subscribe(cb)
    dev.attrsig.put(55)
    assert dev.attrsig.get() == 55
    assert cb.called
    assert dev.sub_attrsig.get() == init_value
    dev.sub_attrsig.put(0)


def test_attribute_signal_attributeerror():
    class MyDevice(Device):
        sig1 = Component(AttributeSignal, 'unknownattribute')
        sig2 = Component(AttributeSignal, '__class__.__name')
        sig3 = Component(AttributeSignal, 'unknown.attribute')

    dev = MyDevice('', name='mydev')
    pytest.raises(AttributeError, dev.sig1.get)
    pytest.raises(AttributeError, dev.sig2.get)
    pytest.raises(AttributeError, dev.sig3.get)
    pytest.raises(AttributeError, getattr, dev.sig3, 'base')


def test_shadowing_bs_interface_raises_typeerror():
    class LegalDevice(Device):
        cpt = Component(FakeSignal, 'cpt')

    with pytest.raises(TypeError):
        class IllegalDevice(Device):
            # 'read' shadows the bluesky interface and should not be allowed
            read = Component(FakeSignal, 'read')


def test_array_attribute_signal():
    init_value = range(10)

    class MyDevice(Device):
        attrsig = Component(ArrayAttributeSignal, 'prop')

        @property
        def prop(self):
            return init_value

    dev = MyDevice('', name='mydev')
    np.testing.assert_array_equal(dev.attrsig.get(), init_value)
    assert isinstance(dev.attrsig.get(), np.ndarray)
    assert isinstance(dev.attrsig.get(), np.ndarray)


def test_signal_names():

    class MyDevice(Device):
        cpt = Component(FakeSignal, 'suffix')

    d = MyDevice('', name='test')
    with pytest.warns(UserWarning):
        signal_names = d.signal_names

    assert signal_names is d.component_names


def test_summary():
    class MyDevice(Device):
        cpt = Component(FakeSignal, 'suffix')

    d = MyDevice('', name='test')
    # smoke tests
    d.summary()
    d.__str__()
    d.__repr__()


def test_labels():
    class MyDevice(Device):
        cpt = Component(FakeSignal, 'suffix')

    d = MyDevice('', name='test', labels={'a', 'b'})
    assert d._ophyd_labels_ == {'a', 'b'}


def test_device_put():
    class MyDevice(Device):
        a = Component(Signal)
        b = Component(Signal)

    d = MyDevice('', name='test')
    d.put((1, 2))
    assert d.get() == (1, 2)

    devtuple = d.get_device_tuple()
    d.put(devtuple(a=10, b=12))
    assert d.get() == (10, 12)

    with pytest.raises(ValueError):
        d.put((1, 2, 3))
