import pytest
import numpy as np

from stylo.interpolate import Driver, Channel
from math import floor
from pytest import raises
from hypothesis import given, assume
from hypothesis.strategies import text, lists, integers,\
                                  composite, floats


# Some useful strategies
names = text(min_size=1, average_size=12)
pveint = integers(min_value=1, max_value=1024)
real = floats(min_value=-1e6, max_value=1e6)


@composite
def data(draw):
    length = draw(pveint)
    start = draw(real)
    stop = draw(real)

    return (0, np.linspace(start, stop, length))


class TestProperties(object):

    @given(c_names=lists(names, average_size=4))
    def test_channels_property(self, c_names):
        assume(len(set(c_names)) == len(c_names))

        driver = Driver()

        for name in c_names:
            driver.add_channel(name)

        # This should simply return a list of the available
        # channels
        assert set(driver.channels) == set(c_names)

        # This attribute should be read only
        with raises(AttributeError) as err:
            driver.channels = 2

        assert 'can\'t set attribute' in str(err.value)

    @given(fps=pveint)
    def test_fps_property_with_good_values(self, fps):

        driver = Driver()

        # By default fps should be 25
        assert driver.FPS == 25

        # We should be able to change this
        driver.FPS = fps
        assert driver.FPS == fps

    @given(fps=pveint)
    def test_fps_property_with_bad_values(self, fps):

        driver = Driver()

        with raises(TypeError) as err:
            driver.FPS = 'a str'

        assert 'must be an integer' in str(err.value)

        with raises(ValueError) as err:
            driver.FPS = -fps

        assert 'must be larger than zero' in str(err.value)

    @pytest.mark.slow
    @given(segments=lists(data(), min_size=1, average_size=4),
           names=lists(names, min_size=1, average_size=4))
    def test_len_property(self, names, segments):

        assume(len(names) == len(set(names)))
        assume(len(segments) == len(names))
        lengths = []

        driver = Driver()

        for name, data in zip(names, segments):
            driver.add_channel(name, segments=[data])
            lengths.append(len(data[1]))

        # When we ask for the length of the driver, it should be
        # be equal to the longest of its component channels
        assert len(driver) == max(lengths)


class TestChannelManagement(object):

    @given(name=names)
    def test_adding_new_channel(self, name):

        driver = Driver()
        driver.add_channel(name, FPS=50, cycle=True)

        # The name should have been added into the internal record
        # of channels
        assert name in driver._channels

        # We should now be able to access the Channel object
        # directly
        assert isinstance(driver.__getattribute__(name), (Channel,))

        # It should also have respected its own __init__ arguments
        assert driver.__getattribute__(name).FPS == 50
        assert driver.__getattribute__(name).cycle

    @given(name=names)
    def test_adding_existing_channel(self, name):

        channel = Channel()
        driver = Driver()

        driver.add_channel(name, channel=channel)
        assert name in driver._channels
        assert isinstance(driver.__getattribute__(name), (Channel,))

        # By default the Channel should be added with a zero offset.
        assert driver._channels[name] == 0

    @given(name=names)
    def test_add_channel_throws_errors(self, name):

        driver = Driver()

        # Name should only be a str
        with raises(TypeError) as err:
            driver.add_channel(1)

        assert 'must be a string' in str(err.value)

        # And the string cannot be empty
        with raises(ValueError) as err:
            driver.add_channel('')

        assert 'cannot be the empty string' in str(err.value)

        driver.add_channel(name)

        # Adding a new channel with an exisiting name should throw an
        # error
        with raises(RuntimeError) as err:
            driver.add_channel(name)

        assert 'already exists' in str(err.value)

        with raises(TypeError) as err:
            driver.add_channel('new name', 3)

        assert 'Expected Channel instance' in str(err.value)

    @given(name=names)
    def test_deleting_channel(self, name):

        driver = Driver()
        driver.add_channel(name)

        assert name in driver._channels
        assert isinstance(driver.__getattribute__(name), (Channel,))

        driver.del_channel(name)

        assert name not in driver._channels

        with raises(AttributeError):
            driver.__getattribute__(name)

    @given(name=names)
    def test_del_channel_throws_errors(self, name):

        driver = Driver()

        with raises(TypeError) as err:
            driver.del_channel(2)

        assert 'by a string' in str(err.value)

        with raises(RuntimeError) as err:
            driver.del_channel(name)

        assert 'does not exist' in str(err.value)


class TestGetItem(object):

    @given(name=names)
    def test_get_channel_by_name(self, name):

        driver = Driver()
        driver.add_channel(name)

        channel = driver[name]

        assert isinstance(channel, (Channel,))

        @given(frame=pveint)
        def test_to_frame_number_with_int(self, frame):

            driver = Driver()
            result = driver._to_frame_number(frame)

            assert (result == frame)

        @given(frame=real, fps=pveint)
        def test_to_frame_number_with_float(self, frame, fps):

            driver = Driver(FPS=fps)
            result = driver._to_frame_number(frame)

            assert (result == floor(frame * fps))
