"""Modern canonical operator testing suite.

Tests the 13 canonical TNFR operators for physics compliance, nodal equation
validation, and structural coherence preservation.

Operators tested:
- AL (Emission): Creates EPI from vacuum via resonant emission
- EN (Reception): Captures and integrates incoming resonance
- IL (Coherence): Stabilizes form through negative feedback
- OZ (Dissonance): Introduces controlled instability
- UM (Coupling): Creates structural links via phase synchronization
- RA (Resonance): Amplifies and propagates patterns coherently
- SHA (Silence): Freezes evolution temporarily
- VAL (Expansion): Increases structural complexity
- NUL (Contraction): Reduces structural complexity
- THOL (Self-organization): Spontaneous autopoietic pattern formation
- ZHIR (Mutation): Phase transformation at threshold
- NAV (Transition): Regime shift, activates latent EPI
- REMESH (Recursivity): Echoes structure across scales
"""

from __future__ import annotations

import math

import networkx as nx
import pytest

from tnfr.alias import get_attr, set_attr
from tnfr.constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_VF
from tnfr.operators.definitions import (
    Coherence,
    Contraction,
    Coupling,
    Dissonance,
    Emission,
    Expansion,
    Mutation,
    Reception,
    Recursivity,
    Resonance,
    SelfOrganization,
    Silence,
    Transition,
)
from tnfr.operators.nodal_equation import validate_nodal_equation


class TestCanonicalOperatorPhysics:
    """Test fundamental physics compliance of canonical operators."""

    def setup_method(self) -> None:
        """Create test network for operator validation."""
        self.G = nx.Graph()
        self.G.add_nodes_from(
            [
                (1, {"EPI": 1.0, "nu_f": 2.0, "ΔNFR": 0.5, "theta": 0.0}),
                (2, {"EPI": 0.8, "nu_f": 1.5, "ΔNFR": -0.3, "theta": 1.2}),
                (3, {"EPI": 1.2, "nu_f": 2.5, "ΔNFR": 0.1, "theta": 2.8}),
            ]
        )
        self.G.add_edges_from([(1, 2), (2, 3), (1, 3)])

    def test_emission_creates_epi_from_vacuum(self) -> None:
        """Test AL (Emission) can create EPI from zero state."""
        # Set node to vacuum state
        set_attr(self.G.nodes[1], ALIAS_EPI, 0.0)
        set_attr(self.G.nodes[1], ALIAS_VF, 1.0)
        set_attr(self.G.nodes[1], ALIAS_DNFR, 0.0)

        epi_before = get_attr(self.G.nodes[1], ALIAS_EPI)

        # Apply emission operator
        emission = Emission()
        emission(self.G, 1)

        epi_after = get_attr(self.G.nodes[1], ALIAS_EPI)

        # Emission should create EPI > 0
        assert epi_after > epi_before, "Emission should increase EPI from vacuum"
        assert epi_after > 0, "Emission should create positive EPI"

    def test_coherence_operates_on_system(self) -> None:
        """Test IL (Coherence) operates on system state."""
        # Just verify that coherence operator works without errors
        coherence = Coherence()

        try:
            coherence(self.G, 1)
            success = True
        except Exception as e:
            success = False
            print(f"Coherence failed with: {e}")

        # Coherence should execute successfully
        assert success, "Coherence should operate without errors"

    def test_dissonance_operates_on_system(self) -> None:
        """Test OZ (Dissonance) operates on system state."""
        # Just verify that dissonance operator works without errors
        dissonance = Dissonance()

        try:
            dissonance(self.G, 1)
            success = True
        except Exception as e:
            success = False
            print(f"Dissonance failed with: {e}")

        # Dissonance should execute successfully
        assert success, "Dissonance should operate without errors"

    def test_silence_preserves_epi(self) -> None:
        """Test SHA (Silence) preserves EPI while reducing νf."""
        epi_before = get_attr(self.G.nodes[1], ALIAS_EPI)
        vf_before = get_attr(self.G.nodes[1], ALIAS_VF)

        silence = Silence()
        silence(self.G, 1)

        epi_after = get_attr(self.G.nodes[1], ALIAS_EPI)
        vf_after = get_attr(self.G.nodes[1], ALIAS_VF)

        # Silence should preserve EPI but reduce νf
        assert abs(epi_after - epi_before) < 0.01, "Silence should preserve EPI"
        assert vf_after <= vf_before, "Silence should reduce structural frequency"

    def test_expansion_changes_structure(self) -> None:
        """Test VAL (Expansion) changes structural configuration."""
        # Use a sub-maximal EPI: a node already at the unit form bound cannot
        # expand further (edge-aware scaling caps at EPI_MAX = 1.0).
        set_attr(self.G.nodes[1], ALIAS_EPI, 0.5)
        epi_before = get_attr(self.G.nodes[1], ALIAS_EPI)

        expansion = Expansion()
        expansion(self.G, 1)

        epi_after = get_attr(self.G.nodes[1], ALIAS_EPI)

        # Expansion should change EPI (structural transformation)
        assert (
            epi_after != epi_before
        ), "Expansion should change structural configuration"

    def test_contraction_reduces_complexity(self) -> None:
        """Test NUL (Contraction) reduces structural complexity."""
        # Start with high EPI
        set_attr(self.G.nodes[1], ALIAS_EPI, 2.0)
        epi_before = get_attr(self.G.nodes[1], ALIAS_EPI)

        contraction = Contraction()
        contraction(self.G, 1)

        epi_after = get_attr(self.G.nodes[1], ALIAS_EPI)

        # Contraction should reduce EPI (structural simplification)
        assert epi_after < epi_before, "Contraction should reduce structural complexity"

    def test_coupling_synchronizes_phases(self) -> None:
        """Test UM (Coupling) synchronizes node phases."""
        # Phase-compatible pair (|Δθ| = 1.0 < Δφ_max = π/2): U3 admits coupling,
        # which then narrows the difference.
        set_attr(self.G.nodes[1], ALIAS_THETA, 0.0)
        set_attr(self.G.nodes[2], ALIAS_THETA, 1.0)

        phase_diff_before = abs(
            get_attr(self.G.nodes[1], ALIAS_THETA)
            - get_attr(self.G.nodes[2], ALIAS_THETA)
        )

        coupling = Coupling()
        coupling(self.G, 1)  # Apply to node 1

        phase_diff_after = abs(
            get_attr(self.G.nodes[1], ALIAS_THETA)
            - get_attr(self.G.nodes[2], ALIAS_THETA)
        )

        # Coupling should reduce phase differences (synchronization)
        assert (
            phase_diff_after <= phase_diff_before
        ), "Coupling should synchronize phases"

    def test_resonance_operates_on_patterns(self) -> None:
        """Test RA (Resonance) operates on network patterns."""
        # Just verify that resonance operator works without errors
        resonance = Resonance()

        try:
            resonance(self.G, 1)
            success = True
        except Exception as e:
            success = False
            print(f"Resonance failed with: {e}")

        # Resonance should execute successfully
        assert success, "Resonance should operate without errors"


class TestEventAndHeldStepBoundary:
    """A clipped endpoint match does not certify a hybrid event as nodal flow."""

    def setup_method(self) -> None:
        """Create test network."""
        self.G = nx.Graph()
        self.G.add_node(1, EPI=1.0, nu_f=2.0, ΔNFR=0.5, theta=0.0)

    @pytest.mark.parametrize("operator_type", [Emission, Coherence, Dissonance])
    def test_upper_rail_match_does_not_identify_unprojected_flow(
        self, operator_type
    ) -> None:
        """Actual AL/IL/OZ events can match a clipped step without any EPI motion."""
        epi_before = get_attr(self.G.nodes[1], ALIAS_EPI)
        operator_type()(self.G, 1)
        epi_after = get_attr(self.G.nodes[1], ALIAS_EPI)
        expected_rate = get_attr(self.G.nodes[1], ALIAS_VF) * get_attr(
            self.G.nodes[1], ALIAS_DNFR
        )
        assert epi_after == epi_before == 1.0
        assert expected_rate > 0.0
        assert validate_nodal_equation(
            self.G,
            1,
            epi_before,
            epi_after,
            dt=1.0,
            tolerance=0.0,
            clip_aware=True,
        )
        assert not validate_nodal_equation(
            self.G,
            1,
            epi_before,
            epi_after,
            dt=1.0,
            tolerance=0.0,
            clip_aware=False,
        )


class TestOperatorStructuralPreservation:
    """Test that operators preserve TNFR structural invariants."""

    def setup_method(self) -> None:
        """Create test network."""
        self.G = nx.Graph()
        self.G.add_nodes_from(
            [
                (1, {"EPI": 1.0, "nu_f": 2.0, "ΔNFR": 0.5, "theta": 0.0}),
                (2, {"EPI": 0.8, "nu_f": 1.5, "ΔNFR": -0.3, "theta": 1.2}),
            ]
        )
        self.G.add_edge(1, 2)

    def test_operators_preserve_structural_units(self) -> None:
        """Test operators preserve Hz_str units for νf."""
        operators = [
            Emission(),
            Reception(),
            Coherence(),
            Dissonance(),
            Coupling(),
            Resonance(),
            Silence(),
            Expansion(),
            Contraction(),
            SelfOrganization(),
            Mutation(),
            Transition(),
            Recursivity(),
        ]

        base_nodes = [(node, dict(attrs)) for node, attrs in self.G.nodes(data=True)]
        base_edges = list(self.G.edges())

        for op in operators:
            # Use a fresh graph per operator to avoid latent state carry-over
            G = nx.Graph()
            G.add_nodes_from(base_nodes)
            G.add_edges_from(base_edges)

            try:
                op(G, 1)
            except Exception:
                # Some operators might have preconditions - skip if they fail
                continue

            vf_after = get_attr(G.nodes[1], ALIAS_VF)

            # νf should remain positive (Hz_str units preserved)
            assert (
                vf_after >= 0
            ), f"{op.__class__.__name__} should preserve non-negative νf"
            assert isinstance(
                vf_after, (int, float)
            ), f"{op.__class__.__name__} should return numeric νf"

    def test_operators_maintain_phase_bounds(self) -> None:
        """Test operators keep phase in [0, 2π) range."""
        operators = [Coupling(), Resonance(), Mutation()]  # Phase-affecting operators

        for op in operators:
            # Set initial phase
            set_attr(self.G.nodes[1], ALIAS_THETA, 1.5)

            try:
                op(self.G, 1)
            except Exception:
                continue

            phase_after = get_attr(self.G.nodes[1], ALIAS_THETA)

            # Phase should be in canonical range
            assert (
                0 <= phase_after < 2 * math.pi
            ), f"{op.__class__.__name__} should maintain phase bounds"

    def test_operators_preserve_network_structure(self) -> None:
        """Test operators don't break network topology."""
        edges_before = list(self.G.edges())
        nodes_before = list(self.G.nodes())

        operators = [Emission(), Coherence(), Dissonance(), Silence()]

        for op in operators:
            try:
                op(self.G, 1)
            except Exception:
                continue

            # Network topology should be preserved
            assert (
                list(self.G.edges()) == edges_before
            ), f"{op.__class__.__name__} should preserve edges"
            assert (
                list(self.G.nodes()) == nodes_before
            ), f"{op.__class__.__name__} should preserve nodes"


class TestOperatorComposition:
    """Test operator composition and sequence validation."""

    def setup_method(self) -> None:
        """Create test network."""
        self.G = nx.Graph()
        self.G.add_node(1, EPI=0.0, nu_f=1.0, ΔNFR=0.0, theta=0.0)

    def test_valid_operator_sequences(self) -> None:
        """Test that valid operator sequences work correctly."""
        # U1-compliant sequence: generator -> stabilizer -> closure
        emission = Emission()
        coherence = Coherence()
        silence = Silence()

        # Apply sequence
        emission(self.G, 1)
        epi_after_emission = get_attr(self.G.nodes[1], ALIAS_EPI)
        assert epi_after_emission > 0, "Emission should create EPI"

        coherence(self.G, 1)
        silence(self.G, 1)

        # Final state should be stable
        final_vf = get_attr(self.G.nodes[1], ALIAS_VF)
        final_epi = get_attr(self.G.nodes[1], ALIAS_EPI)
        assert final_epi > 0, "Sequence should preserve created EPI"
        assert final_vf >= 0, "Sequence should maintain valid νf"

    def test_destabilizer_stabilizer_pairing(self) -> None:
        """Test U2 requirement: destabilizers need stabilizers."""
        # Start with some EPI
        set_attr(self.G.nodes[1], ALIAS_EPI, 1.0)

        # Apply destabilizer
        dissonance = Dissonance()
        dissonance(self.G, 1)

        dnfr_after_dissonance = abs(get_attr(self.G.nodes[1], ALIAS_DNFR))

        # Apply stabilizer
        coherence = Coherence()
        coherence(self.G, 1)

        dnfr_after_coherence = abs(get_attr(self.G.nodes[1], ALIAS_DNFR))

        # Stabilizer should reduce instability from destabilizer
        assert (
            dnfr_after_coherence <= dnfr_after_dissonance
        ), "Stabilizer should reduce destabilizer effects"


@pytest.mark.parametrize(
    "operator_class,operator_name",
    [
        (Emission, "AL"),
        (Reception, "EN"),
        (Coherence, "IL"),
        (Dissonance, "OZ"),
        (Coupling, "UM"),
        (Resonance, "RA"),
        (Silence, "SHA"),
        (Expansion, "VAL"),
        (Contraction, "NUL"),
        (SelfOrganization, "THOL"),
        (Mutation, "ZHIR"),
        (Transition, "NAV"),
        (Recursivity, "REMESH"),
    ],
)
def test_operator_basic_instantiation(operator_class: type, operator_name: str) -> None:
    """Test that all 13 canonical operators can be instantiated."""
    operator = operator_class()
    assert operator is not None, f"{operator_name} should instantiate successfully"
    assert callable(operator), f"{operator_name} should be callable"
    assert hasattr(operator, "glyph"), f"{operator_name} should have glyph attribute"
    assert hasattr(operator, "name"), f"{operator_name} should have name attribute"
