"""Entropyk — End-to-End Solver Tests.

Tests for System construction, finalization, and solving from Python.
"""

import pytest
import entropyk


class TestSystemConstruction:
    """Tests for System graph building."""

    def test_empty_system(self):
        system = entropyk.System()
        assert system.node_count == 0
        assert system.edge_count == 0

    def test_add_component(self):
        system = entropyk.System()
        idx = system.add_component(entropyk.Condenser(ua=5000.0))
        assert idx == 0
        assert system.node_count == 1

    def test_add_multiple_components(self):
        system = entropyk.System()
        i0 = system.add_component(entropyk.Compressor())
        i1 = system.add_component(entropyk.Condenser())
        i2 = system.add_component(entropyk.ExpansionValve())
        i3 = system.add_component(entropyk.Evaporator())
        assert system.node_count == 4
        assert i0 != i1 != i2 != i3

    def test_add_edge(self):
        system = entropyk.System()
        i0 = system.add_component(entropyk.Compressor())
        i1 = system.add_component(entropyk.Condenser())
        edge_idx = system.add_edge(i0, i1)
        assert edge_idx == 0
        assert system.edge_count == 1

    def test_repr(self):
        system = entropyk.System()
        system.add_component(entropyk.Compressor())
        system.add_component(entropyk.Condenser())
        system.add_edge(0, 1)
        r = repr(system)
        assert "System" in r
        assert "nodes=2" in r
        assert "edges=1" in r


class TestSystemFinalize:
    """Tests for system finalization."""

    def test_simple_cycle_finalize(self):
        """Build and finalize a simple 4-component cycle."""
        system = entropyk.System()
        comp = system.add_component(entropyk.Compressor())
        cond = system.add_component(entropyk.Condenser())
        exv = system.add_component(entropyk.ExpansionValve())
        evap = system.add_component(entropyk.Evaporator())

        system.add_edge(comp, cond)
        system.add_edge(cond, exv)
        system.add_edge(exv, evap)
        system.add_edge(evap, comp)

        system.finalize()
        assert system.state_vector_len > 0


class TestSolverConfigs:
    """Tests for solver configuration objects."""

    def test_newton_default(self):
        config = entropyk.NewtonConfig()
        assert "NewtonConfig" in repr(config)
        assert "100" in repr(config)

    def test_newton_custom(self):
        config = entropyk.NewtonConfig(
            max_iterations=200,
            tolerance=1e-8,
            line_search=True,
            timeout_ms=5000,
        )
        assert "200" in repr(config)

    def test_picard_default(self):
        config = entropyk.PicardConfig()
        assert "PicardConfig" in repr(config)

    def test_picard_custom(self):
        config = entropyk.PicardConfig(
            max_iterations=300,
            tolerance=1e-5,
            relaxation=0.7,
        )
        assert "300" in repr(config)

    def test_picard_invalid_relaxation_raises(self):
        with pytest.raises(ValueError, match="between"):
            entropyk.PicardConfig(relaxation=1.5)

    def test_fallback_default(self):
        config = entropyk.FallbackConfig()
        assert "FallbackConfig" in repr(config)

    def test_fallback_custom(self):
        newton = entropyk.NewtonConfig(max_iterations=50)
        picard = entropyk.PicardConfig(max_iterations=200)
        config = entropyk.FallbackConfig(newton=newton, picard=picard)
        assert "50" in repr(config)

    def test_newton_advanced_params(self):
        config = entropyk.NewtonConfig(
            initial_state=[1.0, 2.0, 3.0],
            use_numerical_jacobian=True,
            line_search_armijo_c=1e-3,
            line_search_max_backtracks=10,
            divergence_threshold=1e5
        )
        assert config is not None

    def test_picard_advanced_params(self):
        config = entropyk.PicardConfig(
            initial_state=[1.0, 2.0],
            timeout_ms=1000,
        )
        assert config is not None

    def test_convergence_criteria(self):
        cc = entropyk.ConvergenceCriteria(
            pressure_tolerance_pa=2.0,
            mass_balance_tolerance_kgs=1e-8,
            energy_balance_tolerance_w=1e-2
        )
        assert "dP=2.0" in repr(cc)
        assert "dM=1.0" in repr(cc)
        assert "dE=1.0" in repr(cc)
        assert cc.pressure_tolerance_pa == 2.0
        
        config = entropyk.NewtonConfig(convergence_criteria=cc)
        assert config is not None

    def test_jacobian_freezing(self):
        jf = entropyk.JacobianFreezingConfig(max_frozen_iters=5, threshold=0.5)
        assert jf.max_frozen_iters == 5
        assert jf.threshold == 0.5
        config = entropyk.NewtonConfig(jacobian_freezing=jf)
        assert config is not None

    def test_timeout_config(self):
        tc = entropyk.TimeoutConfig(return_best_state_on_timeout=False, zoh_fallback=True)
        assert not tc.return_best_state_on_timeout
        assert tc.zoh_fallback
        config = entropyk.NewtonConfig(timeout_config=tc)
        assert config is not None

    def test_solver_strategy(self):
        newton_strat = entropyk.SolverStrategy.newton(tolerance=1e-5)
        picard_strat = entropyk.SolverStrategy.picard(relaxation=0.6)
        default_strat = entropyk.SolverStrategy.default()

        assert newton_strat is not None
        assert picard_strat is not None
        assert default_strat is not None

class TestSolverExecution:
    """Tests that the bindings actually call the Rust solver engine."""

    @pytest.fixture
    def simple_system(self):
        system = entropyk.System()
        # Use simple components to avoid complex physics crashes
        i0 = system.add_component(entropyk.Pipe(length=10.0, diameter=0.1, fluid="Water"))
        i1 = system.add_component(entropyk.Pipe(length=10.0, diameter=0.1, fluid="Water"))
        system.add_edge(i0, i1)
        system.add_edge(i1, i0)
        system.finalize()
        return system

    def test_newton_solve(self, simple_system):
        config = entropyk.NewtonConfig(max_iterations=2, timeout_ms=10)
        try:
            result = config.solve(simple_system)
            assert result is not None
        except entropyk.SolverError:
            # We don't care if it fails to converge, only that it crossed the boundary
            pass

    def test_picard_solve(self, simple_system):
        config = entropyk.PicardConfig(max_iterations=2, timeout_ms=10)
        try:
            result = config.solve(simple_system)
            assert result is not None
        except entropyk.SolverError:
            pass

    def test_strategy_solve(self, simple_system):
        strategy = entropyk.SolverStrategy.newton(max_iterations=2, timeout_ms=10)
        try:
            result = strategy.solve(simple_system)
            assert result is not None
        except entropyk.SolverError:
            pass



class TestConvergedState:
    """Tests for ConvergedState and ConvergenceStatus types."""

    def test_convergence_status_repr(self):
        # We can't easily create a ConvergedState without solving,
        # so we just verify the classes exist
        assert hasattr(entropyk, "ConvergedState")
        assert hasattr(entropyk, "ConvergenceStatus")


class TestAllComponentsInSystem:
    """Test that all component types can be added to a System."""

    @pytest.mark.parametrize("component_factory", [
        lambda: entropyk.Compressor(),
        lambda: entropyk.Condenser(),
        lambda: entropyk.Evaporator(),
        lambda: entropyk.Economizer(),
        lambda: entropyk.ExpansionValve(),
        lambda: entropyk.Pipe(),
        lambda: entropyk.Pump(),
        lambda: entropyk.Fan(),
        lambda: entropyk.FlowSplitter(),
        lambda: entropyk.FlowMerger(),
        lambda: entropyk.FlowSource(),
        lambda: entropyk.FlowSink(),
    ])
    def test_add_component(self, component_factory):
        system = entropyk.System()
        idx = system.add_component(component_factory())
        assert idx >= 0
        assert system.node_count == 1
