import pytest
try:
    import dnp3
except ImportError:
    pytest.skip("pydnp3 not found!", allow_module_level=True)

import copy

from dnp3.points import PointDefinitions
from dnp3.mesa.functions import FunctionDefinitions, FunctionDefinition, StepDefinition

from test_mesa_agent import POINT_DEFINITIONS_PATH, FUNCTION_DEFINITIONS_PATH


POINT_DEFINITIONS = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH)

enable_high_voltage_ride_through_mode = {
    'id': 'enable_high_voltage_ride_through_mode',
    'name': 'Enable High Volatge Ride-Through Mode',
    'ref': 'AN2018 Spec section 2.5.1 Table 33',
    'steps': [
        {
            'step_number': 1,
            'description': 'Set the Reference Voltage if it is not already set',
            'point_name': 'DECP.VRef.AO0',
            'optional': 'I',
            'fcode': ['direct_operate'],
            'response': 'DECP.VRef.AI29'
        },
        {
            'step_number': 2,
            'description': 'Set the Reference Voltage Offset if it is not already set',
            'point_name': 'DECP.VRefOfs.AO1',
            'optional': 'I',
            'fcode': ['direct_operate'],
            'response': 'DECP.VRefOfs.AI30'
        },
        {
            'step_number': 3,
            'description': 'Identify the meter used to measure the voltage. By default this is the System Meter',
            'point_name': 'DHVT.EcpRef.AO22',
            'optional': 'I',
            'fcode': ['direct_operate'],
            'response': 'DHVT.EcpRef.AI71'
        },
        {
            'step_number': 4,
            'description': 'Identify the index of the curve which specifies trip points when the voltage is high',
            'point_name': 'PTOV.BlkRef.AO23',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'PTOV.BlkRef.AI73',
            'func_ref': 'curve'
        },
        {
            'step_number': 5,
            'description': 'Enable the Low/High Voltage Ride-Through Mode',
            'point_name': 'DHVT.ModEna.BO12',
            'optional': 'M',
            'fcode': ['select', 'operate'],
            'response': 'DHVT.ModEna.BI64'
        }
    ]
}

curve_selector_block = {
    'id': 'curve',
    'name': 'Curve',
    'ref': 'AN2018 Spec Curve Definition',
    'steps': [
        {
            'step_number': 1,
            'description': 'Select which curve to edit',
            'point_name': 'DGSMn.InCrv.AO244',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'DGSMn.InCrv.AI328'
        },
        {
            'step_number': 2,
            'description': 'Specify the Curve Mode Type',
            'point_name': 'DGSMn.ModTyp.AO245',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'DGSMn.ModTyp.AI329'
        },
        {
            'step_number': 3,
            'description': 'Specify that the Independent (X-Value) units for the curve',
            'point_name': 'FMARn.IndpUnits.AO247',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'FMARn.IndpUnits.AI331'
        },
        {
            'step_number': 4,
            'description': 'Specify the Dependent (Y-Value) units for the curve',
            'point_name': 'FMARn.DepRef.AO248',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'FMARn.DepRef.AI332',
            'action': 'publish'
        },
        {
            'step_number': 5,
            'description': 'Set X-Value and Y-Values pairs for the curve',
            'point_name': 'FMARn.PairArr.CrvPts.AO249',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'FMARn.PairArr.CrvPts.AI333'
        },
        {
            'step_number': 6,
            'description': 'Set number of points used for the curve',
            'point_name': 'FMARn.PairArr.NumPts.AO246',
            'optional': 'M',
            'fcode': ['direct_operate'],
            'response': 'FMARn.PairArr.NumPts.AI330'
        }
    ]
}


class TestStepDefinition:
    """Regression tests for Step Definition."""

    @property
    def function_id(self):
        return 'enable_high_voltage_ride_through_mode'

    @property
    def step_number(self):
        return 1

    @property
    def step_json(self):
        """Return function enable_high_voltage_ride_through_mode step 1"""
        return copy.deepcopy(enable_high_voltage_ride_through_mode)['steps'][self.step_number-1]

    def validate_step_definition(self, step_json):
        exception = {}
        try:
            step_def = StepDefinition(POINT_DEFINITIONS, self.function_id, step_json)
            step_def.validate()
        except Exception as err:
            exception['key'] = type(err).__name__
            exception['error'] = str(err)
        return exception

    def test_valid_step_definition(self):
        exception = self.validate_step_definition(self.step_json)
        assert exception == {}

    def test_missing_step_number(self):
        """Test raising exception if step missing step_number"""
        step_json = self.step_json
        step_json.pop('step_number')
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Missing step number in function {}'.format(self.function_id)
        }

    def test_missing_point_name(self):
        """Test raising exception if step missing point_name"""
        step_json = self.step_json
        step_json.pop('point_name')
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Missing name in function {} step {}'.format(self.function_id, self.step_number)
        }

    def test_invalid_optionality(self):
        """Test raising exception if optional not O, M, or I"""
        step_json = self.step_json
        step_json.update({
            'optional': 'C'
        })
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Invalid optional value in function {} step {}: C'.format(self.function_id, self.step_number)
        }

    def test_invalid_fcodes_type(self):
        """Test raising exception if fcodes is not a list"""
        step_json = self.step_json
        step_json.update({
            'fcodes': 'direct_operate'
        })
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': "Invalid fcodes in function {} step {}, type=<class 'str'>".format(self.function_id,
                                                                                       self.step_number)
        }

    def test_invalid_fcode_value(self):
        """Test raising exception if a str value in fcodes list is invalid"""
        step_json = self.step_json
        step_json.update({
            'fcodes': ['select_operate']
        })
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Invalid fcode in function {} step {}, fcode=select_operate'.format(self.function_id,
                                                                                         self.step_number)
        }

    def test_invalid_optionality_for_read_fcode(self):
        """Test raising exception if optionality is not OPTIONAL when fcode is read"""
        step_json = self.step_json
        step_json.update({
            'fcodes': ['read', 'response']
        })
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Invalid optionality in function {} step {}: must be OPTIONAL'.format(self.function_id,
                                                                                           self.step_number)
        }

    def test_invalid_response_point(self):
        step_json = self.step_json
        step_json.update({
            'response': 'invalid_point'
        })
        exception = self.validate_step_definition(step_json)
        assert exception == {
            'key': 'AttributeError',
            'error': 'Response point in function {} step {} does not match point definition. '
                     'Error=No point named invalid_point'.format(self.function_id, self.step_number)
        }


class TestFunctionDefinition:
    """Regression tests for Function Definition."""

    @property
    def function_json(self):
        """Return function enable_high_voltage_ride_through_mode"""
        return copy.deepcopy(enable_high_voltage_ride_through_mode)

    @staticmethod
    def validate_function_definition(function_json):
        exception = {}
        try:
            FunctionDefinition(POINT_DEFINITIONS, function_json)
        except Exception as err:
            exception['key'] = type(err).__name__
            exception['error'] = str(err)
        return exception

    def test_valid_function_definition(self):
        exception = self.validate_function_definition(self.function_json)
        assert exception == {}

    def test_missing_function_id(self):
        """Test raising exception if function missing id"""
        function_json = self.function_json
        function_json.pop('id')
        exception = self.validate_function_definition(function_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Missing function ID'
        }

    def test_missing_function_steps(self):
        """Test raising exception if function missing steps"""
        function_json = self.function_json
        function_json.pop('steps')
        exception = self.validate_function_definition(function_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Missing steps for function {}'.format(self.function_json['id'])
        }

    def test_duplicated_step_number(self):
        """Test raising exception if there is duplicated step_number in function"""
        function_json = self.function_json
        function_json['steps'][2].update({
            'step_number': 1
        })
        exception = self.validate_function_definition(function_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Duplicated step number 1 for function {}'.format(self.function_json['id'])
        }

    def test_missing_a_step(self):
        """Test raising exception if function missing a step"""
        function_json = self.function_json
        del function_json['steps'][1]
        exception = self.validate_function_definition(function_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'There are missing steps for function {}'.format(self.function_json['id'])
        }

    def test_selector_block_function(self):
        """Test raising exception if one step in selector block function is optional"""
        function_json = copy.deepcopy(curve_selector_block)
        exception = self.validate_function_definition(function_json)
        assert exception == {}

        # Change step 2 to optional
        function_json['steps'][1]['optional'] = 'O'
        exception = self.validate_function_definition(function_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Function curve - Step 2: optionality must be either INITIALIZE or MANDATORY'
        }


class TestFunctionDefinitions:
    """Regression tests for Function Definitions."""

    @property
    def functions_json(self):
        return [
            copy.deepcopy(enable_high_voltage_ride_through_mode),
            copy.deepcopy(curve_selector_block)
        ]

    @staticmethod
    def validate_functions_definition(functions_json):
        exception = {}
        try:
            function_definitions = FunctionDefinitions(POINT_DEFINITIONS)
            function_definitions.load_functions(functions_json)
        except Exception as err:
            exception['key'] = type(err).__name__
            exception['error'] = str(err)
        return exception

    def test_load_functions_yaml(self):
        try:
            FunctionDefinitions(POINT_DEFINITIONS, FUNCTION_DEFINITIONS_PATH)
            assert True
        except ValueError:
            assert False

    def test_valid_functions_definitions(self):
        exception = self.validate_functions_definition(self.functions_json)
        assert exception == {}

    def test_duplicated_function_id(self):
        """Test raising exception if there are multiple function with same id"""
        functions_json = self.functions_json
        functions_json[1]['id'] = self.functions_json[0]['id']
        exception = self.validate_functions_definition(functions_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Problem parsing FunctionDefinitions. '
                     'Error=There are multiple functions for function id {}'.format(functions_json[1]['id'])
        }

    def test_invalid_func_ref(self):
        """Test raising exception if a step has an invalid func_ref"""
        functions_json = self.functions_json
        functions_json[0]['steps'][3]['func_ref'] = 'invalid_curve'
        exception = self.validate_functions_definition(functions_json)
        assert exception == {
            'key': 'ValueError',
            'error': 'Invalid Function Reference invalid_curve for Step 4 '
                     'in Function enable_high_voltage_ride_through_mode'
        }
