# Copyright 2013 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 unittest

import optlang
from optlang.duality import convert_linear_problem_to_dual
from optlang.glpk_interface import Constraint, Model, Objective, Variable


class DualityTestCase(unittest.TestCase):
    def setUp(self):
        x = Variable("x", lb=0)
        y = Variable("y", lb=0)
        z = Variable("z", lb=0)

        c1 = Constraint(x, ub=10)
        c2 = Constraint(y - 2 * z, lb=0, ub=0)
        c3 = Constraint(x - z, lb=3, ub=15)

        obj = Objective(x + y + z)

        model = Model()
        model.add([c1, c2, c3])
        model.objective = obj
        self.model = model
        self.c1 = c1

    def test_dual_value_equal_primal(self):
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)

    def test_making_primal_unbounded(self):
        self.model.remove(self.c1)
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()
        self.assertEqual(primal_status, optlang.interface.UNBOUNDED)
        self.assertEqual(dual_status, optlang.interface.INFEASIBLE)

    def test_making_primal_infeasible(self):
        # Note: The dual of an infeasible primal will be either unbounded or infeasible. In this case it is unbounded.
        c4 = Constraint(self.model.variables["z"], lb=100)
        self.model.add(c4)
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()
        self.assertEqual(primal_status, optlang.interface.INFEASIBLE)
        self.assertEqual(dual_status, optlang.interface.UNBOUNDED)

    def test_non_standard_raises(self):
        self.model.variables[0].lb = -10
        self.assertRaises(ValueError, convert_linear_problem_to_dual, self.model)

    def test_non_linear_raises(self):
        model = optlang.interface.Model.clone(self.model)  # GLPK interface doesn't allow non-linear constraints
        c4 = optlang.interface.Constraint(2 * model.variables["x"]**2, ub=7)
        model.add(c4)
        self.assertRaises(ValueError, convert_linear_problem_to_dual, model)

    def test_non_continuous_raises(self):
        self.model.variables["z"].type = "integer"
        self.assertRaises(ValueError, convert_linear_problem_to_dual, self.model)

    def test_infinity(self):
        dual = convert_linear_problem_to_dual(self.model, infinity=1000)
        for var in dual.variables:
            self.assertTrue(var.ub <= 1000)
            self.assertTrue(var.lb >= -1000)

    def test_dual_is_standard_form(self):
        dual = convert_linear_problem_to_dual(self.model, infinity=1000)
        dual_2 = convert_linear_problem_to_dual(self.model, maintain_standard_form=False, infinity=1000)
        self.assertTrue(all(var.lb >= 0 for var in dual.variables))
        self.assertFalse(all(var.lb >= 0 for var in dual_2.variables))

    def test_variable_with_positive_lower_bound(self):
        self.model.variables["x"].lb = 1
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)

    def test_variable_with_upper_bound(self):
        self.model.variables["x"].ub = 4
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 7)
        self.assertEqual(dual.objective.value, 7)

    def test_with_minimization(self):
        self.model.objective = Objective(self.model.objective.expression * -1, direction="min")
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, -31)
        self.assertEqual(dual.objective.value, -31)

    def test_equality_constraint_not_zero(self):
        c2 = self.model.constraints[1]
        c2.ub = 1
        c2.lb = 1
        dual = convert_linear_problem_to_dual(self.model, maintain_standard_form=False)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 32)
        self.assertEqual(dual.objective.value, 32)

    def test_empty_constraint(self):
        c4 = Constraint(0, lb=0, ub=13)
        self.model.add(c4)
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)

    def test_free_constraint(self):
        c4 = Constraint(self.model.variables["x"], lb=None, ub=None)
        self.model.add(c4)
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)

    def test_zero_bound_variable(self):
        w = Variable("w", lb=0, ub=0)
        self.model.add(w)
        dual = convert_linear_problem_to_dual(self.model)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)

    def test_explicit_model(self):
        dual = Model()
        convert_linear_problem_to_dual(self.model, dual_model=dual)
        primal_status = self.model.optimize()
        dual_status = dual.optimize()

        self.assertEqual(primal_status, optlang.interface.OPTIMAL)
        self.assertEqual(dual_status, optlang.interface.OPTIMAL)

        self.assertEqual(self.model.objective.value, 31)
        self.assertEqual(dual.objective.value, 31)
