"""Tests for the structural-diffusion characterization of the nodal equation.

Verifies that the EPI channel of ΔNFR is the random-walk graph Laplacian
(the discrete diffusion operator), so the nodal equation
∂EPI/∂t = νf·ΔNFR is structurally a diffusion equation.
"""

from __future__ import annotations

import math
import random

import networkx as nx
import numpy as np
import pytest

from tnfr.alias import get_attr
from tnfr.constants.aliases import ALIAS_DNFR, ALIAS_EPI
from tnfr.dynamics import default_compute_delta_nfr
from tnfr.physics.structural_diffusion import (
    DiscreteModeCertificate,
    OverdampedProjectionCertificate,
    OverdampedRegimeCertificate,
    RandomWalkCertificate,
    StructuralDiffusionCertificate,
    StructuralFlowCertificate,
    StructuralStabilityCertificate,
    UndampedLimitCertificate,
    commute_time,
    compute_emergent_pulse,
    compute_nodal_pulse,
    current_divergence,
    damped_wave_rates,
    degree_weighted_total,
    dispersion_relation,
    effective_resistance,
    fiedler_partition,
    instability_threshold,
    nodal_domain_count,
    random_walk_matrix,
    relaxation_spectrum,
    stationary_distribution,
    structural_current,
    structural_diffusion_operator,
    structural_diffusivity,
    structural_eigenmodes,
    structural_field,
    structural_frequency_rank,
    symmetric_normalized_laplacian,
    verify_discrete_modes,
    verify_overdamped_projection,
    verify_overdamped_regime,
    verify_structural_diffusion,
    verify_structural_flow,
    verify_structural_random_walk,
    verify_structural_stability,
    verify_undamped_limit,
)


def _canonical_graph(n: int = 60, seed: int = 11) -> nx.Graph:
    rng = random.Random(seed)
    G = nx.watts_strogatz_graph(n, 6, 0.2, seed=seed)
    for node in G.nodes():
        G.nodes[node]["theta"] = rng.uniform(0.0, 2.0 * math.pi)
        G.nodes[node]["EPI"] = rng.uniform(-0.4, 0.4)
        G.nodes[node]["nu_f"] = rng.uniform(0.5, 1.5)
    default_compute_delta_nfr(G)
    return G


@pytest.mark.parametrize(
    "name,call",
    [
        ("decimals", lambda graph: structural_frequency_rank(graph, decimals=True)),
        ("dt", lambda graph: verify_structural_diffusion(graph, dt=True)),
        ("steps", lambda graph: verify_structural_diffusion(graph, steps=True)),
        (
            "tolerance",
            lambda graph: verify_structural_diffusion(graph, tolerance=True),
        ),
        ("gamma", lambda graph: damped_wave_rates(graph, gamma=True)),
        (
            "projection gamma",
            lambda graph: verify_overdamped_projection(graph, gamma=True),
        ),
        (
            "n_time_samples",
            lambda graph: verify_overdamped_projection(graph, n_time_samples=True),
        ),
        (
            "projection tolerance",
            lambda graph: verify_overdamped_projection(graph, tolerance=True),
        ),
        (
            "undamped gamma",
            lambda graph: verify_undamped_limit(graph, gamma=True),
        ),
        (
            "undamped tolerance",
            lambda graph: verify_undamped_limit(graph, tolerance=True),
        ),
        ("n_modes", lambda graph: compute_emergent_pulse(graph, n_modes=True)),
        (
            "mode tolerance",
            lambda graph: verify_discrete_modes(graph, tolerance=True),
        ),
        (
            "reaction_rate",
            lambda graph: dispersion_relation(graph, reaction_rate=True),
        ),
        (
            "stability tolerance",
            lambda graph: verify_structural_stability(graph, tolerance=True),
        ),
        (
            "walk tolerance",
            lambda graph: verify_structural_random_walk(graph, tolerance=True),
        ),
        (
            "flow tolerance",
            lambda graph: verify_structural_flow(graph, tolerance=True),
        ),
    ],
)
def test_public_numeric_controls_reject_booleans(name, call):
    graph = _canonical_graph(8)
    with pytest.raises(ValueError, match="not boolean"):
        call(graph)


@pytest.mark.parametrize("parameter", ["nu_f", "pressure", "dt", "steps", "tolerance"])
def test_overdamped_regime_rejects_boolean_numeric_controls(parameter):
    controls = {
        "nu_f": 0.7,
        "pressure": 1.3,
        "dt": 0.01,
        "steps": 300,
        "tolerance": 1e-9,
    }
    controls[parameter] = np.bool_(True)
    with pytest.raises(ValueError, match=rf"{parameter}.*not boolean"):
        verify_overdamped_regime(**controls)


@pytest.mark.parametrize("channel", ["EPI", "nu_f"])
@pytest.mark.parametrize("boolean", [True, False, np.bool_(True), np.bool_(False)])
def test_diffusion_state_readers_reject_boolean_channels(channel, boolean):
    graph = _canonical_graph(8)
    graph.nodes[1][channel] = boolean

    if channel == "EPI":
        with pytest.raises(ValueError, match="EPI.*not boolean"):
            structural_field(graph)
    else:
        with pytest.raises(ValueError, match="frequency.*not boolean"):
            structural_diffusivity(graph)


class TestDiffusionOperator:
    """The structural diffusion operator is the random-walk Laplacian."""

    def test_operator_shape(self) -> None:
        G = _canonical_graph(40)
        nodes, lap = structural_diffusion_operator(G)
        assert len(nodes) == 40
        assert lap.shape == (40, 40)

    def test_rows_sum_to_zero(self) -> None:
        # L_rw = I − D⁻¹W has zero row sums (diffusion conserves locally).
        G = _canonical_graph(40)
        _, lap = structural_diffusion_operator(G)
        assert np.allclose(lap.sum(axis=1), 0.0)

    def test_diagonal_is_one(self) -> None:
        G = _canonical_graph(30)
        _, lap = structural_diffusion_operator(G)
        # connected (non-isolated) nodes have diagonal 1.0
        assert np.allclose(np.diag(lap), 1.0)


class TestNodalEquationIsDiffusion:
    """The canonical ΔNFR EPI channel equals −L_rw·EPI (machine precision)."""

    def test_dnfr_epi_channel_is_graph_laplacian(self) -> None:
        G = _canonical_graph(50)
        nodes, lap = structural_diffusion_operator(G)
        epi = structural_field(G, nodes)
        # isolate the EPI channel on a clean structural replica
        from tnfr.constants.aliases import ALIAS_VF

        g2 = nx.Graph()
        for node in nodes:
            g2.add_node(
                node,
                EPI=float(get_attr(G.nodes[node], ALIAS_EPI, 0.0)),
                theta=float(G.nodes[node].get("theta", 0.0)),
                nu_f=float(get_attr(G.nodes[node], ALIAS_VF, 0.0)),
            )
        g2.add_edges_from(G.edges())
        g2.graph["DNFR_WEIGHTS"] = {"phase": 0.0, "epi": 1.0, "vf": 0.0, "topo": 0.0}
        default_compute_delta_nfr(g2)
        dnfr = np.array([float(get_attr(g2.nodes[n], ALIAS_DNFR, 0.0)) for n in nodes])
        assert np.allclose(dnfr, -(lap @ epi), atol=1e-12)

    def test_certificate_confirms_laplacian(self) -> None:
        G = _canonical_graph(60)
        cert = verify_structural_diffusion(G)
        assert cert.dnfr_is_graph_laplacian
        assert cert.max_laplacian_residual < 1e-12


class TestDiffusionConservation:
    """Diffusion conserves the degree-weighted structural total."""

    def test_degree_weighted_total_conserved(self) -> None:
        G = _canonical_graph(60)
        # Degree-weighted conservation requires a common nodal frequency.
        for node in G:
            G.nodes[node]["nu_f"] = 1.0
        cert = verify_structural_diffusion(G)
        assert cert.degree_weighted_conserved
        assert cert.max_conservation_drift < 1e-9

    def test_degree_weighted_total_value(self) -> None:
        import pytest

        G = _canonical_graph(40)
        # the helper matches a direct degree-weighted sum
        nodes, _ = structural_diffusion_operator(G)
        epi = structural_field(G, nodes)
        deg = np.array([G.degree(n) for n in nodes], dtype=float)
        assert degree_weighted_total(G) == pytest.approx(float(deg @ epi))


class TestDiffusionRelaxation:
    """The field relaxes to a uniform diffusive equilibrium."""

    def test_relaxes_to_uniform(self) -> None:
        G = _canonical_graph(60)
        cert = verify_structural_diffusion(G)
        assert cert.relaxes_to_uniform
        assert cert.final_field_std < 1e-3

    def test_spectrum_has_zero_mode(self) -> None:
        # λ₁ = 0 (the conserved uniform diffusion mode).
        G = _canonical_graph(50)
        spec = relaxation_spectrum(G)
        assert abs(float(spec[0])) < 1e-9
        assert float(spec[1]) > 0.0  # spectral gap positive

    def test_diffusivity_is_mean_vf(self) -> None:
        import pytest

        from tnfr.constants.aliases import ALIAS_VF

        G = _canonical_graph(40)
        nodes, _ = structural_diffusion_operator(G)
        vf = [float(get_attr(G.nodes[n], ALIAS_VF, 0.0)) for n in nodes]
        assert structural_diffusivity(G) == pytest.approx(float(np.mean(vf)), rel=1e-6)


class TestDiffusionCertificate:
    """The certificate aggregates the diffusion verdict."""

    def test_certificate_valid(self) -> None:
        G = _canonical_graph(60)
        cert = verify_structural_diffusion(G)
        assert isinstance(cert, StructuralDiffusionCertificate)
        assert cert.is_valid_diffusion
        assert "VALID" in cert.summary()
        assert cert.diffusivity > 0.0
        assert cert.spectral_gap > 0.0

    def test_graph_not_mutated(self) -> None:
        # verify runs the ΔNFR check on a replica; caller's graph is untouched.
        G = _canonical_graph(40)
        before = {n: float(get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes()}
        weights_before = G.graph.get("DNFR_WEIGHTS")
        verify_structural_diffusion(G)
        after = {n: float(get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes()}
        assert before == after
        assert G.graph.get("DNFR_WEIGHTS") == weights_before


class TestOverdampedRegime:
    """The bare nodal equation is the first-order overdamped drift law."""

    def test_drift_velocity_is_nu_f_times_pressure(self) -> None:
        import pytest

        cert = verify_overdamped_regime(nu_f=0.7, pressure=1.3)
        assert cert.drift_velocity == pytest.approx(0.7 * 1.3)

    def test_velocity_is_constant_under_sustained_pressure(self) -> None:
        # first-order: held pressure -> constant velocity (drift), not accel.
        cert = verify_overdamped_regime()
        assert cert.velocity_is_constant
        assert cert.max_velocity_variation < 1e-9

    def test_position_grows_linearly(self) -> None:
        import pytest

        cert = verify_overdamped_regime(nu_f=0.7, pressure=1.3)
        assert cert.position_linear_in_time
        # slope equals the drift velocity νf·F (not quadratic acceleration)
        assert cert.position_slope == pytest.approx(0.7 * 1.3, abs=1e-6)

    def test_mobility_law_linear_in_nu_f(self) -> None:
        cert = verify_overdamped_regime()
        assert cert.mobility_linear_in_nu_f

    def test_drift_linear_in_pressure(self) -> None:
        cert = verify_overdamped_regime()
        assert cert.drift_linear_in_pressure

    def test_is_first_order_not_inertial(self) -> None:
        cert = verify_overdamped_regime()
        assert not cert.is_second_order

    def test_certificate_valid(self) -> None:
        cert = verify_overdamped_regime()
        assert isinstance(cert, OverdampedRegimeCertificate)
        assert cert.is_overdamped_drift
        assert "VALID" in cert.summary()
        assert "mobility law" in cert.summary()


class TestOverdampedProjection:
    """Diffusion is the overdamped projection of the substrate wave flow."""

    def test_damped_wave_roots_satisfy_characteristic_equation(self) -> None:
        import pytest

        G = _canonical_graph(40)
        gamma = 50.0
        lambdas, s_slow, s_fast = damped_wave_rates(G, gamma)
        # s^2 + gamma s + lambda = 0  =>  s_slow + s_fast = -gamma,
        # s_slow * s_fast = lambda (Vieta).
        assert s_slow + s_fast == pytest.approx(-gamma * np.ones_like(lambdas))
        assert s_slow * s_fast == pytest.approx(lambdas)

    def test_slow_root_converges_to_diffusion_rate(self) -> None:
        # the slow root -> -lambda_k/gamma (the diffusion rate nu_f*lambda_k)
        G = _canonical_graph(40)
        cert = verify_overdamped_projection(G, gamma=50.0)
        assert cert.max_rate_rel_error < 1e-2

    def test_bridge_error_scales_as_inverse_gamma_squared(self) -> None:
        import pytest

        # rate_error * gamma^2 -> constant ~ lambda_max as gamma grows
        G = _canonical_graph(40)
        c1 = verify_overdamped_projection(G, gamma=50.0)
        c2 = verify_overdamped_projection(G, gamma=200.0)
        # larger gamma => the constant sits closer to lambda_max
        err1 = abs(c1.rate_error_times_gamma_sq - c1.lambda_max)
        err2 = abs(c2.rate_error_times_gamma_sq - c2.lambda_max)
        assert err2 <= err1 + 1e-9
        assert c2.rate_error_times_gamma_sq == pytest.approx(c2.lambda_max, rel=0.05)

    def test_nu_f_is_inverse_damping(self) -> None:
        import pytest

        G = _canonical_graph(40)
        cert = verify_overdamped_projection(G, gamma=40.0)
        assert cert.nu_f_effective == pytest.approx(1.0 / 40.0)

    def test_slowest_mode_matches_diffusion_spectral_gap(self) -> None:
        import pytest

        G = _canonical_graph(40)
        cert = verify_overdamped_projection(G, gamma=100.0)
        assert cert.slowest_slow_rate == pytest.approx(
            cert.slowest_diffusion_rate, rel=2e-2
        )

    def test_trajectory_collapses_onto_diffusion(self) -> None:
        G = _canonical_graph(40)
        cert = verify_overdamped_projection(G, gamma=100.0)
        assert cert.trajectory_max_rel_error < 1e-2

    def test_certificate_valid(self) -> None:
        G = _canonical_graph(40)
        cert = verify_overdamped_projection(G, gamma=50.0)
        assert isinstance(cert, OverdampedProjectionCertificate)
        assert cert.is_valid_projection
        assert "VALID" in cert.summary()


class TestUndampedLimit:
    """The gamma->0 end of the bridge: damped wave -> standing waves."""

    def test_small_gamma_recovers_standing_waves(self) -> None:
        G = _canonical_graph(40)
        cert = verify_undamped_limit(G, gamma=1e-3)
        assert cert.matches_discrete_modes
        assert cert.max_freq_rel_error < 1e-2

    def test_decay_rate_is_half_gamma(self) -> None:
        import pytest

        # underdamped envelope decay Re(s) = gamma/2
        G = _canonical_graph(40)
        gamma = 1e-3
        cert = verify_undamped_limit(G, gamma=gamma)
        assert cert.max_decay_rate == pytest.approx(gamma / 2.0, rel=1e-6)

    def test_frequency_error_scales_as_gamma_squared(self) -> None:
        # freq error / gamma^2 -> constant as gamma shrinks
        G = _canonical_graph(40)
        c1 = verify_undamped_limit(G, gamma=1e-2)
        c2 = verify_undamped_limit(G, gamma=1e-3)
        # the constant stabilises: the two normalised values agree closely
        assert (
            abs(c2.freq_error_times_inv_gamma_sq - c1.freq_error_times_inv_gamma_sq)
            < 0.05 * c1.freq_error_times_inv_gamma_sq
        )

    def test_frequencies_match_eigenmode_sqrt(self) -> None:
        import pytest

        # standing-wave frequencies are sqrt of the diffusion eigenvalues,
        # in ascending-eigenvalue order including the uniform mode (omega~0),
        # matching the verify_discrete_modes convention.
        G = _canonical_graph(40)
        cert = verify_undamped_limit(G, gamma=1e-3)
        _, lap = structural_diffusion_operator(G)
        lam = np.sort(np.clip(np.linalg.eigvals(lap).real, 0.0, None))
        expected = [float(x) ** 0.5 for x in lam[:6]]
        assert len(cert.standing_wave_frequencies) == len(expected)
        for got, exp in zip(cert.standing_wave_frequencies, expected):
            assert got == pytest.approx(exp, abs=1e-6)

    def test_certificate_valid(self) -> None:
        G = _canonical_graph(40)
        cert = verify_undamped_limit(G, gamma=1e-3)
        assert isinstance(cert, UndampedLimitCertificate)
        assert cert.is_valid_undamped_limit
        assert "VALID" in cert.summary()


class TestDiscreteModes:
    """A bounded manifold has discrete standing-wave eigenmodes."""

    def test_isolated_node_has_zero_diffusion_mode(self) -> None:
        G = nx.Graph()
        G.add_edge("left", "right", weight=2.5)
        G.add_node("isolated")
        nx.set_node_attributes(G, 1.0, "nu_f")

        eigvals, eigvecs = structural_eigenmodes(G)
        _, lap = symmetric_normalized_laplacian(G)
        _, diffusion = structural_diffusion_operator(G)

        # Every connected component has a stationary mode: an isolated NFR
        # cannot acquire reorganization pressure from absent neighbours.
        assert np.allclose(eigvals, [0.0, 0.0, 2.0], atol=1e-12)
        assert np.allclose(lap @ eigvecs, eigvecs * eigvals, atol=1e-12)
        assert np.allclose(eigvals, np.sort(np.linalg.eigvals(diffusion).real))
        assert np.allclose(relaxation_spectrum(G), eigvals)

    def test_edgeless_network_has_no_collective_vibration(self) -> None:
        G = nx.empty_graph(3)
        eigvals, eigvecs = structural_eigenmodes(G)

        assert np.array_equal(eigvals, np.zeros(3))
        assert np.allclose(eigvecs.T @ eigvecs, np.eye(3))
        pulse = compute_emergent_pulse(G)
        assert pulse["fundamental"] == 0.0
        assert pulse["vibration_energy"] == 0.0

    def test_spectrum_is_discrete_and_finite(self) -> None:
        G = nx.path_graph(30)
        eigvals, eigvecs = structural_eigenmodes(G)
        assert eigvals.shape == (30,)
        assert eigvecs.shape == (30, 30)
        assert np.all(np.isfinite(eigvals))

    def test_modes_orthonormal(self) -> None:
        G = nx.path_graph(30)
        _, eigvecs = structural_eigenmodes(G)
        gram = eigvecs.T @ eigvecs
        assert np.allclose(gram, np.eye(30), atol=1e-9)

    def test_uniform_zero_mode(self) -> None:
        # λ₁ = 0 (uniform mode / conserved diffusion mode)
        G = nx.path_graph(30)
        eigvals, _ = structural_eigenmodes(G)
        assert abs(float(eigvals[0])) < 1e-9
        assert float(eigvals[1]) > 0.0  # spectral gap positive

    def test_path_modes_are_cosine_standing_waves(self) -> None:
        # path-graph L_sym mode k is a degree-weighted cosine standing wave
        G = nx.path_graph(40)
        _, eigvecs = structural_eigenmodes(G)
        k = 3
        i = np.arange(40)
        deg = np.array([G.degree(n) for n in G.nodes()], dtype=float)
        cos_pred = np.sqrt(deg) * np.cos(np.pi * k * (i + 0.5) / 40)
        cos_pred /= np.linalg.norm(cos_pred)
        mode = eigvecs[:, k] / np.linalg.norm(eigvecs[:, k])
        assert abs(float(np.dot(mode, cos_pred))) > 0.99

    def test_nodal_domain_count_grows_on_path(self) -> None:
        # Courant: k-th mode has exactly k sign changes on a 1D manifold
        G = nx.path_graph(40)
        _, eigvecs = structural_eigenmodes(G)
        counts = [nodal_domain_count(eigvecs[:, k]) for k in range(6)]
        assert counts == [0, 1, 2, 3, 4, 5]

    def test_spectrum_matches_diffusion_operator(self) -> None:
        # L_sym spectrum == L_rw (diffusion operator) spectrum
        G = nx.watts_strogatz_graph(40, 4, 0.2, seed=5)
        eigvals, _ = structural_eigenmodes(G)
        _, lrw = structural_diffusion_operator(G)
        rw_spec = np.sort(np.linalg.eigvals(lrw).real)
        assert np.allclose(np.sort(eigvals), rw_spec, atol=1e-7)

    def test_standing_wave_frequencies_are_sqrt_lambda(self) -> None:
        G = nx.path_graph(30)
        eigvals, _ = structural_eigenmodes(G)
        cert = verify_discrete_modes(G)
        for k, f in enumerate(cert.standing_wave_frequencies):
            assert f == __import__("pytest").approx(float(np.sqrt(eigvals[k])))

    def test_certificate_valid_across_topologies(self) -> None:
        for G in (
            nx.path_graph(40),
            nx.cycle_graph(40),
            nx.watts_strogatz_graph(40, 4, 0.2, seed=5),
        ):
            cert = verify_discrete_modes(G)
            assert isinstance(cert, DiscreteModeCertificate)
            assert cert.is_valid_discrete_modes
            assert "VALID" in cert.summary()


class TestStructuralStability:
    """The dispersion relation governs structural linear stability."""

    @staticmethod
    def _graph(builder):
        G = builder()
        for nd in G.nodes():
            G.nodes[nd]["nu_f"] = 1.0
        return G

    def test_dispersion_at_zero_is_negative_relaxation(self) -> None:
        G = self._graph(lambda: nx.watts_strogatz_graph(50, 6, 0.2, seed=7))
        sigma0 = dispersion_relation(G, 0.0)
        rates = relaxation_spectrum(G)
        assert np.allclose(np.sort(-sigma0), np.sort(rates), atol=1e-7)

    def test_pure_diffusion_decays_nonuniform_modes(self) -> None:
        # at r=0 every non-uniform mode decays (sigma_k < 0 for k >= 1)
        G = self._graph(lambda: nx.cycle_graph(40))
        sigma0 = dispersion_relation(G, 0.0)
        assert np.all(sigma0[1:] < 1e-9)

    def test_instability_threshold_is_nu_times_lambda2(self) -> None:
        import pytest

        G = self._graph(lambda: nx.watts_strogatz_graph(50, 6, 0.2, seed=7))
        eigvals, _ = structural_eigenmodes(G)
        nu = structural_diffusivity(G)
        assert instability_threshold(G) == pytest.approx(float(nu * eigvals[1]))

    def test_below_threshold_only_uniform_grows(self) -> None:
        G = self._graph(lambda: nx.cycle_graph(40))
        rc = instability_threshold(G)
        sigma = dispersion_relation(G, 0.5 * rc)
        # no non-uniform mode unstable below threshold
        assert np.all(sigma[1:] < 1e-9)

    def test_above_threshold_fiedler_grows(self) -> None:
        G = self._graph(lambda: nx.cycle_graph(40))
        rc = instability_threshold(G)
        sigma = dispersion_relation(G, rc + 0.01)
        # the Fiedler mode (k=1) is now unstable
        assert sigma[1] > 0.0

    def test_fiedler_partition_splits_barbell_evenly(self) -> None:
        # the two communities of a barbell are the weakest cut
        G = self._graph(lambda: nx.barbell_graph(20, 0))
        part_a, part_b = fiedler_partition(G)
        assert {len(part_a), len(part_b)} == {20}

    def test_certificate_valid_across_topologies(self) -> None:
        for builder in (
            lambda: nx.cycle_graph(40),
            lambda: nx.barbell_graph(20, 0),
            lambda: nx.watts_strogatz_graph(60, 6, 0.2, seed=7),
        ):
            G = self._graph(builder)
            cert = verify_structural_stability(G)
            assert isinstance(cert, StructuralStabilityCertificate)
            assert cert.is_valid_stability
            assert cert.first_unstable_mode == 1  # Fiedler
            assert cert.pure_diffusion_stable
            assert "VALID" in cert.summary()


class TestStructuralRandomWalk:
    """The diffusion operator generates a random walk with resistance."""

    def test_operator_is_walk_generator(self) -> None:
        # L_rw = I - P exactly
        G = nx.watts_strogatz_graph(40, 6, 0.2, seed=7)
        nodes, lrw = structural_diffusion_operator(G)
        _, p = random_walk_matrix(G)
        assert np.allclose(lrw, np.eye(len(nodes)) - p, atol=1e-12)

    def test_transition_is_row_stochastic(self) -> None:
        G = nx.watts_strogatz_graph(40, 6, 0.2, seed=7)
        _, p = random_walk_matrix(G)
        assert np.allclose(p.sum(axis=1), 1.0)

    def test_stationary_is_degree(self) -> None:
        # pi proportional to degree, and pi @ P == pi
        G = nx.watts_strogatz_graph(40, 6, 0.2, seed=7)
        nodes, pi = stationary_distribution(G)
        _, p = random_walk_matrix(G)
        deg = np.array([G.degree(n) for n in nodes], dtype=float)
        assert np.allclose(pi, deg / deg.sum())
        assert np.allclose(pi @ p, pi, atol=1e-9)

    def test_effective_resistance_is_metric(self) -> None:
        G = nx.watts_strogatz_graph(40, 6, 0.2, seed=7)
        _, r = effective_resistance(G)
        assert np.allclose(r, r.T)  # symmetric
        assert np.all(r >= -1e-9)  # non-negative
        assert np.allclose(np.diag(r), 0.0)  # zero self-resistance
        # triangle inequality on a sample
        for i, j, k in [(0, 10, 20), (5, 15, 25), (1, 30, 8)]:
            assert r[i, k] <= r[i, j] + r[j, k] + 1e-7

    def test_commute_time_is_2m_resistance(self) -> None:
        G = nx.watts_strogatz_graph(40, 6, 0.2, seed=7)
        nodes, r = effective_resistance(G)
        _, c = commute_time(G)
        m = G.number_of_edges()
        assert np.allclose(c, 2.0 * m * r, atol=1e-7)

    def test_commute_time_matches_monte_carlo(self) -> None:
        # anchor: analytic commute time predicts a Monte-Carlo random walk
        G = nx.watts_strogatz_graph(30, 4, 0.2, seed=3)
        nodes, c = commute_time(G)
        idx = {n: i for i, n in enumerate(nodes)}
        A = nx.to_numpy_array(G, nodelist=nodes)
        nbrs = [np.where(A[k] > 0)[0] for k in range(len(nodes))]
        rng = np.random.default_rng(1)
        s, t = 0, 12
        total, trials = 0, 300
        for _ in range(trials):
            cur, steps, hit = s, 0, False
            while steps < 100000:
                cur = int(rng.choice(nbrs[cur]))
                steps += 1
                if not hit and cur == t:
                    hit = True
                elif hit and cur == s:
                    break
            total += steps
        mc = total / trials
        analytic = float(c[idx[s], idx[t]])
        assert abs(mc - analytic) / analytic < 0.12  # statistical tolerance

    def test_certificate_valid_across_topologies(self) -> None:
        for G in (
            nx.cycle_graph(40),
            nx.barbell_graph(20, 0),
            nx.watts_strogatz_graph(50, 6, 0.2, seed=7),
        ):
            cert = verify_structural_random_walk(G)
            assert isinstance(cert, RandomWalkCertificate)
            assert cert.is_valid_random_walk
            assert cert.operator_is_walk_generator
            assert cert.stationary_is_degree
            assert cert.commute_equals_2m_resistance
            assert "VALID" in cert.summary()


def _flow_graph(n: int = 40, seed: int = 7) -> nx.Graph:
    G = nx.watts_strogatz_graph(n, 6, 0.2, seed=seed)
    rng = np.random.default_rng(seed)
    for node in G.nodes():
        G.nodes[node]["EPI"] = float(rng.uniform(-0.4, 0.4))
    return G


class TestStructuralFlow:
    """The diffusion transport carries a Fick current obeying Kirchhoff."""

    def test_current_is_antisymmetric(self) -> None:
        # J_ij = EPI_i - EPI_j = -J_ji (directed edge flow)
        G = _flow_graph()
        _, j = structural_current(G)
        assert np.allclose(j, -j.T, atol=1e-12)

    def test_current_supported_on_edges_only(self) -> None:
        # no current across non-adjacent nodes
        G = _flow_graph()
        nodes, j = structural_current(G)
        A = nx.to_numpy_array(G, nodelist=nodes)
        assert np.allclose(j[A == 0.0], 0.0)

    def test_kirchhoff_current_law(self) -> None:
        # net outflow div(J)_i = (L*EPI)_i (continuity = current law)
        G = _flow_graph()
        nodes, div = current_divergence(G)
        A = nx.to_numpy_array(G, nodelist=nodes)
        L = np.diag(A.sum(1)) - A
        epi = np.array(
            [get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in nodes],
            dtype=float,
        )
        assert np.allclose(div, L @ epi, atol=1e-12)

    def test_total_flux_balances(self) -> None:
        # closed network: sum of net outflows = 0 (no sources)
        G = _flow_graph()
        _, div = current_divergence(G)
        assert abs(float(div.sum())) < 1e-9

    def test_equilibrium_zero_current(self) -> None:
        # a uniform EPI field carries zero current everywhere
        G = nx.watts_strogatz_graph(30, 4, 0.2, seed=5)
        for node in G.nodes():
            G.nodes[node]["EPI"] = 0.37
        _, j = structural_current(G)
        assert np.allclose(j, 0.0, atol=1e-12)
        _, div = current_divergence(G)
        assert np.allclose(div, 0.0, atol=1e-12)

    def test_ohm_law_injected_current(self) -> None:
        # injected unit current s->t induces potential drop = R_eff(s,t)
        G = _flow_graph()
        nodes = list(G.nodes())
        idx = {n: i for i, n in enumerate(nodes)}
        A = nx.to_numpy_array(G, nodelist=nodes)
        L = np.diag(A.sum(1)) - A
        lp = np.linalg.pinv(L)
        _, r = effective_resistance(G)
        s, t = idx[nodes[0]], idx[nodes[15]]
        b = np.zeros(len(nodes))
        b[s], b[t] = 1.0, -1.0
        v = lp @ b
        drop = v[s] - v[t]
        assert np.isclose(drop, r[s, t], atol=1e-7)

    def test_certificate_valid_across_topologies(self) -> None:
        builders = (
            lambda: nx.cycle_graph(40),
            lambda: nx.barbell_graph(15, 0),
            lambda: nx.watts_strogatz_graph(50, 6, 0.2, seed=7),
        )
        rng = np.random.default_rng(0)
        for build in builders:
            G = build()
            for node in G.nodes():
                G.nodes[node]["EPI"] = float(rng.uniform(-0.4, 0.4))
            cert = verify_structural_flow(G)
            assert isinstance(cert, StructuralFlowCertificate)
            assert cert.is_valid_flow
            assert cert.current_antisymmetric
            assert cert.kirchhoff_holds
            assert cert.total_flux_balances
            assert cert.equilibrium_zero_current
            assert cert.ohm_law_holds
            assert "VALID" in cert.summary()


class TestEmergentPulse:
    """The emergent pulse read-out: the resonant rhythm the substrate plays."""

    def test_fundamental_is_slowest_nonuniform_resonance(self) -> None:
        import pytest

        # ring C_24: the slowest resonance is omega_2 = sqrt(1 - cos(2 pi/24))
        G = nx.cycle_graph(24)
        pulse = compute_emergent_pulse(G)
        expected = math.sqrt(1.0 - math.cos(2.0 * math.pi / 24))
        assert pulse["fundamental"] == pytest.approx(expected, rel=1e-6)

    def test_excludes_uniform_mode(self) -> None:
        # the lambda ~ 0 uniform mode is not a resonance
        G = nx.cycle_graph(24)
        pulse = compute_emergent_pulse(G)
        assert pulse["n_modes"] == 23
        assert all(w > 1e-9 for w in pulse["resonant_spectrum"])

    def test_vibration_energy_is_half_sum_lambda(self) -> None:
        import pytest

        G = nx.cycle_graph(24)
        eigvals, _ = structural_eigenmodes(G)
        pulse = compute_emergent_pulse(G)
        assert pulse["vibration_energy"] == pytest.approx(
            0.5 * float(np.sum(eigvals)), rel=1e-9
        )

    def test_fractal_signature_counts_degeneracy(self) -> None:
        # K_6: L_sym eigenvalue 6/5 has multiplicity 5 (the standard irrep
        # of S_6) -> the self-similar / fractal degeneracy signature
        G = nx.complete_graph(6)
        pulse = compute_emergent_pulse(G)
        assert pulse["spectral_multiplicity"] == 5
        assert pulse["dominant_beat"] >= 0.0


class TestNodalPulse:
    """The per-NFR pulse: each NFR oscillates; resonance couples the pulses."""

    @staticmethod
    def _pulsing_ring(n, vf, phases):
        G = nx.cycle_graph(n)
        for i, k in enumerate(G.nodes()):
            G.nodes[k].update(nu_f=float(vf[i]), theta=float(phases[i]), EPI=0.0)
        return G

    def test_phase_locked_pulses_have_unit_coherence(self) -> None:
        import pytest

        # all NFRs share one phase -> collective + local resonance = 1
        G = self._pulsing_ring(12, [1.0] * 12, [0.0] * 12)
        pulse = compute_nodal_pulse(G)
        assert pulse["phase_coherence"] == pytest.approx(1.0, abs=1e-9)
        assert pulse["mean_local_resonance"] == pytest.approx(1.0, abs=1e-9)

    def test_evenly_spread_pulses_unlock_collective_resonance(self) -> None:
        # phases evenly around the circle -> collective R = 0, yet each NFR
        # still locally resonates with its two neighbours at cos(2 pi / n)
        import pytest

        n = 8
        phases = [2.0 * math.pi * i / n for i in range(n)]
        G = self._pulsing_ring(n, [1.0] * n, phases)
        pulse = compute_nodal_pulse(G)
        assert pulse["phase_coherence"] == pytest.approx(0.0, abs=1e-9)
        assert pulse["mean_local_resonance"] == pytest.approx(
            math.cos(2.0 * math.pi / n), abs=1e-9
        )

    def test_frequency_stats_are_per_nfr_pulse_rates(self) -> None:
        import pytest

        vf = [0.5, 1.0, 1.5, 2.0]
        G = self._pulsing_ring(4, vf, [0.0] * 4)
        pulse = compute_nodal_pulse(G)
        assert pulse["mean_frequency"] == pytest.approx(1.25)
        assert pulse["frequency_spread"] == pytest.approx(float(np.std(vf)))
        assert pulse["n_pulsing"] == 4
        assert pulse["n_nodes"] == 4

    def test_resonance_gate_is_canonical_u3_bound(self) -> None:
        import pytest

        from tnfr.constants.canonical import DELTA_PHI_MAX

        G = self._pulsing_ring(6, [1.0] * 6, [0.0] * 6)
        pulse = compute_nodal_pulse(G)
        assert pulse["resonance_gate"] == pytest.approx(float(DELTA_PHI_MAX))

    def test_nodal_pulse_read_does_not_record_affinity_history(self) -> None:
        from tnfr.glyph_history import ensure_history

        graph = self._pulsing_ring(6, [1.0] * 6, [0.0] * 6)
        history = ensure_history(graph)
        before = {
            key: list(history.get(key, ())) for key in ("W_sparse", "W_i", "W_stats")
        }

        compute_nodal_pulse(graph)

        after = {
            key: list(history.get(key, ())) for key in ("W_sparse", "W_i", "W_stats")
        }
        assert after == before

    def test_inactive_nfr_not_pulsing(self) -> None:
        # nu_f = 0 -> the node cannot reorganize -> not a live pulse
        G = self._pulsing_ring(4, [1.0, 0.0, 1.0, 0.0], [0.0] * 4)
        pulse = compute_nodal_pulse(G)
        assert pulse["n_pulsing"] == 2
        assert pulse["n_nodes"] == 4
