"""Tests for circuit validation (M4 DX improvement).

Tests the new validation features:
- Ground reference detection
- Floating node detection
- Voltage source loop detection (parallel voltage sources)
- Current source loop detection (series current sources)
"""

from spicelab.core.circuit import Circuit
from spicelab.core.components import Idc, Resistor, Vdc
from spicelab.core.net import GND, Net
from spicelab.validators.circuit_validation import (
    ValidationResult,
    ValidationWarning,
)


class TestGroundReferenceCheck:
    """Tests for ground reference validation."""

    def test_valid_circuit_with_gnd(self):
        """Circuit with GND connection should pass."""
        c = Circuit("with_gnd")
        r = Resistor("1", "1k")
        c.add(r)
        c.connect(r.ports[0], Net("n1"))
        c.connect(r.ports[1], GND)

        result = c.validate()
        # Should not have "No ground" error
        ground_errors = [e for e in result.errors if "ground" in e.message.lower()]
        assert len(ground_errors) == 0

    def test_circuit_without_gnd_fails(self):
        """Circuit without GND should fail validation."""
        c = Circuit("no_gnd")
        r = Resistor("1", "1k")
        c.add(r)
        c.connect(r.ports[0], Net("n1"))
        c.connect(r.ports[1], Net("n2"))

        result = c.validate()
        assert not result.is_valid
        ground_errors = [e for e in result.errors if "ground" in e.message.lower()]
        assert len(ground_errors) == 1


class TestFloatingNodeDetection:
    """Tests for floating node detection."""

    def test_properly_connected_circuit_no_floating(self):
        """Fully connected circuit should have no floating nodes."""
        c = Circuit("connected")
        v = Vdc("1", 5.0)
        r = Resistor("1", "1k")
        c.add(v, r)

        n1 = Net("n1")
        c.connect(v.ports[0], n1)
        c.connect(r.ports[0], n1)  # n1 has 2 connections
        c.connect(v.ports[1], GND)
        c.connect(r.ports[1], GND)  # GND has 2 connections

        result = c.validate()
        floating_errors = [e for e in result.errors if "floating" in e.message.lower()]
        assert len(floating_errors) == 0

    def test_floating_node_detected(self):
        """Node with only one connection should be detected as floating."""
        c = Circuit("floating")
        v = Vdc("1", 5.0)
        r = Resistor("1", "1k")
        c.add(v, r)

        n1 = Net("n1")
        c.connect(v.ports[0], n1)  # n1 only has 1 connection
        c.connect(v.ports[1], GND)
        c.connect(r.ports[0], Net("floating_net"))  # This net only has 1 connection
        c.connect(r.ports[1], GND)

        result = c.validate()
        floating_errors = [e for e in result.errors if "floating" in e.message.lower()]
        assert len(floating_errors) >= 1

    def test_multiple_floating_nodes(self):
        """Multiple floating nodes should all be reported."""
        c = Circuit("multi_floating")
        r1 = Resistor("1", "1k")
        r2 = Resistor("2", "2k")
        c.add(r1, r2)

        # Each resistor connected to separate nets, all floating
        c.connect(r1.ports[0], Net("a"))
        c.connect(r1.ports[1], Net("b"))
        c.connect(r2.ports[0], Net("c"))
        c.connect(r2.ports[1], Net("d"))

        result = c.validate()
        floating_errors = [e for e in result.errors if "floating" in e.message.lower()]
        # Should detect 4 floating nodes (a, b, c, d)
        assert len(floating_errors) == 4


class TestVoltageSourceLoopDetection:
    """Tests for parallel voltage source detection."""

    def test_single_voltage_source_ok(self):
        """Single voltage source should pass."""
        c = Circuit("single_v")
        v = Vdc("1", 5.0)
        r = Resistor("1", "1k")
        c.add(v, r)

        n1 = Net("n1")
        c.connect(v.ports[0], n1)
        c.connect(r.ports[0], n1)
        c.connect(v.ports[1], GND)
        c.connect(r.ports[1], GND)

        result = c.validate()
        vsrc_errors = [e for e in result.errors if "voltage source loop" in e.message.lower()]
        assert len(vsrc_errors) == 0

    def test_parallel_voltage_sources_detected(self):
        """Parallel voltage sources should be detected."""
        c = Circuit("parallel_v")
        v1 = Vdc("1", 5.0)
        v2 = Vdc("2", 10.0)
        r = Resistor("1", "1k")
        c.add(v1, v2, r)

        n1 = Net("vcc")
        c.connect(v1.ports[0], n1)
        c.connect(v2.ports[0], n1)  # Both V+ on same node
        c.connect(v1.ports[1], GND)
        c.connect(v2.ports[1], GND)  # Both V- on GND -> parallel!
        c.connect(r.ports[0], n1)
        c.connect(r.ports[1], GND)

        result = c.validate()
        vsrc_errors = [e for e in result.errors if "voltage source loop" in e.message.lower()]
        assert len(vsrc_errors) == 1
        # Component refs are "1" and "2"
        assert "1" in vsrc_errors[0].message and "2" in vsrc_errors[0].message

    def test_series_voltage_sources_ok(self):
        """Series voltage sources should pass."""
        c = Circuit("series_v")
        v1 = Vdc("1", 5.0)
        v2 = Vdc("2", 5.0)
        r = Resistor("1", "1k")
        c.add(v1, v2, r)

        n1 = Net("n1")
        n2 = Net("n2")
        c.connect(v1.ports[0], n1)
        c.connect(v1.ports[1], n2)  # V1- connected to V2+
        c.connect(v2.ports[0], n2)
        c.connect(v2.ports[1], GND)
        c.connect(r.ports[0], n1)
        c.connect(r.ports[1], GND)

        result = c.validate()
        vsrc_errors = [e for e in result.errors if "voltage source loop" in e.message.lower()]
        assert len(vsrc_errors) == 0


class TestCurrentSourceLoopDetection:
    """Tests for series current source detection (open circuits)."""

    def test_single_current_source_ok(self):
        """Single current source with load should pass."""
        c = Circuit("single_i")
        i = Idc("1", "1m")
        r = Resistor("1", "1k")
        c.add(i, r)

        n1 = Net("n1")
        c.connect(i.ports[0], n1)
        c.connect(r.ports[0], n1)  # Load at same node
        c.connect(i.ports[1], GND)
        c.connect(r.ports[1], GND)

        result = c.validate()
        isrc_errors = [e for e in result.errors if "current source loop" in e.message.lower()]
        assert len(isrc_errors) == 0

    def test_series_current_sources_detected(self):
        """Series current sources with no load path should be detected."""
        c = Circuit("series_i")
        i1 = Idc("1", "1m")
        i2 = Idc("2", "2m")
        r = Resistor("1", "1k")
        c.add(i1, i2, r)

        # I1 and I2 share a node with no other connection -> series!
        n1 = Net("series_node")
        c.connect(i1.ports[0], n1)  # I1+ to series_node
        c.connect(i2.ports[1], n1)  # I2- to series_node (only I sources here)
        c.connect(i1.ports[1], GND)
        c.connect(i2.ports[0], GND)
        # R connected elsewhere to have ground
        c.connect(r.ports[0], GND)
        c.connect(r.ports[1], GND)

        result = c.validate()
        isrc_errors = [e for e in result.errors if "current source loop" in e.message.lower()]
        assert len(isrc_errors) == 1
        assert "1" in isrc_errors[0].message and "2" in isrc_errors[0].message

    def test_parallel_current_sources_ok(self):
        """Parallel current sources should pass (they add up)."""
        c = Circuit("parallel_i")
        i1 = Idc("1", "1m")
        i2 = Idc("2", "1m")
        r = Resistor("1", "1k")
        c.add(i1, i2, r)

        n1 = Net("n1")
        # Both current sources and resistor at same nodes -> parallel
        c.connect(i1.ports[0], n1)
        c.connect(i2.ports[0], n1)
        c.connect(r.ports[0], n1)  # Load provides current path
        c.connect(i1.ports[1], GND)
        c.connect(i2.ports[1], GND)
        c.connect(r.ports[1], GND)

        result = c.validate()
        isrc_errors = [e for e in result.errors if "current source loop" in e.message.lower()]
        assert len(isrc_errors) == 0

    def test_current_source_with_resistor_path_ok(self):
        """Current sources in series but with resistor load should pass."""
        c = Circuit("series_with_load")
        i1 = Idc("1", "1m")
        i2 = Idc("2", "1m")
        r = Resistor("1", "1k")
        c.add(i1, i2, r)

        n1 = Net("n1")
        # I1 and I2 share n1, but R also connects there
        c.connect(i1.ports[0], n1)
        c.connect(i2.ports[1], n1)
        c.connect(r.ports[0], n1)  # Resistor provides path!
        c.connect(i1.ports[1], GND)
        c.connect(i2.ports[0], GND)
        c.connect(r.ports[1], GND)

        result = c.validate()
        isrc_errors = [e for e in result.errors if "current source loop" in e.message.lower()]
        assert len(isrc_errors) == 0


class TestValidationResult:
    """Tests for ValidationResult functionality."""

    def test_valid_result_str(self):
        """Valid circuit should show success message."""
        result = ValidationResult(is_valid=True, errors=[], warnings=[])
        assert "passed" in str(result).lower()

    def test_invalid_result_shows_errors(self):
        """Invalid result should show error details."""
        errors = [
            ValidationWarning(
                severity="error",
                message="Test error",
                component_ref="R1",
                suggestion="Fix it",
            )
        ]
        result = ValidationResult(is_valid=False, errors=errors, warnings=[])
        output = str(result)
        assert "ERRORS" in output
        assert "Test error" in output
        assert "R1" in output
        assert "Fix it" in output

    def test_has_issues_with_warnings(self):
        """has_issues should return True for warnings."""
        warnings = [
            ValidationWarning(
                severity="warning",
                message="Test warning",
            )
        ]
        result = ValidationResult(is_valid=True, errors=[], warnings=warnings)
        assert result.has_issues()


class TestStrictMode:
    """Tests for strict validation mode."""

    def test_strict_mode_converts_warnings_to_errors(self):
        """Strict mode should treat warnings as errors."""
        # Create a valid circuit (not floating) with suspicious values
        c = Circuit("with_warning")
        v = Vdc("1", 5.0)
        r = Resistor("1", 0.0001)  # Very small resistance -> warning
        c.add(v, r)

        n1 = Net("n1")
        c.connect(v.ports[0], n1)
        c.connect(r.ports[0], n1)  # n1 has 2 connections - not floating
        c.connect(v.ports[1], GND)
        c.connect(r.ports[1], GND)  # GND has 2 connections

        # Non-strict: should be valid (just warning)
        result_normal = c.validate(strict=False)
        # Strict: warnings become errors
        result_strict = c.validate(strict=True)

        # Non-strict should pass even with warnings
        # Strict mode converts warnings to errors
        # Check that both modes work as expected
        assert result_normal.is_valid or len(result_normal.warnings) >= 0
        assert result_strict.is_valid == (len(result_strict.errors) == 0)


class TestCircuitValidateMethod:
    """Tests for Circuit.validate() method."""

    def test_circuit_has_validate_method(self):
        """Circuit class should have validate method."""
        c = Circuit("test")
        assert hasattr(c, "validate")
        assert callable(c.validate)

    def test_validate_returns_validation_result(self):
        """validate() should return ValidationResult."""
        c = Circuit("test")
        result = c.validate()
        assert isinstance(result, ValidationResult)
