# Copyright 2016 Novo Nordisk Foundation Center for Biosustainability,
# Technical University of Denmark.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import abc
import copy
import json
import os
import pickle
import unittest
from functools import partial

import sympy

import optlang
from optlang import interface, symbolics


__test__ = False

TESTMODELPATH = os.path.join(os.path.dirname(__file__), 'data/model.json')
TESTMILPMODELPATH = os.path.join(os.path.dirname(__file__), 'data/simple_milp.json')


class AbstractVariableTestCase(unittest.TestCase, metaclass=abc.ABCMeta):
    @unittest.skip('Abstract test method not implemented.')
    def test_magic(self):
        pass

    def setUp(self):
        self.var = self.interface.Variable('test')
        self.model = self.interface.Model()

    def test_set_wrong_type_raises(self):
        self.assertRaises(ValueError, self.interface.Variable, name="test", type="mayo")
        self.assertRaises(Exception, setattr, self.var, 'type', 'ketchup')
        self.model.add(self.var)
        self.model.update()
        self.assertRaises(ValueError, setattr, self.var, "type", "mustard")
        self.var.type = "integer"
        self.assertEqual(self.var.type, "integer")

    def test_change_type(self):
        var = self.interface.Variable("test")
        var.type = "binary"
        self.assertEqual(var.type, "binary")

    def test_change_name(self):
        self.model.add(self.var)
        self.model.update()
        self.var.name = "test_2"
        self.assertEqual(self.var.name, "test_2")
        self.assertEqual(str(self.var), self.var.name)
        self.model.remove(self.var)
        self.model.update()

    def test_non_string_name_raises(self):
        for name in [2, None, True, ["name1", "name2"]]:
            with self.assertRaises(TypeError):
                self.interface.Variable(name)

    @abc.abstractmethod
    def test_get_primal(self):
        pass

    def test_get_dual(self):
        with open(TESTMODELPATH) as infile:
            model = self.interface.Model.from_json(json.load(infile))
        model.optimize()
        self.assertEqual(model.status, 'optimal')
        self.assertAlmostEqual(model.objective.value, 0.8739215069684305)
        self.assertTrue(isinstance(model.variables[0].dual, float))

    def test_setting_lower_bound_higher_than_upper_bound_raises(self):
        self.model.add(self.var)
        self.var.ub = 0
        self.assertRaises(ValueError, setattr, self.model.variables["test"], 'lb', 100.)

    def test_setting_nonnumerical_bounds_raises(self):
        self.assertRaises(TypeError, setattr, self.var, "lb", "Minestrone")
        self.assertRaises(TypeError, setattr, self.var, "ub", "Minestrone")
        self.model.add(self.var)
        self.assertRaises(TypeError, setattr, self.model.variables[0], 'lb', 'Chicken soup')
        self.assertRaises(TypeError, setattr, self.model.variables[0], 'ub', 'Chicken soup')

    @abc.abstractmethod
    def test_changing_variable_names_is_reflected_in_the_solver(self):
        pass

    def test_setting_bounds(self):
        self.var.ub = 5
        self.model.objective = self.interface.Objective(self.var)
        self.model.optimize()
        self.assertEqual(self.var.primal, 5)
        self.var.ub = 4
        self.model.optimize()
        self.assertEqual(self.var.primal, 4)
        self.var.lb = -3
        self.model.objective.direction = "min"
        self.model.optimize()
        self.assertEqual(self.var.primal, -3)
        self.var.lb = sympy.Number(-4)  # Sympy numbers should be valid bounds
        self.model.optimize()
        self.assertEqual(self.var.primal, -4)

    def test_set_bounds_method(self):
        var = self.interface.Variable("test", lb=-10)
        c = self.interface.Constraint(var, lb=-100)
        model = self.interface.Model()
        obj = self.interface.Objective(var)
        model.add(c)
        model.objective = obj

        for lb, ub in ((1, 10), (-1, 5), (11, 12)):
            obj.direction = "max"
            var.set_bounds(lb, ub)
            model.optimize()
            self.assertAlmostEqual(var.primal, ub)
            obj.direction = "min"
            model.optimize()
            self.assertAlmostEqual(var.primal, lb)

        var.set_bounds(None, 0)
        model.optimize()
        self.assertAlmostEqual(var.primal, -100)

        obj.direction = "max"
        var.set_bounds(1, None)
        self.assertEqual(model.optimize(), optlang.interface.UNBOUNDED)

        self.assertRaises(ValueError, var.set_bounds, 2, 1)

    def test_set_bounds_to_none(self):
        model = self.model
        var = self.var
        model.objective = self.interface.Objective(var)
        self.assertEqual(model.optimize(), interface.UNBOUNDED)
        var.ub = 10
        self.assertEqual(model.optimize(), interface.OPTIMAL)
        var.ub = None
        self.assertEqual(model.optimize(), interface.UNBOUNDED)
        self.model.objective.direction = "min"
        var.lb = -10
        self.assertEqual(model.optimize(), interface.OPTIMAL)
        var.lb = None
        self.assertEqual(model.optimize(), interface.UNBOUNDED)

    def test_invalid_name_raises(self):
        with self.assertRaises(Exception):
            self.interface.Variable("")
        with self.assertRaises(Exception):
            self.interface.Variable("This space")
        with self.assertRaises(Exception):
            self.interface.Variable("This\ttab")

    def test_new_invalid_name_raises(self):
        with self.assertRaises(Exception):
            self.var.name = ""
        with self.assertRaises(Exception):
            self.var.name = "This space"
        with self.assertRaises(Exception):
            self.var.name = "This\ttab"

        self.assertEqual(self.var.name, 'test')


class AbstractConstraintTestCase(unittest.TestCase, metaclass=abc.ABCMeta):

    def setUp(self):
        with open(TESTMODELPATH) as infile:
            self.model = self.interface.Model.from_json(json.load(infile))
        self.constraint = self.interface.Constraint(
            self.interface.Variable('chip') + self.interface.Variable('chap'),
            name='woodchips',
            lb=100
        )

    def test_indicator_constraint_support(self):
        if self.interface.Constraint._INDICATOR_CONSTRAINT_SUPPORT:
            constraint = self.interface.Constraint(
                self.interface.Variable('chip_2'),
                indicator_variable=self.interface.Variable('chip', type='binary'), active_when=0, lb=0,
                ub=0,
                name='indicator_constraint_fwd_1'
            )
            model = self.interface.Model()
            model.add(constraint)
            model.update()
            self.assertEqual(constraint.problem, model)
            self.assertIn(constraint, model.constraints)
            model.remove(constraint.name)
            model.update()
            self.assertEqual(constraint.problem, None)
            self.assertNotIn(constraint, model.constraints)
        else:
            self.assertRaises(
                optlang.exceptions.IndicatorConstraintsNotSupported,
                self.interface.Constraint,
                self.interface.Variable('chip') + self.interface.Variable('chap'),
                indicator_variable=self.interface.Variable('indicator', type='binary')
            )

    @abc.abstractmethod
    def test_get_primal(self):
        pass

    def test_get_dual(self):
        self.assertEqual(self.constraint.dual, None)
        self.model.optimize()
        self.assertEqual(self.model.status, 'optimal')
        self.assertAlmostEqual(self.model.objective.value, 0.8739215069684305)
        self.assertTrue(isinstance(self.model.constraints[0].dual, float))

    def test_change_constraint_name(self):
        constraint = self.interface.Constraint.clone(self.constraint)
        self.assertEqual(constraint.name, 'woodchips')
        constraint.name = 'ketchup'
        self.assertEqual(constraint.name, 'ketchup')
        self.assertEqual([constraint.name for constraint in self.model.constraints],
                         ['M_13dpg_c', 'M_2pg_c', 'M_3pg_c', 'M_6pgc_c', 'M_6pgl_c', 'M_ac_c', 'M_ac_e',
                          'M_acald_c', 'M_acald_e', 'M_accoa_c', 'M_acon_C_c', 'M_actp_c', 'M_adp_c', 'M_akg_c',
                          'M_akg_e', 'M_amp_c', 'M_atp_c', 'M_cit_c', 'M_co2_c', 'M_co2_e', 'M_coa_c', 'M_dhap_c',
                          'M_e4p_c', 'M_etoh_c', 'M_etoh_e', 'M_f6p_c', 'M_fdp_c', 'M_for_c', 'M_for_e', 'M_fru_e',
                          'M_fum_c', 'M_fum_e', 'M_g3p_c', 'M_g6p_c', 'M_glc_D_e', 'M_gln_L_c', 'M_gln_L_e',
                          'M_glu_L_c', 'M_glu_L_e', 'M_glx_c', 'M_h2o_c', 'M_h2o_e', 'M_h_c', 'M_h_e', 'M_icit_c',
                          'M_lac_D_c', 'M_lac_D_e', 'M_mal_L_c', 'M_mal_L_e', 'M_nad_c', 'M_nadh_c', 'M_nadp_c',
                          'M_nadph_c', 'M_nh4_c', 'M_nh4_e', 'M_o2_c', 'M_o2_e', 'M_oaa_c', 'M_pep_c', 'M_pi_c',
                          'M_pi_e', 'M_pyr_c', 'M_pyr_e', 'M_q8_c', 'M_q8h2_c', 'M_r5p_c', 'M_ru5p_D_c', 'M_s7p_c',
                          'M_succ_c', 'M_succ_e', 'M_succoa_c', 'M_xu5p_D_c'])
        for i, constraint in enumerate(self.model.constraints):
            constraint.name = 'c' + str(i)
        self.assertEqual([constraint.name for constraint in self.model.constraints],
                         ['c' + str(i) for i in range(0, len(self.model.constraints))])

    def test_setting_lower_bound_higher_than_upper_bound_raises(self):
        self.assertRaises(ValueError, setattr, self.model.constraints[0], 'lb', 10000000000.)
        self.assertRaises(ValueError, setattr, self.model.constraints[0], "ub", -1000000000.)

        self.assertRaises(ValueError, self.interface.Constraint, 0, lb=0, ub=-1)

    def test_setting_bounds(self):
        var = self.interface.Variable("test", lb=-10)
        c = self.interface.Constraint(var, lb=0)
        model = self.interface.Model()
        obj = self.interface.Objective(var)
        model.add(c)
        model.objective = obj

        c.ub = 5
        model.optimize()
        self.assertAlmostEqual(var.primal, 5)
        c.ub = 4
        model.optimize()
        self.assertAlmostEqual(var.primal, 4)
        c.lb = -3
        model.objective.direction = "min"
        model.optimize()
        self.assertAlmostEqual(var.primal, -3)
        c.lb = sympy.Number(-4)  # Sympy numbers should be valid bounds
        model.optimize()
        self.assertAlmostEqual(var.primal, -4)

    def test_setting_nonnumerical_bounds_raises(self):
        var = self.interface.Variable("test")
        constraint = self.interface.Constraint(var, lb=0)
        self.assertRaises(TypeError, setattr, constraint, "lb", "noodle soup")
        self.assertRaises(TypeError, setattr, self.model.constraints[0], 'lb', 'Chicken soup')
        self.assertRaises(TypeError, setattr, constraint, "ub", "noodle soup")
        self.assertRaises(TypeError, setattr, self.model.constraints[0], 'ub', 'Chicken soup')

    def test_set_constraint_bounds_to_none(self):
        model = self.interface.Model()
        var = self.interface.Variable("test")
        const = self.interface.Constraint(var, lb=-10, ub=10)
        obj = self.interface.Objective(var)
        model.add(const)
        model.objective = obj
        self.assertEqual(model.optimize(), interface.OPTIMAL)
        const.ub = None
        self.assertEqual(model.optimize(), interface.UNBOUNDED)
        const.ub = 10
        const.lb = None
        obj.direction = "min"
        self.assertEqual(model.optimize(), interface.UNBOUNDED)
        const.lb = -10
        self.assertEqual(model.optimize(), interface.OPTIMAL)

    def test_constraint_get_linear_coefficients_raises(self):
        self.assertRaises(Exception, self.constraint.get_linear_coefficients, [])

    def test_constraint_set_linear_coefficients_raises(self):
        self.assertRaises(Exception, self.constraint.set_linear_coefficients, {})

    def test_move_constant_to_rhs(self):
        x = self.interface.Variable("x")
        c1 = self.interface.Constraint(x + 3, lb=0, ub=0)
        self.assertEqual(c1.expression - x, 0)
        self.assertEqual(c1.lb, -3)
        self.assertEqual(c1.ub, -3)

        c2 = self.interface.Constraint(x - 3, lb=0, ub=0)
        self.assertEqual(c2.expression - x, 0)
        self.assertEqual(c2.lb, 3)
        self.assertEqual(c2.ub, 3)

        c3 = self.interface.Constraint(x - 3, lb=0)
        self.assertEqual(c3.expression - x, 0)
        self.assertEqual(c3.lb, 3)

        c4 = self.interface.Constraint(x - 3, ub=0)
        self.assertEqual(c4.expression - x, 0)
        self.assertEqual(c4.ub, 3)

        c5 = self.interface.Constraint(x + 3, lb=0)
        self.assertEqual(c5.expression - x, 0)
        self.assertEqual(c5.lb, -3)

        c6 = self.interface.Constraint(x + 3, ub=0)
        self.assertEqual(c6.expression - x, 0)
        self.assertEqual(c6.ub, -3)

    def test_invalid_name_raises(self):
        with self.assertRaises(Exception):
            self.interface.Constraint(1, name="")
        with self.assertRaises(Exception):
            self.interface.Constraint(1, name="This space")
        with self.assertRaises(Exception):
            self.interface.Constraint(1, name="This\ttab")

    def test_new_invalid_name_raises(self):
        const = self.interface.Constraint(1, name="MyConstraint")
        with self.assertRaises(Exception):
            const.name = ""
        with self.assertRaises(Exception):
            const.name = "This space"
        with self.assertRaises(Exception):
            const.name = "This\ttab"

    def test_construct_with_sloppy(self):
        x, y, z, w = self.model.variables[:4]
        const = self.interface.Constraint(
            symbolics.add([symbolics.mul(symbolics.One, var) for var in [x, y, z]]),
            lb=0,
            sloppy=True
        )
        self.model.add(const)
        self.model.update()

        self.assertTrue(const.get_linear_coefficients([x, y, z, w]) == {x: 1, y: 1, z: 1, w: 0})


class AbstractObjectiveTestCase(unittest.TestCase, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def setUp(self):
        pass

    @abc.abstractmethod
    def test_change_direction(self):
        pass

    def test_objective_get_linear_coefficients_raises(self):
        objective = self.interface.Objective(0)
        self.assertRaises(Exception, objective.get_linear_coefficients, [])

    def test_objective_set_linear_coefficients_raises(self):
        objective = self.interface.Objective(0)
        self.assertRaises(Exception, objective.set_linear_coefficients, {})

    def test_objective_value_is_none(self):
        objective = self.interface.Objective(0)
        self.assertIs(objective.value, None)

    def test_invalid_name_raises(self):
        with self.assertRaises(Exception):
            self.interface.Objective(1, name="")
        with self.assertRaises(Exception):
            self.interface.Objective(1, name="This space")
        with self.assertRaises(Exception):
            self.interface.Objective(1, name="This\ttab")

    def test_new_invalid_name_raises(self):
        obj = self.interface.Objective(1, name="MyObjective")
        with self.assertRaises(Exception):
            obj.name = ""
        with self.assertRaises(Exception):
            obj.name = "This space"
        with self.assertRaises(Exception):
            obj.name = "This\ttab"

    def test_construct_with_sloppy(self):
        x, y, z, w = self.model.variables[:4]
        obj = self.interface.Objective(
            symbolics.add([symbolics.mul((symbolics.One, var)) for var in [x, y, z]]),
            direction="min",
            sloppy=True
        )
        self.model.objective = obj

        self.assertTrue(obj.get_linear_coefficients([x, y, z, w]) == {x: 1, y: 1, z: 1, w: 0})


class AbstractModelTestCase(unittest.TestCase, metaclass=abc.ABCMeta):

    def setUp(self):
        with open(TESTMODELPATH) as infile:
            self.model = self.interface.Model.from_json(json.load(infile))

    def test_create_empty_model(self):
        model = self.interface.Model()
        self.assertEqual(len(model.constraints), 0)
        self.assertEqual(len(model.variables), 0)
        self.assertEqual(model.objective.expression - 0, 0)

    @abc.abstractmethod
    def test_pickle_ability(self):
        pass

    def test_pickle_empty_model(self):
        model = self.interface.Model()
        self.assertEqual(model.objective.expression - 0, 0)
        self.assertEqual(len(model.variables), 0)
        self.assertEqual(len(model.constraints), 0)
        pickle_string = pickle.dumps(model)
        from_pickle = pickle.loads(pickle_string)
        self.assertEqual(from_pickle.objective.expression - 0, 0)
        self.assertEqual(len(from_pickle.variables), 0)
        self.assertEqual(len(from_pickle.constraints), 0)

    def test_copy(self):
        self.model.optimize()
        value = self.model.objective.value
        model_copy = copy.copy(self.model)
        self.assertIsNot(self.model, model_copy)
        model_copy.optimize()
        self.assertAlmostEqual(value, model_copy.objective.value)
        self.assertEqual([(var.lb, var.ub, var.name, var.type) for var in model_copy.variables.values()],
                         [(var.lb, var.ub, var.name, var.type) for var in self.model.variables.values()])
        self.assertEqual([(constr.lb, constr.ub, constr.name) for constr in model_copy.constraints],
                         [(constr.lb, constr.ub, constr.name) for constr in self.model.constraints])

    def test_deepcopy(self):
        self.model.optimize()
        value = self.model.objective.value
        model_copy = copy.deepcopy(self.model)
        self.assertIsNot(self.model, model_copy)
        self.assertIsNot(self.model.problem, model_copy.problem)
        model_copy.optimize()
        self.assertAlmostEqual(value, model_copy.objective.value)
        self.assertEqual([(var.lb, var.ub, var.name, var.type) for var in model_copy.variables.values()],
                         [(var.lb, var.ub, var.name, var.type) for var in self.model.variables.values()])
        self.assertEqual([(constr.lb, constr.ub, constr.name) for constr in model_copy.constraints],
                         [(constr.lb, constr.ub, constr.name) for constr in self.model.constraints])

    @abc.abstractmethod
    def test_config_gets_copied_too(self):
        pass

    @abc.abstractmethod
    def test_init_from_existing_problem(self):
        pass

    def test_add_variable(self):
        model = self.interface.Model()
        var = self.interface.Variable('x')
        self.assertEqual(var.problem, None)
        model.add(var)
        self.assertTrue(var in model.variables)
        self.assertEqual(model.variables['x'].problem, var.problem)
        self.assertEqual(model.variables['x'].problem, model)
        var = self.interface.Variable('y', lb=-13)
        model.add(var)
        self.assertTrue(var in model.variables)
        self.assertEqual(model.variables['x'].lb, None)
        self.assertEqual(model.variables['x'].ub, None)
        self.assertEqual(model.variables['y'].lb, -13)
        self.assertEqual(model.variables['x'].ub, None)

    def test_add_integer_var(self):
        var = self.interface.Variable('int_var', lb=-13, ub=499., type='integer')
        self.model.add(var)
        self.assertEqual(self.model.variables['int_var'].type, 'integer')
        self.assertEqual(self.model.variables['int_var'].ub, 499.)
        self.assertEqual(self.model.variables['int_var'].lb, -13)

    @abc.abstractmethod
    def test_add_non_cplex_conform_variable(self):
        pass

    def test_remove_variable(self):
        var = self.model.variables[0]
        self.assertEqual(var.problem, self.model)
        self.model.remove(var)
        self.assertNotIn(var, self.model.variables)
        self.assertEqual(var.problem, None)

    def test_remove_variable_str(self):
        var = self.model.variables.values()[0]
        self.model.remove(var.name)
        self.assertNotIn(var, self.model.variables)
        self.assertEqual(var.problem, None)

    def test_add_constraints(self):
        x = self.interface.Variable('x', type='binary')
        y = self.interface.Variable('y', lb=-181133.3, ub=12000., type='continuous')
        z = self.interface.Variable('z', lb=0., ub=3, type='integer')
        constr1 = self.interface.Constraint(0.3 * x + 0.4 * y + 66. * z, lb=-100, ub=0., name='test')
        constr2 = self.interface.Constraint(2.333 * x + y + 3.333, ub=100.33, name='test2')
        constr3 = self.interface.Constraint(2.333 * x + y + z, ub=100.33, lb=-300)
        constr4 = self.interface.Constraint(77 * x, lb=10, name='Mul_constraint')
        constr5 = self.interface.Constraint(x, ub=-10, name='Only_var_constraint')
        constr6 = self.interface.Constraint(3, ub=88., name='Number_constraint')
        self.model.add(constr1)
        self.model.update()
        self.model.add(constr2)
        self.model.update()
        self.model.add(constr3, sloppy=True)
        self.model.update()
        self.model.add([constr4, constr5, constr6])
        self.model.update()
        self.assertIn(constr1.name, self.model.constraints)
        self.assertIn(constr2.name, self.model.constraints)
        self.assertIn(constr3.name, self.model.constraints)
        self.assertIn(constr4.name, self.model.constraints)
        self.assertIn(constr5.name, self.model.constraints)
        self.assertIn(constr6.name, self.model.constraints)

    def test_remove_constraints(self):
        x = self.interface.Variable('x', type='binary')
        y = self.interface.Variable('y', lb=-181133.3, ub=12000., type='continuous')
        z = self.interface.Variable('z', lb=4, ub=4, type='integer')
        constr1 = self.interface.Constraint(0.3 * x + 0.4 * y + 66. * z, lb=-100, ub=0., name='test')
        self.assertEqual(constr1.problem, None)
        self.model.add(constr1)
        self.model.update()
        self.assertEqual(constr1.problem, self.model)
        self.assertIn(constr1, self.model.constraints)
        self.model.remove(constr1.name)
        self.model.update()
        self.assertEqual(constr1.problem, None)
        self.assertNotIn(constr1, self.model.constraints)

    def test_add_nonlinear_constraint_raises(self):
        x = self.interface.Variable('x', type='binary')
        y = self.interface.Variable('y', lb=-181133.3, ub=12000., type='continuous')
        z = self.interface.Variable('z', lb=3, ub=3, type='integer')
        with self.assertRaises(ValueError):
            constraint = self.interface.Constraint(0.3 * x + 0.4 * y ** x + 66. * z, lb=-100, ub=0., name='test')
            self.model.add(constraint)
            self.model.update()

    def test_objective_get_linear_coefficients(self):
        coefs = self.model.objective.get_linear_coefficients(self.model.variables)
        self.assertEqual(len(coefs), len(self.model.variables))
        expr = sum(c * v for v, c in coefs.items())
        self.assertEqual((expr - self.model.objective.expression).expand() - 0, 0)

    def test_constraint_get_linear_coefficients(self):
        constraint = self.model.constraints[5]
        coefs = constraint.get_linear_coefficients(self.model.variables)
        self.assertEqual(len(coefs), len(self.model.variables))
        expr = sum(c * v for v, c in coefs.items())
        self.assertEqual((expr - constraint.expression).expand() - 0, 0)

    @abc.abstractmethod
    def test_change_of_constraint_is_reflected_in_low_level_solver(self):
        pass

    @abc.abstractmethod
    def test_constraint_set_problem_to_None_caches_the_latest_expression_from_solver_instance(self):
        pass

    @abc.abstractmethod
    def test_change_of_objective_is_reflected_in_low_level_solver(self):
        pass

    @abc.abstractmethod
    def test_change_variable_bounds(self):
        pass

    def test_change_variable_type(self):
        self.model.variables[-1].type = "integer"
        self.assertEqual(self.model.variables[-1].type, "integer")

    @abc.abstractmethod
    def test_change_constraint_bounds(self):
        pass

    def test_initial_objective(self):
        self.assertEqual(self.model.objective.expression, 1.0 * self.model.variables["R_Biomass_Ecoli_core_w_GAM"])

    def test_set_objective_in_constructor(self):
        var = self.interface.Variable("x", ub=5)
        obj = self.interface.Objective(var, direction="max")
        model = self.interface.Model(objective=obj, variables=[var])
        model.optimize()
        self.assertIs(model.objective, obj)
        self.assertAlmostEqual(model.objective.value, 5)


    def test_optimize(self):
        self.model.optimize()
        self.assertEqual(self.model.status, 'optimal')
        self.assertAlmostEqual(self.model.objective.value, 0.8739215069684303)

    def test_optimize_milp(self):
        with open(TESTMILPMODELPATH) as infile:
            milp_model = self.interface.Model.from_json(json.load(infile))
        milp_model.optimize()
        self.assertEqual(milp_model.status, 'optimal')
        self.assertAlmostEqual(milp_model.objective.value, 122.5)
        for variable in milp_model.variables:
            if variable.type == 'integer':
                self.assertEqual(variable.primal % 1, 0)

    def test_change_objective(self):
        v1, v2 = self.model.variables.values()[0:2]
        self.model.objective = self.interface.Objective(1. * v1 + 1. * v2)
        self.assertEqual(self.model.objective.direction, "max")
        self.assertEqual(
            float((self.model.objective.expression -
             (1.0 * self.model.variables["R_PGK"] + 1.0 * self.model.variables["R_Biomass_Ecoli_core_w_GAM"])).expand()),
            0.
        )
        self.model.objective = self.interface.Objective(v1 + v2)
        self.assertEqual(self.model.objective.direction, "max")
        self.assertEqual(
            float((self.model.objective.expression -
             (1.0 * self.model.variables["R_PGK"] + 1.0 * self.model.variables["R_Biomass_Ecoli_core_w_GAM"])).expand()),
            0.
        )

    def test_number_objective(self):
        self.model.objective = self.interface.Objective(0.)
        self.assertEqual(self.model.objective.expression - 0, 0)
        self.assertEqual(self.model.objective.direction, "max")
        self.assertEqual(self.model.optimize(), "optimal")

    def test_raise_on_non_linear_objective(self):
        """Test that an exception is raised when a non-linear objective is added to the model."""
        v1, v2 = self.model.variables.values()[0:2]
        self.assertRaises(ValueError, self.interface.Objective, v1 * v2 ** 3)

    @abc.abstractmethod
    def test_iadd_objective(self):
        pass

    @abc.abstractmethod
    def test_imul_objective(self):
        pass

    @abc.abstractmethod
    def test_set_copied_objective(self):
        pass

    @abc.abstractmethod
    def test_timeout(self):
        pass

    @abc.abstractmethod
    def test_set_linear_coefficients_objective(self):
        pass

    def test_instantiating_model_with_different_solver_problem_raises(self):
        self.assertRaises(TypeError, self.interface.Model, problem='Chicken soup')

    @abc.abstractmethod
    def test_set_linear_coefficients_constraint(self):
        pass

    def test_primal_values(self):
        self.model.optimize()
        primals = self.model.primal_values
        for var in self.model.variables:
            self.assertEqual(var.primal, primals[var.name])
        self.assertEqual(set(var.name for var in self.model.variables), set(primals))

    def test_reduced_costs(self):
        self.model.optimize()
        reduced_costs = self.model.reduced_costs
        for var in self.model.variables:
            self.assertEqual(var.dual, reduced_costs[var.name])
        self.assertEqual(set(var.name for var in self.model.variables), set(reduced_costs))

    def test_dual_values(self):
        self.model.optimize()
        constraint_primals = self.model.constraint_values  # TODO Fix this method name
        for constraint in self.model.constraints:
            self.assertEqual(constraint.primal, constraint_primals[constraint.name])
        self.assertEqual(set(const.name for const in self.model.constraints), set(constraint_primals))

    def test_shadow_prices(self):
        self.model.optimize()
        shadow_prices = self.model.shadow_prices
        for constraint in self.model.constraints:
            self.assertEqual(constraint.dual, shadow_prices[constraint.name])
        self.assertEqual(set(const.name for const in self.model.constraints), set(shadow_prices))

    def test_change_objective_can_handle_removed_vars(self):
        self.model.objective = self.interface.Objective(self.model.variables[0])
        self.model.remove(self.model.variables[0])
        self.model.update()
        self.model.objective = self.interface.Objective(self.model.variables[1] * 2)

    def test_clone_model_with_json(self):
        self.assertEqual(self.model.configuration.verbosity, 0)
        self.model.configuration.verbosity = 3
        self.model.optimize()
        opt = self.model.objective.value
        cloned_model = self.interface.Model.clone(self.model)
        self.assertEqual(cloned_model.configuration.verbosity, 3)
        self.assertEqual(len(cloned_model.variables), len(self.model.variables))
        self.assertEqual(len(cloned_model.constraints), len(self.model.constraints))
        cloned_model.optimize()
        self.assertAlmostEqual(cloned_model.objective.value, opt)

    def test_clone_model_with_lp(self):
        self.assertEqual(self.model.configuration.verbosity, 0)
        self.model.configuration.verbosity = 3
        self.model.optimize()
        opt = self.model.objective.value
        cloned_model = self.interface.Model.clone(self.model, use_lp=True)
        self.assertEqual(cloned_model.configuration.verbosity, 3)
        self.assertEqual(len(cloned_model.variables), len(self.model.variables))
        self.assertEqual(len(cloned_model.constraints), len(self.model.constraints))
        cloned_model.optimize()
        self.assertAlmostEqual(cloned_model.objective.value, opt)

    def test_clone_model_without_json(self):
        self.assertEqual(self.model.configuration.verbosity, 0)
        self.model.configuration.verbosity = 3
        self.model.optimize()
        opt = self.model.objective.value
        cloned_model = self.interface.Model.clone(self.model, use_json=False)
        self.assertEqual(cloned_model.configuration.verbosity, 3)
        self.assertEqual(len(cloned_model.variables), len(self.model.variables))
        self.assertEqual(len(cloned_model.constraints), len(self.model.constraints))
        cloned_model.optimize()
        print(cloned_model.objective.expression)
        print(cloned_model.objective.value)
        self.assertAlmostEqual(cloned_model.objective.value, opt)

    def test_remove_variable_not_in_model_raises(self):
        var = self.interface.Variable("test")
        self.assertRaises(Exception, self.model._remove_variables, [var])

    def test_objective_set_linear_coefficients(self):
        x = self.interface.Variable("x", lb=0)
        y = self.interface.Variable("y", lb=0)
        c1 = self.interface.Constraint((y + 2 * (x - 3)).expand(), ub=0)
        c2 = self.interface.Constraint(y + (1 / 2) * x - 3, ub=0)
        obj = self.interface.Objective(x)
        model = self.interface.Model()
        model.add([c1, c2])
        model.objective = obj

        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, 3)
        self.assertAlmostEqual(y.primal, 0)

        obj.set_linear_coefficients({y: 1})
        self.assertEqual(float((obj.expression - (x + y)).expand()), 0.0)
        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, 2)
        self.assertAlmostEqual(y.primal, 2)

        obj.set_linear_coefficients({x: 0})
        self.assertEqual(float((obj.expression - y).expand()), 0.0)
        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, 0)
        self.assertAlmostEqual(y.primal, 3)

    def test_constraint_set_linear_coefficients(self):
        x = self.interface.Variable("x", lb=0, ub=1000)
        y = self.interface.Variable("y", lb=0)
        c1 = self.interface.Constraint(y, ub=1)
        obj = self.interface.Objective(x)
        model = self.interface.Model()
        model.add([c1])
        model.objective = obj

        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, x.ub)

        c1.set_linear_coefficients({x: 1})
        self.assertEqual((c1.expression - (x + y)).expand() - 0, 0)
        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, 1)

        c1.set_linear_coefficients({x: 2})
        self.assertEqual((c1.expression - (2 * x + y)).expand() - 0, 0)
        self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
        self.assertAlmostEqual(x.primal, 0.5)

    def test_objective_handles_constants_2(self):
        offset = 2
        self.model.optimize()
        optimal = self.model.objective.value
        objective = self.interface.Objective(self.model.objective.expression + offset)
        self.model.objective = objective
        self.model.optimize()
        self.assertAlmostEqual(self.model.objective.value, optimal + offset)

    # def test_objective_handles_constants_3(self):
    #     offset = 3
    #     self.model.optimize()
    #     optimal = self.model.objective.value
    #     self.model.objective += offset
    #     self.model.optimize()
    #     self.assertEqual(self.model.objective.value, optimal + offset)

    def test_objective_expression_includes_constant(self):
        objective = self.model.objective
        self.model.objective = self.interface.Objective(objective.expression + 3)
        self.model.update()
        self.assertEqual(
            float((self.model.objective.expression -
                  (objective.expression + 3.)).expand().evalf()),
            0.
        )

    def test_is_integer(self):
        model = self.model
        self.assertFalse(model.is_integer)
        self.assertFalse(optlang.interface.Model.is_integer.fget(model))

        model.variables[0].type = "integer"
        self.assertTrue(model.is_integer)
        self.assertTrue(optlang.interface.Model.is_integer.fget(model))

        model.variables[0].type = "continuous"
        model.variables[1].type = "binary"
        self.assertTrue(model.is_integer)

        model.variables[1].type = "continuous"
        self.assertFalse(model.is_integer)

    def test_binary_variables(self):
        model = self.interface.Model()
        var = self.interface.Variable("x", type="binary")
        obj = self.interface.Objective(var)
        model.objective = obj

        for lb, ub in ((0, 0), (0, 1), (1, 1)):
            var.ub = ub
            var.lb = lb

            obj.direction = "max"
            model.optimize()
            self.assertAlmostEqual(var.primal, ub)

            obj.direction = "min"
            model.optimize()
            self.assertAlmostEqual(var.primal, lb)

    def test_integer_variable_dual(self):
        model = self.interface.Model()
        x = self.interface.Variable("x", lb=0)
        y = self.interface.Variable("y", lb=0)
        c = self.interface.Constraint(x + y, ub=1)
        model.add(c)
        model.objective = self.interface.Objective(x)

        model.optimize()
        self.assertEqual(y.dual, -1)

        x.type = "integer"
        model.optimize()
        self.assertRaises(ValueError, partial(getattr, y, "dual"))

        x.type = "continuous"
        model.optimize()
        self.assertEqual(y.dual, -1)

    def test_integer_constraint_dual(self):
        model = self.interface.Model()
        x = self.interface.Variable("x")
        c = self.interface.Constraint(x, ub=1)
        model.add(c)
        model.objective = self.interface.Objective(x)

        model.optimize()
        self.assertEqual(c.dual, 1)

        x.type = "integer"
        model.optimize()
        self.assertRaises(ValueError, partial(getattr, c, "dual"))

        x.type = "continuous"
        model.optimize()
        self.assertEqual(c.dual, 1)

    def test_integer_batch_duals(self):
        model = self.interface.Model()
        x = self.interface.Variable("x")
        c = self.interface.Constraint(x, ub=1)
        model.add(c)
        model.objective = self.interface.Objective(x)
        model.optimize()

        self.assertEqual(model.reduced_costs[x.name], 0)
        self.assertEqual(model.shadow_prices[c.name], 1)

        x.type = "integer"
        model.optimize()

        with self.assertRaises(ValueError):
            model.reduced_costs
        with self.assertRaises(ValueError):
            model.shadow_prices

        x.type = "continuous"
        model.optimize()

        self.assertEqual(model.reduced_costs[x.name], 0)
        self.assertEqual(model.shadow_prices[c.name], 1)

    def test_large_objective(self):
        model = self.interface.Model()
        model.add([self.interface.Variable(str(i), lb=1) for i in range(1100)])
        model.optimize()

        obj = self.interface.Objective(
            optlang.symbolics.add([optlang.symbolics.mul((optlang.symbolics.One, v)) for v in model.variables]),
            direction="min"
        )
        model.objective = obj
        model.optimize()
        self.assertAlmostEqual(model.objective.value, len(model.variables))

    def test_implicitly_convert_milp_to_lp(self):
        model = self.interface.Model()
        var1 = self.interface.Variable("x", ub=1)
        var2 = self.interface.Variable("y", type="integer")
        var3 = self.interface.Variable("z", type="integer")
        model.add([var1, var2])
        model.optimize()
        self.assertTrue(model.is_integer)

        var2.type = "continuous"
        model.optimize()
        self.assertAlmostEqual(model.reduced_costs["x"], 0)

        var2.type = "integer"
        model.add(var3)
        model.optimize()
        self.assertTrue(model.is_integer)

        model.remove(var2)
        model.optimize()
        model.remove(var3)
        model.optimize()
        self.assertAlmostEqual(model.reduced_costs["x"], 0)

    def test_change_constraint_name(self):
        model = self.model
        const = model.constraints[0]
        old_name = const.name
        new_name = "test"
        const.name = new_name
        self.assertEqual(const.name, new_name)
        const.name = old_name
        self.assertEqual(const.name, old_name)


class AbstractConfigurationTestCase(unittest.TestCase, metaclass=abc.ABCMeta):

    def setUp(self):
        pass

    def test_tolerance_parameters(self):
        model = self.interface.Model()
        params = dir(model.configuration.tolerances)
        for param in params:
            val = getattr(model.configuration.tolerances, param)
            print(val)
            setattr(model.configuration.tolerances, param, 2 * val)
            self.assertEqual(
                getattr(model.configuration.tolerances, param), 2 * val
            )


class AbstractQuadraticProgrammingTestCase(unittest.TestCase, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def setUp(self):
        pass

    @abc.abstractmethod
    def test_convex_obj(self):
        pass

    @abc.abstractmethod
    def test_non_convex_obj(self):
        pass

    @abc.abstractmethod
    def test_qp_convex(self):
        pass

    @abc.abstractmethod
    def test_qp_non_convex(self):
        pass
