"""Tests for CNL Parser.

Test cases derived from valid and invalid examples in cnl_grammar.md.
Includes tests for both "The" and "A" articles (merged from v-copilot + v-jules).
"""

import pytest

from cnl_parser import ParseError, parse


# === Concept 1: Threshold Firing — Valid Examples ===


def test_threshold_firing_sensory_exceeds_numeric():
    result = parse("The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0")
    assert result["concept"] == "threshold_firing"
    assert result["subject"] == "sensory neuron"
    assert result["action"] == "fire"
    assert result["condition"] == "exceeds 1.0"


def test_threshold_firing_motor_exceeds_threshold():
    result = parse("The motor neuron MUST fire ONLY IF membrane potential exceeds threshold")
    assert result["concept"] == "threshold_firing"
    assert result["subject"] == "motor neuron"
    assert result["condition"] == "exceeds threshold"


def test_threshold_firing_must_not_below():
    result = parse(
        "The sensory neuron MUST NOT fire ONLY IF membrane potential is below threshold"
    )
    assert result["concept"] == "threshold_firing"
    assert result["action"] == "fire"
    assert result["condition"] == "is below threshold"


def test_threshold_firing_emit_spike():
    result = parse(
        "The motor neuron MUST emit a spike ONLY IF membrane potential exceeds 0.5"
    )
    assert result["concept"] == "threshold_firing"
    assert result["action"] == "emit a spike"
    assert result["condition"] == "exceeds 0.5"


def test_threshold_firing_article_a():
    """Merged from v-jules: accept 'A' article."""
    result = parse("A sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0")
    assert result["concept"] == "threshold_firing"
    assert result["subject"] == "sensory neuron"


def test_threshold_firing_if_without_only():
    """Merged from v-jules: accept 'IF' without 'ONLY'."""
    result = parse("A motor neuron MUST fire IF membrane potential exceeds 1.0")
    assert result["concept"] == "threshold_firing"
    assert result["subject"] == "motor neuron"
    assert result["condition"] == "exceeds 1.0"


# === Concept 1: Threshold Firing — Invalid Examples ===


def test_threshold_firing_invalid_freeform():
    with pytest.raises(ParseError):
        parse("The sensory neuron fires when it wants to")


def test_threshold_firing_invalid_no_subject():
    with pytest.raises(ParseError):
        parse("Fire the neuron")


# === Concept 2: Refractory Period — Valid Examples ===


def test_refractory_sensory_with_duration():
    result = parse(
        "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds"
    )
    assert result["concept"] == "refractory_period"
    assert result["subject"] == "sensory neuron"
    assert result["action"] == "fire"
    assert result["condition"] == "refractory period of 0.002 seconds"


def test_refractory_motor_no_duration():
    result = parse(
        "The motor neuron MUST NOT respond to input DURING the refractory period"
    )
    assert result["concept"] == "refractory_period"
    assert result["subject"] == "motor neuron"
    assert result["action"] == "respond to input"
    assert result["condition"] == "refractory period"


def test_refractory_after_firing():
    result = parse(
        "The sensory neuron MUST remain inactive AFTER firing for 0.002 seconds"
    )
    assert result["concept"] == "refractory_period"
    assert result["subject"] == "sensory neuron"
    assert result["action"] == "remain inactive"
    assert result["condition"] == "refractory period of 0.002 seconds"


def test_refractory_motor_with_duration():
    result = parse(
        "The motor neuron MUST NOT fire DURING the refractory period of 0.001 seconds"
    )
    assert result["concept"] == "refractory_period"
    assert result["condition"] == "refractory period of 0.001 seconds"


def test_refractory_article_a():
    """Merged from v-jules: accept 'A' article."""
    result = parse(
        "A sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds"
    )
    assert result["concept"] == "refractory_period"
    assert result["subject"] == "sensory neuron"


def test_refractory_accept_input():
    """Merged from v-jules: accept 'accept input' action."""
    result = parse(
        "A motor neuron MUST NOT accept input DURING the refractory period"
    )
    assert result["concept"] == "refractory_period"
    assert result["action"] == "accept input"
    assert result["condition"] == "refractory period"


# === Concept 2: Refractory Period — Invalid Examples ===


def test_refractory_invalid_vague():
    with pytest.raises(ParseError):
        parse("The neuron should rest for a while after firing")


def test_refractory_invalid_no_subject():
    with pytest.raises(ParseError):
        parse("Refractory period is 2 ms")


# === Concept 3: Membrane Potential Decay — Valid Examples ===


def test_decay_sensory_with_tau():
    result = parse(
        "The sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["subject"] == "sensory neuron membrane potential"
    assert result["action"] == "decay"
    assert result["condition"] == "time constant of 0.02 seconds"


def test_decay_motor_with_tau():
    result = parse(
        "The motor neuron membrane potential MUST decay WITH time constant of 0.05 seconds"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["condition"] == "time constant of 0.05 seconds"


def test_decay_toward_resting():
    result = parse(
        "The sensory neuron membrane potential MUST decay toward resting potential "
        "WITH time constant of 0.02 seconds"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["action"] == "decay toward resting potential"


def test_decay_must_not_increase():
    result = parse(
        "The motor neuron membrane potential MUST NOT increase WITHOUT input"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["action"] == "increase"
    assert result["condition"] == "no input"


def test_decay_sensory_small_tau():
    result = parse(
        "The sensory neuron membrane potential MUST decay WITH time constant of 0.01 seconds"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["condition"] == "time constant of 0.01 seconds"


def test_decay_article_a():
    """Merged from v-jules: accept 'A' article."""
    result = parse(
        "A sensory neuron membrane potential MUST decay WITH time constant of 0.02 seconds"
    )
    assert result["concept"] == "membrane_potential_decay"
    assert result["subject"] == "sensory neuron membrane potential"


# === Concept 3: Membrane Potential Decay — Invalid Examples ===


def test_decay_invalid_vague():
    with pytest.raises(ParseError):
        parse("The membrane potential decays slowly")


def test_decay_invalid_imperative():
    with pytest.raises(ParseError):
        parse("Set tau to 20ms")


# === Concept 4: Synaptic Weight — Valid Examples ===


def test_synaptic_weight_have():
    result = parse(
        "The connection from sensory neuron to motor neuron MUST have "
        "WITH synaptic weight of 1.0"
    )
    assert result["concept"] == "synaptic_weight"
    assert result["subject"] == "sensory neuron to motor neuron"
    assert result["action"] == "have"
    assert result["condition"] == "synaptic weight of 1.0"


def test_synaptic_weight_transmit():
    result = parse(
        "The connection from sensory neuron to motor neuron MUST transmit "
        "WITH synaptic weight of 0.5"
    )
    assert result["concept"] == "synaptic_weight"
    assert result["action"] == "transmit"
    assert result["condition"] == "synaptic weight of 0.5"


def test_synaptic_weight_must_not():
    result = parse(
        "The connection from sensory neuron to motor neuron MUST NOT have "
        "WITH synaptic weight of 0.0"
    )
    assert result["concept"] == "synaptic_weight"
    assert result["condition"] == "synaptic weight of 0.0"


def test_synaptic_weight_scale():
    result = parse(
        "The connection from sensory neuron to motor neuron MUST scale input "
        "WITH synaptic weight of 2.0"
    )
    assert result["concept"] == "synaptic_weight"
    assert result["action"] == "scale input"
    assert result["condition"] == "synaptic weight of 2.0"


# === Concept 4: Synaptic Weight — Invalid Examples ===


def test_synaptic_weight_invalid_vague():
    with pytest.raises(ParseError):
        parse("Connect neuron A to neuron B strongly")


def test_synaptic_weight_invalid_assignment():
    with pytest.raises(ParseError):
        parse("Weight = 1.5")


# === General edge cases ===


def test_empty_sentence():
    with pytest.raises(ParseError):
        parse("")


def test_whitespace_only():
    with pytest.raises(ParseError):
        parse("   ")


def test_raw_field_preserved():
    sentence = "The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0"
    result = parse(sentence)
    assert result["raw"] == sentence


def test_all_results_have_required_keys():
    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",
    ]
    expected_keys = {"concept", "subject", "condition", "action", "verb", "negated", "raw"}
    for s in sentences:
        result = parse(s)
        assert set(result.keys()) == expected_keys, f"Key mismatch in: {s}"


def test_verb_polarity_must():
    result = parse("The sensory neuron MUST fire ONLY IF membrane potential exceeds 1.0")
    assert result["verb"] == "MUST"
    assert result["negated"] is False


def test_verb_polarity_must_not():
    result = parse(
        "The sensory neuron MUST NOT fire DURING the refractory period of 0.002 seconds"
    )
    assert result["verb"] == "MUST NOT"
    assert result["negated"] is True
