"""Test the AsyncParameterTree classes.

This module implements unit test cases for the AsyncParameterAccessor and AsyncParameterTree
classes.

Tim Nicholls, STFC Detector Systems Software Group.
"""

import asyncio
import math

from copy import deepcopy

import pytest

from tests.async_utils import AwaitableTestFixture, asyncio_fixture_decorator
from odin_control.adapters.async_parameter_tree import (
    AsyncParameterAccessor, AsyncParameterTree, ParameterTreeError
)


class AsyncParameterAccessorTestFixture(AwaitableTestFixture):
    """Test fixture of AsyncParameterAccessor test cases."""
    def __init__(self):

        super(AsyncParameterAccessorTestFixture, self).__init__(AsyncParameterAccessor)

        self.static_ro_path = 'static_ro'
        self.static_ro_value = 2.76923
        self.static_ro_accessor = AsyncParameterAccessor(
            self.static_ro_path + '/', self.static_ro_value
        )

        self.sync_ro_value = 1234
        self.sync_ro_path = 'sync_ro'
        self.sync_ro_accessor = AsyncParameterAccessor(
            self.sync_ro_path + '/', self.sync_ro_get
        )

        self.sync_rw_value = 'foo'
        self.sync_rw_path = 'sync_rw'
        self.sync_rw_accessor = AsyncParameterAccessor(
            self.sync_rw_path + '/', self.sync_rw_get, self.sync_rw_set
        )

        self.async_ro_value = 5593
        self.async_ro_path = 'async_ro'
        self.async_ro_accessor = AsyncParameterAccessor(
            self.async_ro_path + '/', self.async_ro_get
        )

        self.async_rw_value = math.pi
        self.async_rw_path = 'async_rw'
        self.async_rw_accessor = AsyncParameterAccessor(
            self.async_rw_path + '/', self.async_rw_get, self.async_rw_set
        )

        self.md_param_path ='mdparam'
        self.md_param_value = 456
        self.md_param_metadata = {
            'min' : 100,
            'max' : 1000,
            "allowed_values": [100, 123, 456, 789, 1000],
            "name": "Test Parameter",
            "description": "This is a test parameter",
            "units": "furlongs/fortnight",
            "display_precision": 0,
        }
        self.md_accessor = AsyncParameterAccessor(
            self.md_param_path + '/', self.async_md_get, self.async_md_set, **self.md_param_metadata
        )

        self.md_minmax_path = 'minmaxparam'
        self.md_minmax_value = 500
        self.md_minmax_metadata = {
            'min': 100,
            'max': 1000
        }
        self.md_minmax_accessor = AsyncParameterAccessor(
            self.md_minmax_path + '/', self.async_md_minmax_get, self.async_md_minmax_set,
            **self.md_minmax_metadata
        )

    def sync_ro_get(self):
        return self.sync_ro_value

    def sync_rw_get(self):
        return self.sync_rw_value

    def sync_rw_set(self, value):
        self.sync_rw_value = value

    async def async_ro_get(self):
        await asyncio.sleep(0)
        return self.async_ro_value

    async def async_rw_get(self):
        await asyncio.sleep(0)
        return self.async_rw_value

    async def async_rw_set(self, value):
        await asyncio.sleep(0)
        self.async_rw_value = value

    async def async_md_get(self):
        await asyncio.sleep(0)
        return self.md_param_value

    async def async_md_set(self, value):
        await asyncio.sleep(0)
        self.async_md_param_value = value

    async def async_md_minmax_get(self):
        await asyncio.sleep(0)
        return self.md_minmax_value

    async def async_md_minmax_set(self, value):
        await asyncio.sleep(0)
        self.md_minmax_value = value

@pytest.fixture(scope="class")
def event_loop():
    """Redefine the pytest.asyncio event loop fixture to have class scope."""
    loop = asyncio.get_event_loop()
    yield loop
    loop.close()

@asyncio_fixture_decorator(scope="class")
async def test_param_accessor():
    """Test fixture used in testing ParameterAccessor behaviour."""
    test_param_accessor = await AsyncParameterAccessorTestFixture()
    yield test_param_accessor

@pytest.mark.asyncio
class TestAsyncParameterAccessor():
    """Class to test AsyncParameterAccessor behaviour"""

    async def test_static_ro_accessor_get(self, test_param_accessor):
        """Test that a static RO accessor get call returns the correct value."""
        value = await test_param_accessor.static_ro_accessor.get()
        assert value == test_param_accessor.static_ro_value

    async def test_static_ro_accessor_set(self, test_param_accessor):
        """Test that a static RO accessor set call raises an error."""
        new_val = 1.234
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.static_ro_accessor.set(new_val)

        assert "Parameter {} is read-only".format(test_param_accessor.static_ro_path) \
            in str(excinfo.value)

    async def test_sync_ro_accessor_get(self, test_param_accessor):
        """Test that a synchronous callable RO accessor get call returns the correct value."""
        value = await test_param_accessor.sync_ro_accessor.get()
        assert value == test_param_accessor.sync_ro_value

    async def test_sync_ro_accessor_set(self, test_param_accessor):
        """Test that a synchronous callable RO accessor set call raises an error."""
        new_val = 91265
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.sync_ro_accessor.set(new_val)

        assert "Parameter {} is read-only".format(test_param_accessor.sync_ro_path) \
            in str(excinfo.value)

    async def test_sync_rw_accessor_get(self, test_param_accessor):
        """Test that a synchronous callable RW accessor returns the correct value."""
        value = await test_param_accessor.sync_rw_accessor.get()
        assert value == test_param_accessor.sync_rw_value

    async def test_sync_rw_accessor_set(self, test_param_accessor):
        """Test that a synchronous callable RW accessor set call sets the correct value."""
        old_val = test_param_accessor.sync_rw_value
        new_val = 'bar'
        await test_param_accessor.sync_rw_accessor.set(new_val)
        value = await test_param_accessor.sync_rw_accessor.get()
        assert value == new_val

        await test_param_accessor.sync_rw_accessor.set(old_val)

    async def test_async_ro_accessor_get(self, test_param_accessor):
        """Test that an asynchronous callable RO accessor get call returns the correct value."""
        value = await test_param_accessor.async_ro_accessor.get()
        assert value == test_param_accessor.async_ro_value

    async def test_async_ro_accessor_set(self, test_param_accessor):
        """Test that an asynchronous callable RO accessor set call raises an error."""
        new_val = 91265
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.async_ro_accessor.set(new_val)

        assert "Parameter {} is read-only".format(test_param_accessor.async_ro_path) \
            in str(excinfo.value)

    async def test_async_rw_accessor_get(self, test_param_accessor):
        """Test that an asynchronous callable RW accessor get returns the correct value."""
        value = await test_param_accessor.async_rw_accessor.get()
        assert value == test_param_accessor.async_rw_value

    async def test_async_rw_accessor_set(self, test_param_accessor):
        """Test that an asynchronous callable RW accessor sets the correct value."""
        old_val = test_param_accessor.async_rw_value
        new_val = old_val * 2
        await test_param_accessor.async_rw_accessor.set(new_val)
        value = await test_param_accessor.async_rw_accessor.get()
        assert value == new_val

    async def test_static_ro_accessor_default_metadata(self, test_param_accessor):
        """Test that a static RW accessor has the appropriate default metadata."""
        param = await test_param_accessor.static_ro_accessor.get(with_metadata=True)
        assert(isinstance(param, dict))
        assert param['value'] == test_param_accessor.static_ro_value
        assert param['type'] == type(test_param_accessor.static_ro_value).__name__
        assert not param['writeable']

    async def test_sync_ro_accessor_default_metadata(self, test_param_accessor):
        """Test that a synchronous callable RO accesor has the appropriate default metadata."""
        param = await test_param_accessor.sync_ro_accessor.get(with_metadata=True)
        assert param['value'] == test_param_accessor.sync_ro_value
        assert param['type'] == type(test_param_accessor.sync_ro_value).__name__
        assert not param['writeable']

    async def test_sync_rw_accessor_default_metadata(self, test_param_accessor):
        """Test that a synchronous callable RW accesor has the appropriate default metadata."""
        param = await test_param_accessor.sync_rw_accessor.get(with_metadata=True)
        assert param['value'] == test_param_accessor.sync_rw_value
        assert param['type'] == type(test_param_accessor.sync_rw_value).__name__
        assert param['writeable']

    async def test_sync_ro_accessor_default_metadata(self, test_param_accessor):
        """Test that a synchronous callable RO accesor has the appropriate default metadata."""
        param = await test_param_accessor.sync_ro_accessor.get(with_metadata=True)
        assert param['value'] == test_param_accessor.sync_ro_value
        assert param['type'] == type(test_param_accessor.sync_ro_value).__name__
        assert param['writeable'] == False

    async def test_async_rw_accessor_default_metadata(self, test_param_accessor):
        """Test that an asynchronous callable RW accesor has the appropriate default metadata."""
        param = await test_param_accessor.async_rw_accessor.get(with_metadata=True)
        assert param['value'] == test_param_accessor.async_rw_value
        assert param['type'] == type(test_param_accessor.async_rw_value).__name__
        assert param['writeable'] == True

    async def test_async_ro_accessor_default_metadata(self, test_param_accessor):
        """Test that an asynchronous callable RO accesor has the appropriate default metadata."""
        param = await test_param_accessor.async_ro_accessor.get(with_metadata=True)
        assert param['value'] == test_param_accessor.async_ro_value
        assert param['type'] == type(test_param_accessor.async_ro_value).__name__
        assert param['writeable'] == False

    async def test_metadata_param_accessor_metadata(self, test_param_accessor):
        """Test that a parameter accessor has the correct metadata fields."""
        param = await test_param_accessor.md_accessor.get(with_metadata=True)
        for md_field in test_param_accessor.md_param_metadata:
            assert md_field in param
            assert param[md_field] == test_param_accessor.md_param_metadata[md_field]
        assert param['value'] == test_param_accessor.md_param_value
        assert param['type'] == type(test_param_accessor.md_param_value).__name__
        assert param['writeable'] == True

    async def test_param_accessor_bad_metadata_arg(self, test_param_accessor):
        """Test that a parameter accessor with a bad metadata argument raises an error."""
        bad_metadata_argument = 'foo'
        bad_metadata = {bad_metadata_argument: 'bar'}
        with pytest.raises(ParameterTreeError) as excinfo:
            _ = await AsyncParameterAccessor(
                test_param_accessor.static_ro_path + '/',
                test_param_accessor.static_ro_value, **bad_metadata
            )

        assert "Invalid metadata argument: {}".format(bad_metadata_argument) \
            in str(excinfo.value)

    async def test_param_accessor_set_type_mismatch(self, test_param_accessor):
        """
        Test that setting the value of a parameter accessor with the incorrected type raises
        an error.
        """
        bad_value = 'bar'
        bad_value_type = type(bad_value).__name__

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.async_rw_accessor.set(bad_value)

        assert "Type mismatch setting {}: got {} expected {}".format(
                test_param_accessor.async_rw_path, bad_value_type,
                type(test_param_accessor.async_rw_value).__name__
            ) in str(excinfo.value)

    async def test_param_accessor_bad_allowed_value(self, test_param_accessor):
        """
        Test the setting the value of a parameter accessor to a disallowed value raises an error.
        """
        bad_value = 222
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.md_accessor.set(bad_value)

        assert "{} is not an allowed value for {}".format(
                bad_value, test_param_accessor.md_param_path
            ) in str(excinfo.value)

    async def test_param_accessor_value_below_min(self, test_param_accessor):
        """
        Test that setting the value of a parameter accessor below the minimum allowed raises an
        error.
        """
        bad_value = 1
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.md_minmax_accessor.set(bad_value)

        assert "{} is below the minimum value {} for {}".format(
                bad_value, test_param_accessor.md_minmax_metadata['min'], 
                test_param_accessor.md_minmax_path
            ) in str(excinfo.value)

    async def test_param_accessor_value_above_max(self, test_param_accessor):
        """
        Test that setting the value of a parameter accessor above the maximum allowed raises an
        error.
        """
        bad_value = 100000
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_accessor.md_minmax_accessor.set(bad_value)

        assert "{} is above the maximum value {} for {}".format(
                bad_value, test_param_accessor.md_minmax_metadata['max'], 
                test_param_accessor.md_minmax_path
            ) in str(excinfo.value)


class AsyncParameterTreeTestFixture(AwaitableTestFixture):
    """Container class for use in fixtures testing AsyncParameterTree."""

    def __init__(self):

        super(AsyncParameterTreeTestFixture, self).__init__(AsyncParameterTree)

        self.int_value = 1234
        self.float_value = 3.1415
        self.bool_value = True
        self.str_value = 'theString'
        self.list_values = list(range(4))

        self.simple_dict = {
            'intParam': self.int_value,
            'floatParam': self.float_value,
            'boolParam': self.bool_value,
            'strParam':  self.str_value,
        }

        self.accessor_params = {
            'one': 1,
            'two': 2,
            'pi': 3.14
        }
        self.simple_tree = AsyncParameterTree(self.simple_dict)

        # Set up nested dict of parameters for a more complex tree
        self.nested_dict = self.simple_dict.copy()
        self.nested_dict['branch'] = {
            'branchIntParam': 4567,
            'branchStrParam': 'theBranch',
        }
        self.nested_tree = AsyncParameterTree(self.nested_dict)

        self.complex_tree = AsyncParameterTree({
            'intParam': self.int_value,
            'callableRoParam': (lambda: self.int_value, None),
            'callableAccessorParam': (self.get_accessor_param, None),
            'listParam': self.list_values,
            'branch': AsyncParameterTree(deepcopy(self.nested_dict)),
        })

        self.list_tree = AsyncParameterTree({
            'main' : [
                self.simple_dict.copy(),
                list(self.list_values)
                ]
        })

        self.simple_list_tree = AsyncParameterTree({
            'list_param': [10, 11, 12, 13]
        })

    async def async_ro_get(self):
        await asyncio.sleep(0)
        return self.async_ro_value

    async def nested_async_ro_get(self):
        await asyncio.sleep(0)
        return self.nested_async_ro_value

    async def get_accessor_param(self):
        await asyncio.sleep(0)
        return self.accessor_params


@asyncio_fixture_decorator(scope="class")
async def test_param_tree():
    """Test fixture used in testing AsyncParameterTree behaviour."""
    test_param_accessor = await AsyncParameterTreeTestFixture()
    yield test_param_accessor


@pytest.mark.asyncio
class TestAsyncParameterTree():

    async def test_simple_tree_returns_dict(self, test_param_tree):
        """Test the get on a simple tree returns a dict."""
        dt_vals = await test_param_tree.simple_tree.get('')
        assert dt_vals, test_param_tree.simple_dict
        assert True

    async def test_simple_tree_single_values(self, test_param_tree):
        """Test that getting single values from a simple tree returns the correct values."""
        dt_int_val = await test_param_tree.simple_tree.get('intParam')
        assert dt_int_val['value'] == test_param_tree.int_value

        dt_float_val = await test_param_tree.simple_tree.get('floatParam')
        assert dt_float_val['value'] == test_param_tree.float_value

        dt_bool_val = await test_param_tree.simple_tree.get('boolParam')
        assert dt_bool_val['value'] == test_param_tree.bool_value

        dt_str_val = await test_param_tree.simple_tree.get('strParam')
        assert dt_str_val['value'] == test_param_tree.str_value

    async def test_simple_tree_missing_value(self, test_param_tree):
        """Test that getting a missing value from a simple tree raises an error."""
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.simple_tree.get('missing')

        assert 'Invalid path: missing' in str(excinfo.value)

    async def test_nested_tree_returns_nested_dict(self, test_param_tree):
        """Test that getting a nested tree return a dict."""
        nested_dt_vals = await test_param_tree.nested_tree.get('')
        assert nested_dt_vals == test_param_tree.nested_dict

    async def test_nested_tree_branch_returns_dict(self, test_param_tree):
        """Test that getting a tree from within a nested tree returns a dict."""
        branch_vals = await test_param_tree.nested_tree.get('branch')
        assert branch_vals == test_param_tree.nested_dict['branch']

    async def test_nested_tree_trailing_slash(self, test_param_tree):
        """Test that getting a tree with trailing slash returns the correct dict."""
        branch_vals = await test_param_tree.nested_tree.get('branch/')
        assert branch_vals == test_param_tree.nested_dict['branch']

    async def test_set_with_extra_branch_paths(self, test_param_tree):
        """
        Test that modifiying a branch in a tree with extra parameters raises an error.
        """
        branch_data = deepcopy(test_param_tree.nested_dict['branch'])
        branch_data['extraParam'] = 'oops'

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.complex_tree.set('branch', branch_data)

        assert 'Invalid path' in str(excinfo.value)

    async def test_complex_tree_calls_leaf_nodes(self, test_param_tree):
        """
        Test that accessing valyus in a complex tree returns the correct values for
        static and callable parameters.
        """
        complex_vals = await test_param_tree.complex_tree.get('')
        assert complex_vals['intParam'] == test_param_tree.int_value
        assert complex_vals['callableRoParam'] == test_param_tree.int_value

    async def test_complex_tree_access_list_param(self, test_param_tree):
        """Test that getting a list parameter from a complex tree returns the appropriate values."""
        list_param_vals = await test_param_tree.complex_tree.get('listParam')
        assert list_param_vals['value'] == test_param_tree.list_values

    async def test_complex_tree_callable_readonly(self, test_param_tree):
        """
        Test that attempting to set the value of a RO callable parameter in a tree raises an
        error.
        """
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.complex_tree.set('callableRoParam', 1234)

        assert 'Parameter callableRoParam is read-only' in str(excinfo.value)

    async def test_complex_tree_set_invalid_path(self, test_param_tree):
        """
        Test that attempting to set the value of an element in a complex tree on a path
        that does not exist raises an error.
        """
        invalid_path = 'invalidPath/toNothing'

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.complex_tree.set(invalid_path, 0)

        assert 'Invalid path: {}'.format(invalid_path) in str(excinfo.value)

    async def test_complex_tree_set_top_level(self, test_param_tree):
        """Test that setting the top level of a complex tree correctly sets all values."""
        complex_vals = await test_param_tree.complex_tree.get('')
        complex_vals_copy = deepcopy(complex_vals)
        del complex_vals_copy['callableRoParam']
        del complex_vals_copy['callableAccessorParam']

        await test_param_tree.complex_tree.set('', complex_vals_copy)
        complex_vals2 = await test_param_tree.complex_tree.get('')
        assert complex_vals == complex_vals2

    async def test_complex_tree_inject_spurious_dict(self, test_param_tree):
        """
        Test that attempting to attempt a dictionary into the position of a non-dict parameter
        raises in error.
        """
        param_data = {'intParam': 9876}

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.complex_tree.set('intParam', param_data)

        assert 'Type mismatch updating intParam' in str(excinfo.value)

    async def test_list_tree_get_indexed(self, test_param_tree):
        """
        Test that it is possible to get a value by index from a list parameter.
        """
        ret = await test_param_tree.list_tree.get("main/1")
        assert ret['value'] == test_param_tree.list_values

    async def test_list_tree_set_indexed(self, test_param_tree):
        """
        Test that it is possible to set a value by index on a list parameter.
        """
        await test_param_tree.list_tree.set("main/1/2", 7)
        assert await test_param_tree.list_tree.get("main/1/2") == {'value': 7}

    async def test_list_tree_set_from_root(self, test_param_tree):
        """Test that it is possible to set a list tree from its root."""
        tree_data = {
            'main' : [
                {
                    'intParam': 0,
                    'floatParam': 0.00,
                    'boolParam': False,
                    'strParam':  "test",
                },
                [1,2,3,4]
            ]
        }

        await test_param_tree.list_tree.set("",tree_data)
        result = await test_param_tree.list_tree.get("main")
        assert result['value'] == tree_data['main']

    async def test_list_tree_from_dict(self, test_param_tree):
        """Test that a list tree can be set with a dict of index/values."""
        new_list_param = {0: 0, 1: 1, 2: 2, 3: 3}
        await test_param_tree.simple_list_tree.set('list_param', new_list_param)
        result = await test_param_tree.simple_list_tree.get('list_param')
        assert result['value']== list(new_list_param.values())
            

    async def test_list_tree_from_dict_bad_index(self, test_param_tree):
        """
        Test that setting a list tree from a dict with an index outside the current range
        raises an error.
        """
        new_list_param = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_param_tree.simple_list_tree.set('list_param', new_list_param)

        assert "Invalid path: list_param/4 list index out of range" in str(excinfo.value)

    async def test_bad_tuple_node_raises_error(self, test_param_tree):
        """Test that constructing a parameter tree with an immutable tuple raises an error."""
        bad_node = 'bad'
        bad_data = tuple(range(4))
        bad_tree = {
            bad_node: bad_data
        }
        with pytest.raises(ParameterTreeError) as excinfo:
            tree = AsyncParameterTree(bad_tree)

        assert "not a valid leaf node" in str(excinfo.value)


class AsyncRwParameterTreeTestFixture(AwaitableTestFixture):
    """Container class for use in async read-write parameter tree test fixtures."""

    def __init__(self):

        super(AsyncRwParameterTreeTestFixture, self).__init__(AsyncParameterTree)

        self.int_rw_param = 4576
        self.int_ro_param = 255374
        self.int_rw_value = 9876
        self.int_wo_param = 0

        self.rw_value_set_called = False

        self.nested_rw_param = 53.752
        self.nested_ro_value = 9.8765

        nested_tree = AsyncParameterTree({
            'nestedRwParam': (self.nestedRwParamGet, self.nestedRwParamSet),
            'nestedRoParam': self.nested_ro_value
        })

        self.rw_callable_tree = AsyncParameterTree({
            'intCallableRwParam': (self.intCallableRwParamGet, self.intCallableRwParamSet),
            'intCallableRoParam': (self.intCallableRoParamGet, None),
            'intCallableWoParam': (None, self.intCallableWoParamSet),
            'intCallableRwValue': (self.int_rw_value, self.intCallableRwValueSet),
            'branch': nested_tree
        })

    async def intCallableRwParamSet(self, value):
        await asyncio.sleep(0)
        self.int_rw_param = value

    async def intCallableRwParamGet(self):
        await asyncio.sleep(0)
        return self.int_rw_param

    async def intCallableRoParamGet(self):
        await asyncio.sleep(0)
        return self.int_ro_param

    async def intCallableWoParamSet(self, value):
        await asyncio.sleep(0)
        self.int_wo_param = value

    async def intCallableRwValueSet(self, value):
        await asyncio.sleep(0)
        self.rw_value_set_called = True

    async def nestedRwParamSet(self, value):
        await asyncio.sleep(0)
        self.nested_rw_param = value

    async def nestedRwParamGet(self):
        await asyncio.sleep(0)
        return self.nested_rw_param


@asyncio_fixture_decorator(scope="class")
async def test_rw_tree():
    """Test fixture for use in testing read-write parameter trees."""
    test_rw_tree = await AsyncRwParameterTreeTestFixture()
    yield test_rw_tree


@pytest.mark.asyncio
class TestAsyncRwParameterTree():
    """Class to test behaviour of async read-write parameter trees."""

    async def test_rw_tree_simple_get_values(self, test_rw_tree):
        """Test getting simple values from a RW tree returns the correct values."""
        dt_rw_int_param = await test_rw_tree.rw_callable_tree.get('intCallableRwParam')
        assert dt_rw_int_param['value'] == test_rw_tree.int_rw_param

        dt_ro_int_param = await test_rw_tree.rw_callable_tree.get('intCallableRoParam')
        assert dt_ro_int_param['value'] == test_rw_tree.int_ro_param

        dt_rw_int_value = await test_rw_tree.rw_callable_tree.get('intCallableRwValue')
        assert dt_rw_int_value['value'] == test_rw_tree.int_rw_value

    async def test_rw_tree_simple_set_value(self, test_rw_tree):
        """Test that setting a value in a RW tree updates and returns the correct value."""
        new_int_value = 91210
        await test_rw_tree.rw_callable_tree.set('intCallableRwParam', new_int_value)

        dt_rw_int_param = await test_rw_tree.rw_callable_tree.get('intCallableRwParam')
        assert dt_rw_int_param['value'] == new_int_value

    async def test_rw_tree_set_ro_param(self, test_rw_tree):
        """Test that attempting to set a RO parameter raises an error."""
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_rw_tree.rw_callable_tree.set('intCallableRoParam', 0)

        assert 'Parameter intCallableRoParam is read-only' in str(excinfo.value)

    async def test_rw_callable_tree_set_wo_param(self, test_rw_tree):
        """Test that setting a write-only parameter (!!) sets the correct value."""
        new_value = 1234
        await test_rw_tree.rw_callable_tree.set('intCallableWoParam', new_value)
        assert test_rw_tree.int_wo_param == new_value

    async def test_rw_callable_tree_set_rw_value(self, test_rw_tree):
        """Test that setting a callable RW value calls the appropriate set method."""
        new_value = 1234
        await test_rw_tree.rw_callable_tree.set('intCallableRwValue', new_value)
        assert test_rw_tree.rw_value_set_called

    async def test_rw_callable_nested_param_get(self, test_rw_tree):
        """Test the getting a nested callable RW parameter returns the correct value."""
        dt_nested_param = await test_rw_tree.rw_callable_tree.get('branch/nestedRwParam')
        assert dt_nested_param['value'] == test_rw_tree.nested_rw_param

    async def test_rw_callable_nested_param_set(self, test_rw_tree):
        """Test that setting a nested callable RW parameter sets the correct value."""
        new_float_value = test_rw_tree.nested_rw_param + 2.3456
        await test_rw_tree.rw_callable_tree.set('branch/nestedRwParam', new_float_value)
        assert test_rw_tree.nested_rw_param == new_float_value

    async def test_rw_callable_nested_tree_set(self, test_rw_tree):
        """Test the setting a value within a callable nested tree updated the value correctly."""
        nested_branch = await test_rw_tree.rw_callable_tree.get('branch')
        new_rw_param_val = 45.876
        nested_branch['nestedRwParam'] = new_rw_param_val
        await test_rw_tree.rw_callable_tree.set('branch', nested_branch)
        result = await test_rw_tree.rw_callable_tree.get('branch')
        assert result['nestedRwParam'], new_rw_param_val

    async def test_rw_callable_nested_tree_set_trailing_slash(self, test_rw_tree):
        """
        Test that setting a callable nested tree with a trailing slash in the path
        sets the value correctly.
        """
        nested_branch = await test_rw_tree.rw_callable_tree.get('branch/')
        new_rw_param_val = 24.601
        nested_branch['nestedRwParam'] = new_rw_param_val
        await test_rw_tree.rw_callable_tree.set('branch/', nested_branch)
        result = await test_rw_tree.rw_callable_tree.get('branch/')
        assert result['nestedRwParam'] == new_rw_param_val


class AsyncParameterTreeMetadataTestFixture(AwaitableTestFixture):
    """Container class for use in test fixtures testing parameter tree metadata."""

    def __init__(self):

        super(AsyncParameterTreeMetadataTestFixture, self).__init__(AsyncParameterTree)

        self.int_rw_param = 100
        self.float_ro_param = 4.6593
        self.int_ro_param = 1000
        self.int_enum_param = 0
        self.int_enum_param_allowed_values = [0, 1, 2, 3, 5, 8, 13]
        self.min_no_max_param = 1

        self.int_rw_param_metadata = {
            "min": 0,
            "max": 1000,
            "units": "arbitrary",
            "name": "intCallableRwParam",
            "description": "A callable integer RW parameter"
        }

        self.metadata_tree_dict = {
            'name': 'Metadata Tree',
            'description': 'A paramter tree to test metadata',
            'floatRoParam': (self.floatRoParamGet,),
            'intRoParam': (self.intRoParamGet, {"units": "seconds"}),
            'intCallableRwParam': (
                self.intCallableRwParamGet, self.intCallableRwParamSet, self.int_rw_param_metadata
            ),
            'intEnumParam': (
                self.intEnumParamGet, self.intEnumParamSet,
                {"allowed_values": self.int_enum_param_allowed_values}
            ),
            'valueParam': (24601,),
            'minNoMaxParam': (self.minNoMaxParamGet, self.minNoMaxParamSet,{'min': 0})
        }
        self.metadata_tree = AsyncParameterTree(self.metadata_tree_dict)

    def intCallableRwParamSet(self, value):
        self.int_rw_param = value

    def intCallableRwParamGet(self):
        return self.int_rw_param

    def floatRoParamGet(self):
        return self.float_ro_param

    def intRoParamGet(self):
        return self.int_ro_param

    async def intEnumParamGet(self):
        await asyncio.sleep(0)
        return self.int_enum_param

    async def intEnumParamSet(self, value):
        await asyncio.sleep(0)
        self.int_enum_param = value

    async def minNoMaxParamGet(self):
        await asyncio.sleep(0)
        return self.min_no_max_param

    async def minNoMaxParamSet(self, value):
        await asyncio.sleep(0)
        self.min_no_max_param = value


@asyncio_fixture_decorator(scope="class")
async def test_tree_metadata():
    """Test fixture for use in testing parameter tree metadata."""
    test_tree_metadata = await AsyncParameterTreeMetadataTestFixture()
    yield test_tree_metadata

@pytest.mark.asyncio
class TestAsyncParameterTreeMetadata():

    async def test_callable_rw_param_metadata(self, test_tree_metadata):
        """Test that a getting RW parameter with metadata returns the appropriate metadata."""
        int_param_with_metadata = await test_tree_metadata.metadata_tree.get(
            "intCallableRwParam",with_metadata=True)
        result = await test_tree_metadata.metadata_tree.get("intCallableRwParam")
        int_param =  result["value"]

        expected_metadata = test_tree_metadata.int_rw_param_metadata
        expected_metadata["value"] = int_param
        expected_metadata["type"] = 'int'
        expected_metadata["writeable"] = True

        assert await int_param_with_metadata == expected_metadata

    async def test_enum_param_allowed_values(self, test_tree_metadata):
        """Test that setting an enumerated parameter with an allowed value succeeds."""
        for value in test_tree_metadata.int_enum_param_allowed_values:
            await test_tree_metadata.metadata_tree.set("intEnumParam", value)
            result = await test_tree_metadata.metadata_tree.get("intEnumParam")
            set_value = result["value"]
            assert value == set_value

    async def test_enum_param_bad_value(self, test_tree_metadata):
        """
        Test that attempting to set a disallowed value for an enumerated parameter raises an error.
        """
        bad_value = test_tree_metadata.int_enum_param_allowed_values[-1] + 1
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_metadata.metadata_tree.set("intEnumParam", bad_value)

        assert "{} is not an allowed value".format(bad_value) in str(excinfo.value)

    async def test_ro_param_has_writeable_metadata_field(self, test_tree_metadata):
        """Test that a RO parameter has the writeable metadata field set to false."""
        ro_param = await test_tree_metadata.metadata_tree.get("floatRoParam", with_metadata=True)
        result = await ro_param
        assert not result["writeable"]

    async def test_ro_param_not_writeable(self, test_tree_metadata):
        """Test that attempting to write to a RO parameter with metadata raises an error."""
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_metadata.metadata_tree.set("floatRoParam", 3.141275)
        assert "Parameter {} is read-only".format("floatRoParam") in str(excinfo.value)

    async def test_value_param_not_writeable(self, test_tree_metadata):
        """Test that a value parameter is not writeable and has the correct metadata flag."""
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_metadata.metadata_tree.set("valueParam", 90210)

        assert "Parameter {} is read-only".format("valueParam") in str(excinfo.value)

        param = await test_tree_metadata.metadata_tree.get("valueParam", with_metadata=True)
        result = await param
        assert not result["writeable"]

    async def test_rw_param_min_no_max(self, test_tree_metadata):
        """Test that a parameter with a minimum but no maximum works as expected."""
        new_value = 2
        await test_tree_metadata.metadata_tree.set("minNoMaxParam", new_value)
        param = await test_tree_metadata.metadata_tree.get("minNoMaxParam", with_metadata=True)
        result = await param
        assert result["value"] == new_value
        assert result["writeable"] == True

    async def test_rw_param_below_min_value(self, test_tree_metadata):
        """
        Test that attempting to set a value for a RW parameter below the specified minimum
        raises an error.
        """
        low_value = -1
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_metadata.metadata_tree.set("intCallableRwParam", low_value)

        assert "{} is below the minimum value {} for {}".format(
                low_value, test_tree_metadata.int_rw_param_metadata["min"],
                "intCallableRwParam") in str(excinfo.value)

    async def test_rw_param_above_max_value(self, test_tree_metadata):
        """
        Test that attempting to set a value for a RW parameter above the specified maximum
        raises an error.
        """
        high_value = 100000
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_metadata.metadata_tree.set("intCallableRwParam", high_value)

        assert "{} is above the maximum value {} for {}".format(
                high_value, test_tree_metadata.int_rw_param_metadata["max"],
                "intCallableRwParam") in str(excinfo.value)


class AsyncParameterTreeMutableTestFixture(AwaitableTestFixture):

    def __init__(self):

        super(AsyncParameterTreeMutableTestFixture, self).__init__(AsyncParameterTree)

        self.read_value = 64
        self.write_value = "test"

        self.param_tree_dict = {
            'extra': 'wibble',
            'bonus': 'win',
            'nest': {
                'double_nest': {
                    'nested_val': 125,
                    'dont_touch': "let me stay!",
                    'write': (self.get_write, self.set_write)
                },
                'list': [0, 1, {'list_test': "test"}, 3]
            },
            'read': (self.get_read,),
            'empty': {}
        }

        self.param_tree = AsyncParameterTree(self.param_tree_dict)
        self.param_tree.mutable = True

    async def get_read(self):
        return self.read_value

    async def get_write(self):
        return self.write_value

    async def set_write(self, data):
        self.write_value = data

@asyncio_fixture_decorator()
async def test_tree_mutable():
    """Test fixture for use in testing parameter tree metadata."""
    test_tree_mutable = await AsyncParameterTreeMutableTestFixture()
    yield test_tree_mutable

@pytest.mark.asyncio
class TestAsyncParamTreeMutable():
    """Class to test the behaviour of mutable async parameter trees"""

    async def test_mutable_put_differnt_data_type(self, test_tree_mutable):

        new_data = 75
        await test_tree_mutable.param_tree.set('bonus', new_data)
        val = await test_tree_mutable.param_tree.get('bonus')
        assert val['value'] == new_data

    async def test_mutable_put_new_branch_node(self, test_tree_mutable):

        new_node = {"new": 65}
        await test_tree_mutable.param_tree.set('extra', new_node)

        val = await test_tree_mutable.param_tree.get('extra')
        assert val == new_node

    async def test_mutable_put_new_sibling_node(self, test_tree_mutable):

        new_node = {'new': 65}
        path = 'nest'

        await test_tree_mutable.param_tree.set(path, new_node)
        val = await test_tree_mutable.param_tree.get(path)
        assert 'new' in val

    async def test_mutable_put_overwrite_param_accessor_read_only(self, test_tree_mutable):

        new_node = {"Node": "Broke Accessor"}
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_mutable.param_tree.set('read', new_node)

        assert "is read-only" in str(excinfo.value)

    async def test_mutable_put_overwrite_param_accessor_read_write(self, test_tree_mutable):

        new_node = {"Node": "Broke Accessor"}
        path = 'nest/double_nest/write'

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_mutable.param_tree.set(path, new_node)

        assert "Type mismatch setting" in str(excinfo.value)

    async def test_mutable_put_replace_nested_path(self, test_tree_mutable):

        new_node = {"double_nest": 294}
        path = 'nest'

        await test_tree_mutable.param_tree.set(path, new_node)
        val = await test_tree_mutable.param_tree.get(path)
        assert val['double_nest'] == new_node['double_nest']

    async def test_mutable_put_merge_nested_path(self, test_tree_mutable):

        new_node = {
            "double_nest": {
                'nested_val': {
                    "additional_val": "New value Here!",
                    "add_int": 648
                }
            }
        }
        path = 'nest'

        await test_tree_mutable.param_tree.set(path, new_node)
        val = await test_tree_mutable.param_tree.get(path)
        assert val['double_nest']['nested_val'] == new_node['double_nest']['nested_val']
        assert 'dont_touch' in val['double_nest']

    async def test_mutable_delete_method(self, test_tree_mutable):

        path = 'nest/double_nest'

        test_tree_mutable.param_tree.delete(path)
        tree = await test_tree_mutable.param_tree.get('')
        assert 'double_nest' not in tree['nest']
        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_mutable.param_tree.get(path)

        assert "Invalid path" in str(excinfo.value)

    async def test_mutable_delete_immutable_tree(self, test_tree_mutable):

        test_tree_mutable.param_tree.mutable = False

        with pytest.raises(ParameterTreeError) as excinfo:
            path = 'nest/double_nest'
            await test_tree_mutable.param_tree.delete(path)

        assert "Invalid Delete Attempt" in str(excinfo.value)

    async def test_mutable_delete_entire_tree(self, test_tree_mutable):

        path = ''

        test_tree_mutable.param_tree.delete(path)
        val = await test_tree_mutable.param_tree.get(path)
        assert not val

    async def test_mutable_delete_invalid_path(self, test_tree_mutable):

        path = 'nest/not_real'

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_mutable.param_tree.delete(path)

        assert "Invalid path" in str(excinfo.value)

    async def test_mutable_delete_from_list(self, test_tree_mutable):

        path = 'nest/list/3'

        test_tree_mutable.param_tree.delete(path)
        val = await test_tree_mutable.param_tree.get('nest/list')
        assert '3' not in val['value']

    async def test_mutable_delete_from_dict_in_list(self, test_tree_mutable):
        path = 'nest/list/2/list_test'

        test_tree_mutable.param_tree.delete(path)
        val = await test_tree_mutable.param_tree.get('nest/list')
        assert {'list_test': "test"} not in val['value']

    async def test_mutable_nested_tree_in_immutable_tree(self, test_tree_mutable):

        new_tree = await AsyncParameterTree({
            'immutable_param': "Hello",
            "nest": {
                "tree": test_tree_mutable.param_tree
            }
        })

        new_node = {"new": 65}
        path = 'nest/tree/extra'
        await new_tree.set(path, new_node)
        val = await new_tree.get(path)
        assert val == new_node

    async def test_mutable_nested_tree_external_change(self, test_tree_mutable):

        new_tree = await AsyncParameterTree({
            'immutable_param': "Hello",
            "tree": test_tree_mutable.param_tree
        })

        new_node = {"new": 65}
        path = 'tree/extra'
        await test_tree_mutable.param_tree.set('extra', new_node)
        val = await new_tree.get(path)
        assert val == new_node

    async def test_mutable_nested_tree_delete(self, test_tree_mutable):

        new_tree = await AsyncParameterTree({
            'immutable_param': "Hello",
            "tree": test_tree_mutable.param_tree
        })

        path = 'tree/bonus'
        new_tree.delete(path)

        tree = await new_tree.get('')

        assert 'bonus' not in tree['tree']

        with pytest.raises(ParameterTreeError) as excinfo:
            await test_tree_mutable.param_tree.get(path)

        assert "Invalid path" in str(excinfo.value)

    async def test_mutable_nested_tree_root_tree_not_affected(self, test_tree_mutable):

        new_tree = await AsyncParameterTree({
            'immutable_param': "Hello",
            "nest": {
                "tree": test_tree_mutable.param_tree
            }
        })

        new_node = {"new": 65}
        path = 'immutable_param'

        with pytest.raises(ParameterTreeError) as excinfo:
            await new_tree.set(path, new_node)

        assert "Type mismatch" in str(excinfo.value)

    async def test_mutable_add_to_empty_dict(self, test_tree_mutable):

        new_node = {"new": 65}
        path = 'empty'
        await test_tree_mutable.param_tree.set(path, new_node)
        val = await test_tree_mutable.param_tree.get(path)
        assert val== new_node
