"""Constraint coverage tests for CPO (IBM DOcplex CP Optimizer) backend.

These tests require IBM CPLEX Optimization Studio to be installed.
They will be skipped if CPO is not available.
"""

import pytest
from pycsp3 import *
from pycsp3.functions import Table
from pycsp3_solvers_extra import solve
from pycsp3_solvers_extra.backends import get_backend

# Import CPO backend to trigger auto-configuration
try:
    from pycsp3_solvers_extra.backends.cpo_backend import CPO_AVAILABLE, _CPO_CONFIGURED
except ImportError:
    CPO_AVAILABLE = False
    _CPO_CONFIGURED = False

# Skip all tests if CPO is not available or not configured
pytestmark = pytest.mark.skipif(
    not (CPO_AVAILABLE and _CPO_CONFIGURED),
    reason="CPO backend not available or CP Optimizer not found"
)


class TestBasicSatisfaction:
    """Tests for basic satisfaction problems."""

    def test_simple_alldifferent(self):
        """Test simple AllDifferent constraint."""
        x = VarArray(size=4, dom=range(1, 5))
        satisfy(AllDifferent(x))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert len(set(sol)) == 4, "AllDifferent violated"

    def test_sum_equals(self):
        """Test Sum constraint with equality."""
        x = VarArray(size=3, dom=range(1, 10))
        satisfy(Sum(x) == 15)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sum(sol) == 15

    def test_sum_less_than(self):
        """Test Sum constraint with less than."""
        x = VarArray(size=3, dom=range(1, 10))
        satisfy(Sum(x) < 10)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sum(sol) < 10

    def test_unsatisfiable(self):
        """Test detection of unsatisfiable problem."""
        x = VarArray(size=5, dom=range(1, 4))  # Only 3 values for 5 vars
        satisfy(AllDifferent(x))

        status = solve(solver="cpo")
        assert status == UNSAT


class TestOptimization:
    """Tests for optimization problems."""

    def test_minimize_sum(self):
        """Test minimizing sum."""
        x = VarArray(size=3, dom=range(1, 10))
        satisfy(AllDifferent(x))
        minimize(Sum(x))

        status = solve(solver="cpo")
        assert status == OPTIMUM

        sol = values(x)
        assert sum(sol) == 6  # 1 + 2 + 3
        assert bound() == 6

    def test_maximize_sum(self):
        """Test maximizing sum."""
        x = VarArray(size=3, dom=range(1, 10))
        satisfy(AllDifferent(x))
        maximize(Sum(x))

        status = solve(solver="cpo")
        assert status == OPTIMUM

        sol = values(x)
        assert sum(sol) == 24  # 7 + 8 + 9
        assert bound() == 24


class TestTableConstraints:
    """Tests for table/extension constraints."""

    def test_table_positive(self):
        """Test positive table constraint (allowed tuples)."""
        x = Var(dom=range(1, 4))
        y = Var(dom=range(1, 4))

        satisfy(
            Table(scope=[x, y], supports=[(1, 2), (2, 3), (3, 1)])
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        vx, vy = value(x), value(y)
        assert (vx, vy) in [(1, 2), (2, 3), (3, 1)]


class TestCountingConstraints:
    """Tests for counting constraints."""

    def test_count_equals(self):
        """Test Count constraint with equality."""
        x = VarArray(size=5, dom=range(1, 4))
        satisfy(
            Count(x, value=1) == 2,
            AllDifferent(x[:3])
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol.count(1) == 2

    def test_atleast(self):
        """Test Count with >= (at least)."""
        x = VarArray(size=5, dom=range(1, 4))
        satisfy(Count(x, value=1) >= 2)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol.count(1) >= 2

    def test_atmost(self):
        """Test Count with <= (at most)."""
        x = VarArray(size=5, dom=range(1, 4))
        satisfy(Count(x, value=1) <= 2)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol.count(1) <= 2

    def test_among_via_count(self):
        """Test Among decomposition via Count with multiple values."""
        x = VarArray(size=5, dom=range(1, 5))
        satisfy(Count(x, values=[1, 2]) == 3)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sum(1 for v in sol if v in (1, 2)) == 3

    def test_cardinality_closed(self):
        """Test Cardinality decomposition with closed values."""
        x = VarArray(size=5, dom=range(1, 5))
        satisfy(Cardinality(x, occurrences={1: 2, 2: 3}, closed=True))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol.count(1) == 2
        assert sol.count(2) == 3
        assert set(sol) <= {1, 2}

    def test_cardinality_range(self):
        """Test Cardinality decomposition with range occurrences."""
        x = VarArray(size=4, dom=range(1, 4))
        satisfy(Cardinality(x, occurrences={1: range(1, 3)}))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        count_ones = sol.count(1)
        assert 1 <= count_ones <= 2


class TestElementConstraint:
    """Tests for Element constraint."""

    def test_element_variable_array(self):
        """Test Element with variable array (VarArray[var_index])."""
        x = VarArray(size=5, dom=range(1, 10))
        i = Var(dom=range(5))
        result = Var(dom=range(1, 10))

        satisfy(
            AllDifferent(x),
            x[i] == result,
            result == 5
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        idx = value(i)
        assert sol[idx] == 5

    def test_element_matrix(self):
        """Test Element with matrix indices (VarArray[row, col])."""
        m = VarArray(size=[2, 3], dom=range(1, 7))
        i = Var(dom=range(2))
        j = Var(dom=range(3))
        satisfy(m[i, j] == 5)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        row = value(i)
        col = value(j)
        assert value(m[row][col]) == 5


class TestMinMaxConstraints:
    """Tests for Minimum/Maximum constraints."""

    def test_minimum(self):
        """Test Minimum constraint."""
        x = VarArray(size=4, dom=range(1, 10))
        satisfy(
            AllDifferent(x),
            Minimum(x) == 3
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert min(sol) == 3

    def test_maximum(self):
        """Test Maximum constraint."""
        x = VarArray(size=4, dom=range(1, 10))
        satisfy(
            AllDifferent(x),
            Maximum(x) == 7
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert max(sol) == 7


class TestOrderingConstraints:
    """Tests for ordering constraints."""

    def test_increasing(self):
        """Test Increasing constraint."""
        x = VarArray(size=4, dom=range(1, 10))
        satisfy(Increasing(x))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        for i in range(len(sol) - 1):
            assert sol[i] <= sol[i + 1]

    def test_strictly_increasing(self):
        """Test Strictly Increasing constraint."""
        x = VarArray(size=4, dom=range(1, 10))
        satisfy(Increasing(x, strict=True))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        for i in range(len(sol) - 1):
            assert sol[i] < sol[i + 1]


class TestChannelConstraint:
    """Tests for Channel/Inverse constraint."""

    def test_channel(self):
        """Test Channel constraint."""
        n = 4
        x = VarArray(size=n, dom=range(n))
        y = VarArray(size=n, dom=range(n))
        satisfy(Channel(x, y))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol_x = values(x)
        sol_y = values(y)
        # Verify inverse relationship
        for i in range(n):
            assert sol_y[sol_x[i]] == i
            assert sol_x[sol_y[i]] == i


class TestAllEqual:
    """Tests for AllEqual constraint."""

    def test_all_equal(self):
        """Test AllEqual constraint."""
        x = VarArray(size=4, dom=range(1, 5))
        satisfy(AllEqual(x))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert len(set(sol)) == 1

    def test_not_all_equal(self):
        """Test NotAllEqual constraint (at least 2 distinct values)."""
        x = VarArray(size=4, dom=range(1, 5))
        satisfy(NotAllEqual(x))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert len(set(sol)) > 1, "NotAllEqual requires at least 2 distinct values"


class TestIntensionConstraints:
    """Tests for intension (expression) constraints."""

    def test_simple_comparison(self):
        """Test simple comparison constraint."""
        x = Var(dom=range(1, 10))
        y = Var(dom=range(1, 10))
        satisfy(x < y)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        assert value(x) < value(y)

    def test_arithmetic_expression(self):
        """Test arithmetic expression constraint."""
        x = Var(dom=range(1, 10))
        y = Var(dom=range(1, 10))
        z = Var(dom=range(1, 20))
        satisfy(x + y == z)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        assert value(x) + value(y) == value(z)

    def test_multiplication(self):
        """Test multiplication constraint."""
        x = Var(dom=range(1, 10))
        y = Var(dom=range(1, 10))
        satisfy(x * y == 12)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        assert value(x) * value(y) == 12


class TestNValuesConstraints:
    """Tests for NValues constraint."""

    def test_nvalues_equals(self):
        """Test NValues equality."""
        x = VarArray(size=4, dom=range(1, 4))
        satisfy(NValues(x) == 3)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        assert len(set(values(x))) == 3


class TestSchedulingConstraints:
    """Tests for scheduling constraints."""

    def test_nooverlap(self):
        """Test NoOverlap constraint."""
        origins = VarArray(size=2, dom=range(0, 4))
        satisfy(NoOverlap(origins=origins, lengths=[2, 2]))

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        s0, s1 = values(origins)
        assert s0 + 2 <= s1 or s1 + 2 <= s0

    def test_cumulative(self):
        """Test Cumulative constraint."""
        origins = VarArray(size=2, dom=range(0, 4))
        satisfy(Cumulative(origins=origins, lengths=[2, 2], heights=[2, 2]) <= 2)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        s0, s1 = values(origins)
        assert s0 + 2 <= s1 or s1 + 2 <= s0

    def test_cumulative_variable_heights(self):
        """Test Cumulative with variable heights."""
        origins = VarArray(size=2, dom=range(1))
        heights = VarArray(size=2, dom=range(1, 3))
        satisfy(
            origins[0] == 0,
            origins[1] == 0,
            Cumulative(origins=origins, lengths=[2, 2], heights=heights) <= 2,
        )

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        assert values(heights) == [1, 1]


class TestKnapsackConstraint:
    """Tests for Knapsack constraint."""

    def test_knapsack_basic(self):
        """Test basic Knapsack constraint with weight limit and profit condition."""
        # 5 items with binary selection
        x = VarArray(size=5, dom=range(2))
        weights = [2, 3, 4, 5, 6]
        profits = [3, 4, 5, 6, 7]
        capacity = 10

        satisfy(Knapsack(x, weights=weights, wlimit=capacity, profits=profits) >= 10)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        total_weight = sum(w * s for w, s in zip(weights, sol))
        total_profit = sum(p * s for p, s in zip(profits, sol))

        assert total_weight <= capacity, f"Weight {total_weight} exceeds capacity {capacity}"
        assert total_profit >= 10, f"Profit {total_profit} < 10"

    def test_knapsack_exact_weight(self):
        """Test Knapsack with exact weight condition."""
        x = VarArray(size=4, dom=range(2))
        weights = [1, 2, 3, 4]
        profits = [1, 2, 3, 4]

        satisfy(Knapsack(x, weights=weights, wcondition=eq(5), profits=profits) >= 1)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        total_weight = sum(w * s for w, s in zip(weights, sol))
        assert total_weight == 5

    def test_knapsack_maximize_profit(self):
        """Test Knapsack with profit maximization."""
        x = VarArray(size=4, dom=range(2))
        weights = [2, 3, 4, 5]
        profits = [3, 4, 5, 6]
        capacity = 7

        # Use a profit variable and maximize it
        profit = Var(dom=range(100))
        satisfy(
            Knapsack(x, weights=weights, wlimit=capacity, profits=profits) == profit
        )
        maximize(profit)

        status = solve(solver="cpo")
        assert status == OPTIMUM

        sol = values(x)
        total_weight = sum(w * s for w, s in zip(weights, sol))
        total_profit = sum(p * s for p, s in zip(profits, sol))

        assert total_weight <= capacity
        # Optimal: select items 0+3 or 1+2 (weight=7, profit=9)
        assert total_profit == 9

    def test_knapsack_unsatisfiable(self):
        """Test Knapsack with impossible constraints."""
        x = VarArray(size=3, dom=range(2))
        weights = [5, 6, 7]
        profits = [1, 1, 1]

        # Weight limit is 4 but all items weigh more than 4
        # and we require profit >= 1 (need at least one item)
        satisfy(Knapsack(x, weights=weights, wlimit=4, profits=profits) >= 1)

        status = solve(solver="cpo")
        assert status == UNSAT

    def test_knapsack_multi_copy(self):
        """Test Knapsack with multiple copies of items."""
        # Allow up to 3 copies of each item
        x = VarArray(size=3, dom=range(4))
        weights = [2, 3, 4]
        profits = [3, 4, 5]
        capacity = 10

        satisfy(Knapsack(x, weights=weights, wlimit=capacity, profits=profits) >= 12)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        total_weight = sum(w * s for w, s in zip(weights, sol))
        total_profit = sum(p * s for p, s in zip(profits, sol))

        assert total_weight <= capacity
        assert total_profit >= 12


class TestRegularConstraint:
    """Tests for Regular constraint (finite automaton)."""

    def test_regular_basic(self):
        """Test basic Regular constraint with simple automaton."""
        # Automaton that accepts sequences ending with 1
        # States: a (start), b (seen 1, accepting)
        x = VarArray(size=5, dom={0, 1})

        a, b = "a", "b"
        transitions = [
            (a, 0, a),  # Stay in a on 0
            (a, 1, b),  # Go to b on 1
            (b, 0, a),  # Back to a on 0
            (b, 1, b),  # Stay in b on 1
        ]
        A = Automaton(start=a, final=b, transitions=transitions)
        satisfy(x in A)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol[-1] == 1, "Sequence should end with 1"

    def test_regular_binary_pattern(self):
        """Test Regular constraint matching binary pattern 11...0."""
        # Must start with two 1s and end with 0
        x = VarArray(size=4, dom={0, 1})

        a, b, c, d = "a", "b", "c", "d"
        transitions = [
            (a, 1, b),  # First 1
            (b, 1, c),  # Second 1
            (c, 0, d),  # Then 0 (accepting)
            (c, 1, c),  # More 1s allowed
            (d, 0, d),  # More 0s allowed
        ]
        A = Automaton(start=a, final=d, transitions=transitions)
        satisfy(x in A)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sol[0] == 1 and sol[1] == 1, "Must start with 11"
        assert sol[-1] == 0, "Must end with 0"

    def test_regular_no_consecutive_ones(self):
        """Test Regular constraint: no two consecutive 1s."""
        x = VarArray(size=6, dom={0, 1})

        # States: a (start/after 0), b (after 1)
        a, b = "a", "b"
        transitions = [
            (a, 0, a),
            (a, 1, b),
            (b, 0, a),
            # No (b, 1, _) transition - reject consecutive 1s
        ]
        A = Automaton(start=a, final=[a, b], transitions=transitions)
        satisfy(x in A)

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        for i in range(len(sol) - 1):
            assert not (sol[i] == 1 and sol[i + 1] == 1), "No consecutive 1s allowed"

    def test_regular_unsatisfiable(self):
        """Test Regular constraint with impossible automaton."""
        x = VarArray(size=3, dom={0, 1})

        # Automaton requires value 2 which is not in domain
        a, b = "a", "b"
        transitions = [
            (a, 2, b),  # Need 2 to reach accepting state
        ]
        A = Automaton(start=a, final=b, transitions=transitions)
        satisfy(x in A)

        status = solve(solver="cpo")
        assert status == UNSAT


class TestMDDConstraint:
    """Tests for MDD constraint (decomposed to regular)."""

    @staticmethod
    def _exactly_one_one_mdd():
        transitions = [
            ("r", 0, "a"),
            ("r", 1, "b"),
            ("a", 0, "c"),
            ("a", 1, "d"),
            ("b", 0, "d"),
            ("b", 1, "e"),
            ("c", 1, "t"),
            ("d", 0, "t"),
            ("e", 2, "t"),  # Dead transition to keep a single terminal
        ]
        return MDD(transitions)

    def test_mdd_exactly_one_one_sat(self):
        x = VarArray(size=3, dom={0, 1})
        satisfy(x in self._exactly_one_one_mdd())

        status = solve(solver="cpo")
        assert status in (SAT, OPTIMUM)

        sol = values(x)
        assert sum(sol) == 1

    def test_mdd_exactly_one_one_unsat(self):
        x = VarArray(size=3, dom={0, 1})
        satisfy(x in self._exactly_one_one_mdd())
        satisfy(Sum(x) >= 2)

        status = solve(solver="cpo")
        assert status == UNSAT
