"""
ADR-0006 — Field Energy Operator tests.

Covers:
  - EnergyClass enum properties (vault_candidate, governance_critical)
  - aspect_weight() lookup table (Hebrew and Greek aspect forms)
  - FieldEnergyOperator.compute() — all four input axes
  - Class boundary thresholds (E0–E4)
  - Anchor-adjacent E4 escalation
  - EnergyProfile.requires_architect_review
  - propagate_step() energy recomputation
  - Aspect weight preservation across propagation steps
"""

import numpy as np
import pytest

from core.physics.energy import (
    EnergyClass,
    EnergyProfile,
    FieldEnergyOperator,
    aspect_weight,
)
from field.state import FieldState
from field.propagate import propagate_step
from algebra.versor import unitize_versor
from algebra.rotor import make_rotor_from_angle


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _clean_versor() -> np.ndarray:
    """Return a Cl(4,1) unit versor suitable for FieldState.F."""
    v = np.zeros(32, dtype=np.float64)
    v[0] = 1.0
    return unitize_versor(v)


def _identity_rotor() -> np.ndarray:
    v = np.zeros(32, dtype=np.float64)
    v[0] = 1.0
    return v


_op = FieldEnergyOperator()


# ---------------------------------------------------------------------------
# EnergyClass properties
# ---------------------------------------------------------------------------

class TestEnergyClassProperties:
    def test_e0_is_vault_candidate(self):
        assert EnergyClass.E0.vault_candidate is True

    def test_e1_is_vault_candidate(self):
        assert EnergyClass.E1.vault_candidate is True

    def test_e2_is_not_vault_candidate(self):
        assert EnergyClass.E2.vault_candidate is False

    def test_e3_is_not_vault_candidate(self):
        assert EnergyClass.E3.vault_candidate is False

    def test_e4_is_governance_critical(self):
        assert EnergyClass.E4.governance_critical is True

    def test_e3_is_not_governance_critical(self):
        assert EnergyClass.E3.governance_critical is False


# ---------------------------------------------------------------------------
# aspect_weight lookup
# ---------------------------------------------------------------------------

class TestAspectWeight:
    def test_none_features_returns_zero(self):
        assert aspect_weight(None) == 0.0

    def test_empty_features_returns_zero(self):
        assert aspect_weight({}) == 0.0

    def test_qatal_is_low(self):
        w = aspect_weight({"aspect": "qatal"})
        assert w == pytest.approx(0.15)

    def test_aorist_is_low(self):
        w = aspect_weight({"tense": "aorist"})
        assert w == pytest.approx(0.15)

    def test_imperative_is_highest(self):
        w = aspect_weight({"mood": "imperative"})
        assert w == pytest.approx(0.90)

    def test_yiqtol_is_high(self):
        w = aspect_weight({"aspect": "yiqtol"})
        assert w == pytest.approx(0.65)

    def test_wayyiqtol_is_mid(self):
        w = aspect_weight({"aspect": "wayyiqtol"})
        assert w == pytest.approx(0.45)

    def test_unknown_aspect_returns_zero(self):
        assert aspect_weight({"aspect": "unknown_form"}) == 0.0

    def test_case_insensitive(self):
        assert aspect_weight({"aspect": "IMPERATIVE"}) == pytest.approx(0.90)

    def test_max_of_multiple_features(self):
        # qatal + imperative: max should be imperative
        w = aspect_weight({"aspect": "qatal", "mood": "imperative"})
        assert w == pytest.approx(0.90)


# ---------------------------------------------------------------------------
# FieldEnergyOperator — class boundary thresholds
# ---------------------------------------------------------------------------

class TestFieldEnergyOperatorThresholds:
    """Drive raw score into each class by controlling inputs."""

    def test_all_zero_inputs_gives_e0(self):
        ep = _op.compute()
        assert ep.energy_class is EnergyClass.E0
        assert ep.raw < 0.16

    def test_low_convergence_no_activation_gives_e1(self):
        # convergence_density=2, no recency, no residual, no aspect
        # convergence contribution: 0.35 * log1p(2)/log1p(8) ≈ 0.35 * 0.404 ≈ 0.141
        ep = _op.compute(convergence_density=2)
        assert ep.energy_class in {EnergyClass.E0, EnergyClass.E1}

    def test_moderate_inputs_gives_e2(self):
        # convergence=4 -> ~0.30 contrib; activation=4/8*1=0.5 -> 0.125 contrib
        ep = _op.compute(
            convergence_density=4,
            activation_count=4,
            current_cycle=5,
            last_activation_cycle=4,
        )
        assert ep.energy_class is EnergyClass.E2

    def test_high_convergence_and_activation_gives_e3(self):
        ep = _op.compute(
            convergence_density=8,
            activation_count=8,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=0.5,
        )
        assert ep.energy_class is EnergyClass.E3

    def test_imperative_aspect_and_full_convergence_gives_e4(self):
        ep = _op.compute(
            convergence_density=8,
            activation_count=8,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=1.0,
            morphology_features={"mood": "imperative"},
        )
        assert ep.energy_class is EnergyClass.E4

    def test_e4_raw_boundary(self):
        # raw >= 0.82 without anchor_adjacent should be E4
        # Use max inputs to guarantee raw >= 0.82
        ep = _op.compute(
            convergence_density=8,
            activation_count=8,
            current_cycle=0,
            last_activation_cycle=0,
            coherence_residual=1.0,
            morphology_features={"mood": "imperative"},
        )
        assert ep.energy_class is EnergyClass.E4
        assert ep.raw >= 0.82


# ---------------------------------------------------------------------------
# Anchor-adjacent escalation
# ---------------------------------------------------------------------------

class TestAnchorAdjacentEscalation:
    def test_anchor_adjacent_escalates_to_e4_at_lower_raw(self):
        # Without anchor: raw ~0.72 might be E3
        ep_no_anchor = _op.compute(
            convergence_density=8,
            activation_count=6,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=0.3,
            anchor_adjacent=False,
        )
        ep_anchor = _op.compute(
            convergence_density=8,
            activation_count=6,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=0.3,
            anchor_adjacent=True,
        )
        # anchor_adjacent path escalates at raw >= 0.72 instead of >= 0.82
        if ep_anchor.raw >= 0.72:
            assert ep_anchor.energy_class is EnergyClass.E4
        # Without anchor and same raw, must be lower class
        if ep_no_anchor.raw < 0.82:
            assert ep_no_anchor.energy_class is not EnergyClass.E4

    def test_anchor_adjacent_stored_on_profile(self):
        ep = _op.compute(anchor_adjacent=True)
        assert ep.anchor_adjacent is True


# ---------------------------------------------------------------------------
# EnergyProfile.requires_architect_review
# ---------------------------------------------------------------------------

class TestRequiresArchitectReview:
    def test_e4_always_requires_review(self):
        ep = _op.compute(
            convergence_density=8,
            activation_count=8,
            current_cycle=0,
            last_activation_cycle=0,
            coherence_residual=1.0,
            morphology_features={"mood": "imperative"},
        )
        assert ep.energy_class is EnergyClass.E4
        assert ep.requires_architect_review is True

    def test_e3_anchor_adjacent_requires_review(self):
        # Force E3 but with anchor_adjacent=True
        # E3: raw in [0.62, 0.82). Build that range.
        ep = _op.compute(
            convergence_density=8,
            activation_count=8,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=0.2,
            anchor_adjacent=True,
        )
        # If raw landed in E3 range and anchor_adjacent, review required
        if ep.energy_class is EnergyClass.E3:
            assert ep.requires_architect_review is True

    def test_e2_does_not_require_review(self):
        ep = _op.compute(
            convergence_density=4,
            activation_count=4,
            current_cycle=5,
            last_activation_cycle=4,
        )
        if ep.energy_class is EnergyClass.E2:
            assert ep.requires_architect_review is False


# ---------------------------------------------------------------------------
# propagate_step energy recomputation
# ---------------------------------------------------------------------------

class TestPropagateStepEnergyRecomputation:
    def _make_state_with_energy(self, energy: EnergyProfile | None = None) -> FieldState:
        F = _clean_versor()
        return FieldState(F=F, node=0, step=0, energy=energy)

    def _rotor(self) -> np.ndarray:
        return _identity_rotor()

    def test_no_energy_propagates_none(self):
        state = self._make_state_with_energy(None)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy is None

    def test_step_increments(self):
        ep = _op.compute(convergence_density=2, activation_count=2, current_cycle=0)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.step == 1

    def test_energy_is_recomputed_not_carried_verbatim(self):
        """After propagation the EnergyProfile object must be a new instance."""
        ep = _op.compute(convergence_density=4, activation_count=3, current_cycle=0)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy is not ep

    def test_activation_count_increments(self):
        ep = _op.compute(convergence_density=4, activation_count=3, current_cycle=0)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy.activation_count == ep.activation_count + 1

    def test_convergence_density_preserved(self):
        ep = _op.compute(convergence_density=6, activation_count=2, current_cycle=0)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy.convergence_density == 6

    def test_anchor_adjacent_preserved(self):
        ep = _op.compute(convergence_density=3, anchor_adjacent=True)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy.anchor_adjacent is True

    def test_aspect_weight_preserved_across_step(self):
        """Aspect weight baked at injection must survive propagation."""
        ep = _op.compute(
            convergence_density=4,
            activation_count=2,
            current_cycle=0,
            morphology_features={"mood": "imperative"},
        )
        assert ep.aspect_weight == pytest.approx(0.90)
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy.aspect_weight == pytest.approx(0.90)

    def test_coherence_residual_reset_to_zero_on_propagation(self):
        """Propagation is not a corrective pass; residual must be zero."""
        ep = _op.compute(
            convergence_density=4,
            activation_count=2,
            coherence_residual=0.8,
        )
        state = self._make_state_with_energy(ep)
        new_state = propagate_step(state, self._rotor())
        assert new_state.energy.coherence_residual == pytest.approx(0.0)

    def test_multiple_steps_monotonically_age(self):
        """Repeated propagation cools energy as recency decays."""
        ep = _op.compute(
            convergence_density=4,
            activation_count=4,
            current_cycle=0,
            last_activation_cycle=0,
        )
        state = self._make_state_with_energy(ep)
        # 20 steps of propagation — recency term exp(-age/12) decays
        for _ in range(20):
            state = propagate_step(state, _identity_rotor())
        # After 20 cold steps, energy class should not be E4
        assert state.energy.energy_class is not EnergyClass.E4


# ---------------------------------------------------------------------------
# carry_aspect_weight consolidation
# ---------------------------------------------------------------------------

class TestCarryAspectWeight:
    def test_carry_aspect_matches_morphology_derived(self):
        """carry_aspect_weight produces identical raw/class as morphology-derived aspect."""
        base_kw = dict(
            convergence_density=4,
            activation_count=4,
            current_cycle=1,
            last_activation_cycle=0,
            coherence_residual=0.0,
            anchor_adjacent=False,
        )
        from_morph = _op.compute(**base_kw, morphology_features={"mood": "imperative"})
        from_carry = _op.compute(**base_kw, carry_aspect_weight=0.90)
        assert from_carry.raw == pytest.approx(from_morph.raw)
        assert from_carry.energy_class is from_morph.energy_class
        assert from_carry.aspect_weight == pytest.approx(from_morph.aspect_weight)

    def test_carry_zero_falls_back_to_morphology(self):
        ep = _op.compute(
            morphology_features={"aspect": "yiqtol"},
            carry_aspect_weight=0.0,
        )
        assert ep.aspect_weight == pytest.approx(0.65)

    def test_propagate_step_preserves_baked_aspect_weight(self):
        """Aspect weight injected at the gate survives propagation via carry."""
        ep = _op.compute(
            convergence_density=4,
            activation_count=2,
            current_cycle=0,
            morphology_features={"mood": "imperative"},
        )
        state = FieldState(F=_clean_versor(), node=0, step=0, energy=ep)
        new_state = propagate_step(state, _identity_rotor())
        assert new_state.energy.aspect_weight == pytest.approx(0.90)
        assert new_state.energy.raw > 0.0


# ---------------------------------------------------------------------------
# EnergyProfile field storage round-trip on FieldState
# ---------------------------------------------------------------------------

class TestEnergyProfileRoundTrip:
    def test_field_state_carries_energy_profile(self):
        ep = _op.compute(convergence_density=3, activation_count=2)
        F = _clean_versor()
        state = FieldState(F=F, node=0, step=0, energy=ep)
        assert state.energy is ep
        assert state.energy.energy_class in list(EnergyClass)

    def test_field_state_advance_preserves_energy(self):
        ep = _op.compute(convergence_density=3)
        F = _clean_versor()
        state = FieldState(F=F, node=0, step=0, energy=ep)
        new_F = _clean_versor()
        advanced = state.advance(new_F, new_node=1)
        assert advanced.energy is ep
        assert advanced.step == 1
