import tempfile
import unittest
import highspy
from highspy.highs import highs_linear_expression, qsum
import numpy as np
from sys import platform
import signal

class TestHighsPy(unittest.TestCase):
    def assertArrayEqual(self, first, second, msg=None):
        first, second = list(first), list(second)
        self.assertEqual(len(first), len(second), msg)
        for a, b in zip(first, second):
            self.assertEqual(a, b, msg)

    def assertArrayAlmostEqual(self, first, second, places=7, msg=None):
        first, second = list(first), list(second)
        self.assertEqual(len(first), len(second), msg)
        for a, b in zip(first, second):
            self.assertAlmostEqual(a, b, places, msg)

    def assertMappingAlmostEqual(self, first, second, places=7, msg=None):
        self.assertIsInstance(first, dict)
        self.assertIsInstance(second, dict)
        self.assertEqual(first.keys(), second.keys(), msg)
        for k in first:
            if isinstance(first[k], dict):
                self.assertMappingAlmostEqual(first[k], second[k], places, msg)
            else:
                self.assertAlmostEqual(first[k], second[k], places, msg)

    def assertEqualExpr(self, expr, idxs, vals, constant=None, bounds=None):
        self.assertEqual(list(map(int, expr.idxs)), list(map(int, idxs)), "variable index")
        self.assertArrayEqual(expr.vals, vals, "variable values")
        self.assertEqual(expr.constant, constant, "constant")
        self.assertEqual(expr.bounds, (bounds[0], bounds[1]) if bounds is not None else None, "bounds")

    def get_basic_model(self):
        """
        min y
        s.t.
        -x + y >= 2
         x + y >= 0
        """
        inf = highspy.kHighsInf
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)
        h.addVars(2, np.array([-inf, -inf]), np.array([inf, inf]))
        h.changeColsCost(2, np.array([0, 1]), np.array([0, 1], dtype=np.double))
        num_cons = 2
        lower = np.array([2, 0], dtype=np.double)
        upper = np.array([inf, inf], dtype=np.double)
        num_new_nz = 4
        starts = np.array([0, 2])
        indices = np.array([0, 1, 0, 1])
        values = np.array([-1, 1, 1, 1], dtype=np.double)
        h.addRows(num_cons, lower, upper, num_new_nz, starts, indices, values)
        return h

    def get_example_model(self):
        """
        minimize    f  =  x0 +  x1
        subject to              x1 <= 7
                    5 <=  x0 + 2x1 <= 15
                    6 <= 3x0 + 2x1
                    0 <= x0 <= 4; 1 <= x1
        """
        inf = highspy.kHighsInf
        h = highspy.Highs()
        # Define a HighsLp instance
        lp = highspy.HighsLp()
        lp.num_col_ = 2
        lp.num_row_ = 3
        lp.col_cost_ = np.array([1, 1], dtype=np.double)
        lp.col_lower_ = np.array([0, 1], dtype=np.double)
        lp.col_upper_ = np.array([4, inf], dtype=np.double)
        lp.row_lower_ = np.array([-inf, 5, 6], dtype=np.double)
        lp.row_upper_ = np.array([7, 15, inf], dtype=np.double)
        lp.a_matrix_.start_ = np.array([0, 2, 5])
        lp.a_matrix_.index_ = np.array([1, 2, 0, 1, 2])
        lp.a_matrix_.value_ = np.array([1, 3, 1, 2, 2], dtype=np.double)
        h.passModel(lp)
        return h

    def test_example_model_builder(self):
        """
        minimize    f  =  x0 +  x1
        subject to              x1 <= 7
                    5 <=  x0 + 2x1 <= 15
                    6 <= 3x0 + 2x1
                    0 <= x0 <= 4; 1 <= x1
        """
        h = highspy.Highs()

        x0 = h.addVariable(lb=0, ub=4, obj=1)
        x1 = h.addVariable(lb=1, ub=7, obj=1)

        h.addConstr(5 <= x0 + 2 * x1 <= 15)
        h.addConstr(6 <= 3 * x0 + 2 * x1)

        lp = h.getLp()

        self.assertEqual(lp.num_col_, 2)
        self.assertEqual(lp.num_row_, 2)
        self.assertAlmostEqual(lp.col_cost_[0], 1)
        self.assertAlmostEqual(lp.col_lower_[0], 0)
        self.assertAlmostEqual(lp.col_upper_[0], 4)
        self.assertAlmostEqual(lp.row_lower_[0], 5)
        self.assertAlmostEqual(lp.row_upper_[0], 15)
        self.assertAlmostEqual(lp.row_lower_[1], 6)
        self.assertAlmostEqual(lp.row_upper_[1], highspy.kHighsInf)

    def get_infeasible_model(self):
        inf = highspy.kHighsInf
        lp = highspy.HighsLp()
        lp.num_col_ = 2
        lp.num_row_ = 2
        lp.col_cost_ = np.array([10, 15], dtype=np.double)
        lp.col_lower_ = np.array([0, 0], dtype=np.double)
        lp.col_upper_ = np.array([inf, inf], dtype=np.double)
        lp.row_lower_ = np.array([3, 1], dtype=np.double)
        lp.row_upper_ = np.array([3, 1], dtype=np.double)
        lp.a_matrix_.start_ = np.array([0, 2, 4])
        lp.a_matrix_.index_ = np.array([0, 1, 0, 1])
        lp.a_matrix_.value_ = np.array([2, 1, 1, 3], dtype=np.double)
        lp.offset_ = 0
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)
        status = h.passModel(lp)
        self.assertEqual(status, highspy.HighsStatus.kOk)
        h.setOptionValue("presolve", "off")
        return h

    def test_version(self):
        h = self.get_basic_model()
        self.assertEqual(h.version(), "1.15.0")
        self.assertEqual(h.versionMajor(), 1)
        self.assertEqual(h.versionMinor(), 15)
        self.assertEqual(h.versionPatch(), 0)

    def test_basics(self):
        h = self.get_basic_model()
        h.passColName(0, "Col0")
        h.passColName(1, "Col1")
        h.passRowName(0, "Row0")
        h.passRowName(1, "Row1")
        #        h.setOptionValue('output_flag', True)
        h.writeModel("")
        h.setOptionValue("output_flag", False)
        self.assertEqual(h.setOptionValue("presolve", "off"), highspy.HighsStatus.kOk)
        #        h.setOptionValue('output_flag', True)
        h.run()

        # Info can be obtained from the class instance, specific call
        # and, in the case of objective_function_value,
        # h.getObjectiveValue()
        info = h.getInfo()
        objective_function_value0 = info.objective_function_value
        self.assertAlmostEqual(objective_function_value0, 1)
        [status, objective_function_value1] = h.getInfoValue("objective_function_value")
        self.assertAlmostEqual(objective_function_value0, objective_function_value1)
        self.assertAlmostEqual(h.getObjectiveValue(), objective_function_value0)

        simplex_iteration_count0 = info.simplex_iteration_count
        self.assertAlmostEqual(simplex_iteration_count0, 2)
        [status, simplex_iteration_count1] = h.getInfoValue("simplex_iteration_count")
        self.assertAlmostEqual(simplex_iteration_count0, simplex_iteration_count1)

        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -1)
        self.assertAlmostEqual(sol.col_value[1], 1)

        #        h.setOptionValue('output_flag', False)
        """
        min y
        s.t.
        -x + y >= 3
        x + y >= 0
        """
        inf = highspy.kHighsInf
        h.changeRowBounds(0, 3, inf)
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -1.5)
        self.assertAlmostEqual(sol.col_value[1], 1.5)

        # now make y integer
        h.changeColsIntegrality(1, np.array([1]), np.array([highspy.HighsVarType.kInteger]))
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -1.5)
        self.assertAlmostEqual(sol.col_value[1], 2)

        """
        now delete the first constraint and add a new one

        min y
        s.t.
        x + y >= 0
        -x + y >= 0
        """
        h.deleteRows(1, np.array([0]))
        h.addRows(1, np.array([0], dtype=np.double), np.array([inf]), 2, np.array([0]), np.array([0, 1]), np.array([-1, 1], dtype=np.double))
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], 0)
        self.assertAlmostEqual(sol.col_value[1], 0)

        # change the upper bound of x to -5
        h.changeColsBounds(1, np.array([0]), np.array([-inf], dtype=np.double), np.array([-5], dtype=np.double))
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -5)
        self.assertAlmostEqual(sol.col_value[1], 5)

        # now maximize
        h.changeColCost(1, -1)
        h.changeRowBounds(0, -inf, 0)
        h.changeRowBounds(1, -inf, 0)
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -5)
        self.assertAlmostEqual(sol.col_value[1], -5)

        h.changeColCost(1, 1)
        [status, sense] = h.getObjectiveSense()
        self.assertEqual(sense, highspy.ObjSense.kMinimize)
        h.changeObjectiveSense(highspy.ObjSense.kMaximize)
        [status, sense] = h.getObjectiveSense()
        self.assertEqual(sense, highspy.ObjSense.kMaximize)
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -5)
        self.assertAlmostEqual(sol.col_value[1], -5)

        self.assertAlmostEqual(h.getObjectiveValue(), -5)

        h.changeObjectiveOffset(1)
        [status, offset] = h.getObjectiveOffset()
        self.assertAlmostEqual(offset, 1)
        h.run()
        self.assertAlmostEqual(h.getObjectiveValue(), -4)

        info = h.getInfo()
        mip_node_count0 = info.mip_node_count
        self.assertAlmostEqual(mip_node_count0, 0)
        [status, mip_node_count1] = h.getInfoValue("mip_node_count")
        self.assertEqual(status, highspy.HighsStatus.kOk)
        self.assertAlmostEqual(mip_node_count0, mip_node_count1)

    def test_example(self):
        h = self.get_example_model()
        lp = h.getLp()
        #
        # Extract column 0
        iCol = 0
        [status, cost, lower, upper, get_num_nz] = h.getCol(iCol)
        self.assertEqual(cost, lp.col_cost_[iCol])
        self.assertEqual(lower, lp.col_lower_[iCol])
        self.assertEqual(upper, lp.col_upper_[iCol])
        index = np.empty(get_num_nz)
        value = np.empty(get_num_nz, dtype=np.double)
        [status, index, value] = h.getColEntries(iCol)
        for iEl in range(get_num_nz):
            self.assertEqual(index[iEl], lp.a_matrix_.index_[iEl])
            self.assertEqual(value[iEl], lp.a_matrix_.value_[iEl])
        #
        # Extract columns 0 and 1
        indices = np.array([0, 1])
        [status, get_num_col, cost, lower, upper, get_num_nz] = h.getCols(2, indices)
        for get_col in range(get_num_col):
            iCol = indices[get_col]
            self.assertEqual(cost[get_col], lp.col_cost_[iCol])
            self.assertEqual(lower[get_col], lp.col_lower_[iCol])
            self.assertEqual(upper[get_col], lp.col_upper_[iCol])
        start = np.empty(get_num_col)
        index = np.empty(get_num_nz)
        value = np.empty(get_num_nz, dtype=np.double)
        [status, start, index, value] = h.getColsEntries(2, indices)
        for iCol in range(lp.num_col_):
            self.assertEqual(start[iCol], lp.a_matrix_.start_[iCol])
        for iEl in range(get_num_nz):
            self.assertEqual(index[iEl], lp.a_matrix_.index_[iEl])
            self.assertEqual(value[iEl], lp.a_matrix_.value_[iEl])
        #
        # Extract row 1
        iRow = 1
        [status, lower, upper, get_num_nz] = h.getRow(iRow)
        self.assertEqual(lower, lp.row_lower_[iRow])
        self.assertEqual(upper, lp.row_upper_[iRow])
        index = np.empty(get_num_nz)
        value = np.empty(get_num_nz, dtype=np.double)
        [status, index, value] = h.getRowEntries(iRow)
        #
        # Extract rows 0 and 2
        indices = np.array([0, 2])
        [status, get_num_row, lower, upper, get_num_nz] = h.getRows(2, indices)
        for get_row in range(get_num_row):
            iRow = indices[get_row]
            self.assertEqual(lower[get_row], lp.row_lower_[iRow])
            self.assertEqual(upper[get_row], lp.row_upper_[iRow])
        start = np.empty(get_num_row)
        index = np.empty(get_num_nz)
        value = np.empty(get_num_nz, dtype=np.double)
        [status, start, index, value] = h.getRowsEntries(2, indices)

    def test_options(self):
        h = highspy.Highs()

        # test vanilla get option value method

        [status, output_flag] = h.getOptionValue("output_flag")
        [status, solver] = h.getOptionValue("solver")
        [status, primal_feasibility_tolerance] = h.getOptionValue("primal_feasibility_tolerance")
        [status, simplex_update_limit] = h.getOptionValue("simplex_update_limit")
        self.assertEqual(output_flag, True)
        self.assertEqual(solver, "choose")
        self.assertEqual(primal_feasibility_tolerance, 1e-7)
        self.assertEqual(simplex_update_limit, 5000)
        # Illegal name
        option_value = h.getOptionValue("simplex_limit")
        self.assertEqual(option_value[0], highspy.HighsStatus.kError)

        # test bool option
        [status, type] = h.getOptionType("output_flag")
        self.assertEqual(type, highspy.HighsOptionType.kBool)

        h.setOptionValue("output_flag", True)
        [status, value] = h.getOptionValue("output_flag")
        self.assertTrue(value)
        h.setOptionValue("output_flag", False)
        [status, value] = h.getOptionValue("output_flag")
        self.assertFalse(value)

        # test string option
        [status, type] = h.getOptionType("presolve")
        self.assertEqual(type, highspy.HighsOptionType.kString)
        h.setOptionValue("presolve", "off")
        [status, value] = h.getOptionValue("presolve")
        self.assertEqual(value, "off")
        h.setOptionValue("presolve", "on")
        [status, value] = h.getOptionValue("presolve")
        self.assertEqual(value, "on")

        # test int option
        [status, type] = h.getOptionType("threads")
        self.assertEqual(type, highspy.HighsOptionType.kInt)
        h.setOptionValue("threads", 1)
        [status, value] = h.getOptionValue("threads")
        self.assertEqual(value, 1)
        h.setOptionValue("threads", 2)
        [status, value] = h.getOptionValue("threads")
        self.assertEqual(value, 2)

        # test double option
        [status, type] = h.getOptionType("time_limit")
        self.assertEqual(type, highspy.HighsOptionType.kDouble)
        h.setOptionValue("time_limit", 1.7)
        [status, value] = h.getOptionValue("time_limit")
        self.assertAlmostEqual(float(value), 1.7)
        h.setOptionValue("time_limit", 2.7)
        [status, value] = h.getOptionValue("time_limit")
        self.assertAlmostEqual(float(value), 2.7)

    def test_clear(self):
        h = self.get_basic_model()
        self.assertEqual(h.getNumCol(), 2)
        self.assertEqual(h.getNumRow(), 2)
        self.assertEqual(h.getNumNz(), 4)

        [status, orig_feas_tol] = h.getOptionValue("primal_feasibility_tolerance")
        new_feas_tol = float(orig_feas_tol) + 1
        h.setOptionValue("primal_feasibility_tolerance", new_feas_tol)
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), new_feas_tol)
        h.clear()
        self.assertEqual(h.getNumCol(), 0)
        self.assertEqual(h.getNumRow(), 0)
        self.assertEqual(h.getNumNz(), 0)
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), float(orig_feas_tol))

        h = self.get_basic_model()
        h.setOptionValue("primal_feasibility_tolerance", new_feas_tol)
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), new_feas_tol)
        h.clearModel()
        self.assertEqual(h.getNumCol(), 0)
        self.assertEqual(h.getNumRow(), 0)
        self.assertEqual(h.getNumNz(), 0)
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), new_feas_tol)

        h = self.get_basic_model()
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -1)
        self.assertAlmostEqual(sol.col_value[1], 1)
        h.clearSolver()
        self.assertEqual(h.getNumCol(), 2)
        self.assertEqual(h.getNumRow(), 2)
        self.assertEqual(h.getNumNz(), 4)
        sol = h.getSolution()
        self.assertFalse(sol.value_valid)
        self.assertFalse(sol.dual_valid)

        h = self.get_basic_model()
        [status, orig_feas_tol] = h.getOptionValue("primal_feasibility_tolerance")
        new_feas_tol = float(orig_feas_tol) + 1
        h.setOptionValue("primal_feasibility_tolerance", new_feas_tol)
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), new_feas_tol)
        h.resetOptions()
        [status, value] = h.getOptionValue("primal_feasibility_tolerance")
        self.assertAlmostEqual(float(value), float(orig_feas_tol))

    def test_ranging(self):
        inf = highspy.kHighsInf
        h = self.get_basic_model()
        # Cost ranging
        # c0 2 -1 1 0
        # c1 0 0 inf inf
        #
        ## Bound ranging
        ## Columns
        # c0 1 -inf inf 1
        # c1 1 1 inf 1
        ## Rows
        # r0 -inf -inf inf inf
        # r1 -inf -inf inf inf
        h.run()
        [status, ranging] = h.getRanging()
        self.assertEqual(ranging.col_cost_dn.objective_[0], 2)
        self.assertEqual(ranging.col_cost_dn.value_[0], -1)
        self.assertEqual(ranging.col_cost_up.value_[0], 1)
        self.assertEqual(ranging.col_cost_up.objective_[0], 0)
        self.assertEqual(ranging.col_cost_dn.objective_[1], 0)
        self.assertEqual(ranging.col_cost_dn.value_[1], 0)
        self.assertEqual(ranging.col_cost_up.value_[1], inf)
        self.assertEqual(ranging.col_cost_up.objective_[1], inf)
        #
        self.assertEqual(ranging.col_bound_dn.objective_[0], 1)
        self.assertEqual(ranging.col_bound_dn.value_[0], -inf)
        self.assertEqual(ranging.col_bound_up.value_[0], inf)
        self.assertEqual(ranging.col_bound_up.objective_[0], 1)
        self.assertEqual(ranging.col_bound_dn.objective_[1], 1)
        self.assertEqual(ranging.col_bound_dn.value_[1], 1)
        self.assertEqual(ranging.col_bound_up.value_[1], inf)
        self.assertEqual(ranging.col_bound_up.objective_[1], 1)
        #
        self.assertEqual(ranging.row_bound_dn.objective_[0], -inf)
        self.assertEqual(ranging.row_bound_dn.value_[0], -inf)
        self.assertEqual(ranging.row_bound_up.value_[0], inf)
        self.assertEqual(ranging.row_bound_up.objective_[0], inf)
        self.assertEqual(ranging.row_bound_dn.objective_[1], -inf)
        self.assertEqual(ranging.row_bound_dn.value_[1], -inf)
        self.assertEqual(ranging.row_bound_up.value_[1], inf)
        self.assertEqual(ranging.row_bound_up.objective_[1], inf)

    # this catches error in our highs_bindings (pybind11) when passed arrays are not contiguous
    def test_numpy_slice(self):
        h = highspy.Highs()

        N = 10
        zero, ones = np.zeros(N), np.ones(N)
        h.addCols(N, zero, zero, ones, 0, [], [], [])

        x = np.arange(N, dtype=np.int32)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        tmp = x[1::2]  # [1, 3, 5, 7, 9]

        h.addRow(1, 1, len(tmp), tmp, ones)

        # needs to be rowwise for test to work correctly
        self.assertEqual(h.getLp().a_matrix_.format_, highspy.MatrixFormat.kRowwise)
        self.assertEqual(h.getLp().a_matrix_.index_, list(tmp))

    def test_constraint_removal(self):
        h = highspy.Highs()
        x = h.addVariable(lb=-highspy.kHighsInf)
        y = h.addVariable(lb=-highspy.kHighsInf)
        c1 = h.addConstr(-x + y >= 2)
        h.addConstr(x + y >= 0)
        self.assertEqual(h.numConstrs, 2)
        h.removeConstr(c1)
        self.assertEqual(h.numConstrs, 1)

    def test_infeasible_model(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)
        h.setOptionValue("presolve", "off")

        x = h.addVariable()
        y = h.addVariable()

        c1 = h.addConstr(x + y == 3)
        h.addConstr(x + y == 1)

        status = h.minimize(10 * x + 15 * y)
        self.assertEqual(status, highspy.HighsStatus.kOk)

        status = h.getModelStatus()
        self.assertEqual(status, highspy.HighsModelStatus.kInfeasible)

        # change the model to be feasible
        h.chgCoeff(c1, y, 3)

        status = h.run()
        self.assertEqual(status, highspy.HighsStatus.kOk)

        status = h.getModelStatus()
        self.assertEqual(status, highspy.HighsModelStatus.kOptimal)

    def test_simple_basics_builder(self):
        h = highspy.Highs()
        h.silent()

        x, y = h.addVariables(2, lb=-highspy.kHighsInf)
        c = h.addConstrs(-x + y >= 2, x + y >= 0)
        h.minimize(y)

        self.assertArrayAlmostEqual(h.val([x, y]), [-1, 1])
        self.assertArrayAlmostEqual(h.variableValue([x, y]), [-1, 1])
        self.assertArrayAlmostEqual(h.variableValues([x, y]), [-1, 1])
        self.assertArrayAlmostEqual(h.allVariableValues(), [-1, 1])

        # -x + y >= 3
        h.changeRowBounds(0, 3, highspy.kHighsInf)
        h.run()
        self.assertArrayAlmostEqual(h.val([x, y]), [-1.5, 1.5])

        # make y integer
        h.setInteger(y)
        h.run()
        self.assertArrayAlmostEqual(h.val([x, y]), [-1, 2])

        # delete the first constraint and add a new one
        h.removeConstr(c[0], c)
        self.assertEqual(list(map(int, c)), [-1, 0])

        h.addConstr(-x + y >= 0)
        h.run()
        self.assertArrayAlmostEqual(h.val([x, y]), [0, 0])

    def test_basics_builder(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)

        x = h.addVariable(lb=-highspy.kHighsInf)
        y = h.addVariable(lb=-highspy.kHighsInf)

        c1 = h.addConstr(-x + y >= 2)
        c2 = h.addConstr(x + y >= 0)

        h.minimize(y)

        self.assertAlmostEqual(h.val(x), -1)
        self.assertAlmostEqual(h.val(y), 1)

        """
        min y
        s.t.
        -x + y >= 3
        x + y >= 0
        """
        h.changeRowBounds(0, 3, highspy.kHighsInf)
        h.run()

        self.assertAlmostEqual(h.val(x), -1.5)
        self.assertAlmostEqual(h.val(y), 1.5)

        sol = h.getSolution()
        self.assertAlmostEqual(h.variableDual(x), sol.col_dual[0])

        self.assertAlmostEqual(h.variableDuals(x), sol.col_dual[0])
        self.assertArrayAlmostEqual(h.variableDuals([x, y]), [sol.col_dual[0], sol.col_dual[1]])

        self.assertMappingAlmostEqual({"x": x, "y": y}, {"x": sol.col_dual[0], "y": sol.col_dual[1]})
        self.assertArrayAlmostEqual(h.variableDuals(np.asarray([x, y])), [sol.col_dual[0], sol.col_dual[1]])

        self.assertArrayAlmostEqual(h.allVariableDuals(), sol.col_dual)

        c1, c2 = h.getConstrs()
        self.assertAlmostEqual(h.constrValue(c1), sol.row_value[0])

        self.assertAlmostEqual(h.constrValues(c1), sol.row_value[0])
        self.assertArrayAlmostEqual(h.constrValues([c1, c2]), [sol.row_value[0], sol.row_value[1]])
        self.assertArrayAlmostEqual(h.constrValues([c2, c1]), [sol.row_value[1], sol.row_value[0]])  # order matters
        self.assertMappingAlmostEqual(h.constrValues({"c1": c1, "c2": c2}), {"c1": sol.row_value[0], "c2": sol.row_value[1]})
        self.assertArrayAlmostEqual(h.constrValues(np.asarray([c1, c2])), [sol.row_value[0], sol.row_value[1]])

        self.assertArrayAlmostEqual(h.allConstrValues(), sol.row_value)

        self.assertAlmostEqual(h.constrDual(c1), sol.row_dual[0])

        self.assertAlmostEqual(h.constrDuals(c1), sol.row_dual[0])
        self.assertArrayAlmostEqual(h.constrDuals([c1, c2]), sol.row_dual)
        self.assertArrayAlmostEqual(h.constrDuals([c2, c1]), sol.row_dual[::-1])  # order matters
        self.assertMappingAlmostEqual(h.constrDuals({"c1": c1, "c2": c2}), {"c1": sol.row_dual[0], "c2": sol.row_dual[1]})
        self.assertArrayAlmostEqual(h.constrDuals(np.asarray([c1, c2])), sol.row_dual)

        self.assertArrayAlmostEqual(h.allConstrDuals(), sol.row_dual)

        # now make y integer
        h.changeColsIntegrality(1, np.array([1]), np.array([highspy.HighsVarType.kInteger]))
        h.run()
        sol = h.getSolution()
        self.assertAlmostEqual(sol.col_value[0], -1)
        self.assertAlmostEqual(sol.col_value[1], 2)

        """
        now delete the first constraint and add a new one

        min y
        s.t.
        x + y >= 0
        -x + y >= 0
        """
        h.removeConstr(c1)

        c1 = h.addConstr(-x + y >= 0)

        h.run()

        self.assertAlmostEqual(h.val(x), 0)
        self.assertAlmostEqual(h.val(y), 0)

        # change the upper bound of x to -5
        h.changeColsBounds(1, np.array([0]), np.array([-highspy.kHighsInf], dtype=np.double), np.array([-5], dtype=np.double))
        h.run()
        self.assertAlmostEqual(h.val(x), -5)
        self.assertAlmostEqual(h.val(y), 5)

        # now maximize
        h.changeRowBounds(1, -highspy.kHighsInf, 0)
        h.changeRowBounds(0, -highspy.kHighsInf, 0)
        h.minimize(-y)

        self.assertAlmostEqual(h.val(x), -5)
        self.assertAlmostEqual(h.val(y), -5)

        self.assertEqual(h.getObjectiveSense()[1], highspy.ObjSense.kMinimize)
        h.maximize(y)
        self.assertEqual(h.getObjectiveSense()[1], highspy.ObjSense.kMaximize)

        self.assertAlmostEqual(h.val(x), -5)
        self.assertAlmostEqual(h.val(y), -5)

        self.assertAlmostEqual(h.getObjectiveValue(), -5)

        h.maximize(y + 1)
        self.assertAlmostEqual(h.getObjectiveOffset()[1], 1)
        self.assertAlmostEqual(h.getObjectiveValue(), -4)

    def test_addVariable(self):
        h = highspy.Highs()
        h.addVariable()
        self.assertEqual(h.numVariables, 1)

        # exception
        self.assertRaises(Exception, lambda: h.addVariable(lb=h.inf))
        self.assertRaises(Exception, lambda: h.addVariables(2, lb=h.inf))

    def test_addConstr(self):
        h = highspy.Highs()
        x = h.addVariable()
        y = h.addVariable()

        c1 = h.addConstr(2 * x + 3 * y <= 10, name="c1")
        self.assertEqual(h.numVariables, 2)
        self.assertEqual(h.numConstrs, 1)
        self.assertEqual(h.getNumNz(), 2)
        self.assertEqual(c1.name, "c1")
        c1.name = "c2"
        self.assertEqual(c1.name, "c2")

        lp = h.getLp()
        self.assertAlmostEqual(lp.row_lower_[0], -highspy.kHighsInf)
        self.assertAlmostEqual(lp.row_upper_[0], 10)

        self.assertEqual(lp.a_matrix_.index_[0], 0)
        self.assertEqual(lp.a_matrix_.index_[1], 1)

        self.assertAlmostEqual(lp.a_matrix_.value_[0], 2)
        self.assertAlmostEqual(lp.a_matrix_.value_[1], 3)

    def test_removeConstr(self):
        h = highspy.Highs()
        x = h.addVariable()
        y = h.addVariable()
        c = h.addConstr(2 * x + 3 * y <= 10)
        self.assertEqual(h.numConstrs, 1)

        h.removeConstr(c)
        self.assertEqual(h.numVariables, 2)
        self.assertEqual(h.numConstrs, 0)
        self.assertRaises(Exception, lambda: c.name)

    def test_val(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)

        x = [h.addVariable(), h.addVariable()]
        h.addConstr(2 * x[0] + 3 * x[1] <= 10)
        h.maximize(x[0])

        self.assertAlmostEqual(h.val(x[0]), 5)

        vals = h.vals(x)
        self.assertAlmostEqual(vals[0], 5)
        self.assertAlmostEqual(vals[1], 0)

        # test linear expr
        self.assertAlmostEqual(h.val(2 * x[0] + 3 * x[1]), 10)
        self.assertAlmostEqual(h.val(1 * x[0] + 3 * x[1]), 5)

        self.assertAlmostEqual(h.val(1 * x[0] + 3 * x[1] <= 4), False)
        self.assertAlmostEqual(h.val(1 * x[0] + 3 * x[1] == 5), True)
        self.assertAlmostEqual(h.val(1 * x[0] + 3 * x[1] >= 6), False)

    def test_var_name(self):
        h = highspy.Highs()

        # name set, but not in the model
        x = h.addVariable(name="x")
        self.assertEqual(x.name, "x")

        # change name before adding to the model
        x.name = "y"
        self.assertEqual(x.name, "y")

        # add to the model
        self.assertEqual(h.numVariables, 1)
        self.assertEqual(h.getLp().col_names_[0], "y")

        # change name after adding to the model
        x.name = "z"
        self.assertEqual(h.getLp().col_names_[0], "z")

        # change name via the model
        h.passColName(0, "a")
        self.assertEqual(h.getLp().col_names_[0], "a")
        self.assertEqual(x.name, "a")
        self.assertEqual(h.variableName(x), "a")
        self.assertRaises(Exception, lambda: h.variableName(1))
        self.assertEqual(h.variableNames([x]), ["a"])
        self.assertEqual(h.variableNames({"key": x}), {"key": "a"})
        self.assertEqual(h.allVariableNames(), ["a"])

        y = h.addVariable()
        h.deleteVariable(y)
        self.assertRaises(Exception, lambda: y.name)

    def test_binary(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)

        x = [h.addBinary(), h.addBinary()]
        h.addConstr(2 * x[0] + 3 * x[1] <= 10)
        h.maximize(x[0])

        lp = h.getLp()
        self.assertAlmostEqual(lp.col_lower_[0], 0)
        self.assertAlmostEqual(lp.col_upper_[0], 1)
        self.assertEqual(lp.integrality_[0], highspy.HighsVarType.kInteger)

        self.assertAlmostEqual(h.val(x[0]), 1)

        vals = h.vals(x)
        self.assertAlmostEqual(vals[0], 1)
        self.assertAlmostEqual(vals[1], 0)

    def test_integer(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)

        x = [h.addIntegral(), h.addVariable()]
        h.addConstr(2 * x[0] + 3 * x[1] <= 10.6)
        h.maximize(x[0] + x[1])

        lp = h.getLp()
        self.assertEqual(lp.integrality_[0], highspy.HighsVarType.kInteger)
        self.assertEqual(lp.integrality_[1], highspy.HighsVarType.kContinuous)

        self.assertAlmostEqual(h.val(x[0]), 5)

        vals = h.vals(x)
        self.assertAlmostEqual(vals[0], 5)
        self.assertAlmostEqual(vals[1], 0.2)

        y = h.addIntegrals(2)
        self.assertEqual(h.getLp().integrality_[2], highspy.HighsVarType.kInteger)
        self.assertEqual(h.getLp().integrality_[3], highspy.HighsVarType.kInteger)

        h.setContinuous(y[0])
        self.assertEqual(h.getLp().integrality_[2], highspy.HighsVarType.kContinuous)
        self.assertEqual(h.getLp().integrality_[3], highspy.HighsVarType.kInteger)

        h.setInteger(y)
        self.assertEqual(h.getLp().integrality_[2], highspy.HighsVarType.kInteger)
        self.assertEqual(h.getLp().integrality_[3], highspy.HighsVarType.kInteger)

        h.setContinuous(y)
        self.assertEqual(h.getLp().integrality_[2], highspy.HighsVarType.kContinuous)
        self.assertEqual(h.getLp().integrality_[3], highspy.HighsVarType.kContinuous)

    def test_objective(self):
        h = highspy.Highs()
        h.setOptionValue("output_flag", False)

        x = [h.addVariable(), h.addVariable()]
        h.addConstr(2 * x[0] + 3 * x[1] <= 10)

        self.assertRaises(Exception, h.maximize, x[0] + x[1] <= 3)
        self.assertRaises(Exception, h.minimize, x[0] + x[1] <= 3)

    def test_constraint_builder(self):
        h = highspy.Highs()
        (x, y) = [h.addVariable(), h.addVariable()]

        # -inf <= 2x + 3y <= inf
        c1 = 2 * x + 3 * y
        self.assertEqualExpr(c1, [x, y], [2, 3])

        # -inf <= 2x + 3y <= 2x
        c1 = 2 * x + 3 * y <= 2 * x
        self.assertEqualExpr(c1, [x, y, x], [2, 3, -2], None, [-highspy.kHighsInf, 0])

        # -inf <= 2x + 3y <= 2x
        c1 = 2 * x >= 2 * x + 3 * y
        self.assertEqualExpr(c1, [x, y, x], [2, 3, -2], None, [-highspy.kHighsInf, 0])

        # failure add constraint without inequality
        self.assertRaises(Exception, lambda: h.addConstr(x + 3 * y))
        self.assertRaises(Exception, lambda: h.addConstrs(x == 1, x + 3 * y))
        self.assertRaises(Exception, lambda: h.addConstrs({"a": x == 1, "b": x + 3 * y}))

        # ensure model is rolled back on error
        self.assertEqual(h.numConstrs, 0)
        self.assertRaises(Exception, lambda: h.addConstrs([x == 1] * 100 + [x + 3 * y]))
        self.assertEqual(h.numConstrs, 0)

        # failure - bounds already set
        self.assertRaises(Exception, lambda: 1 <= (4 <= 2 * x + 3 * y))
        self.assertRaises(Exception, lambda: 2 >= (4 >= 2 * x + 3 * y))
        self.assertRaises(Exception, lambda: (2 * x + 3 * y <= 2) <= 4)
        self.assertRaises(Exception, lambda: (1 <= 2 * x + 3 * y) <= 5)
        self.assertRaises(Exception, lambda: 1 <= (2 * x + 3 * y <= 5))
        self.assertRaises(Exception, lambda: 1 <= (5 >= 2 * x + 3 * y))
        self.assertRaises(Exception, lambda: (5 >= 2 * x + 3 * y) >= 1)
        self.assertRaises(Exception, lambda: 5 >= (2 * x + 3 * y >= 1))

        c1 = 2 * x + 3 * y <= (2 <= 4)  # 2x + 3y <= float(True)
        self.assertEqualExpr(c1, [x, y], [2, 3], None, [-highspy.kHighsInf, 1])

        # failure, non-linear terms
        self.assertRaises(Exception, lambda: 2 * x * 3 * y)

        # failure, order matters when having variables on both sides of inequality
        self.assertRaises(Exception, lambda: (4 * x <= 2 * x + 3 * y) <= 5)
        self.assertRaises(Exception, lambda: 4 * x <= (2 * x + 3 * y <= 5))
        self.assertRaises(Exception, lambda: (4 * x <= 2 * x + 3 * y) <= 5 * x)

        c1 = 5 * x <= 2 * x + 3 * y <= 5 * x
        self.assertEqualExpr(c1, [x, y, x], [2, 3, -5], None, [0, 0])

        # test various combinations with different inequalities
        self.assertRaises(Exception, lambda: (2 * x + 3 * y == 3 * y) == 3)
        self.assertRaises(Exception, lambda: 2 * x + 3 * y == (3 * y == 3))
        self.assertRaises(Exception, lambda: 2 * x + 3 * y == (3 * y <= 3))
        self.assertRaises(Exception, lambda: 2 * x + 3 * y == (3 * y >= 3))

        c1 = 2 * x + 3 * y == x
        self.assertEqualExpr(c1, [x, y, x], [2, 3, -1], None, [0, 0])

        c1 = 2 * x + 3 * y == 5
        self.assertEqualExpr(c1, [x, y], [2, 3], None, [5, 5])

        c1 = 5 == 2 * x + 3 * y
        self.assertEqualExpr(c1, [x, y], [2, 3], None, [5, 5])

        # 2*x + 3*y == 4.5
        c1 = 2 * x + 3 * y + 0.5 == 5
        self.assertEqualExpr(c1, [x, y], [2, 3], None, [4.5, 4.5])
        h.addConstr(c1)
        self.assertArrayAlmostEqual((h.getLp().row_lower_[0], h.getLp().row_upper_[0]), (4.5, 4.5))

    def test_add_multiple_variables(self):
        # test basic functionality
        h = highspy.Highs()
        x = h.addVariables(2)
        self.assertEqual(h.numVariables, 2)

        # test multiple dimensions
        h = highspy.Highs()
        x = h.addVariables(2, 3, 4, out_array=False)
        self.assertEqual(h.numVariables, 2 * 3 * 4)
        self.assertEqual(isinstance(x, dict), True)

        # test multiple dimensions array
        h = highspy.Highs()
        x = h.addVariables(2, 3, 4)
        self.assertEqual(h.numVariables, 2 * 3 * 4)
        self.assertEqual(x.shape, (2, 3, 4))

        # test binary variables with objective and names
        h = highspy.Highs()
        x = h.addBinaries(20, obj=range(20), name_prefix="t_")
        self.assertEqual(h.numVariables, 20)
        self.assertEqual(h.getLp().col_names_[0], "t_0")
        self.assertEqual(h.getLp().col_names_[19], "t_19")
        self.assertEqual(h.getLp().col_cost_[0], 0)
        self.assertEqual(h.getLp().col_cost_[19], 19)

        # test prefix item with indices, not variable offset
        h = highspy.Highs()
        x = h.addVariables("a", "b", "c", name_prefix="t_")  # ('a','b','c')
        self.assertEqual(h.numVariables, 1)
        self.assertEqual(x["a", "b", "c"].name, "t_('a','b','c')")

        # Testing different ways of adding variables
        # Some are unlikely to be used, but this is expected behaviour
        N = 0
        h = highspy.Highs()
        h.addVariables(("a", "b", "c"), obj={"b": 20, "c": 10, "a": 50})
        N += 3
        self.assertEqual(h.numVariables, N)
        lp = h.getLp()
        self.assertEqual(lp.col_cost_[0], 50)
        self.assertEqual(lp.col_cost_[1], 20)
        self.assertEqual(lp.col_cost_[2], 10)

        h.addVariables(["a", "b", "c", "d"])  # 'a', 'b', 'c', 'd'
        N += 4
        self.assertEqual(h.numVariables, N)
    
        h.addVariables("abc")  # 'a', 'b', 'c'
        N += 3
        self.assertEqual(h.numVariables, N)

        h.addVariables(("ab", "b", "c", "d"))  # 'ab', 'b', 'c', 'd'
        N += 4
        self.assertEqual(h.numVariables, N)

        x5 = h.addVariables("ab", "b", "c", "d")  # ('a', 'b', 'c', 'd'), ('b', 'b', 'c', 'd')
        N += 2
        self.assertEqual(h.numVariables, N)
        self.assertTrue(("a", "b", "c", "d") in x5.keys())
        self.assertTrue(("b", "b", "c", "d") in x5.keys())

        h.addVariables(5, "a", 2, "b", "c")  # range(5), 'a', range(2), 'b', 'c'
        N += 5 * 2
        self.assertEqual(h.numVariables, N)

        h.addVariables([5, "a", 2, "b", "c"])  # 5, 'a', 2, 'b', 'c'
        N += 5
        self.assertEqual(h.numVariables, N)

        h.addVariables([(20, 1), (1, 2), (2, 6)], ub=[3, 2, 1], name_prefix="t")  # (20, 1), (1,2), (2,6)
        N += 3
        self.assertEqual(h.numVariables, N)

        h.addBinaries((20, 1), (1, 2), (2, 6))  # product((20, 1), (1,2), (2,6))) = (20, 1, 2), ..., (1, 2, 6)
        N += 8
        self.assertEqual(h.numVariables, N)

    def test_add_single_constraints(self):
        h = highspy.Highs()
        (x, y) = h.addVariables(2)
        added_constraints = h.addConstrs([2 * x + 3 * y <= 5])
        self.assertEqual(len(added_constraints), 1)
        self.assertEqual(h.numConstrs, 1)

    def test_add_multiple_constraints(self):
        # test manual constraints
        h = highspy.Highs()
        (x, y, z) = h.addVariables(3)
        added_constraints = h.addConstrs([x + y <= 5, 2 * x + 3 * y >= 10, x - z == 2])
        self.assertEqual(len(added_constraints), 3)
        self.assertEqual(h.numConstrs, 3)

        # test list comprehension constraints
        h = highspy.Highs()
        x = h.addVariables(5)
        h.addConstr(sum(x) == 1) # type: ignore # sum does not preserve typing, but want to test anyhow
        self.assertEqual(h.numConstrs, 1)

        h.addConstrs(x[i] + x[j] <= 1 for i in range(5) for j in range(5))
        self.assertEqual(h.numConstrs, 26)

        # test names and sequence types (list, tuple, nothing)
        h = highspy.Highs()
        (x1, x2, x3) = h.addVariables(3)

        h.addConstrs((x2 - x1 >= 2), name_prefix="a")
        self.assertEqual(h.numConstrs, 1)
        h.addConstrs((x2 - x1 >= 2))
        self.assertEqual(h.numConstrs, 2)

        h.addConstrs(x2 - x1 >= 2, name_prefix="b")
        self.assertEqual(h.numConstrs, 3)
        h.addConstrs(x2 - x1 >= 2)
        self.assertEqual(h.numConstrs, 4)

        h.addConstrs([x2 - x1 >= 2], name_prefix="c")
        self.assertEqual(h.numConstrs, 5)
        h.addConstrs([x2 - x1 >= 2])
        self.assertEqual(h.numConstrs, 6)

    # r/w basis tests below works on unix but not windows?
    def test_write_basis_before_running(self):
        if platform == "linux" or platform == "darwin":
            h = self.get_basic_model()
            with tempfile.NamedTemporaryFile() as f:
                h.writeBasis(f.name)
                contents = f.read()
                self.assertEqual(contents, b"HiGHS_basis_file v2\nNone\n")

    def test_write_basis_after_running(self):
        if platform == "linux" or platform == "darwin":
            h = self.get_basic_model()
            h.run()
            with tempfile.NamedTemporaryFile() as f:
                h.writeBasis(f.name)
                contents = f.read()
                self.assertEqual(contents, b"HiGHS_basis_file v2\nValid\n# Columns 2\nc0 1\nc1 1\n# Rows 2\nr0 0\nr1 0\n")

    def test_read_basis(self):
        if platform == "linux" or platform == "darwin":
            # Read basis from one run model into an unrun model
            expected_status_before = highspy.HighsBasisStatus.kNonbasic
            expected_status_after = highspy.HighsBasisStatus.kBasic

            h1 = self.get_basic_model()
            self.assertEqual(h1.getBasis().col_status[0], expected_status_before)
            h1.run()
            self.assertEqual(h1.getBasis().col_status[0], expected_status_after)

            h2 = self.get_basic_model()
            self.assertEqual(h2.getBasis().col_status[0], expected_status_before)

            with tempfile.NamedTemporaryFile() as f:
                h1.writeBasis(f.name)
                h2.readBasis(f.name)
                self.assertEqual(h2.getBasis().col_status[0], expected_status_after)

    def test_solve(self):
        """Test the solve method to ensure it runs the solver."""
        h = highspy.Highs()
        h.silent()
        h.addBinary(obj=1)
        h.setMaximize()
        h.solve()
        self.assertEqual(h.getSolution().col_value[0], 1)
        self.assertEqual(h.getObjectiveSense()[1], highspy.highs.ObjSense.kMaximize)

        h.setMinimize()
        h.optimize()
        self.assertEqual(h.getSolution().col_value[0], 0)
        self.assertEqual(h.getObjectiveSense()[1], highspy.highs.ObjSense.kMinimize)

    def test_minimize(self):
        """Test the minimize method with and without an objective."""
        h = highspy.Highs()
        h.silent()
        x, y, z = h.addBinaries(3, obj=-1)
        h.minimize()
        self.assertEqual(list(h.val([x, y, z])), [1, 1, 1])

        h.minimize(-x)
        self.assertEqual(h.val(x), 1)

        h.minimize(x)
        self.assertEqual(h.val(x), 0)

        h.minimize(x - y)
        self.assertEqual(h.val(x), 0)
        self.assertEqual(h.val(y), 1)

        self.assertRaises(Exception, h.minimize, x - y <= 5)
        self.assertRaises(Exception, h.minimize, 0 <= x - y <= 5)
        self.assertRaises(Exception, h.minimize, 4 <= x - y)
        self.assertRaises(Exception, h.minimize, 4)

    def test_maximize(self):
        """Test the maximize method with and without an objective."""
        h = highspy.Highs()
        h.silent()
        x, y, z = h.addBinaries(3, obj=1)
        h.maximize()
        self.assertEqual(list(h.val([x, y, z])), [1, 1, 1])

        h.maximize(x)
        self.assertEqual(h.val(x), 1)

        h.maximize(-x)
        self.assertEqual(h.val(x), 0)

        h.maximize(y - x)
        self.assertEqual(h.val(x), 0)
        self.assertEqual(h.val(y), 1)

        self.assertRaises(Exception, h.maximize, x - y <= 5)
        self.assertRaises(Exception, h.maximize, 0 <= x - y <= 5)
        self.assertRaises(Exception, h.maximize, 4 <= x - y)
        self.assertRaises(Exception, h.maximize, 4)

    def test_get_expr(self):
        h = self.get_basic_model()

        expr = h.getExpr(0)  # -x + y >= 2
        self.assertEqualExpr(expr, [0, 1], [-1, 1], None, [2, highspy.kHighsInf])

        expr = h.getExpr(1)  # x + y >= 0
        self.assertEqualExpr(expr, [0, 1], [1, 1], None, [0, highspy.kHighsInf])

        c = h.getConstrs()
        self.assertEqualExpr(c[0].expr(), [0, 1], [-1, 1], None, [2, highspy.kHighsInf])
        self.assertEqualExpr(c[1].expr(), [0, 1], [1, 1], None, [0, highspy.kHighsInf])

        self.assertRaises(Exception, lambda: h.getExpr(2))

    def test_add_variables(self):
        """Test adding multiple variables to the model."""
        h = highspy.Highs()

        var = h.addVariables()
        self.assertEqual(var, None)

        keys = ["a", "b", "c"]
        v = h.addVariables(keys)
        self.assertTrue(isinstance(v, dict))
        self.assertEqual(int(v["a"]), 0)

        h.addConstr(v["a"] + v["b"] + v["c"] == 1)
        h.maximize(v["a"])
        self.assertEqual(h.val(v), {"a": 1.0, "b": 0, "c": 0})
        self.assertEqual(list(map(int, h.getVariables())), [0, 1, 2])

        # provided len(parameters) != len(indices)
        self.assertRaises(Exception, lambda: h.addVariables(5, type=[highspy.HighsVarType.kContinuous, highspy.HighsVarType.kInteger]))
        self.assertRaises(Exception, lambda: h.addVariables(5, name=["a", "b"]))

        # some parameters not valid
        self.assertRaises(Exception, lambda: h.addVariables(5, type=[highspy.HighsVarType.kContinuous, None]))
        self.assertRaises(Exception, lambda: h.addVariables(5, name=["a", None]))
        self.assertRaises(Exception, lambda: h.addVariables(5, out_array=["a"]))  # type: ignore[call-overload]
        self.assertRaises(Exception, lambda: h.addVariables(5, name_prefix=["a"]))

        # correct usage
        y = h.addVariables(5, type=[highspy.HighsVarType.kContinuous] * 5)
        self.assertArrayEqual(y.idx(), [3, 4, 5, 6, 7])

        y2 = h.addVariables(
            ["a", "b", "c"], type={"a": highspy.HighsVarType.kContinuous, "b": highspy.HighsVarType.kInteger, "c": highspy.HighsVarType.kContinuous}
        )
        self.assertEqual({k: int(v) for k, v in y2.items()}, {"a": 8, "b": 9, "c": 10})

        y3 = h.addVariables(2, name=["a", "b"])
        self.assertArrayEqual(y3.idx(), [11, 12])

    def test_add_variable_as_key(self):
        # tests to see if we can use a variable as a key in a dictionary
        # previously this would fail because of the lb <= expr <= ub chain
        h = highspy.Highs()
        var = h.addBinary()

        values = {var: 0}

        # chain uses: `__bool__(lb <= expr) and (expr <= ub)`
        # key lookup: `hash(key) == hash(var) and key == var`,
        #    but `key == var` is translated as `__bool__(highs_linear_expression(key == var))`
        #    and __bool__ invokes the highs_linear_expression.chain
        # performing both in sequence can lead to an assert after a false positive in highs_linear_expression.__is_active_chain()
        values[h.getVariables()[0]]
        h.addConstr(h.getVariables()[0] <= 1)
        self.assertEqualExpr(h.getConstrs()[0].expr(), [0], [1], None, [-h.inf, 1])

        # test inf edge cases (these will raise exceptions if adding constraints)
        self.assertEqualExpr(h.inf <= h.getVariables()[0] <= h.inf, [0], [1], None, [h.inf, h.inf])
        self.assertEqualExpr(-h.inf >= h.getVariables()[0] >= -h.inf, [0], [1], None, [-h.inf, -h.inf])

        # test that we can set equality via chain
        self.assertEqualExpr(1 <= h.getVariables()[0] <= 1, [0], [1], None, [1, 1])
        self.assertEqualExpr(1 >= h.getVariables()[0] >= 1, [0], [1], None, [1, 1])

        # test equality chain (left hand side ignored)
        self.assertEqualExpr(1 == h.getVariables()[0] >= 5, [0], [1], None, [5, h.inf])
        self.assertEqualExpr(5 <= h.getVariables()[0] == 1, [0], [1], None, [1, 1])


    def test_delete_variable(self):
        h = highspy.Highs()

        keys = ["a", "b", "c"]
        D = h.addVariables(keys)
        X = h.addVariables(5)

        self.assertEqual({k: int(v) for k, v in D.items()}, {"a": 0, "b": 1, "c": 2})
        self.assertArrayEqual(X.idx(), [3, 4, 5, 6, 7])

        # delete variable and update collections
        h.deleteVariable(D["b"], D, X)

        self.assertEqual(h.numVariables, 7)
        self.assertEqual({k: int(v) for k, v in D.items()}, {"a": 0, "b": -1, "c": 1})
        self.assertArrayEqual(X.idx(), [2, 3, 4, 5, 6])

        # delete variable and update collections
        h.deleteVariable(X[3], D, X)

        self.assertEqual(h.numVariables, 6)
        self.assertEqual({k: int(v) for k, v in D.items()}, {"a": 0, "b": -1, "c": 1})
        self.assertArrayEqual(X.idx(), [2, 3, 4, -1, 5])

        # delete variable and update collections
        h.deleteVariable(X[2], D, *X)

        self.assertEqual(h.numVariables, 5)
        self.assertEqual({k: int(v) for k, v in D.items()}, {"a": 0, "b": -1, "c": 1})
        self.assertArrayEqual(X.idx(), [2, 3, -1, -1, 4])

    def test_remove_constraint(self):
        h = highspy.Highs()

        keys = ["a", "b", "c"]
        D = h.addVariables(keys)
        X = h.addVariables(5)

        c1 = h.addConstr(D["a"] + D["b"] + D["c"] == 1)
        cX = h.addConstrs((x >= 1 for x in X))
        c2 = h.addConstr(qsum(X) == 1)
        c3 = h.addConstr(qsum(D.values()) == 1)
        hash_test = {c1: "c1", c2: "c2", c3: "c3"}

        self.assertEqual(h.numConstrs, 8)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [0, 1, 2, 3, 4, 5, 6, 7])

        # delete constr and update collections
        h.removeConstr(cX[2], c1, cX, c2, c3)

        self.assertEqual(h.numConstrs, 7)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [0, 1, 2, -1, 3, 4, 5, 6])

        # delete variable and update collections
        h.removeConstr(c1, c1, cX)

        self.assertEqual(h.numConstrs, 6)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [-1, 0, 1, -1, 2, 3, 5, 6])

        # add dictionary of constraints with highs_var keys
        cM = h.addConstrs({x: x >= 1 for x in X})
        self.assertEqual(h.numConstrs, 11)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [-1, 0, 1, -1, 2, 3, 5, 6])
        self.assertEqual({int(k): int(c) for k, c in cM.items()}, {3: 6, 4: 7, 5: 8, 6: 9, 7: 10})

        # add dictionary of constraints with string keys
        cD = h.addConstrs({k: x >= 1 for k, x in D.items()})
        self.assertEqual(h.numConstrs, 14)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [-1, 0, 1, -1, 2, 3, 5, 6])
        self.assertEqual({int(k): int(c) for k, c in cM.items()}, {3: 6, 4: 7, 5: 8, 6: 9, 7: 10})
        self.assertEqual({k: int(c) for k, c in cD.items()}, {"a": 11, "b": 12, "c": 13})

        # delete constraint from dictionary
        h.removeConstr(cM[X[2]], c1, cX, c2, c3, cM, cD)
        self.assertEqual(h.numConstrs, 13)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [-1, 0, 1, -1, 2, 3, 5, 6])  # unchanged
        self.assertEqual({int(k): int(c) for k, c in cM.items()}, {3: 6, 4: 7, 5: -1, 6: 8, 7: 9})
        self.assertEqual({k: int(c) for k, c in cD.items()}, {"a": 10, "b": 11, "c": 12})

        # delete constraint from dictionary
        h.removeConstr(cD["b"], c1, cX, c2, c3, cM, cD)
        self.assertEqual(h.numConstrs, 12)
        self.assertEqual([int(c1)] + list(map(int, cX)) + [int(c2), int(c3)], [-1, 0, 1, -1, 2, 3, 5, 6])  # unchanged
        self.assertEqual({int(k): int(c) for k, c in cM.items()}, {3: 6, 4: 7, 5: -1, 6: 8, 7: 9})  # unchanged
        self.assertEqual({k: int(c) for k, c in cD.items()}, {"a": 10, "b": -1, "c": 11})

        # delete non-existent constraint
        self.assertRaises(Exception, lambda: h.removeConstr(cD["b"], c1, cX, c2, c3, cM, cD))

    def test_qsum(self):
        """Test summation."""
        h = highspy.Highs()
        X = h.addVariables(10)

        # qsum
        expr = qsum(X)
        self.assertEqualExpr(expr, X, [1] * 10)

        expr = qsum((2 * x for x in X))
        self.assertEqualExpr(expr, X, [2] * 10)

        expr = qsum((qsum(X) for k in range(11))).simplify()
        self.assertEqualExpr(expr, X, [11] * 10)

        # sum
        expr = sum(X)
        self.assertEqualExpr(expr, X, [1] * 10, 0)

        expr = sum((2 * x for x in X))
        self.assertEqualExpr(expr, X, [2] * 10, 0)

        # init sum with empty expression
        expr = sum((2 * x for x in X), highs_linear_expression())
        self.assertEqualExpr(expr, X, [2] * 10)

        # manual sum
        expr = h.expr()
        for x in X:
            expr += x
        self.assertEqualExpr(expr, X, [1] * 10)

    def test_user_interrupts(self):
        N = 8
        h = highspy.Highs()
        h.silent()

        x = h.addBinaries(N, N)
        y = np.fliplr(x)

        h.addConstrs(x.sum(axis = 0) == 1)  # each col has exactly one queen
        h.addConstrs(x.sum(axis = 1) == 1)  # each row has exactly one queen

        h.addConstrs(x.diagonal(k).sum() <= 1 for k in range(-N + 1, N))  # each diagonal has at most one queen
        h.addConstrs(y.diagonal(k).sum() <= 1 for k in range(-N + 1, N))  # each 'reverse' diagonal has at most one queen

        h.HandleUserInterrupt = True
        h.startSolve()
        self.assertRaises(Exception, lambda: h.startSolve())
        h.cancelSolve()
        h.wait()

        h = self.get_basic_model()
        h.HandleKeyboardInterrupt = True
        self.assertEqual(h.HandleKeyboardInterrupt, True)
        self.assertEqual(h.HandleUserInterrupt, True)

        h.solve()
        h.minimize()
        h.maximize()
        h.minimize()

        h.joinSolve(h.startSolve())
        h.joinSolve(h.startSolve(), 0)

        # replace wait function with Ctrl+C signal
        __tmp_wait = highspy.highs.Highs.wait
        highspy.highs.Highs.wait = lambda self, t: signal.raise_signal(signal.SIGINT)  # type: ignore[assignment]
        h.HandleKeyboardInterrupt = False

        self.assertEqual(h.HandleKeyboardInterrupt, False)
        self.assertEqual(h.HandleUserInterrupt, False)

        h.joinSolve(h.startSolve(), 0)
        h.startSolve()
        h.joinSolve(None, 0)

        with self.assertRaises(SystemExit):
            h.startSolve()
            h.joinSolve(None, 5)
            unittest.main(exit=False)

        # restore wait function (avoid issues in other tests)
        highspy.highs.Highs.wait = __tmp_wait

    def test_callbacks(self):
        N = 8
        h = highspy.Highs()
        h.silent(False)

        x = h.addBinaries(N, N)
        y = np.fliplr(x)

        h.addConstrs(h.qsum(x[i, :]) == 1 for i in range(N))  # each row has exactly one queen
        h.addConstrs(h.qsum(x[:, j]) == 1 for j in range(N))  # each col has exactly one queen

        h.addConstrs(h.qsum(x.diagonal(k)) <= 1 for k in range(-N + 1, N))  # each diagonal has at most one queen
        h.addConstrs(h.qsum(y.diagonal(k)) <= 1 for k in range(-N + 1, N))  # each 'reverse' diagonal has at most one queen

        do_nothing = lambda e: None

        check_callback = []
        chk_callback = lambda e: check_callback.append(e)

        check_solution = []
        chk_solution = lambda e: check_solution.append(e.val(x))

        # check callback is added and called
        h.cbLogging += chk_callback
        h.cbMipSolution += chk_solution
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        self.assertEqual(len(h.cbMipSolution.callbacks), 1)
        h.solve()
        self.assertNotEqual(len(check_callback), 0)
        self.assertNotEqual(len(check_solution), 0)
        check_callback.clear()
        check_solution.clear()

        # check callback is removed and not called
        h.cbLogging -= chk_callback
        h.cbMipSolution -= chk_solution
        self.assertEqual(len(h.cbLogging.callbacks), 0)
        self.assertEqual(len(h.cbMipSolution.callbacks), 0)
        h.solve()
        self.assertEqual(len(check_callback), 0)
        self.assertEqual(len(check_solution), 0)

        h.disableCallbacks()
        self.assertRaises(Exception, lambda: h.cbLogging.subscribe(do_nothing))
        h.enableCallbacks()

        h.cbLogging += chk_callback
        h.cbSimplexInterrupt += do_nothing
        h.cbIpmInterrupt += do_nothing
        h.cbMipSolution += do_nothing
        h.cbMipImprovingSolution += do_nothing
        h.cbMipLogging += do_nothing
        h.cbMipInterrupt += do_nothing
        h.cbMipGetCutPool += do_nothing
        h.cbMipDefineLazyConstraints += do_nothing

        self.assertEqual(len(h.cbLogging.callbacks), 1)

        # check callback is disabled and not called
        h.disableCallbacks()
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        h.solve()
        self.assertEqual(len(check_callback), 0)
        check_callback.clear()

        # check callback is enabled and called
        h.enableCallbacks()
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        h.solve()
        self.assertNotEqual(len(check_callback), 0)
        check_callback.clear()

        h.cbLogging -= chk_callback
        h.cbSimplexInterrupt -= do_nothing
        h.cbIpmInterrupt -= do_nothing
        h.cbMipSolution -= do_nothing
        h.cbMipImprovingSolution -= do_nothing
        h.cbMipLogging -= do_nothing
        h.cbMipInterrupt -= do_nothing
        h.cbMipGetCutPool -= do_nothing
        h.cbMipDefineLazyConstraints -= do_nothing

        self.assertEqual(len(h.cbLogging.callbacks), 0)
        h.cbLogging.subscribe(do_nothing)
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        h.cbLogging.unsubscribe(do_nothing)
        self.assertEqual(len(h.cbLogging.callbacks), 0)
        h.cbLogging.unsubscribe(do_nothing)  # does not throw error if not exists
        self.assertEqual(len(h.cbLogging.callbacks), 0)

        h.cbLogging.subscribe(do_nothing, "test")
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        h.cbLogging.unsubscribe_by_data("test")
        self.assertEqual(len(h.cbLogging.callbacks), 0)

        h.cbLogging += do_nothing
        h.cbLogging += do_nothing
        h.cbLogging += do_nothing
        self.assertEqual(len(h.cbLogging.callbacks), 3)
        h.cbLogging.clear()
        self.assertEqual(len(h.cbLogging.callbacks), 0)

        self.assertRaises(Exception, lambda: h.__setattr__("cbLogging", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbSimplexInterrupt", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbIpmInterrupt", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipSolution", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipImprovingSolution", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipLogging", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipInterrupt", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipGetCutPool", None))
        self.assertRaises(Exception, lambda: h.__setattr__("cbMipDefineLazyConstraints", None))


    def test_usercallbacks(self):
        N = 8
        h = highspy.Highs()
        h.silent()

        x = h.addBinaries(N, N)
        y = np.fliplr(x)

        h.addConstrs(h.qsum(x[i, :]) == 1 for i in range(N))  # each row has exactly one queen
        h.addConstrs(h.qsum(x[:, j]) == 1 for j in range(N))  # each col has exactly one queen

        h.addConstrs(h.qsum(x.diagonal(k)) <= 1 for k in range(-N + 1, N))  # each diagonal has at most one queen
        h.addConstrs(h.qsum(y.diagonal(k)) <= 1 for k in range(-N + 1, N))  # each 'reverse' diagonal has at most one queen

        # minimize index where queen is placed
        h.minimize((x.astype(int) * x).sum())
        sol = h.val(x)
        self.assertEqual(sol.shape, (N, N))

        # verify callback called
        check_called = [False]

        def check_called_func(e):
            check_called[0] = True

        h.cbMipUserSolution += check_called_func

        # verify initialization
        h.cbMipUserSolution += lambda e: self.assertEqual(e.data_in.user_has_solution, False)

        def partial_solution(e):
            # different sizes
            self.assertEqual(e.data_in.setSolution(range(N*N), sol[::2]), highspy.HighsStatus.kError)

            # get every 2nd element from 2d numpy sol array
            self.assertEqual(e.data_in.setSolution(x[:, ::2], sol[:, ::2]), highspy.HighsStatus.kOk)
            self.assertEqual(list(e.data_in.user_solution[0:4]), [sol[0,0],highspy.kHighsUndefined,sol[0,2],highspy.kHighsUndefined])
            self.assertEqual(e.data_in.repairSolution(), highspy.HighsStatus.kOk)
            self.assertEqual(list(e.data_in.user_solution[0:8]), list(sol[0,0:8]))

            def try_change_ptr(e):
                e.data_in.user_solution = [0] * (N*N)

            self.assertRaises(Exception, lambda: try_change_ptr(e))

            # modify directly
            e.data_in.user_solution[:] = highspy.kHighsUndefined
            self.assertEqual(e.data_in.user_solution[8], highspy.kHighsUndefined)
            e.data_in.user_has_solution = False

            # set subset partial without index
            # note: we're setting a sub-optimal feasible solution here
            self.assertEqual(e.data_in.setSolution([0., 0., 0., 0., 0., 0., highspy.kHighsUndefined, 0.]), highspy.HighsStatus.kOk)
            self.assertEqual(e.data_in.repairSolution(), highspy.HighsStatus.kOk)
            self.assertEqual(list(e.data_in.user_solution[0:8]), [0., 0., 0., 0., 0., 0., 1., 0.])
            self.assertEqual(e.data_in.user_has_solution, True)

            # set partial solution with fractional value
            self.assertEqual(e.data_in.setSolution([0., 0.5]), highspy.HighsStatus.kOk)
            self.assertEqual(e.data_in.repairSolution(), highspy.HighsStatus.kError)

        # verify partial solution
        h.cbMipUserSolution += partial_solution

        # verify full solution
        h.cbMipUserSolution += lambda e: self.assertEqual(e.data_in.setSolution([0] * (N * N + 1)), highspy.HighsStatus.kError)
        h.cbMipUserSolution += lambda e: self.assertEqual(e.data_in.setSolution(sol), highspy.HighsStatus.kOk)

        h.clearSolver()
        h.solve()
        self.assertEqual(check_called[0], True)

    def test_releaseMemory(self):
        """Test that releaseMemory() frees memory and allows solving again."""
        # Create and solve first problem
        h = self.get_example_model()
        h.run()
        first_objective = h.getInfo().objective_function_value

        # Release memory
        status = h.releaseMemory()
        self.assertEqual(status, highspy.HighsStatus.kOk)

        # Solve a different problem to verify the solver still works
        h2 = self.get_basic_model()
        h2.run()
        second_objective = h2.getInfo().objective_function_value

        # Verify objectives are different (different problems)
        self.assertNotEqual(first_objective, second_objective)
    def test_addVars_invalid_parameter(self):
        """ensure_real raises on invalid parameter"""
        h = highspy.Highs()
        h.silent()
        self.assertRaises(Exception, lambda: h.addVariables(3, lb="invalid"))

    def test_delete_variable_with_list(self):
        """deleteVariable with list"""
        h = highspy.Highs()
        X = h.addVariables(3)

        var_list = X.tolist()
        h.deleteVariable(X[1], var_list)
        self.assertEqual(var_list, [0, -1, 1])

    def test_set_type_with_list(self):
        """setInteger/setContinuous"""
        h = highspy.Highs()
        h.silent()
        x = h.addVariable()
        y = h.addVariable()

        h.setInteger([x, y])
        lp = h.getLp()
        self.assertEqual(lp.integrality_[0], highspy.HighsVarType.kInteger)
        self.assertEqual(lp.integrality_[1], highspy.HighsVarType.kInteger)

        h = highspy.Highs()
        h.silent()
        X = h.addIntegrals(3)

        h.setContinuous([X[0], X[1]])
        lp = h.getLp()
        self.assertEqual(lp.integrality_[0], highspy.HighsVarType.kContinuous)
        self.assertEqual(lp.integrality_[1], highspy.HighsVarType.kContinuous)
        self.assertEqual(lp.integrality_[2], highspy.HighsVarType.kInteger)

    def test_idx_static_method(self):
        """highs.idx() method"""
        h = highspy.Highs()
        x = h.addVariable()
        y = h.addVariable()

        result = h.idx([x, y])
        self.assertArrayEqual(result, [0, 1])

        result = h.idx(x, y)
        self.assertArrayEqual(result, [0, 1])

    def test_clear_callbacks(self):
        """clearCallbacks() method"""
        h = highspy.Highs()
        h.silent()

        def do_nothing(e):
            pass

        h.cbLogging += do_nothing
        h.cbMipSolution += do_nothing
        self.assertEqual(len(h.cbLogging.callbacks), 1)
        self.assertEqual(len(h.cbMipSolution.callbacks), 1)

        h.clearCallbacks()
        self.assertEqual(len(h.cbLogging.callbacks), 0)
        self.assertEqual(len(h.cbMipSolution.callbacks), 0)

    def test_array_ge_le(self):
        """HighspyArray >= and <="""
        h = highspy.Highs()
        h.silent()
        x = h.addVariables(3)

        ge_result = x >= 0
        self.assertEqual(len(ge_result), 3)
        for i in range(3):
            expr = ge_result[i]
            self.assertIsInstance(expr, highspy.highs.highs_linear_expression)
            if isinstance(expr, highspy.highs.highs_linear_expression):
                self.assertArrayEqual(expr.bounds, [0.0, h.inf])

        le_result = x <= 5
        self.assertEqual(len(le_result), 3)
        for i in range(3):
            expr = le_result[i]
            self.assertIsInstance(expr, highspy.highs.highs_linear_expression)
            if isinstance(expr, highspy.highs.highs_linear_expression):
                self.assertArrayEqual(expr.bounds, [-h.inf, 5.0])

    def test_array_sum_no_highs(self):
        """HighspyArray.sum() without Highs raises"""
        from highspy.highs import HighspyArray
        arr = HighspyArray(np.array([1, 2, 3], dtype=object), None)
        self.assertRaises(Exception, lambda: arr.sum())

    def test_index_protocol(self):
        """vars/cons be used as indices"""
        h = highspy.Highs()
        x = h.addVariable()
        c = h.addConstr(x <= 5)

        values = [10, 20, 30]
        self.assertEqual(values[x], 10)
        self.assertEqual(values[c], 10)

    def test_var_eq_ne(self):
        """highs_var == and != methods"""
        h = highspy.Highs()
        x = h.addVariable()
        self.assertTrue(x.__eq__(None))
        self.assertTrue(x.__ne__(None))
        self.assertRaises(Exception, lambda: x.__ne__(5))

    def test_with_context_manager(self):
        with highspy.Highs() as h:
            x = h.addVariable()
            h.minimize(x)
            self.assertEqual(h.val(x), 0.0)

        # manually call enter/exit to test threading, e.g.,
        # 
        # with Highs() as h:
        #    ...
        #    h.startSolve()
        #
        h = highspy.Highs()
        h.__enter__()

        # nqueens
        N = 10
        x = h.addBinaries(N, N)
        y = np.fliplr(x)

        h.addConstrs(x.sum(axis = 0) == 1)
        h.addConstrs(x.sum(axis = 1) == 1),
        h.addConstrs(x.diagonal(k).sum() <= 1 for k in range(-N + 1, N)) 
        h.addConstrs(y.diagonal(k).sum() <= 1 for k in range(-N + 1, N))

        h.HandleUserInterrupt = True
        h.startSolve()
        self.assertEqual(h.is_solver_running(), True)

        h.__exit__(None, None, None)
        self.assertEqual(h.is_solver_running(), False)


class TestHighsLinearExpressionPy(unittest.TestCase):
    def setUp(self):
        self.h = highspy.Highs()
        self.h.silent()

        self.x = self.h.addVariables(10)

    def assertEqualExpr(self, expr, idxs, vals, constant=None, bounds=None):
        self.assertEqual(list(map(int, expr.idxs)), list(map(int, idxs)), "variable index")
        self.assertEqual(expr.vals, vals, "variable values")
        self.assertEqual(expr.constant, constant, "constant")
        self.assertEqual(expr.bounds, (bounds[0], bounds[1]) if bounds is not None else None, "bounds")

    def test_init_empty(self):
        # Test initialization with no arguments
        expr = highspy.highs.highs_linear_expression()
        self.assertEqualExpr(expr, [], [])

        expr = self.h.expr()
        self.assertEqualExpr(expr, [], [])
        self.assertRaises(Exception, lambda: self.h.expr([]))  # type: ignore[arg-type]
        self.assertRaises(Exception, lambda: self.h.expr(self.h))  # type: ignore[arg-type]

    def test_init_var(self):
        # Test initialization with a highs_var
        expr = highspy.highs.highs_linear_expression(self.x[0])
        self.assertEqualExpr(expr, [self.x[0]], [1.0])

        expr = 1.0 * self.x[0]
        self.assertEqualExpr(expr, [self.x[0]], [1.0])

        expr = self.h.expr(self.x[0])
        self.assertEqualExpr(expr, [self.x[0]], [1.0])

    def test_init_const(self):
        # Test initialization with a constant
        expr = highspy.highs.highs_linear_expression(5)
        self.assertEqualExpr(expr, [], [], 5)

        expr = self.h.expr(5)
        self.assertEqualExpr(expr, [], [], 5)

    def test_mutable(self):
        x, y, z = self.x[0:3]

        expr = x + 2 * y
        expr2 = expr  # reference to expr
        expr3 = expr.copy()  # copy of expr
        self.assertEqualExpr(expr, [x, y], [1, 2])
        self.assertEqualExpr(expr2, [x, y], [1, 2])
        self.assertEqualExpr(expr3, [x, y], [1, 2])

        expr += z
        self.assertEqualExpr(expr, [x, y, z], [1, 2, 1])
        self.assertEqualExpr(expr2, [x, y, z], [1, 2, 1])
        self.assertEqualExpr(expr3, [x, y], [1, 2])

        expr *= 2
        self.assertEqualExpr(expr, [x, y, z], [2, 4, 2])
        self.assertEqualExpr(expr2, [x, y, z], [2, 4, 2])
        self.assertEqualExpr(expr3, [x, y], [1, 2])

        # test simplify
        expr += x + y + z
        expr += x + y + z
        expr += x + y + z
        self.assertEqualExpr(expr, [x, y, z] + [x, y, z] * 3, [2, 4, 2] + [1, 1, 1] * 3)

        expr4 = expr.simplify()
        self.assertEqualExpr(expr, [x, y, z] + [x, y, z] * 3, [2, 4, 2] + [1, 1, 1] * 3)
        self.assertEqualExpr(expr4, [x, y, z], [5, 7, 5])

        # test edge cases
        e1 = x + z <= 3
        e2 = 2 * y

        # addition
        self.assertRaises(Exception, lambda: e1 + (e2 + 3))  # cannot add if one has bounds and the other has constant
        self.assertRaises(Exception, lambda: e1 + 5)  # cannot add constant to expr with bounds
        self.assertRaises(Exception, lambda: e1 + [])  # type: ignore[operator]  # unknown type

        expr = e1 + (1 <= (e2 + 4) <= 2)
        self.assertEqualExpr(expr, [x, z, y], [1, 1, 2], None, [-self.h.inf, 1])

        expr = e2.copy()
        expr += e1
        self.assertEqualExpr(expr, [y, x, z], [2, 1, 1], None, [-self.h.inf, 3])

        # subtract
        self.assertRaises(Exception, lambda: e1 - (e2 + 3))  # cannot add if one has bounds and the other has constant
        self.assertRaises(Exception, lambda: e1 - 5)  # cannot add constant to expr with bounds
        self.assertRaises(Exception, lambda: e1 - [])  # type: ignore[operator]  # unknown type

        expr = e1 - (1 <= (e2 + 4) <= 2)  # (-inf <= x + z <= 3) + (2 <= -2y <= 3)
        self.assertEqualExpr(expr, [x, z, y], [1, 1, -2], None, [-self.h.inf, 6])

        expr = e2.copy()
        expr -= e1
        self.assertEqualExpr(expr, [y, x, z], [2, -1, -1], None, [-3, self.h.inf])

    def test_immutable(self):
        x, y, z = self.x[0:3]

        expr = (1 * x - 2 * y).simplify()
        self.assertEqualExpr(expr, [x, y], [1, -2])

        expr2 = expr + z
        self.assertEqualExpr(expr, [x, y], [1, -2])
        self.assertEqualExpr(expr2, [x, y, z], [1, -2, 1])

        expr2 = expr + 5
        self.assertEqualExpr(expr, [x, y], [1, -2])
        self.assertEqualExpr(expr2, [x, y], [1, -2], 5)

        expr2 = expr <= 3
        self.assertEqualExpr(expr, [x, y], [1, -2])
        self.assertEqualExpr(expr2, [x, y], [1, -2], None, [-self.h.inf, 3])

        expr2 = 0 <= expr <= 3
        self.assertEqualExpr(expr, [x, y], [1, -2])
        self.assertEqualExpr(expr2, [x, y], [1, -2], None, [0, 3])

        expr2 = 0 <= expr <= 3
        expr2 += z
        self.assertEqualExpr(expr, [x, y], [1, -2])
        self.assertEqualExpr(expr2, [x, y, z], [1, -2, 1], None, [0, 3])

    def test_negation(self):
        # Test negation of a highs_var
        expr = -self.x[0]
        self.assertEqualExpr(expr, [self.x[0]], [-1])

        # Test negation of a highs_linear_expression
        x, y = self.x[0:2]
        expr = x - 2 * y
        self.assertEqualExpr(expr, [x, y], [1, -2])
        negr = -expr
        self.assertEqualExpr(negr, [x, y], [-1, 2])

        # Test negation of a highs_linear_expression
        expr = -self.h.qsum(self.x)
        self.assertEqualExpr(expr, self.x.idx(), [-1] * len(self.x))

    def test_equality(self):
        x, y = self.x[0:2]

        expr = x == y
        self.assertEqualExpr(expr, [x, y], [1, -1], None, [0, 0])

        expr = x + y == [1, 2]
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [1, 2])
        self.assertRaises(Exception, lambda: x == [x, y])
        self.assertRaises(Exception, lambda: x == self.h)
        self.assertRaises(Exception, lambda: x != self.h)
        self.assertRaises(Exception, lambda: x + y != y)
        self.assertRaises(Exception, lambda: x + 5 != self.h)

    def test_le_inequality(self):
        x, y = self.x[0:2]

        # Test inequality of two highs_linear_expressions
        expr = x <= y
        self.assertEqualExpr(expr, [x, y], [1, -1], None, [-self.h.inf, 0])
        self.assertRaises(Exception, lambda: x <= self.h)  # type: ignore[operator]

    def test_ge_inequality(self):
        x, y = self.x[0:2]

        # Test inequality of two highs_linear_expressions
        expr = x >= y  # y - x <= 0
        self.assertEqualExpr(expr, [y, x], [1, -1], None, [-self.h.inf, 0])
        self.assertRaises(Exception, lambda: x >= self.h)  # type: ignore[operator]
        self.assertRaises(Exception, lambda: x + 4 >= self.h)  # type: ignore[operator]
        self.assertRaises(Exception, lambda: x + 4 >= (y <= 2))

    def test_chain_inequality(self):
        x, y, z = self.x[0:3]

        # test basic chain inequality
        expr = 2 <= x + y <= 6
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [2, 6])

        expr = 2 <= (x + y) <= 6
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [2, 6])

        expr = 2 <= x <= 6
        self.assertEqualExpr(expr, [x], [1], None, [2, 6])

        # advanced chain use cases
        expr = y <= 6 + x <= y  # -6 <= x - y <= -6
        self.assertEqualExpr(expr, [x, y], [1, -1], None, [-6, -6])

        expr = x <= 6 <= x  # x == 6
        self.assertEqualExpr(expr, [x], [1], None, [6, 6])

        expr = x + y <= 6 <= x + y  # 6 <= x + y <= 6
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [6, 6])

        expr = x + y + 1 <= 6 <= x + y + 2  # 4 <= x + y <= 5
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [4, 5])

        # test chain ordering with a constant
        t1 = x + y
        t2 = x - y
        self.assertEqualExpr(t1, [x, y], [1, 1])
        self.assertEqualExpr(t2, [x, y], [1, -1])

        expr = (t1 + t2).simplify()  # 2x + 0y
        self.assertEqualExpr(expr, [x, y], [2, 0])

        t3 = 2 <= expr <= 4  # 2 <= 2x + 0y <= 4
        self.assertEqualExpr(t3, [x, y], [2, 0], None, [2, 4])

        vx, vl = [x, y, x, y], [1, 1, 1, -1]

        t3 = 2 <= (t1 + t2) <= 4  # 2 <= 2x + 0y <= 4
        self.assertEqualExpr(t3, vx, vl, None, [2, 4])

        t3 = 2 <= t1 + t2 <= 4  # 2 <= 2x + 0y <= 4
        self.assertEqualExpr(t3, vx, vl, None, [2, 4])

        t3 = (t1 + t2) <= 4  # -inf <= 2x + 0y <= 4
        self.assertEqualExpr(t3, vx, vl, None, [-self.h.inf, 4])

        t3 = t1 + t2 <= 4  # -inf <= 2x + 0y <= 4
        self.assertEqualExpr(t3, vx, vl, None, [-self.h.inf, 4])

        t3 = 2 <= t1 + t2  # 2 <= 2x + 0y <= inf
        self.assertEqualExpr(t3, vx, vl, None, [2, self.h.inf])

        t3 = t1 + t2 + 5  # 2x + 0y + 5
        self.assertEqualExpr(t3, vx, vl, 5)

        t3 = 5 + t1 + t2  # 2x + 0y + 5
        self.assertEqualExpr(t3, vx, vl, 5)

        t3 = 5 <= t1 + t2 + 5  # 0 <= 2x + 0y <= inf
        self.assertEqualExpr(t3, vx, vl, None, [0, self.h.inf])

        t3 = 5 + t1 + t2 <= 4  # -inf <= 2x + 0y <= -1
        self.assertEqualExpr(t3, vx, vl, None, [-self.h.inf, -1])

        t3 = 2 <= 5 + t1 + t2  # -3 <= 2x + 0y <= inf
        self.assertEqualExpr(t3, vx, vl, None, [-3, self.h.inf])

        t3 = 2 <= 5 + t1 + t2 <= 6  # -3 <= 2x + 0y <= 1
        self.assertEqualExpr(t3, vx, vl, None, [-3, 1])

        # test chain with variables on both sides
        t3 = 5 + x - x <= y <= 10  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 5 + 2 * x - 2 * x <= y <= 10  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 5 <= y <= 10 + 2 * x - 2 * x  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 5 - x + x <= y <= 10 + y - y  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 10 >= y >= 5 + x - x  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 10 >= y >= 5 + 2 * x - 2 * x  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 10 + 2 * x - 2 * x >= y >= 5  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        t3 = 10 + y - y >= y >= 5 - x + x  # 5 <= y <= 10
        self.assertEqualExpr(t3, [y], [1], None, [5, 10])

        vx, vl, _ = list(self.x), [1] * len(self.x), [-1] * len(self.x)
        t3 = qsum(self.x) <= 10  # -inf <= sum(x) <= 10
        self.assertEqualExpr(t3, vx, vl, None, [-self.h.inf, 10])

        t3 = qsum(self.x) <= y  # -inf <= sum(x) - y <= 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [-self.h.inf, 0])

        t3 = y <= qsum(self.x) <= y  # sum(x) == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = y >= qsum(self.x) >= y  # sum(x) == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = qsum(self.x) == y  # sum(x) - y == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = y == qsum(self.x)  # sum(x) - y == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = y + 1 <= qsum(self.x) <= y + 6  # 1 <= sum(x) - y <= 6
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [1, 6])

        t3 = y + 6 >= qsum(self.x) >= y + 1  # 1 <= sum(x) - y <= 6
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [1, 6])

        t3 = qsum(self.x) >= y >= qsum(self.x)  # sum(x) - y == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = qsum(self.x) <= y <= qsum(self.x)  # sum(x) - y == 0
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [0, 0])

        t3 = qsum(self.x) + 5 >= y >= qsum(self.x) + 2  # -5 <= sum(x) - y <= -2
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [-5, -2])

        t3 = qsum(self.x) + 2 <= y <= qsum(self.x) + 5  # -5 <= sum(x) - y <= -2
        self.assertEqualExpr(t3, vx + [y], vl + [-1], None, [-5, -2])

        t3 = (qsum(self.x) == 1 + qsum(self.x)).simplify()  # [] == 1
        self.assertEqualExpr(t3, vx, [0] * len(vx), None, [1, 1])

        self.assertRaises(Exception, lambda: x <= y <= 1)
        self.assertRaises(Exception, lambda: (qsum(self.x) + 5 >= y) >= qsum(self.x) + 2)
        self.assertRaises(Exception, lambda: qsum(self.x) + 2 <= (y <= qsum(self.x) + 5))
        self.assertRaises(Exception, lambda: qsum(self.x) + 2 <= y <= 5)
        self.assertRaises(Exception, lambda: 2 <= y <= 5 + qsum(self.x))

    def test_order_priority(self):
        x, y, z = self.x[:3]

        # prefer more variables
        self.assertEqualExpr(x <= y + z, [y, z, x], [1, 1, -1], None, [0, self.h.inf])
        self.assertEqualExpr(y + z >= x, [y, z, x], [1, 1, -1], None, [0, self.h.inf])
        self.assertEqualExpr(x >= y + z, [y, z, x], [1, 1, -1], None, [-self.h.inf, 0])
        self.assertEqualExpr(y + z <= x, [y, z, x], [1, 1, -1], None, [-self.h.inf, 0])
        self.assertEqualExpr(y + z == x, [y, z, x], [1, 1, -1], None, [0, 0])
        self.assertEqualExpr(x == y + z, [y, z, x], [1, 1, -1], None, [0, 0])
        self.assertEqualExpr(x + y == 2 * x + 2 * y + z, [x, y, z, x, y], [2, 2, 1, -1, -1], None, [0, 0])

        # prefer constant
        self.assertEqualExpr(y <= x + 2, [y, x], [1, -1], None, [-self.h.inf, 2])
        self.assertEqualExpr(x + 2 >= y, [y, x], [1, -1], None, [-self.h.inf, 2])
        self.assertEqualExpr(x + 2 <= y, [y, x], [1, -1], None, [2, self.h.inf])
        self.assertEqualExpr(y >= x + 2, [y, x], [1, -1], None, [2, self.h.inf])
        self.assertEqualExpr(y + 2 == x, [x, y], [1, -1], None, [2, 2])
        self.assertEqualExpr(x == y + 2, [x, y], [1, -1], None, [2, 2])

        # prefer 'left'
        self.assertEqualExpr(x + 2 <= y + 3, [x, y], [1, -1], None, [-self.h.inf, 1])
        self.assertEqualExpr(y + 3 >= x + 2, [x, y], [1, -1], None, [-self.h.inf, 1])
        self.assertEqualExpr(x + 2 >= y + 3, [y, x], [1, -1], None, [-self.h.inf, -1])
        self.assertEqualExpr(y + 3 <= x + 2, [y, x], [1, -1], None, [-self.h.inf, -1])
        self.assertEqualExpr(x + 2 == y + 3, [x, y], [1, -1], None, [1, 1])
        self.assertEqualExpr(y + 3 == x + 2, [y, x], [1, -1], None, [-1, -1])

        self.assertEqualExpr(6 <= x + y <= 8, [x, y], [1, 1], None, [6, 8])
        self.assertEqualExpr(6 + x <= y <= 8 + x, [y, x], [1, -1], None, [6, 8])
        self.assertEqualExpr(6 + x <= y + 2 <= 8 + x, [y, x], [1, -1], None, [4, 6])
        self.assertEqualExpr(x <= 6 <= x, [x], [1], None, [6, 6])
        self.assertEqualExpr(x <= y + z <= x + 5, [y, z, x], [1, 1, -1], None, [0, 5])

    def test_chain_inequality_hacks(self):
        x, y = self.x[0:2]

        # Test hacks around chain inequality
        # These don't need to be supported, good to check if logic changes
        t = x + y
        self.assertEqualExpr(t, [x, y], [1, 1])

        bool(t <= 10)
        expr = 5 <= t
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, 10])
        expr = 5 <= t
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, self.h.inf])

        bool(t <= 10)
        expr = t <= 18
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [-self.h.inf, 18])

        bool(t <= 10)
        expr = t >= 5
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, 10])
        expr = t >= 5
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, self.h.inf])

        expr = bool(t <= 10) and t >= 5
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, 10])

        # test hack if expression is modified
        bool(t <= 10)
        t += y
        expr = 5 <= t
        self.assertEqualExpr(expr, [x, y, y], [1, 1, 1], None, [5, self.h.inf])

        # test hack if using 'equal' temporary expr
        bool(x + y <= 10)
        expr = 5 <= x + y
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, 10])
        expr = 5 <= x + y
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, self.h.inf])

        # test hack if using 'not-equal' temporary expr
        bool(x + 3 * y <= 10)
        expr = 5 <= x + y
        self.assertEqualExpr(expr, [x, y], [1, 1], None, [5, self.h.inf])

    def test_bounds_already_set(self):
        x, y = self.x[0:2]
        self.assertRaises(Exception, lambda: (1 <= 2 * x + 3 * y) <= 5)
        self.assertRaises(Exception, lambda: (x <= 4) <= 5)
        self.assertRaises(Exception, lambda: 2 <= (x <= 4) <= 5)
        self.assertRaises(Exception, lambda: 2 <= (4 <= x))
        self.assertRaises(Exception, lambda: 2 <= (x <= 4))
        self.assertRaises(Exception, lambda: 4 >= (x >= 2))

        # Cannot a constant if bounds already exist
        self.assertRaises(Exception, lambda: (x + y <= 3) + 5)
        self.assertEqualExpr((x + y <= 3) + (self.h.expr() == 5), [x, y], [1, 1], None, [-self.h.inf, 8])

        # Cannot a expr with constant if bounds already exist
        self.assertRaises(Exception, lambda: (x + y <= 3) + (x + 5))
        self.assertEqualExpr((x + y <= 3) + (x == -5), [x, y, x], [1, 1, 1], None, [-self.h.inf, -2])

    def test_addition(self):
        x, y = self.x[0:2]

        # Test addition of two variables
        expr = x + y
        self.assertEqualExpr(expr, [x, y], [1, 1])

        # Test addition of two exprs (with bounds)
        e1 = 2 <= x - y <= 4
        e2 = 2 <= 2 * x - y <= 4
        self.assertEqualExpr(e1, [x, y], [1, -1], None, [2, 4])
        self.assertEqualExpr(e2, [x, y], [2, -1], None, [2, 4])

        expr = e1 + e2
        self.assertEqualExpr(expr, [x, y, x, y], [1, -1, 2, -1], None, [4, 8])

        expr = (e1 + e2).simplify()
        self.assertEqualExpr(expr, [x, y], [3, -2], None, [4, 8])

        # Test multiplication/addition of two exprs (with bounds)
        expr = (2 * e1 + 3 * e2).simplify()
        self.assertEqualExpr(expr, [x, y], [8, -5], None, [10, 20])

    def test_subtraction(self):
        x, y = self.x[0:2]

        # Test subtraction of two highs_linear_expressions
        expr = x - y
        self.assertEqualExpr(expr, [x, y], [1, -1])

        # Test subtraction of two exprs (with bounds)
        e1 = 2 <= x - y <= 4
        e2 = 2 <= 2 * x - y <= 4
        self.assertEqualExpr(e1, [x, y], [1, -1], None, [2, 4])
        self.assertEqualExpr(e2, [x, y], [2, -1], None, [2, 4])

        expr = e1 + (-1.0 * e2)
        self.assertEqualExpr(expr, [x, y, x, y], [1, -1, -2, 1], None, [-2, 2])

        expr = e1 + (-e2)
        self.assertEqualExpr(expr, [x, y, x, y], [1, -1, -2, 1], None, [-2, 2])

        expr = e1 - e2
        self.assertEqualExpr(expr, [x, y, x, y], [1, -1, -2, 1], None, [-2, 2])

        expr = (e1 - e2).simplify()
        self.assertEqualExpr(expr, [x, y], [-1, 0], None, [-2, 2])

        # Test multiplication/subtraction of two exprs (with bounds)
        expr = (2 * e1 - e2).simplify()
        self.assertEqualExpr(expr, [x, y], [0, -1], None, [0, 6])

        # test rsub
        expr = 5 - (x + y)
        self.assertEqualExpr(expr, [x, y], [-1, -1], 5)

    def test_multiply(self):
        x, y = self.x[0:2]

        # basic tests
        e1 = x * 3
        self.assertEqualExpr(e1, [x], [3])
        e1 = 3 * x
        self.assertEqualExpr(e1, [x], [3])

        e1 = x - y
        self.assertEqualExpr(e1, [x, y], [1, -1])

        e2 = 2 * e1
        self.assertEqualExpr(e2, [x, y], [2, -2])

        e1 *= 3
        self.assertEqualExpr(e1, [x, y], [3, -3])

        e2 = -1 * e1
        self.assertEqualExpr(e2, [x, y], [-3, 3])

        e2 = 0 * e1
        self.assertEqualExpr(e2, [x, y], [0, 0])

        e2 = highs_linear_expression(-1) * e1
        self.assertEqualExpr(e2, [x, y], [-3, 3])

        self.assertRaises(Exception, lambda: e1 * x)
        self.assertRaises(Exception, lambda: e1 * highs_linear_expression(x))
        self.assertRaises(Exception, lambda: e1 * highs_linear_expression(x - x))

        # Test with constant
        e1 = x - y + 4
        self.assertEqualExpr(e1, [x, y], [1, -1], 4)

        e2 = e1 * 2.5
        self.assertEqualExpr(e2, [x, y], [2.5, -2.5], 10)

        e1 *= 2.5
        self.assertEqualExpr(e1, [x, y], [2.5, -2.5], 10)

        e2 = -1.0 * e1
        self.assertEqualExpr(e2, [x, y], [-2.5, 2.5], -10)

        e1 *= -1.0
        self.assertEqualExpr(e1, [x, y], [-2.5, 2.5], -10)

        e1 *= highs_linear_expression(-1.0)
        self.assertEqualExpr(e1, [x, y], [2.5, -2.5], 10)

        # Test with bounds
        e1 = x - 2 * y == [1, 4]
        self.assertEqualExpr(e1, [x, y], [1, -2], None, [1, 4])

        e2 = e1 * 2.5
        self.assertEqualExpr(e2, [x, y], [2.5, -5], None, [2.5, 10])

        e1 *= 2.5
        self.assertEqualExpr(e1, [x, y], [2.5, -5], None, [2.5, 10])

        e2 = -1.0 * e1
        self.assertEqualExpr(e2, [x, y], [-2.5, 5], None, [-10, -2.5])

        e1 *= -1.0
        self.assertEqualExpr(e1, [x, y], [-2.5, 5], None, [-10, -2.5])

        e1 *= highs_linear_expression(-1.0)
        self.assertEqualExpr(e1, [x, y], [2.5, -5], None, [2.5, 10])

        e1 = highs_linear_expression(-1.0)
        e1 *= x + 4
        self.assertEqualExpr(e1, [x], [-1], -4)

        e1 = highs_linear_expression(-1.0)
        e1 *= x - 4 <= 3
        self.assertEqualExpr(e1, [x], [-1], None, [-7, self.h.inf])

        e1 = highs_linear_expression(1.0)
        e1 *= x - 4 <= 3
        self.assertEqualExpr(e1, [x], [1], None, [-self.h.inf, 7])

    def test_divide(self):
        x, y = self.x[0:2]

        expr = x / 4
        self.assertEqualExpr(expr, [x], [0.25])

        expr = (x + 2 * y + 8) / 4
        self.assertEqualExpr(expr, [x, y], [0.25, 0.5], 2)

        expr = (4 <= x + 2 * y <= 8) / 2
        self.assertEqualExpr(expr, [x, y], [0.5, 1], None, [2, 4])

        expr = (x + 2 * y) / highs_linear_expression(4)
        self.assertEqualExpr(expr, [x, y], [0.25, 0.5])

        expr = x + 2 * y + 8
        expr /= 4
        self.assertEqualExpr(expr, [x, y], [0.25, 0.5], 2)

        expr = 4 <= x + 2 * y <= 8
        expr /= -2
        self.assertEqualExpr(expr, [x, y], [-0.5, -1], None, [-4, -2])

        self.assertRaises(Exception, lambda: x / y)
        self.assertRaises(Exception, lambda: (x + y) / (x + 1))
        self.assertRaises(Exception, lambda: 2 / x)
        self.assertRaises(Exception, lambda: 2 / (x + y))
        self.assertRaises(ZeroDivisionError, lambda: x / 0)
        self.assertRaises(ZeroDivisionError, lambda: (x + y) / highs_linear_expression(0))

    def test_simplify(self):
        x, y, z = self.x[0:3]

        # basics
        expr = x + y + z
        self.assertEqualExpr(expr, [x, y, z], [1, 1, 1])
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x, y, z], [1, 1, 1])

        expr = z + x + y  # simplify reorders variables
        self.assertEqualExpr(expr, [z, x, y], [1, 1, 1])
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x, y, z], [1, 1, 1])

        expr = x + x + x + x + x
        self.assertEqualExpr(expr, [x] * 5, [1] * 5)
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x], [5])

        expr = x - x + x - x + x
        self.assertEqualExpr(expr, [x] * 5, [1, -1, 1, -1, 1])
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x], [1])

        expr = -x
        expr += x
        expr *= -5
        self.assertEqualExpr(expr, [x, x], [5, -5])
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x], [0])

        # with constant
        expr = x + y + 1 + y
        self.assertEqualExpr(expr, [x, y, y], [1, 1, 1], 1)
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x, y], [1, 2], 1)

        # with bounds
        expr = 0 <= x + y + 1 + y <= 5
        self.assertEqualExpr(expr, [x, y, y], [1, 1, 1], None, [-1, 4])
        expr = expr.simplify()
        self.assertEqualExpr(expr, [x, y], [1, 2], None, [-1, 4])

    def test_evaluate(self):
        h = self.h
        x, y = h.addVariables(2, lb=-h.inf)

        h.addConstrs(-x + y >= 2, x + y >= 0)
        h.minimize(y)

        self.assertAlmostEqual(h.val(x), -1)
        self.assertAlmostEqual(h.val(y), 1)
        self.assertAlmostEqual(h.val(x + y), 0)
        self.assertAlmostEqual(h.val(y - x), 2)
        self.assertEqual(h.val(y - x >= 2), True)
        self.assertEqual(h.val(y - x <= 1), False)

    def test_repr_str(self):
        x, y = self.x[0:2]
        self.assertEqual(repr(x), "highs_var(0)")
        self.assertEqual(str(x), "highs_var(0)")

        c = self.h.addConstr(x + y <= 5)
        self.assertEqual(repr(c), "highs_cons(0)")
        self.assertEqual(str(c), "highs_cons(0)")

        expr = c.expr()
        self.assertEqual(repr(expr), "-inf <= 1.0_v0  1.0_v1 <= 5.0")
        self.assertEqual(str(expr), "-inf <= 1.0_v0  1.0_v1 <= 5.0")

        expr = x + y
        self.assertEqual(repr(expr), "1.0_v0  1.0_v1")
        self.assertEqual(str(expr), "1.0_v0  1.0_v1")

        expr = x + y + x + x + y
        self.assertEqual(repr(expr), "1.0_v0  1.0_v1  1.0_v0  1.0_v0  1.0_v1")
        self.assertEqual(str(expr), "3.0_v0  2.0_v1")

        expr = y + x + 5
        self.assertEqual(repr(expr), "1.0_v1  1.0_v0  5.0")
        self.assertEqual(str(expr), "1.0_v0  1.0_v1  5.0")

        expr = y + x == 5
        self.assertEqual(repr(expr), "1.0_v1  1.0_v0 == 5.0")
        self.assertEqual(str(expr), "1.0_v0  1.0_v1 == 5.0")

        expr = y + x <= 5
        self.assertEqual(repr(expr), "-inf <= 1.0_v1  1.0_v0 <= 5.0")
        self.assertEqual(str(expr), "-inf <= 1.0_v0  1.0_v1 <= 5.0")

    def test_lexicographic_optimization(self):
        # max f1 = X1
        # max f2 = 3 X1 + 4 X2
        # st  X1 <= 2
        #     X2 <= 4
        #     5 X1 + 4 X2 <= 20
        model = highspy.Highs()
        model.setOptionValue("blend_multi_objectives", False)
        num_vars = 2
        model.addVars(num_vars, np.array([0.0, 0.0]), np.array([2.0, 4.0]))
        obj1 = highspy.HighsLinearObjective()
        obj1.offset = 0
        obj1.coefficients = [1, 0]
        obj1.priority = 2
        obj1.rel_tolerance = 0.1
        obj1.abs_tolerance = 0.2
        obj2 = highspy.HighsLinearObjective()
        obj2.offset=0
        obj2.coefficients=[3, 4]
        obj2.priority=1
        model.addLinearObjective(obj1)
        model.addLinearObjective(obj2)
        model.addRow(0.0, 20.0, num_vars, np.arange(num_vars), np.array([5.0, 4.0]))
        model.run()

        status = model.getModelStatus()
        self.assertEqual(status, highspy.HighsModelStatus.kOptimal)

    def test_get_fixed_lp(self):
        # Min    f  = -3x_0 - 2x_1 - x_2
        # s.t.          x_0 +  x_1 + x_2 <=  7
        #              4x_0 + 2x_1 + x_2  = 12
        #              x_0 >=0; x_1 >= 0; x_2 binary
        inf = highspy.kHighsInf
        model = highspy.Highs()
        num_vars = 3
        model.addVars(num_vars, np.array([0.0, 0.0, 0.0]), np.array([2.0, 4.0, inf]))
        num_cons = 2
        lower = np.array([-inf, 12], dtype=np.double)
        upper = np.array([7, 12], dtype=np.double)
        num_new_nz = 6
        starts = np.array([0, 2, 4])
        indices = np.array([0, 1, 0, 1, 0, 1])
        values = np.array([1, 4, 1, 2, 1, 1], dtype=np.double)
        model.addRows(num_cons, lower, upper, num_new_nz, starts, indices, values)
        model.changeColsIntegrality(1, np.array([2]), np.array([highspy.HighsVarType.kInteger]))
        model.setOptionValue("presolve", "off")
        model.run()
        mip_objective_function_value = model.getInfo().objective_function_value
        solution = model.getSolution()
        [status, fixed_lp] = model.getFixedLp()
        self.assertEqual(status, highspy.HighsStatus.kOk)
        model.passModel(fixed_lp)
        model.setSolution(solution)
        model.run()
        self.assertEqual(model.getInfo().objective_function_value, mip_objective_function_value)
        self.assertEqual(model.getInfo().simplex_iteration_count, 0)

    def test_get_objectives(self):
        # Build a simple model with 3 vars and a primary (single) objective
        h = highspy.Highs()
        h.silent()

        x, y, z, _ = h.addVariables(4, lb=0, ub=10)
        h.maximize(x + 2 * y + 3 * z + 5)

        # Test getObjective (primary objective)
        obj_expr, sense = h.getObjective()
        self.assertEqual(list(map(int, obj_expr.idxs)), [0, 1, 2])
        self.assertEqual(obj_expr.vals, [1.0, 2.0, 3.0])
        self.assertEqual(obj_expr.constant, 5.0)
        self.assertEqual(sense, highspy.ObjSense.kMaximize)

        # No multi-objectives yet
        self.assertEqual(h.getNumLinearObjectives(), 0)

        # Add two linear objectives (multi-objective data structure)
        o1 = highspy.HighsLinearObjective()
        o1.offset = 10.0
        o1.coefficients = [1.0, 0.0, 0.0, 0.0]
        o1.priority = 5
        o1.abs_tolerance = 0.01
        o1.rel_tolerance = 0.02

        o2 = highspy.HighsLinearObjective()
        o2.offset = -3.5
        o2.coefficients = [0.0, 1.0, 1.0, 0.0]
        o2.priority = 3
        o2.abs_tolerance = 0.0
        o2.rel_tolerance = 0.0

        self.assertEqual(h.addLinearObjective(o1), highspy.HighsStatus.kOk)
        self.assertEqual(h.addLinearObjective(o2), highspy.HighsStatus.kOk)

        # Verify count
        self.assertEqual(h.getNumLinearObjectives(), 2)

        # Retrieve and verify first added linear objective
        lo0 = h.getLinearObjective(0)
        self.assertAlmostEqual(lo0.offset, o1.offset)
        self.assertEqual(list(lo0.coefficients), o1.coefficients)
        self.assertEqual(lo0.priority, o1.priority)
        self.assertAlmostEqual(lo0.abs_tolerance, o1.abs_tolerance)
        self.assertAlmostEqual(lo0.rel_tolerance, o1.rel_tolerance)

        # Retrieve and verify second added linear objective
        lo1 = h.getLinearObjective(1)
        self.assertAlmostEqual(lo1.offset, o2.offset)
        self.assertEqual(list(lo1.coefficients), o2.coefficients)
        self.assertEqual(lo1.priority, o2.priority)
        self.assertAlmostEqual(lo1.abs_tolerance, o2.abs_tolerance)
        self.assertAlmostEqual(lo1.rel_tolerance, o2.rel_tolerance)

        # Ensure original objective remains unchanged
        obj_expr2, sense2 = h.getObjective()
        self.assertEqual(list(map(int, obj_expr2.idxs)), [0, 1, 2])
        self.assertEqual(obj_expr2.vals, [1.0, 2.0, 3.0])
        self.assertEqual(obj_expr2.constant, 5.0)
        self.assertEqual(sense2, highspy.ObjSense.kMaximize)

    def test_get_iis(self):
        h = highspy.Highs()
#        h.silent()
        # Build an infeasible LP problem
        # Problem: minimize x + y
        # Subject to: x + y <= 0.5 and x + y >= 2.0 (contradictory constraints)
        # Bounds: 0 <= x <= 1, 0 <= y <= 1

        lp = highspy.HighsLp()
        lp.num_col_ = 2
        lp.num_row_ = 2

        # Objective: minimize x + y
        lp.col_cost_ = [1.0, 1.0]

        # Variable bounds: 0 <= x <= 1, 0 <= y <= 1
        lp.col_lower_ = [0.0, 0.0]
        lp.col_upper_ = [1.0, 1.0]

        # Constraints:
        # Row 0: x + y <= 0.5
        # Row 1: -x - y <= -2.0  (equivalent to x + y >= 2.0)
        lp.row_lower_ = [-np.inf, -np.inf]
        lp.row_upper_ = [0.5, -2.0]

        # Constraint matrix (CSC format)
        lp.a_matrix_.start_ = [0, 2, 4]
        lp.a_matrix_.index_ = [0, 1, 0, 1]  # Row indices
        lp.a_matrix_.value_ = [1.0, 1.0, -1.0, -1.0]  # Coefficients

        # Pass model to HiGHS
        h.passModel(lp)

        # Solve the problem
        h.run()

        model_status = h.getModelStatus()
        self.assertEqual(model_status, highspy.HighsModelStatus.kInfeasible)

        h.setOptionValue("iis_strategy", highspy.IisStrategy.kIisStrategyIrreducible)
        [status, iis] = h.getIis()

        self.assertEqual(iis.valid_, True)
        self.assertEqual(iis.col_index_[0], 0)
        self.assertEqual(iis.col_index_[1], 1)
        self.assertEqual(iis.row_index_[0], 1)

        self.assertEqual(iis.col_bound_[0], highspy.IisBoundStatus.kIisBoundStatusLower)
        self.assertEqual(iis.col_bound_[1], highspy.IisBoundStatus.kIisBoundStatusUpper)
        self.assertEqual(iis.row_bound_[0], highspy.IisBoundStatus.kIisBoundStatusUpper)

        self.assertEqual(iis.col_status_[0], highspy.IisStatus.kIisStatusInConflict)
        self.assertEqual(iis.col_status_[1], highspy.IisStatus.kIisStatusInConflict)
        self.assertEqual(iis.row_status_[0], highspy.IisStatus.kIisStatusNotInConflict)
        self.assertEqual(iis.row_status_[1], highspy.IisStatus.kIisStatusInConflict)

    def test_expr(self):
        x = self.x[0]
        expr = x + 1
        self.assertTrue(expr.__ne__(None))
        self.assertRaises(Exception, lambda: expr.__ne__(5))
        self.assertRaises(TypeError, lambda: int(expr))

    def test_constant_expr_mul_var(self):
        """expr * constant"""
        x = self.x[0]
        expr = highs_linear_expression(5)
        result = expr * x
        self.assertEqualExpr(result, [x], [5.0])
        self.assertRaises(Exception, lambda: expr * "bad") # type: ignore
