#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import pyutilib.th as unittest
import pyutilib.misc

from pyomo.common import *
from pyomo.common.log import LoggingIntercept

from six import StringIO

class TestData(unittest.TestCase):

    def test_print_PyomoAPIData_string(self):
        #"""Print PyomoAPIData string"""
        data = PyomoAPIData()
        data.a = 1
        data.b = [1,2]
        data.c = PyomoAPIData()
        data.c.x = 1
        data.c.y = 2
        data['aa'] = 'here is more'
        data.clean()
        self.assertEqual(sorted(data.unused()), ['a','aa','b','c'])
        self.assertEqual(
            str(data),
            """a: 1
aa: here is more
b: [1, 2]
c:
    x: 1
    y: 2""")
        self.assertEqual(len(data._dirty_), 0)

    def test_print_PyomoAPIData_repr(self):
        #"""Print PyomoAPIData representation"""
        data = PyomoAPIData()
        data.a = 1
        data.b = [1,2]
        data.c = PyomoAPIData()
        data.c.x = 1
        data.c.y = 2
        data['aa'] = 'here is more'
        data.clean()
        # Because the PyomoAPIData is a dict, the repr() is subject to
        # change between python versions.  Cast back to a dict and
        # verify.
        self.assertEqual(
            eval(repr(data)),
            eval("{'a': 1, 'aa': 'here is more', 'b': [1, 2], "
                 "'c': {'y': 2, 'x': 1}}"))
        self.assertEqual(len(data._dirty_), 0)

    def test_err_unknown_attr(self):
        #"""Unknown attribute"""
        data = PyomoAPIData()
        with self.assertRaisesRegexp(AttributeError, 'Unknown attribute _x'):
            data._x

    def test_err_undeclared_attr(self):
        #"""Undeclared attribute"""
        data = PyomoAPIData()
        data.declare('a')
        with self.assertRaisesRegexp( AttributeError,
                                      "Undeclared attribute 'x'" ):
            data.x

    def test_err_undeclared_list_attr(self):
        #"""Undeclared attribute"""
        data = PyomoAPIData()
        data.declare(['a'])
        with self.assertRaisesRegexp( AttributeError,
                                      "Undeclared attribute 'x'" ):
            data.x


class TestAPI(unittest.TestCase):

    def test1_no_kwds_no_return(self):
        #"""Simple test: no keyword arguments or return values"""
        @pyomo_api
        def test1(data):
            """
            Required:
                data: input data
            """
            data.a = 2
            data.b[0] = 2
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test1(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test1(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        #
        self.assertTrue('test1' in PyomoAPIFactory.services())

    def test1a_no_kwds_single_return(self):
        #"""Simple test: no keyword arguments or return values"""
        @pyomo_api
        def test1a(data):
            """
            Required:
                data: input data
            Return:
                data: output data
            """
            data.a = 2
            data.b[0] = 2
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test1a(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test1a(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test1b_data_kwds_no_return(self):
        #"""Simple test: data keyword argument, no return values"""
        @pyomo_api
        def test1b(data=None):
            data.a = 2
            data.b[0] = 2
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test1b(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test1b(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test2_implicit_no_kwds_return_data(self):
        #"""Simple test: no keyword arguments, returning data"""
        @pyomo_api
        def test2(data):
            data.a = 2
            data.b[0] = 2
            return data
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test2(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test2(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test2a_no_kwds_return_data(self):
        #"""Simple test: no keyword arguments, returning data"""
        @pyomo_api
        def test2a(data):
            """
            Required:
                data: input data
            Return:
                data: output data
            """
            data.a = 2
            data.b[0] = 2
            return data
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test2a(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test2a(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test3_kwds_no_return(self):
        #"""Simple test: keyword arguments, no return values"""
        @pyomo_api
        def test3(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            """
            data.a = y
            data.b[0] = x
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test3(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test3(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test3a_kwds_no_return(self):
        #"""Simple test: keyword arguments, no return values"""
        @pyomo_api
        def test3a(x=1, y=2, data=None):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            """
            data.a = y
            data.b[0] = x
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test3a(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test3a(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test4_kwds_return_data(self):
        #"""Simple test: keyword arguments, simple return value"""
        @pyomo_api
        def test4(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                data: output data
            """
            data.a = y
            data.b[0] = x
            return data
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test4(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test4(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test5_kwds_return_apidata(self):
        #"""Simple test: keyword arguments, non-data return values"""
        @pyomo_api(outputs=('z'))
        def test5(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                data: output data
                z: integer
            """
            data.a = y
            data.b[0] = x
            return PyomoAPIData(z=x)
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test5(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test5(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        self.assertEqual(retval.z, 2)

    def test6_kwds_return_apidata(self):
        #"""Simple test: keyword arguments, non-data return values with data"""
        @pyomo_api(outputs=('z'))
        def test6(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                data: output data
                z: integer
            """
            data.a = y
            data.b[0] = x
            return PyomoAPIData(data=data, z=x)
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test6(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test6(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        self.assertEqual(retval.z, 2)

    def test5a_kwds_return_apidata(self):
        #"""Outputs specified in docstring: keyword arguments, non-data return values"""
        @pyomo_api
        def test5a(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                data: output data
                z: integer
            """
            data.a = y
            data.b[0] = x
            return PyomoAPIData(z=x)
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test5a(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test5a(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        self.assertEqual(retval.z, 2)

    def test6a_kwds_return_apidata(self):
        #"""Outputs specified in docstring: keyword arguments, non-data return values with data"""
        @pyomo_api
        def test6a(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                data: output data
                z: integer
            """
            data.a = y
            data.b[0] = x
            return PyomoAPIData(data=data, z=x)
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test6a(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test6a(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        self.assertEqual(retval.z, 2)

    def test7a_dict_data(self):
        #"""Test with dict data"""
        @pyomo_api
        def test7a(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            """
            data.a = y
            data.b[0] = x
        #
        options = {}
        options['a'] = 1
        options['b'] = [1,2]
        retval = test7a(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test7a(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test7b_dict_data_return_dict(self):
        #"""Test with dict data and return a dictionary"""
        @pyomo_api
        def test7b(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            """
            data.a = y
            data.b[0] = x
            return {'data':data}
        #
        options = {}
        options['a'] = 1
        options['b'] = [1,2]
        retval = test7b(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test7b(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])

    def test7c_dict_data_return_dict(self):
        #"""Test with dict data and return a dictionary"""
        @pyomo_api
        def test7c(data, x=1, y=2):
            """
            Required:
                data: input data
                x: integer
            Optional:
                y: integer
            Return:
                z: integer
            """
            data.a = y
            data.b[0] = x
            return {'data':data, 'z':x}
        #
        options = {}
        options['a'] = 1
        options['b'] = [1,2]
        retval = test7c(options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test7c(data=options, x=2)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        self.assertEqual(retval.z, 2)

    def test8_nested_data(self):
        #"""Simple test with required nested data"""
        @pyomo_api
        def test8(data):
            """
            Required:
                data: input data
                data.foo.bar:
            """
            data.foo.foo = 3
            #data.a = 2
            #data.b[0] = 2
        #
        options = PyomoAPIData()
        options.foo = PyomoAPIData()
        options.foo.bar = 1
        options.a = 1
        options.b = [1,2]
        retval = test8(options)
        self.assertEqual(retval.data.a, 1)
        self.assertEqual(retval.data.b, [1,2])
        self.assertEqual(retval.data.foo.foo, 3)
        #retval = test8(data=options)
        #self.assertEqual(retval.data.a, 2)
        #self.assertEqual(retval.data.b, [2,2])

    def test9_complex_documentation(self):
        #"""Simple test: no keyword arguments or return values"""
        @pyomo_api
        def test9(data):
            """
            This is the
            short
            documentation.

            This

            is

            the

            long documentation.
            Required:
                data: multiline
                      description of
                      input data object
                data.a: More data
            """
            data.a = 2
            data.b[0] = 2
        #
        options = PyomoAPIData()
        options.a = 1
        options.b = [1,2]
        retval = test9(options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        retval = test9(data=options)
        self.assertEqual(retval.data.a, 2)
        self.assertEqual(retval.data.b, [2,2])
        #
        self.assertTrue('test1' in PyomoAPIFactory.services())
        self.assertEqual(test9.__short_doc__, 'This is the\nshort\ndocumentation.')
        self.assertEqual(test9.__long_doc__, 'This\n\nis\n\nthe\n\nlong documentation.')

    def test10_missing_required_arg(self):
        #"""Simple test: no keyword arguments or return values"""
        @pyomo_api
        def test10(data=None, x=1):
            """
            Required:
                x:
            Optional:
                data:
            """
            return PyomoAPIData(z=2*x)
        #
        with self.assertRaisesRegexp(
                RuntimeError,
                "Cannot return value 'z' that is not a predefined output" ):
            retval = test10(x=3)

    def test_err1_varargs(self):
        #"""Expect an error when variable length arguments are supported"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err1(*args): pass
        self.assertIn(
            "Attempting to declare Pyomo task with function 'err1' "
            "that contains variable arguments", buf.getvalue())

    def test_err2_kwds(self):
        #"""Expect an error when variable length keyword arguments are supported"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err2(**kwargs): pass
        self.assertIn(
            "Attempting to declare Pyomo task with function 'err2' "
            "that contains variable keyword arguments", buf.getvalue())

    def test_err3_invalid_return(self):
        #"""Expect an error when return value is not None or Options()"""
        @pyomo_api
        def err3(data):
            return 1

        with self.assertRaisesRegexp(
                RuntimeError,
                "A Pyomo task function must return either None, a "
                "PyomoAPIData object, or an instance of dict"):
            err3(PyomoAPIData())

    def test_err4_missing_input_data(self):
        #"""Expect an error when no data argument is specified"""
        @pyomo_api
        def err4(data):
            data.a = 2
            data.b[0] = 2
        with self.assertRaisesRegexp(
                RuntimeError,
                "A PyomoTask instance must be executed with at "
                "'data' argument"):
            err4()

    def test_err5_unexpected_return(self):
        #"""Expect an error when an unspecified return value is given"""
        @pyomo_api
        def err5(data):
            return PyomoAPIData(z=None)
        with self.assertRaisesRegexp(
                RuntimeError,
                "Cannot return value 'z' that is not a predefined output "
                "of a Pyomo task" ):
            err5(PyomoAPIData())

    def test_err6_missing_input_data(self):
        #"""Expect an error when no data argument is specified"""

        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err6(): pass
        self.assertIn("A Pyomo functor 'err6' must have a 'data argument",
                      buf.getvalue())

        # Note: the TypeError message changes in Python 3.6, so we need
        # a weaker regexp.
        with self.assertRaisesRegexp(
                TypeError,
                "err6\(\) takes .* arguments .* given" ):
            err6(PyomoAPIData())

    def test_err7a_arg_missing_from_docstring(self):
        #"""Argument missing from docstring"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err7a(data, x=1, y=2):
                """
                Optional:
                   y: integer
                """
                pass
        self.assertIn("Argument 'x' is not specified in the docstring",
                      buf.getvalue())

    def test_err7b_arg_missing_from_docstring(self):
        #"""Argument missing from docstring"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err7b(data, x=1, y=2):
                """
                Required:
                    x: integer
               """
                pass
        self.assertIn("Argument 'y' is not specified in the docstring",
                      buf.getvalue())

    def test_err7c_return_missing_from_docstring(self):
        #"""Argument missing from docstring"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api(outputs=('z'))
            def err7c(data, x=1, y=2):
                """
                Required:
                    x: integer
                Optional:
                    y: integer
                """
                return PyomoAPIData(z=1)
        self.assertIn("Return value 'z' is not specified", buf.getvalue())

    def test_err7A_extra_required_arg(self):
        #"""Unexpected required argument"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err7A(data, x=1, y=2):
                """
                Required:
                    x: integer
                    bad: integer
                Optional:
                    y: integer
                """
                pass
        self.assertIn("Unexpected name 'bad' in list of required inputs",
                      buf.getvalue())

    def test_err7B_extra_optional_arg(self):
        #"""Argument missing from docstring"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err7B(data, x=1, y=2):
                """
                Required:
                    x: integer
                Optional:
                    y: integer
                    bad: integer
                """
                pass
        self.assertIn("Unexpected name 'bad' in list of optional inputs",
                      buf.getvalue())

    def test_err7C_extra_return_arg(self):
        #"""Argument missing from docstring"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api(outputs=('z'))
            def err7C(data, x=1, y=2):
                """
                Required:
                    x: integer
                Optional:
                    y: integer
                Return:
                    z: integer
                    bad: integer
                """
                return PyomoAPIData(z=1)
        self.assertIn("Unexpected name 'bad' in list of outputs",
                      buf.getvalue())

    def test_err8a_missing_nested_apidata(self):
        #"""Missing nested value"""
        @pyomo_api
        def err8a(data):
            """
            Required:
                data.x: integer
            """
            pass
        with self.assertRaisesRegexp(
                RuntimeError,
                "None value found for nested attribute 'data.x'" ):
            err8a(PyomoAPIData())

    def test_err8b_missing_nested_arg(self):
        #"""Nested value with None value"""
        @pyomo_api
        def err8b(data):
            """
            Required:
                data.x: integer
            """
            pass
        with self.assertRaisesRegexp(
                RuntimeError,
                "None value found for nested attribute 'data.x'" ):
            err8b(PyomoAPIData(data=PyomoAPIData()))

    def test_err8c_missing_nested_dict_arg(self):
        #"""Nested value with None value"""
        @pyomo_api
        def err8c(data):
            """
            Required:
                data.x.y: integer
            """
            pass
        with self.assertRaisesRegexp(
                RuntimeError,
                "Failed to verify existence of nested attribute 'data.x.y'" ):
            err8c(PyomoAPIData(x={}))

    def test_err10_test_with_nested_data(self):
        #"""Simple test with required nested data"""
        @pyomo_api
        def err10(data):
            """
            Required:
                data: input data
                data.foo.bar:
            """
            data.foo.foo = 3
            with self.assertRaisesRegexp(
                    AttributeError, "Undeclared attribute 'a'" ):
                data.a = 2
        #
        options = PyomoAPIData()
        options.foo = PyomoAPIData()
        options.foo.bar = 1
        options.a = 1
        options.b = [1,2]
        retval = err10(options)
        self.assertEqual(retval.data.a, 1)
        self.assertEqual(retval.data.b, [1,2])
        self.assertEqual(retval.data.foo.foo, 3)

    def test_err10a_duplicate_function(self):
        #"""Expect an error when the same functor is defined twice"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api
            def err10a(data): pass

            @pyomo_api
            def err10a(data): pass
        self.assertIn("Cannot define API err10a, since this API name "
                      "is already defined", buf.getvalue())

    def test_err10b_duplicate_namespace_function(self):
        #"""Expect an error when the same functor is defined twice"""
        buf = StringIO()
        with LoggingIntercept(buf, 'pyomo.common'):
            @pyomo_api(namespace='foo')
            def err10b(data): pass

            @pyomo_api(namespace='foo')
            def err10b(data): pass
        self.assertIn("Cannot define API foo.err10b, since this API name "
                      "is already defined", buf.getvalue())

    def test_err10c_duplicate_namespace_scoping(self):
        #"""Expect an error when the same functor is defined twice"""
        @pyomo_api(namespace='foo')
        def err10c(data): pass

        @pyomo_api()
        def err10c(data): pass

    def test_err11_missing_data(self):
        #"""Expect an error when 'data' is not defined when it is required"""
        @pyomo_api
        def err11(data=None, x=1):
            """
            Required:
                data:
                x:
            """
        with self.assertRaisesRegexp(
                RuntimeError,
                "A PyomoTask instance must be executed with at 'data' "
                "argument" ):
            err11(x=2)

    def test_err12_extra_data(self):
        #"""Expect an error when multiple data options are provided"""
        @pyomo_api
        def err12(data): pass
        with self.assertRaisesRegexp(
                RuntimeError,
                "A PyomoTask instance can only be executed with a single "
                "non-keyword argument" ):
            err12({}, {})

    def test_err13_bad_return_type(self):
        #"""Expect an error when returning something other than None, PyomoAPIData or a dict object"""
        @pyomo_api
        def err13(data):
            return set()
        with self.assertRaisesRegexp(
                RuntimeError,
                "A Pyomo task function must return either None, a "
                "PyomoAPIData object, or an instance of dict" ):
            err13({})

if __name__ == "__main__":
    unittest.main()
