"""Tests for Pydantic models and enums."""

import pytest
from pydantic import ValidationError

from chuk_mcp_solver.models import (
    AllDifferentParams,
    BindingConstraint,
    Constraint,
    ConstraintKind,
    ConstraintSense,
    ElementParams,
    Explanation,
    ImplicationParams,
    LinearConstraintParams,
    LinearTerm,
    Objective,
    ObjectiveSense,
    SearchConfig,
    Solution,
    SolutionVariable,
    SolveConstraintModelRequest,
    SolveConstraintModelResponse,
    SolverMode,
    SolverStatus,
    TableParams,
    Variable,
    VariableDomain,
    VariableDomainType,
)

# ============================================================================
# Enum Tests
# ============================================================================


def test_solver_mode_enum():
    """Test SolverMode enum values."""
    assert SolverMode.SATISFY == "satisfy"
    assert SolverMode.OPTIMIZE == "optimize"
    assert len(SolverMode) == 2


def test_variable_domain_type_enum():
    """Test VariableDomainType enum values."""
    assert VariableDomainType.BOOL == "bool"
    assert VariableDomainType.INTEGER == "integer"
    assert len(VariableDomainType) == 2


def test_constraint_sense_enum():
    """Test ConstraintSense enum values."""
    assert ConstraintSense.LESS_EQUAL == "<="
    assert ConstraintSense.GREATER_EQUAL == ">="
    assert ConstraintSense.EQUAL == "=="
    assert len(ConstraintSense) == 3


def test_objective_sense_enum():
    """Test ObjectiveSense enum values."""
    assert ObjectiveSense.MINIMIZE == "min"
    assert ObjectiveSense.MAXIMIZE == "max"
    assert len(ObjectiveSense) == 2


def test_constraint_kind_enum():
    """Test ConstraintKind enum values."""
    assert ConstraintKind.LINEAR == "linear"
    assert ConstraintKind.ALL_DIFFERENT == "all_different"
    assert ConstraintKind.ELEMENT == "element"
    assert ConstraintKind.TABLE == "table"
    assert ConstraintKind.IMPLICATION == "implication"
    assert ConstraintKind.CUMULATIVE == "cumulative"
    assert ConstraintKind.CIRCUIT == "circuit"
    assert ConstraintKind.RESERVOIR == "reservoir"
    assert ConstraintKind.NO_OVERLAP == "no_overlap"
    assert len(ConstraintKind) == 9


def test_solver_status_enum():
    """Test SolverStatus enum values."""
    assert SolverStatus.OPTIMAL == "optimal"
    assert SolverStatus.FEASIBLE == "feasible"
    assert SolverStatus.SATISFIED == "satisfied"
    assert SolverStatus.INFEASIBLE == "infeasible"
    assert SolverStatus.UNBOUNDED == "unbounded"
    assert SolverStatus.TIMEOUT == "timeout"
    assert SolverStatus.ERROR == "error"
    assert len(SolverStatus) == 7


# ============================================================================
# Variable Domain Tests
# ============================================================================


def test_variable_domain_bool():
    """Test boolean variable domain."""
    domain = VariableDomain(type=VariableDomainType.BOOL)
    assert domain.type == VariableDomainType.BOOL
    assert domain.lower == 0
    assert domain.upper == 1


def test_variable_domain_integer():
    """Test integer variable domain."""
    domain = VariableDomain(type=VariableDomainType.INTEGER, lower=10, upper=20)
    assert domain.type == VariableDomainType.INTEGER
    assert domain.lower == 10
    assert domain.upper == 20


def test_variable_domain_defaults():
    """Test variable domain default values."""
    domain = VariableDomain(type="integer")
    assert domain.lower == 0
    assert domain.upper == 1


# ============================================================================
# Variable Tests
# ============================================================================


def test_variable_minimal():
    """Test variable with minimal fields."""
    var = Variable(id="x", domain={"type": "bool"})
    assert var.id == "x"
    assert var.domain.type == VariableDomainType.BOOL
    assert var.metadata is None


def test_variable_with_metadata():
    """Test variable with metadata."""
    var = Variable(
        id="x", domain={"type": "integer", "lower": 0, "upper": 10}, metadata={"task": "A"}
    )
    assert var.id == "x"
    assert var.metadata == {"task": "A"}


def test_variable_empty_id_fails():
    """Test that empty variable ID fails validation."""
    with pytest.raises(ValidationError):
        Variable(id="", domain={"type": "bool"})


# ============================================================================
# Constraint Parameter Tests
# ============================================================================


def test_linear_term():
    """Test LinearTerm model."""
    term = LinearTerm(var="x", coef=3.5)
    assert term.var == "x"
    assert term.coef == 3.5


def test_linear_constraint_params():
    """Test LinearConstraintParams model."""
    params = LinearConstraintParams(
        terms=[{"var": "x", "coef": 2}, {"var": "y", "coef": 3}], sense="<=", rhs=10
    )
    assert len(params.terms) == 2
    assert params.sense == ConstraintSense.LESS_EQUAL
    assert params.rhs == 10


def test_linear_constraint_params_empty_terms_fails():
    """Test that empty terms list fails validation."""
    with pytest.raises(ValidationError):
        LinearConstraintParams(terms=[], sense="<=", rhs=10)


def test_all_different_params():
    """Test AllDifferentParams model."""
    params = AllDifferentParams(vars=["x", "y", "z"])
    assert params.vars == ["x", "y", "z"]


def test_all_different_params_single_var_fails():
    """Test that single variable fails validation."""
    with pytest.raises(ValidationError):
        AllDifferentParams(vars=["x"])


def test_element_params():
    """Test ElementParams model."""
    params = ElementParams(index_var="idx", array=[10, 20, 30], target_var="result")
    assert params.index_var == "idx"
    assert params.array == [10, 20, 30]
    assert params.target_var == "result"


def test_table_params():
    """Test TableParams model."""
    params = TableParams(vars=["x", "y"], allowed_tuples=[[0, 1], [1, 0]])
    assert params.vars == ["x", "y"]
    assert len(params.allowed_tuples) == 2


# ============================================================================
# Constraint Tests
# ============================================================================


def test_constraint_linear():
    """Test linear constraint."""
    constraint = Constraint(
        id="c1",
        kind="linear",
        params={"terms": [{"var": "x", "coef": 1}], "sense": "<=", "rhs": 5},
    )
    assert constraint.id == "c1"
    assert constraint.kind == ConstraintKind.LINEAR


def test_constraint_all_different():
    """Test all_different constraint."""
    constraint = Constraint(id="c1", kind="all_different", params={"vars": ["x", "y", "z"]})
    assert constraint.kind == ConstraintKind.ALL_DIFFERENT


def test_constraint_with_metadata():
    """Test constraint with metadata."""
    constraint = Constraint(
        id="c1",
        kind="linear",
        params={"terms": [{"var": "x", "coef": 1}], "sense": "<=", "rhs": 5},
        metadata={"description": "Capacity limit"},
    )
    assert constraint.metadata == {"description": "Capacity limit"}


# ============================================================================
# Objective Tests
# ============================================================================


def test_objective_minimize():
    """Test minimize objective."""
    obj = Objective(sense="min", terms=[{"var": "x", "coef": 1}, {"var": "y", "coef": 2}])
    assert obj.sense == ObjectiveSense.MINIMIZE
    assert len(obj.terms) == 2


def test_objective_maximize():
    """Test maximize objective."""
    obj = Objective(sense="max", terms=[{"var": "profit", "coef": 1}])
    assert obj.sense == ObjectiveSense.MAXIMIZE


def test_objective_empty_terms_fails():
    """Test that empty terms list fails validation."""
    with pytest.raises(ValidationError):
        Objective(sense="min", terms=[])


# ============================================================================
# Search Config Tests
# ============================================================================


def test_search_config_defaults():
    """Test SearchConfig default values."""
    config = SearchConfig()
    assert config.max_time_ms is None
    assert config.max_solutions == 1


def test_search_config_with_values():
    """Test SearchConfig with custom values."""
    config = SearchConfig(max_time_ms=5000, max_solutions=10)
    assert config.max_time_ms == 5000
    assert config.max_solutions == 10


def test_search_config_negative_time_fails():
    """Test that negative max_time_ms fails validation."""
    with pytest.raises(ValidationError):
        SearchConfig(max_time_ms=-1)


def test_search_config_zero_solutions_fails():
    """Test that zero max_solutions fails validation."""
    with pytest.raises(ValidationError):
        SearchConfig(max_solutions=0)


# ============================================================================
# Request Tests
# ============================================================================


def test_request_satisfy_mode():
    """Test request in satisfy mode."""
    request = SolveConstraintModelRequest(
        mode="satisfy",
        variables=[{"id": "x", "domain": {"type": "bool"}}],
        constraints=[],
    )
    assert request.mode == SolverMode.SATISFY
    assert len(request.variables) == 1
    assert request.objective is None


def test_request_optimize_mode():
    """Test request in optimize mode."""
    request = SolveConstraintModelRequest(
        mode="optimize",
        variables=[{"id": "x", "domain": {"type": "integer", "lower": 0, "upper": 10}}],
        constraints=[],
        objective={"sense": "min", "terms": [{"var": "x", "coef": 1}]},
    )
    assert request.mode == SolverMode.OPTIMIZE
    assert request.objective is not None


def test_request_empty_variables_fails():
    """Test that empty variables list fails validation."""
    with pytest.raises(ValidationError):
        SolveConstraintModelRequest(mode="satisfy", variables=[], constraints=[])


# ============================================================================
# Solution Tests
# ============================================================================


def test_solution_variable():
    """Test SolutionVariable model."""
    sol_var = SolutionVariable(id="x", value=5.0, metadata={"task": "A"})
    assert sol_var.id == "x"
    assert sol_var.value == 5.0
    assert sol_var.metadata == {"task": "A"}


def test_solution():
    """Test Solution model."""
    solution = Solution(
        variables=[{"id": "x", "value": 1}, {"id": "y", "value": 0}],
        derived={"makespan": 10},
    )
    assert len(solution.variables) == 2
    assert solution.derived == {"makespan": 10}


# ============================================================================
# Explanation Tests
# ============================================================================


def test_binding_constraint():
    """Test BindingConstraint model."""
    bc = BindingConstraint(id="c1", sense="<=", lhs_value=10.0, rhs=10.0)
    assert bc.id == "c1"
    assert bc.sense == ConstraintSense.LESS_EQUAL
    assert bc.lhs_value == 10.0


def test_explanation():
    """Test Explanation model."""
    explanation = Explanation(summary="Found optimal solution", binding_constraints=[])
    assert explanation.summary == "Found optimal solution"
    assert len(explanation.binding_constraints) == 0


# ============================================================================
# Response Tests
# ============================================================================


def test_response_optimal():
    """Test response with optimal status."""
    response = SolveConstraintModelResponse(
        status="optimal",
        objective_value=42.0,
        solutions=[{"variables": [{"id": "x", "value": 1}]}],
    )
    assert response.status == SolverStatus.OPTIMAL
    assert response.objective_value == 42.0


def test_response_infeasible():
    """Test response with infeasible status."""
    response = SolveConstraintModelResponse(
        status="infeasible", explanation={"summary": "No solution exists"}
    )
    assert response.status == SolverStatus.INFEASIBLE
    assert response.objective_value is None


def test_response_has_api_version():
    """Test that response includes API version."""
    response = SolveConstraintModelResponse(status="optimal")
    assert response.apiversion is not None
    assert len(response.apiversion) > 0


# ============================================================================
# Integration Tests (Complex Models)
# ============================================================================


def test_complete_optimization_request():
    """Test a complete optimization request."""
    request = SolveConstraintModelRequest(
        mode="optimize",
        variables=[
            {"id": "x1", "domain": {"type": "bool"}},
            {"id": "x2", "domain": {"type": "bool"}},
        ],
        constraints=[
            {
                "id": "capacity",
                "kind": "linear",
                "params": {
                    "terms": [{"var": "x1", "coef": 2}, {"var": "x2", "coef": 3}],
                    "sense": "<=",
                    "rhs": 4,
                },
            }
        ],
        objective={"sense": "max", "terms": [{"var": "x1", "coef": 5}, {"var": "x2", "coef": 7}]},
        search={"max_time_ms": 1000},
    )

    assert request.mode == SolverMode.OPTIMIZE
    assert len(request.variables) == 2
    assert len(request.constraints) == 1
    assert request.objective is not None
    assert request.search is not None


def test_implication_constraint():
    """Test implication constraint with nested linear constraint."""
    constraint = Constraint(
        id="impl",
        kind="implication",
        params={
            "if_var": "use_feature",
            "then": {
                "id": "cost_constraint",
                "kind": "linear",
                "params": {"terms": [{"var": "cost", "coef": 1}], "sense": ">=", "rhs": 10},
            },
        },
    )

    assert constraint.kind == ConstraintKind.IMPLICATION
    # constraint.params is already an ImplicationParams model
    params: ImplicationParams = constraint.params  # type: ignore
    assert params.if_var == "use_feature"
    assert params.then.kind == ConstraintKind.LINEAR
