from __future__ import annotations

import pytest

from icsd.core.context import AgentExecutionContext, AgentPolicyEnforcement
from icsd.core.errors import InvariantViolationError
from icsd.core.evidence import AuthoritySource, EvidenceBundle
from icsd.core.invariants import Invariant, InvariantProfile, InvariantProfileRegistry, InvariantSet
from icsd.core.transition import Transition

ROBOT_STAGES = {"armed", "active", "actuating", "emergency_stop"}


def _robotics_invariants() -> InvariantSet:
    return InvariantSet(
        [
            Invariant(
                name="stage_known",
                check=lambda state: state.get("stage") in ROBOT_STAGES,
            ),
            Invariant(
                name="sensor_quorum_count_valid",
                check=lambda state: (
                    isinstance(state.get("sensor_quorum_count"), int)
                    and state["sensor_quorum_count"] >= 0
                ),
            ),
            Invariant(
                name="min_sensor_quorum_valid",
                check=lambda state: (
                    isinstance(state.get("min_sensor_quorum"), int)
                    and state["min_sensor_quorum"] >= 1
                ),
            ),
            Invariant(
                name="actuation_requires_sensor_quorum",
                check=lambda state: (
                    state.get("stage") != "actuating"
                    or state.get("sensor_quorum_count", 0) >= state.get("min_sensor_quorum", 1)
                ),
            ),
            Invariant(
                name="actuating_requires_actor",
                check=lambda state: (
                    state.get("stage") != "actuating"
                    or bool(str(state.get("actuated_by", "")).strip())
                ),
            ),
            Invariant(
                name="actuator_matches_stage",
                check=lambda state: (
                    (state.get("stage") == "actuating" and state.get("actuator_enabled") is True)
                    or (
                        state.get("stage") != "actuating" and state.get("actuator_enabled") is False
                    )
                ),
            ),
            Invariant(
                name="emergency_stop_is_safe",
                check=lambda state: (
                    state.get("stage") != "emergency_stop"
                    or (
                        state.get("e_stop_engaged") is True
                        and state.get("actuator_enabled") is False
                        and bool(str(state.get("emergency_by", "")).strip())
                        and bool(str(state.get("emergency_reason", "")).strip())
                    )
                ),
            ),
        ]
    )


def _profile_registry() -> InvariantProfileRegistry:
    return InvariantProfileRegistry(
        [
            InvariantProfile(
                name="robotics-leash",
                policy="policy/robotics-safety-leash",
                policy_version="2026.03",
                invariants=_robotics_invariants(),
            )
        ]
    )


def _agent_context(transition: Transition) -> AgentExecutionContext:
    return AgentExecutionContext(
        transition,
        enforcement=AgentPolicyEnforcement(
            require_profile=True,
            require_policy_version=True,
            minimum_authority_sources=1,
        ),
    )


def _source(
    *,
    source_type: str,
    source_id: str,
    reference: str,
    actor_ref: str,
    location: str,
    sensor_quorum_count: int | None = None,
) -> AuthoritySource:
    details: dict[str, object] = {
        "actor_ref": actor_ref,
        "location": location,
    }
    if sensor_quorum_count is not None:
        details["sensor_quorum_count"] = sensor_quorum_count
    return AuthoritySource(
        source_type=source_type,
        source_id=source_id,
        reference=reference,
        details=details,
    )


def _base_robot_state() -> dict[str, object]:
    return _robotics_invariants().construct_state(
        {
            "robot_id": "ROBOT-CONF-001",
            "stage": "armed",
            "sensor_quorum_count": 0,
            "min_sensor_quorum": 3,
            "actuator_enabled": False,
            "e_stop_engaged": False,
            "activated_by": "",
            "actuated_by": "",
            "emergency_by": "",
            "emergency_reason": "",
        }
    )


def _mutate_active(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "active",
            "activated_by": "operator:console-1",
            "sensor_quorum_count": 1,
            "actuator_enabled": False,
            "e_stop_engaged": False,
            "actuated_by": "",
            "emergency_by": "",
            "emergency_reason": "",
        }
    )


def _mutate_unsafe_actuation(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "actuating",
            "actuated_by": "agent:auto-nav",
            "sensor_quorum_count": 1,
            "actuator_enabled": True,
            "e_stop_engaged": False,
        }
    )


def _mutate_safe_actuation(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "actuating",
            "actuated_by": "agent:auto-nav",
            "sensor_quorum_count": 3,
            "actuator_enabled": True,
            "e_stop_engaged": False,
        }
    )


def _mutate_emergency_stop(data: dict[str, object]) -> None:
    data.update(
        {
            "stage": "emergency_stop",
            "sensor_quorum_count": 0,
            "actuator_enabled": False,
            "e_stop_engaged": True,
            "emergency_by": "operator:safety-override",
            "emergency_reason": "Unsafe motion envelope predicted; hard stop engaged.",
        }
    )


def _policy_provenance(evidence: EvidenceBundle) -> dict[str, object]:
    entry = next(item for item in evidence.provenance if item.source.startswith("policy-profile/"))
    return entry.as_dict()


def test_robotics_conformance_blocks_unsafe_actuation_and_allows_emergency_stop() -> None:
    profile = "robotics-leash"
    policy_version = "2026.03"

    active_transition = Transition(
        name="activate_robot",
        mutate=_mutate_active,
        profile_registry=_profile_registry(),
    )
    unsafe_transition = Transition(
        name="actuating",
        mutate=_mutate_unsafe_actuation,
        profile_registry=_profile_registry(),
    )
    safe_transition = Transition(
        name="actuating",
        mutate=_mutate_safe_actuation,
        profile_registry=_profile_registry(),
    )
    emergency_transition = Transition(
        name="emergency_stop",
        mutate=_mutate_emergency_stop,
        profile_registry=_profile_registry(),
    )

    active_result = _agent_context(active_transition).apply(
        state=_base_robot_state(),
        entity_type="robotics_unit",
        entity_id="ROBOT-CONF-001",
        actor="operator:console-1",
        profile=profile,
        policy_version=policy_version,
        authority_sources=[
            _source(
                source_type="control-console",
                source_id="safety-console-a",
                reference="ops/ROBOT-CONF-001/activate",
                actor_ref="operator:console-1",
                location="plant-zone-7",
                sensor_quorum_count=1,
            )
        ],
        timestamp="2026-03-11T08:00:00Z",
    )

    with pytest.raises(InvariantViolationError) as exc_info:
        _agent_context(unsafe_transition).apply(
            state=active_result.after_state,
            entity_type="robotics_unit",
            entity_id="ROBOT-CONF-001",
            actor="agent:auto-nav",
            profile=profile,
            policy_version=policy_version,
            authority_sources=[
                _source(
                    source_type="sensor-fusion",
                    source_id="quorum-monitor",
                    reference="ops/ROBOT-CONF-001/actuate-attempt-1",
                    actor_ref="agent:auto-nav",
                    location="plant-zone-7",
                    sensor_quorum_count=1,
                )
            ],
            timestamp="2026-03-11T08:01:00Z",
        )
    assert any(
        failure.invariant == "actuation_requires_sensor_quorum"
        for failure in exc_info.value.failures
    )

    safe_result = _agent_context(safe_transition).apply(
        state=active_result.after_state,
        entity_type="robotics_unit",
        entity_id="ROBOT-CONF-001",
        actor="agent:auto-nav",
        profile=profile,
        policy_version=policy_version,
        authority_sources=[
            _source(
                source_type="sensor-fusion",
                source_id="quorum-monitor",
                reference="ops/ROBOT-CONF-001/actuate-1",
                actor_ref="agent:auto-nav",
                location="plant-zone-7",
                sensor_quorum_count=3,
            )
        ],
        timestamp="2026-03-11T08:02:00Z",
    )

    emergency_result = _agent_context(emergency_transition).apply(
        state=safe_result.after_state,
        entity_type="robotics_unit",
        entity_id="ROBOT-CONF-001",
        actor="operator:safety-override",
        profile=profile,
        policy_version=policy_version,
        authority_sources=[
            _source(
                source_type="safety-controller",
                source_id="hard-stop-panel-3",
                reference="ops/ROBOT-CONF-001/e-stop",
                actor_ref="operator:safety-override",
                location="plant-zone-7",
            )
        ],
        timestamp="2026-03-11T08:03:00Z",
    )

    assert emergency_result.after_state["stage"] == "emergency_stop"
    assert emergency_result.after_state["e_stop_engaged"] is True
    assert emergency_result.after_state["actuator_enabled"] is False
    assert emergency_result.after_state["sensor_quorum_count"] == 0
    assert active_result.evidence.verify_integrity(
        before_state=active_result.before_state,
        after_state=active_result.after_state,
    )
    assert safe_result.evidence.verify_integrity(
        before_state=safe_result.before_state,
        after_state=safe_result.after_state,
    )
    assert emergency_result.evidence.verify_integrity(
        before_state=emergency_result.before_state,
        after_state=emergency_result.after_state,
    )

    active_policy = _policy_provenance(active_result.evidence)
    emergency_policy = _policy_provenance(emergency_result.evidence)
    assert active_policy["details"]["profile"] == "robotics-leash"
    assert active_policy["details"]["policy_version"] == "2026.03"
    assert emergency_policy["details"]["profile"] == "robotics-leash"
    assert emergency_policy["details"]["policy_version"] == "2026.03"
    assert emergency_result.evidence.authority_sources[0].source_type == "safety-controller"


def test_robotics_conformance_enforces_agent_flags_and_emergency_override() -> None:
    transition = Transition(
        name="emergency_stop",
        mutate=_mutate_emergency_stop,
        profile_registry=_profile_registry(),
    )
    agent_context = _agent_context(transition)

    with pytest.raises(ValueError, match="requires explicit profile"):
        agent_context.apply(
            state=_base_robot_state(),
            entity_type="robotics_unit",
            entity_id="ROBOT-CONF-002",
            actor="operator:safety-override",
            policy_version="2026.03",
            authority_sources=[
                _source(
                    source_type="safety-controller",
                    source_id="hard-stop-panel-3",
                    reference="ops/ROBOT-CONF-002/e-stop",
                    actor_ref="operator:safety-override",
                    location="plant-zone-7",
                )
            ],
        )

    with pytest.raises(ValueError, match="requires explicit policy_version"):
        agent_context.apply(
            state=_base_robot_state(),
            entity_type="robotics_unit",
            entity_id="ROBOT-CONF-002",
            actor="operator:safety-override",
            profile="robotics-leash",
            authority_sources=[
                _source(
                    source_type="safety-controller",
                    source_id="hard-stop-panel-3",
                    reference="ops/ROBOT-CONF-002/e-stop",
                    actor_ref="operator:safety-override",
                    location="plant-zone-7",
                )
            ],
        )

    with pytest.raises(ValueError, match="at least 1 authority source"):
        agent_context.apply(
            state=_base_robot_state(),
            entity_type="robotics_unit",
            entity_id="ROBOT-CONF-002",
            actor="operator:safety-override",
            profile="robotics-leash",
            policy_version="2026.03",
            authority_sources=[],
        )

    emergency_result = agent_context.apply(
        state=_base_robot_state(),
        entity_type="robotics_unit",
        entity_id="ROBOT-CONF-002",
        actor="operator:safety-override",
        profile="robotics-leash",
        policy_version="2026.03",
        authority_sources=[
            _source(
                source_type="safety-controller",
                source_id="hard-stop-panel-3",
                reference="ops/ROBOT-CONF-002/e-stop",
                actor_ref="operator:safety-override",
                location="plant-zone-7",
            )
        ],
        timestamp="2026-03-11T09:00:00Z",
    )

    assert emergency_result.after_state["stage"] == "emergency_stop"
    assert emergency_result.after_state["actuator_enabled"] is False
    assert emergency_result.after_state["e_stop_engaged"] is True
