# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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 pyomo.common.unittest as unittest

from pyomo.opt import (
    TerminationCondition,
    SolutionStatus,
    SolverStatus,
    check_available_solvers,
)
import pyomo.environ as pyo
import pyomo.kernel as pmo
import sys

diff_tol = 1e-3

mosek_available = check_available_solvers('mosek_direct')
msk_version = [0]
if mosek_available:
    msk_version = pyo.SolverFactory('mosek')._version


@unittest.skipIf(not mosek_available, "MOSEK's python bindings are missing.")
@unittest.pytest.mark.solver("mosek_persistent")
class MOSEKPersistentTests(unittest.TestCase):
    def setUp(self):
        self.stderr = sys.stderr
        sys.stderr = None

    def tearDown(self):
        sys.stderr = self.stderr

    def test_interface_call(self):
        interface_instance = type(pyo.SolverFactory('mosek_persistent'))
        alt_1 = pyo.SolverFactory('mosek', solver_io='persistent')
        self.assertIsInstance(alt_1, interface_instance)

    def test_variable_removal(self):
        m = pyo.ConcreteModel()
        m.x = pyo.Var()
        m.y = pyo.Var()
        opt = pyo.SolverFactory('mosek_persistent')
        opt.set_instance(m)

        self.assertEqual(opt._solver_model.getnumvar(), 2)

        opt.remove_var(m.x)
        self.assertEqual(opt._solver_model.getnumvar(), 1)

        opt.remove_var(m.y)
        self.assertEqual(opt._solver_model.getnumvar(), 0)
        self.assertRaises(ValueError, opt.remove_var, m.x)

        opt.add_var(m.x)
        opt.add_var(m.y)
        self.assertEqual(opt._solver_model.getnumvar(), 2)
        opt.remove_vars(m.x, m.y)
        self.assertEqual(opt._solver_model.getnumvar(), 0)

    def test_constraint_removal_1(self):
        m = pyo.ConcreteModel()
        m.x = pyo.Var()
        m.y = pyo.Var()
        m.z = pyo.Var()
        m.c1 = pyo.Constraint(expr=2 * m.x >= m.y**2)
        m.c2 = pyo.Constraint(expr=m.x**2 >= m.y**2 + m.z**2)
        m.c3 = pyo.Constraint(expr=m.z >= 0)
        m.c4 = pyo.Constraint(expr=m.x + m.y >= 0)
        opt = pyo.SolverFactory('mosek_persistent')
        opt.set_instance(m)

        self.assertEqual(opt._solver_model.getnumcon(), 4)

        opt.remove_constraint(m.c1)
        self.assertEqual(opt._solver_model.getnumcon(), 3)

        opt.remove_constraints(m.c2, m.c3)
        self.assertEqual(opt._solver_model.getnumcon(), 1)

        opt.add_constraint(m.c1)
        self.assertEqual(opt._solver_model.getnumcon(), 2)
        self.assertRaises(ValueError, opt.remove_constraint, m.c2)

    @unittest.skipIf(
        msk_version[0] > 9, "MOSEK 10 does not (yet) have a removeacc method."
    )
    def test_constraint_removal_2(self):
        m = pmo.block()
        m.x = pmo.variable()
        m.y = pmo.variable()
        m.z = pmo.variable()
        m.c1 = pmo.conic.rotated_quadratic.as_domain(2, m.x, [m.y])
        m.c2 = pmo.conic.quadratic(m.x, [m.y, m.z])
        m.c3 = pmo.constraint(m.z >= 0)
        m.c4 = pmo.constraint(m.x + m.y >= 0)
        opt = pmo.SolverFactory('mosek_persistent')
        opt.set_instance(m)

        self.assertEqual(opt._solver_model.getnumcon(), 5)
        self.assertEqual(opt._solver_model.getnumcone(), 2)

        opt.remove_block(m.c1)
        self.assertEqual(opt._solver_model.getnumcon(), 2)
        self.assertEqual(opt._solver_model.getnumcone(), 1)

        opt.remove_constraints(m.c2, m.c3)
        self.assertEqual(opt._solver_model.getnumcon(), 1)
        self.assertEqual(opt._solver_model.getnumcone(), 0)

        self.assertRaises(ValueError, opt.remove_constraint, m.c2)
        opt.add_constraint(m.c2)
        opt.add_block(m.c1)
        self.assertEqual(opt._solver_model.getnumcone(), 2)

    def test_column_addition(self):
        '''
        Test based on lo1.py problem from MOSEK documentation.
        '''
        sol_to_get = [0.0, 0.0, 15.0, 8]

        m = pyo.ConcreteModel()
        m.x = pyo.Var(bounds=(0, None))
        m.y = pyo.Var(bounds=(0, 10))
        m.z = pyo.Var(bounds=(0, None))
        m.c1 = pyo.Constraint(expr=3 * m.x + m.y + 2 * m.z == 30)
        m.c2 = pyo.Constraint(expr=2 * m.x + m.y + 3 * m.z >= 15)
        m.c3 = pyo.Constraint(expr=2 * m.y <= 25)
        m.o = pyo.Objective(expr=3 * m.x + m.y + 5 * m.z, sense=pyo.maximize)
        opt = pyo.SolverFactory('mosek_persistent')
        opt.set_instance(m)

        m.new_var = pyo.Var(bounds=(0, None))
        opt.add_column(m, m.new_var, 1, [m.c2, m.c3], [1, 3])

        self.assertEqual(opt._solver_model.getnumvar(), 4)

        opt.solve(m)
        for i, v in enumerate([m.x, m.y, m.z, m.new_var]):
            with self.subTest(i=v.name):
                self.assertAlmostEqual(v.value, sol_to_get[i], places=0)

    def test_variable_update(self):
        '''
        Test based on milo1.py problem from MOSEK documentation.
        '''
        cont_sol_to_get = [1.948, 4.922]
        int_sol_to_get = [5, 0]

        m = pyo.ConcreteModel()
        m.x = pyo.Var()
        m.y = pyo.Var()
        m.c1 = pyo.Constraint(expr=50 * m.x + 31 * m.y <= 250)
        m.c2 = pyo.Constraint(expr=3 * m.x - 2 * m.y >= -4)
        m.o = pyo.Objective(expr=m.x + 0.64 * m.y, sense=pyo.maximize)
        opt = pyo.SolverFactory('mosek_persistent')
        opt.set_instance(m)
        opt.solve(m)

        self.assertAlmostEqual(m.x.value, cont_sol_to_get[0], places=2)
        self.assertAlmostEqual(m.y.value, cont_sol_to_get[1], places=2)

        m.x.setlb = 0
        m.x.setub = None
        m.x.domain = pyo.Integers
        m.y.setlb = 0
        m.y.setub = None
        m.y.domain = pyo.Integers
        m.z = pyo.Var()

        opt.update_vars(m.x, m.y)
        self.assertRaises(ValueError, opt.update_var, m.z)
        opt.add_var(m.z)

        opt.solve(m)
        self.assertAlmostEqual(m.x.value, int_sol_to_get[0], places=1)
        self.assertAlmostEqual(m.y.value, int_sol_to_get[1], places=1)


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