"""Tests for Nengo Code Generator."""

import nengo
import numpy as np
import pytest

from cnl_parser import parse
from nengo_generator import GeneratorError, generate


def _minimal_specs():
    """Parse a minimal valid CNL spec covering all 4 concepts."""
    sentences = [
        "The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0",
        "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds",
        "The sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds",
        "The connection from sensory neuron to motor neuron MUST have WITH synaptic weight of 1.0",
    ]
    return [parse(s) for s in sentences]


def _default_params():
    return {
        "threshold": 1.0,
        "resting_potential": 0.0,
        "refractory_period": 0.002,
        "tau": 0.02,
        "reset_potential": 0.0,
        "synaptic_weight": 1.0,
    }


def test_generate_returns_network():
    net = generate(_minimal_specs(), _default_params())
    assert isinstance(net, nengo.Network)


def test_network_has_sensory_ensemble():
    net = generate(_minimal_specs(), _default_params())
    assert hasattr(net, "sensory")
    assert isinstance(net.sensory, nengo.Ensemble)
    assert net.sensory.dimensions == 1


def test_network_has_motor_ensemble():
    net = generate(_minimal_specs(), _default_params())
    assert hasattr(net, "motor")
    assert isinstance(net.motor, nengo.Ensemble)
    assert net.motor.dimensions == 1


def test_network_has_connection():
    net = generate(_minimal_specs(), _default_params())
    assert hasattr(net, "connection")
    assert isinstance(net.connection, nengo.Connection)


def test_neuron_params_match_spec():
    net = generate(_minimal_specs(), _default_params())
    neuron_type = net.sensory.neuron_type
    assert isinstance(neuron_type, nengo.LIF)
    assert neuron_type.tau_rc == 0.02
    assert neuron_type.tau_ref == 0.002


def test_synaptic_weight_from_spec():
    specs = _minimal_specs()
    params = _default_params()
    net = generate(specs, params)
    assert net.connection.transform.init == 1.0


def test_custom_synaptic_weight():
    sentences = [
        "The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0",
        "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds",
        "The sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds",
        "The connection from sensory neuron to motor neuron MUST have WITH synaptic weight of 2.5",
    ]
    specs = [parse(s) for s in sentences]
    params = _default_params()
    net = generate(specs, params)
    assert net.connection.transform.init == 2.5


def test_missing_concept_raises_error():
    # Only 3 concepts — missing synaptic_weight
    sentences = [
        "The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0",
        "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds",
        "The sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds",
    ]
    specs = [parse(s) for s in sentences]
    with pytest.raises(GeneratorError, match="synaptic_weight"):
        generate(specs, _default_params())


def test_network_runs_for_one_second():
    """Gate test: generated network must run for 1 second without errors."""
    net = generate(_minimal_specs(), _default_params())
    with nengo.Simulator(net) as sim:
        sim.run(1.0)
    # Should complete without error; verify probe data has expected shape
    assert sim.data[net.motor_probe].shape[0] > 0


def test_network_produces_output():
    """Verify the network produces non-trivial motor output."""
    net = generate(_minimal_specs(), _default_params())
    with nengo.Simulator(net) as sim:
        sim.run(0.5)
    motor_data = sim.data[net.motor_probe]
    # Motor output should have some non-zero activity
    assert np.any(motor_data != 0), "Motor ensemble produced no output"
