"""
Copyright, the CVXPY authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import numpy as np
import pytest

import cvxpy as cp
import cvxpy.settings as s
from cvxpy.reductions.dqcp2dcp.dqcp2dcp import Dqcp2Dcp
from cvxpy.reductions.solvers import bisection
from cvxpy.tests import base_test

SOLVER = cp.CLARABEL


class TestDqcp(base_test.BaseTest):
    def test_basic_with_interval(self) -> None:
        x = cp.Variable()
        expr = cp.ceil(x)

        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_convex())
        self.assertFalse(expr.is_concave())
        self.assertFalse(expr.is_dcp())
        self.assertFalse(expr.is_dgp())

        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        red = Dqcp2Dcp(problem)
        reduced = red.reduce()
        self.assertTrue(reduced.is_dcp())
        self.assertEqual(len(reduced.parameters()), 1)
        soln = bisection.bisect(reduced, low=12, high=17, solver=cp.SCS)
        self.assertAlmostEqual(soln.opt_val, 12.0, places=3)

        problem.unpack(soln)
        self.assertEqual(soln.opt_val, problem.value)
        self.assertAlmostEqual(x.value, 12.0, places=3)

    def test_basic_without_interval(self) -> None:
        x = cp.Variable()
        expr = cp.ceil(x)

        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_convex())
        self.assertFalse(expr.is_concave())
        self.assertFalse(expr.is_dcp())
        self.assertFalse(expr.is_dgp())

        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        red = Dqcp2Dcp(problem)
        reduced = red.reduce()
        self.assertTrue(reduced.is_dcp())
        self.assertEqual(len(reduced.parameters()), 1)
        soln = bisection.bisect(reduced, solver=cp.SCS)
        self.assertAlmostEqual(soln.opt_val, 12.0, places=3)

        problem.unpack(soln)
        self.assertEqual(soln.opt_val, problem.value)
        self.assertAlmostEqual(x.value, 12.0, places=3)

    def test_basic_solve(self) -> None:
        x = cp.Variable()
        expr = cp.ceil(x)

        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_convex())
        self.assertFalse(expr.is_concave())
        self.assertFalse(expr.is_dcp())
        self.assertFalse(expr.is_dgp())

        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())
        problem.solve(SOLVER, qcp=True, low=12, high=17)
        self.assertAlmostEqual(problem.value, 12.0, places=3)
        self.assertAlmostEqual(x.value, 12.0, places=3)

        problem._clear_solution()
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 12.0, places=3)
        self.assertAlmostEqual(x.value, 12.0, places=3)

        problem._clear_solution()
        problem.solve(SOLVER, qcp=True, high=17)
        self.assertAlmostEqual(problem.value, 12.0, places=3)
        self.assertAlmostEqual(x.value, 12.0, places=3)

        problem._clear_solution()
        problem.solve(SOLVER, qcp=True, low=12)
        self.assertAlmostEqual(problem.value, 12.0, places=3)
        self.assertAlmostEqual(x.value, 12.0, places=3)

        problem._clear_solution()
        problem.solve(SOLVER, qcp=True, low=0, high=100)
        self.assertAlmostEqual(problem.value, 12.0, places=3)
        self.assertAlmostEqual(x.value, 12.0, places=3)

    def test_basic_maximization_with_interval(self) -> None:
        x = cp.Variable()
        expr = cp.ceil(x)

        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_convex())
        self.assertFalse(expr.is_concave())
        self.assertFalse(expr.is_dcp())
        self.assertFalse(expr.is_dgp())

        problem = cp.Problem(cp.Maximize(expr), [x >= 12, x <= 17])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 17.0, places=3)

    def test_basic_maximum(self) -> None:
        x, y = cp.Variable(2)
        expr = cp.maximum(cp.ceil(x), cp.ceil(y))

        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17, y >= 17.4])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 18.0)
        self.assertLess(x.value, 17.1)
        self.assertGreater(x.value, 11.9)
        self.assertGreater(y.value, 17.3)

    def test_basic_minimum(self) -> None:
        x, y = cp.Variable(2)
        expr = cp.minimum(cp.ceil(x), cp.ceil(y))

        problem = cp.Problem(cp.Maximize(expr), [x >= 11.9, x <= 15.8, y >= 17.4])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 16.0)
        self.assertLess(x.value, 16.0)
        self.assertGreater(x.value, 14.9)
        self.assertGreater(y.value, 17.3)

    def test_basic_composition(self) -> None:
        x, y = cp.Variable(2)
        expr = cp.maximum(cp.ceil(cp.ceil(x)), cp.ceil(cp.ceil(y)))

        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17, y >= 17.4])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 18.0)
        self.assertLess(x.value, 17.1)
        self.assertGreater(x.value, 11.9)
        self.assertGreater(y.value, 17.3)

        # This problem should have the same solution.
        expr = cp.maximum(cp.floor(cp.ceil(x)), cp.floor(cp.ceil(y)))
        problem = cp.Problem(cp.Minimize(expr), [x >= 12, x <= 17, y >= 17.4])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 18.0)
        self.assertLess(x.value, 17.1)
        self.assertGreater(x.value, 11.9)
        self.assertGreater(y.value, 17.3)

    def test_basic_floor(self) -> None:
        x = cp.Variable()
        expr = cp.floor(x)

        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_convex())
        self.assertFalse(expr.is_concave())
        self.assertFalse(expr.is_dcp())
        self.assertFalse(expr.is_dgp())

        problem = cp.Problem(cp.Minimize(expr), [x >= 11.8, x <= 17])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 11.0)
        self.assertGreater(x.value, 11.7)

    def test_ceil_floor_near_integer_tolerance(self) -> None:
        """Values within solver noise of an integer evaluate to that integer."""
        x = cp.Variable()
        for noisy in [3 - 1e-10, 3 + 1e-10]:
            x.value = noisy
            self.assertEqual(cp.floor(x).value, 3.0)
            self.assertEqual(cp.ceil(x).value, 3.0)
        # Exact integers and clearly-non-integer values are unaffected.
        x.value = 4.0
        self.assertEqual(cp.floor(x).value, 4.0)
        self.assertEqual(cp.ceil(x).value, 4.0)
        x.value = 2.5
        self.assertEqual(cp.floor(x).value, 2.0)
        self.assertEqual(cp.ceil(x).value, 3.0)

    def test_basic_multiply_nonneg(self) -> None:
        x, y = cp.Variable(2, nonneg=True)
        expr = x * y
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_quasiconvex())

        self.assertFalse(expr.is_dcp())

        problem = cp.Problem(cp.Maximize(expr), [x <= 12, y <= 6])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 72, places=1)
        self.assertAlmostEqual(x.value, 12, places=1)
        self.assertAlmostEqual(y.value, 6, places=1)

    def test_basic_multiply_nonpos(self) -> None:
        x, y = cp.Variable(2, nonpos=True)
        expr = x * y
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_quasiconvex())

        self.assertFalse(expr.is_dcp())

        problem = cp.Problem(cp.Maximize(expr), [x >= -12, y >= -6])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 72, places=1)
        self.assertAlmostEqual(x.value, -12, places=1)
        self.assertAlmostEqual(y.value, -6, places=1)

    def test_multiply_solver_failure_during_bisection(self) -> None:
        # Regression test: CLARABEL fails (instead of reporting
        # infeasibility) at query points near the optimum of this badly
        # scaled problem; bisection used to re-solve the identical failing
        # subproblem until it raised "Max iters hit during bisection".
        x = cp.Variable(nonneg=True)
        y = cp.Variable(nonneg=True)
        problem = cp.Problem(cp.Maximize(cp.multiply(x, y)), [x + y <= 1e-4])
        with pytest.warns(RuntimeWarning):
            problem.solve(solver=cp.CLARABEL, qcp=True)
        self.assertIn(problem.status, s.SOLUTION_PRESENT)
        # optimal value is (5e-5)**2 = 2.5e-9; bisection eps is 1e-6
        self.assertAlmostEqual(problem.value, 2.5e-9, places=6)

    def test_basic_multiply_qcvx(self) -> None:
        x = cp.Variable(nonneg=True)
        y = cp.Variable(nonpos=True)
        expr = x * y
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertFalse(expr.is_quasiconcave())

        self.assertFalse(expr.is_dcp())

        problem = cp.Problem(cp.Minimize(expr), [x <= 7, y >= -6])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, -42, places=1)
        self.assertAlmostEqual(x.value, 7, places=1)
        self.assertAlmostEqual(y.value, -6, places=1)

        x = cp.Variable(nonneg=True)
        y = cp.Variable(nonpos=True)
        expr = y * x
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertFalse(expr.is_quasiconcave())

        self.assertFalse(expr.is_dcp())

        problem = cp.Problem(cp.Minimize(expr), [x <= 7, y >= -6])
        self.assertTrue(problem.is_dqcp())
        self.assertFalse(problem.is_dcp())
        self.assertFalse(problem.is_dgp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, -42, places=1)
        self.assertAlmostEqual(x.value, 7, places=1)
        self.assertAlmostEqual(y.value, -6, places=1)

    def test_concave_multiply(self) -> None:
        x, y = cp.Variable(2, nonneg=True)
        expr = cp.sqrt(x) * cp.sqrt(y)
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_quasiconvex())

        problem = cp.Problem(cp.Maximize(expr), [x <= 4, y <= 9])
        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 6, places=1)
        self.assertAlmostEqual(x.value, 4, places=1)
        self.assertAlmostEqual(y.value, 9, places=1)

        x, y = cp.Variable(2, nonneg=True)
        expr = (cp.sqrt(x) + 2.0) * (cp.sqrt(y) + 4.0)
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertFalse(expr.is_quasiconvex())

        problem = cp.Problem(cp.Maximize(expr), [x <= 4, y <= 9])
        problem.solve(cp.SCS, qcp=True)
        # (2 + 2) * (3 + 4) = 28
        self.assertAlmostEqual(problem.objective.value, 28, places=1)
        self.assertAlmostEqual(x.value, 4, places=1)
        self.assertAlmostEqual(y.value, 9, places=1)

    def test_basic_ratio(self) -> None:
        x = cp.Variable()
        y = cp.Variable(nonneg=True)
        expr = x / y
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertTrue(expr.is_quasiconvex())

        problem = cp.Problem(cp.Minimize(expr), [x == 12, y <= 6])
        self.assertTrue(problem.is_dqcp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 2.0, places=1)
        self.assertAlmostEqual(x.value, 12, places=1)
        self.assertAlmostEqual(y.value, 6, places=1)

        x = cp.Variable()
        y = cp.Variable(nonpos=True)
        expr = x / y
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconcave())
        self.assertTrue(expr.is_quasiconvex())

        problem = cp.Problem(cp.Maximize(expr), [x == 12, y >= -6])
        self.assertTrue(problem.is_dqcp())

        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, -2.0, places=1)
        self.assertAlmostEqual(x.value, 12, places=1)
        self.assertAlmostEqual(y.value, -6, places=1)

    def test_lin_frac(self) -> None:
        x = cp.Variable((2,), nonneg=True)
        A = np.array([[1.0, 2.0], [3.0, 4.0]])
        b = np.arange(2)
        C = 2 * A
        d = np.arange(2)
        lin_frac = (cp.matmul(A, x) + b) / (cp.matmul(C, x) + d)
        self.assertTrue(lin_frac.is_dqcp())
        self.assertTrue(lin_frac.is_quasiconvex())
        self.assertTrue(lin_frac.is_quasiconcave())

        problem = cp.Problem(cp.Minimize(cp.sum(x)), [x >= 0, lin_frac <= 1])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 0, places=1)
        np.testing.assert_almost_equal(x.value, 0, decimal=5)

    def test_concave_frac(self) -> None:
        x = cp.Variable(nonneg=True)
        concave_frac = cp.sqrt(x) / cp.exp(x)
        self.assertTrue(concave_frac.is_dqcp())
        self.assertTrue(concave_frac.is_quasiconcave())
        self.assertFalse(concave_frac.is_quasiconvex())

        problem = cp.Problem(cp.Maximize(concave_frac))
        self.assertTrue(problem.is_dqcp())
        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 0.428, places=1)
        self.assertAlmostEqual(x.value, 0.5, places=1)

    def test_length(self) -> None:
        x = cp.Variable(5)
        expr = cp.length(x)
        self.assertTrue(expr.is_dqcp())
        self.assertTrue(expr.is_quasiconvex())
        self.assertFalse(expr.is_quasiconcave())

        problem = cp.Problem(cp.Minimize(expr), [x[0] == 2.0, x[1] == 1.0])
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 2)
        np.testing.assert_almost_equal(x.value, np.array([2, 1, 0, 0, 0]))

    def test_length_example(self) -> None:
        """Fix #1760."""
        n = 10
        np.random.seed(1)
        A = np.random.randn(n, n)
        x_star = np.random.randn(n)
        b = A @ x_star
        epsilon = 1e-2
        x = cp.Variable(n)
        mse = cp.sum_squares(A @ x - b)/n
        problem = cp.Problem(cp.Minimize(cp.length(x)), [mse <= epsilon])
        assert problem.is_dqcp()

        problem.solve(qcp=True)
        assert np.isclose(problem.value, 8)

    def test_length_monototicity(self) -> None:
        n = 5
        x = cp.Variable(n)
        self.assertTrue(cp.length(cp.abs(x)).is_incr(0))
        self.assertFalse(cp.length(cp.abs(x)-1).is_incr(0))
        self.assertTrue(cp.length(cp.abs(x)).is_dqcp())
        self.assertFalse(cp.length(cp.abs(x)-1).is_dqcp())
        self.assertTrue(cp.length(-cp.abs(x)).is_decr(0))
        self.assertFalse(cp.length(-cp.abs(x)+1).is_decr(0))

    def test_infeasible(self) -> None:
        x = cp.Variable(2)
        problem = cp.Problem(
            cp.Minimize(cp.length(x)), [x == -1, cp.ceil(x) >= 1])
        problem.solve(SOLVER, qcp=True)
        self.assertIn(problem.status, (s.INFEASIBLE, s.INFEASIBLE_INACCURATE))

    def test_sign(self) -> None:
        x = cp.Variable()
        problem = cp.Problem(cp.Minimize(cp.sign(x)), [-2 <= x, x <= -0.5])
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, -1)
        self.assertLessEqual(x.value, 0)

        problem = cp.Problem(cp.Maximize(cp.sign(x)), [1 <= x, x <= 2])
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.objective.value, 1.0)
        self.assertGreater(x.value, 0)

        # Check that sign doesn't change value.
        vector = np.array([.1, -.3, .5])
        variable = cp.Variable(len(vector))
        problem = cp.Problem(cp.Maximize(vector @ variable),
                             [cp.norm2(variable) <= 1.])
        problem.solve(solver=cp.SCS)

        value = variable.value.copy()
        cp.sign(variable).value
        self.assertItemsAlmostEqual(value, variable.value)

        # sign is only QCP for univariate input.
        # See issue #1828
        x = cp.Variable(2)
        obj = cp.sum_squares(np.ones(2) - x)
        constr = [cp.sum(cp.sign(x)) <= 1]
        prob = cp.Problem(cp.Minimize(obj), constr)
        assert not prob.is_dqcp()

    def test_dist_ratio(self) -> None:
        x = cp.Variable(2)
        a = np.ones(2)
        b = np.zeros(2)
        problem = cp.Problem(cp.Minimize(cp.dist_ratio(x, a, b)), [x <= 0.8])
        problem.solve(cp.SCS, qcp=True)
        np.testing.assert_almost_equal(problem.objective.value, 0.25, decimal=3)
        np.testing.assert_almost_equal(x.value, np.array([0.8, 0.8]), decimal=3)

    def test_infeasible_exp_constr(self) -> None:
        x = cp.Variable()
        constr = [cp.exp(cp.ceil(x)) <= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.INFEASIBLE)

    def test_infeasible_inv_pos_constr(self) -> None:
        x = cp.Variable(nonneg=True)
        constr = [cp.inv_pos(cp.ceil(x)) <= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.INFEASIBLE)

    def test_infeasible_logistic_constr(self) -> None:
        x = cp.Variable(nonneg=True)
        constr = [cp.logistic(cp.ceil(x)) <= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.INFEASIBLE)

    def test_noop_exp_constr(self) -> None:
        x = cp.Variable()
        constr = [cp.exp(cp.ceil(x)) >= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.OPTIMAL)

    def test_noop_inv_pos_constr(self) -> None:
        x = cp.Variable()
        constr = [cp.inv_pos(cp.ceil(x)) >= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.OPTIMAL)

    def test_noop_logistic_constr(self) -> None:
        x = cp.Variable(nonneg=True)
        constr = [cp.logistic(cp.ceil(x)) >= -5]
        problem = cp.Problem(cp.Minimize(0), constr)
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.OPTIMAL)

    def test_gen_lambda_max_matrix_completion(self) -> None:
        A = cp.Variable((3, 3))
        B = cp.Variable((3, 3), PSD=True)
        gen_lambda_max = cp.gen_lambda_max(A, B)
        known_indices = tuple(zip(*[[0, 0], [0, 2], [1, 1]]))
        constr = [
          A[known_indices] == [1.0, 1.9, 0.8],
          B[known_indices] == [3.0, 1.4, 0.2],
        ]
        problem = cp.Problem(cp.Minimize(gen_lambda_max), constr)
        self.assertTrue(problem.is_dqcp())
        # smoke test
        problem.solve(cp.SCS, qcp=True)

    def test_condition_number(self) -> None:
        A = cp.Variable((2, 2), PSD=True)
        con_num = cp.condition_number(A)
        constr = [
            A[0][0] == 2.0,
            A[1][1] == 3.0,
            A[0][1] <= 2, A[0][1] >= 1,
            A[1][0] <= 2, A[1][0] >= 1,
        ]
        prob = cp.Problem(cp.Minimize(con_num), constr)
        self.assertTrue(prob.is_dqcp())
        # smoke test
        prob.solve(cp.SCS, qcp=True)
        ans = np.asarray([[2.0, 1.0],
                          [1.0, 3.0]])
        self.assertItemsAlmostEqual(A.value, ans, places=1)

    def test_card_ls(self) -> None:
        n = 10
        np.random.seed(0)
        A = np.random.randn(n, n)
        x_star = np.random.randn(n)
        b = cp.matmul(A, x_star)
        epsilon = 1e-3

        x = cp.Variable(n)
        objective_fn = cp.length(x)
        mse = cp.sum_squares(cp.matmul(A, x) - b)/n
        problem = cp.Problem(cp.Minimize(objective_fn), [mse <= epsilon])
        # smoke test
        problem.solve(SOLVER, qcp=True)

    def test_multiply_const(self) -> None:
        x = cp.Variable()
        obj = cp.Minimize(0.5 * cp.ceil(x))
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, 5, places=1)

        x = cp.Variable()
        obj = cp.Minimize(cp.ceil(x) * 0.5)
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, 5, places=1)

        x = cp.Variable()
        obj = cp.Maximize(-0.5 * cp.ceil(x))
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, -5, places=1)

        x = cp.Variable()
        obj = cp.Maximize(cp.ceil(x) * -0.5)
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, -5, places=1)

    def test_div_const(self) -> None:
        x = cp.Variable()
        obj = cp.Minimize(cp.ceil(x) / 0.5)
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, 20, places=1)

        x = cp.Variable()
        obj = cp.Maximize(cp.ceil(x) / -0.5)
        problem = cp.Problem(obj, [x >= 10])
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(x.value, 10, places=1)
        self.assertAlmostEqual(problem.value, -20, places=1)

    def test_reciprocal(self) -> None:
        x = cp.Variable(pos=True)
        problem = cp.Problem(cp.Minimize(1/x))
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 0, places=3)

    def test_abs(self) -> None:
        x = cp.Variable(pos=True)
        problem = cp.Problem(cp.Minimize(cp.abs(1/x)))
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 0, places=3)

        x = cp.Variable(neg=True)
        problem = cp.Problem(cp.Minimize(cp.abs(1/x)))
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 0, places=3)

    def test_tutorial_example(self) -> None:
        x = cp.Variable()
        y = cp.Variable(pos=True)
        objective_fn = -cp.sqrt(x) / y
        problem = cp.Problem(cp.Minimize(objective_fn), [cp.exp(x) <= y])
        # smoke test
        problem.solve(cp.SCS, qcp=True)

    def test_curvature(self) -> None:
        x = cp.Variable(3)
        expr = cp.length(x)
        self.assertEqual(expr.curvature, s.QUASICONVEX)
        expr = -cp.length(x)
        self.assertEqual(expr.curvature, s.QUASICONCAVE)
        expr = cp.ceil(x)
        self.assertEqual(expr.curvature, s.QUASILINEAR)
        self.assertTrue(expr.is_quasilinear())

    def test_tutorial_dqcp(self) -> None:
        # The sign of variables affects curvature analysis.
        x = cp.Variable(nonneg=True)
        concave_frac = x * cp.sqrt(x)
        constraint = [cp.ceil(x) <= 10]
        problem = cp.Problem(cp.Maximize(concave_frac), constraint)
        self.assertTrue(concave_frac.is_quasiconcave())
        self.assertTrue(constraint[0].is_dqcp())
        self.assertTrue(problem.is_dqcp())

        w = cp.Variable()
        fn = w * cp.sqrt(w)
        problem = cp.Problem(cp.Maximize(fn))
        self.assertFalse(fn.is_dqcp())
        self.assertFalse(problem.is_dqcp())

    def test_add_constant(self) -> None:
        # The sign of variables affects curvature analysis.
        x = cp.Variable()
        problem = cp.Problem(cp.Minimize(cp.ceil(x) + 5), [x >= 2])
        problem.solve(SOLVER, qcp=True)
        np.testing.assert_almost_equal(x.value, 2)
        np.testing.assert_almost_equal(problem.objective.value, 7)

    def test_max(self) -> None:
        x = cp.Variable(2, pos=True)
        obj = cp.max((1 - 2*cp.sqrt(x) + x) / x)
        problem = cp.Problem(cp.Minimize(obj), [x[0] <= 0.5, x[1] <= 0.9])
        self.assertTrue(problem.is_dqcp())
        problem.solve(cp.SCS, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 0.1715, places=1)

    def test_min(self) -> None:
        x = cp.Variable(2)
        expr = cp.min(cp.ceil(x))
        problem = cp.Problem(cp.Maximize(expr),
                             [x[0] >= 11.9, x[0] <= 15.8, x[1] >= 17.4])
        self.assertTrue(problem.is_dqcp())
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.objective.value, 16.0)
        self.assertLess(x[0].value, 16.0)
        self.assertGreater(x[0].value, 14.9)
        self.assertGreater(x[1].value, 17.3)

    def test_sum_of_qccv_not_dqcp(self) -> None:
        t = cp.Variable(5, pos=True)
        expr = cp.sum(cp.square(t) / t)
        self.assertFalse(expr.is_dqcp())

    def test_flip_bounds(self) -> None:
        x = cp.Variable(pos=True)
        problem = cp.Problem(cp.Maximize(cp.ceil(x)), [x <= 1])
        problem.solve(SOLVER, qcp=True, low=0, high=0.5)
        self.assertGreater(x.value, 0)
        self.assertLessEqual(x.value, 1)

        problem.solve(SOLVER, qcp=True, low=0, high=None)
        self.assertGreater(x.value, 0)
        self.assertLessEqual(x.value, 1)

        problem.solve(SOLVER, qcp=True, low=None, high=0.5)
        self.assertGreater(x.value, 0)
        self.assertLessEqual(x.value, 1)

    def test_scalar_sum(self) -> None:
        x = cp.Variable(pos=True)
        problem = cp.Problem(cp.Minimize(cp.sum(1/x)))
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 0, places=3)

        problem = cp.Problem(cp.Minimize(cp.cumsum(1/x, axis=None)))
        problem.solve(SOLVER, qcp=True)
        self.assertAlmostEqual(problem.value, 0, places=3)

    def test_parameter_bug(self) -> None:
        """Test bug with parameters arising from interaction of
        DQCP and DPP.

        https://github.com/cvxpy/cvxpy/issues/2386
        """
        x = cp.Variable()

        objective = cp.Minimize(cp.sqrt(x))

        constraints = [x <= 2, x >= 1]

        problem = cp.Problem(objective, constraints)

        problem.solve(qcp=True, solver=cp.SCS)

        self.assertAlmostEqual(x.value, objective.value, places=3)
        self.assertAlmostEqual(x.value, 1, places=3)

    def test_psd_constraint_bug(self) -> None:
        """Test bug with DQCP and PSD constraints.

        https://github.com/cvxpy/cvxpy/issues/2373
        """
        A = cp.Variable((2,2),symmetric=True)

        x = A[0,1]
        y = A[1,1]

        # assertions and constraints
        x = cp.atoms.affine.wraps.nonneg_wrap(x)
        y = cp.atoms.affine.wraps.nonneg_wrap(y)
        constraints = [A >> 0]

        # function
        f = x*y

        # create the problem
        problem = cp.Problem(cp.Maximize(f), constraints)

        # solve
        assert problem.is_dqcp()
        with pytest.raises(
            cp.SolverError,
            match="(Max iters hit during bisection|"
                  "Unable to find suitable interval for bisection)"
        ):
            problem.solve(qcp=True, solver=cp.SCS, max_iters=1)

    def test_dist_ratio_dqcp(self) -> None:
        """dist_ratio DQCP problem should give correct result."""
        x = cp.Variable(2)
        a = np.array([0.0, 0.0])
        b = np.array([3.0, 0.0])
        atom = cp.dist_ratio(x, a, b)
        prob = cp.Problem(cp.Minimize(atom), [cp.norm(x) <= 2])
        prob.solve(solver=SOLVER, qcp=True)
        self.assertEqual(prob.status, s.OPTIMAL)
        self.assertAlmostEqual(prob.value, 0.0, places=3)

    def test_dqcp_power_infeasible(self) -> None:
        """DQCP with power(ceil(x), 2) <= -5 should be infeasible."""
        x = cp.Variable(nonneg=True)
        problem = cp.Problem(cp.Minimize(0), [cp.power(cp.ceil(x), 2) <= -5])
        problem.solve(SOLVER, qcp=True)
        self.assertEqual(problem.status, s.INFEASIBLE)

    def test_bisection_low_zero(self) -> None:
        """Bisection with low=0 should not infinite-loop."""
        x = cp.Variable()
        problem = cp.Problem(cp.Minimize(cp.ceil(x)), [x >= 15, x <= 17])
        problem.solve(SOLVER, qcp=True, low=0, high=100)
        self.assertEqual(problem.status, s.OPTIMAL)
        self.assertAlmostEqual(problem.value, 15.0, places=3)

    def test_length_verbose(self) -> None:
        """Regression test: verbose=True should not crash with length atom.

        Before the fix, _lower_problem() evaluated lazy constraints before
        t.value was assigned, causing TypeError: '<' not supported between
        instances of 'NoneType' and 'int'.
        """
        x = cp.Variable(5)
        problem = cp.Problem(cp.Minimize(cp.length(x)),
                             [x[0] == 2.0, x[1] == 1.0])
        problem.solve(SOLVER, qcp=True, verbose=True)
        self.assertEqual(problem.status, s.OPTIMAL)
        self.assertEqual(problem.objective.value, 2)

    def test_sign_verbose(self) -> None:
        """Regression test: verbose=True should not crash with sign atom."""
        x = cp.Variable()
        problem = cp.Problem(cp.Minimize(cp.sign(x)), [-2 <= x, x <= -0.5])
        problem.solve(SOLVER, qcp=True, verbose=True)
        self.assertEqual(problem.status, s.OPTIMAL)
        self.assertEqual(problem.objective.value, -1)

    def test_dist_ratio_verbose(self) -> None:
        """Regression test: verbose=True should not crash with dist_ratio."""
        x = cp.Variable(2)
        a = np.array([0.0, 0.0])
        b = np.array([3.0, 0.0])
        problem = cp.Problem(cp.Minimize(cp.dist_ratio(x, a, b)),
                             [cp.norm(x) <= 2])
        problem.solve(solver=SOLVER, qcp=True, verbose=True)
        self.assertEqual(problem.status, s.OPTIMAL)
        self.assertAlmostEqual(problem.value, 0.0, places=3)
