from __future__ import annotations

from collections.abc import Mapping
from typing import Any

import pytest

from cmvr_edge_ai.compiler import PipelineCompileError, compile_pipeline
from cmvr_edge_ai.config import load_config_data
from cmvr_edge_ai.core import Envelope, Operator, Sink, Source
from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec


class EmptySource(Source):
    async def messages(self):  # type: ignore[no-untyped-def]
        if False:
            yield Envelope("unused")


class IdentityOperator(Operator):
    async def process(
        self, envelope: Envelope[Any], input_port: str = "input"
    ) -> Envelope[Any]:
        del input_port
        return envelope


class NullSink(Sink):
    async def consume(self, envelope: Envelope[Any], input_port: str = "input") -> None:
        del envelope, input_port


def _source_factory(node_id: str, params: Mapping[str, Any]) -> EmptySource:
    del node_id, params
    return EmptySource()


def _operator_factory(node_id: str, params: Mapping[str, Any]) -> IdentityOperator:
    del node_id, params
    return IdentityOperator()


def _sink_factory(node_id: str, params: Mapping[str, Any]) -> NullSink:
    del node_id, params
    return NullSink()


def _registry() -> PluginRegistry:
    registry = PluginRegistry()
    registry.register(
        PluginSpec(
            "test.text_source@1",
            PluginKind.SOURCE,
            _source_factory,
            outputs={"events": "TextEvent/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.text_operator@1",
            PluginKind.OPERATOR,
            _operator_factory,
            inputs={"input": "TextEvent/v1"},
            outputs={"events": "TextEvent/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.audio_sink@1",
            PluginKind.SINK,
            _sink_factory,
            inputs={"input": "AudioChunk/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.any_sink@1",
            PluginKind.SINK,
            _sink_factory,
            inputs={"input": "*"},
        )
    )
    registry.register(
        PluginSpec(
            "test.command_source@1",
            PluginKind.SOURCE,
            _source_factory,
            outputs={"commands": "RobotCommand/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.approved_source@1",
            PluginKind.SOURCE,
            _source_factory,
            outputs={"commands": "ApprovedRobotCommand/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.safety_gate@1",
            PluginKind.OPERATOR,
            _operator_factory,
            inputs={"input": "RobotCommand/v1"},
            outputs={"commands": "ApprovedRobotCommand/v1"},
            tags=frozenset({"safety_gate"}),
        )
    )
    registry.register(
        PluginSpec(
            "test.approved_operator@1",
            PluginKind.OPERATOR,
            _operator_factory,
            inputs={"input": "ApprovedRobotCommand/v1"},
            outputs={"commands": "ApprovedRobotCommand/v1"},
        )
    )
    registry.register(
        PluginSpec(
            "test.actuator@1",
            PluginKind.SINK,
            _sink_factory,
            inputs={"commands": "ApprovedRobotCommand/v1"},
            tags=frozenset({"actuator"}),
        )
    )
    return registry


def _config(nodes: dict[str, dict[str, Any]], edges: list[dict[str, Any]]):
    return load_config_data(
        {
            "api_version": "cmvr.edge.ai/v1",
            "pipelines": {
                "test": {
                    "nodes": nodes,
                    "edges": edges,
                }
            },
        }
    )


@pytest.mark.parametrize(
    ("edge", "message"),
    [
        (
            {"from": "source.missing", "to": "sink.input"},
            "unknown output port",
        ),
        (
            {"from": "source.events", "to": "sink.missing"},
            "unknown input port",
        ),
    ],
)
def test_compile_rejects_unknown_plugin_ports(
    edge: dict[str, str], message: str
) -> None:
    config = _config(
        {
            "source": {"uses": "test.text_source@1"},
            "sink": {"uses": "test.any_sink@1"},
        },
        [edge],
    )

    with pytest.raises(PipelineCompileError, match=message):
        compile_pipeline(config, "test", _registry())


def test_compile_rejects_incompatible_schemas() -> None:
    config = _config(
        {
            "source": {"uses": "test.text_source@1"},
            "sink": {"uses": "test.audio_sink@1"},
        },
        [{"from": "source.events", "to": "sink.input"}],
    )

    with pytest.raises(PipelineCompileError, match="schema mismatch"):
        compile_pipeline(config, "test", _registry())


def test_compile_accepts_wildcard_schema() -> None:
    config = _config(
        {
            "source": {"uses": "test.text_source@1"},
            "sink": {"uses": "test.any_sink@1"},
        },
        [{"from": "source.events", "to": "sink.input"}],
    )

    compiled = compile_pipeline(config, "test", _registry())

    assert compiled.pipeline_id == "test"
    assert compiled.runtime.edges[0].source_port == "events"


def test_video_contiguous_qos_requires_lossless_overflow() -> None:
    nodes = {
        "source": {"uses": "test.text_source@1"},
        "operator": {"uses": "test.text_operator@1"},
    }
    accepted = _config(
        nodes,
        [
            {
                "from": "source.events",
                "to": "operator.input",
                "qos": {
                    "profile": "video_contiguous",
                    "capacity": 8,
                    "overflow": "block",
                },
            }
        ],
    )

    compiled = compile_pipeline(accepted, "test", _registry())
    assert compiled.runtime.edges[0].capacity == 8

    rejected = _config(
        nodes,
        [
            {
                "from": "source.events",
                "to": "operator.input",
                "qos": {
                    "profile": "video_contiguous",
                    "capacity": 2,
                    "overflow": "drop_oldest",
                },
            }
        ],
    )
    with pytest.raises(PipelineCompileError, match="does not allow"):
        compile_pipeline(rejected, "test", _registry())


def test_compile_rejects_direct_path_into_actuator() -> None:
    config = _config(
        {
            "source": {"uses": "test.approved_source@1"},
            "actuator": {"uses": "test.actuator@1"},
        },
        [{"from": "source.commands", "to": "actuator.commands"}],
    )

    with pytest.raises(PipelineCompileError, match="must pass through a safety gate"):
        compile_pipeline(config, "test", _registry())


def test_compile_accepts_actuator_only_after_safety_gate() -> None:
    config = _config(
        {
            "source": {"uses": "test.command_source@1"},
            "gate": {"uses": "test.safety_gate@1"},
            "actuator": {"uses": "test.actuator@1"},
        },
        [
            {"from": "source.commands", "to": "gate.input"},
            {"from": "gate.commands", "to": "actuator.commands"},
        ],
    )

    compiled = compile_pipeline(config, "test", _registry())

    assert set(compiled.plugin_specs) == {"source", "gate", "actuator"}


def test_compile_rejects_mixed_safe_and_unsafe_actuator_paths() -> None:
    config = _config(
        {
            "source": {"uses": "test.command_source@1"},
            "bypass": {"uses": "test.approved_source@1"},
            "gate": {"uses": "test.safety_gate@1"},
            "actuator": {"uses": "test.actuator@1"},
        },
        [
            {"from": "source.commands", "to": "gate.input"},
            {"from": "gate.commands", "to": "actuator.commands"},
            {"from": "bypass.commands", "to": "actuator.commands"},
        ],
    )

    with pytest.raises(PipelineCompileError, match="unsafe actuator"):
        compile_pipeline(config, "test", _registry())


def test_compile_rejects_operator_between_gate_and_actuator() -> None:
    config = _config(
        {
            "source": {"uses": "test.command_source@1"},
            "gate": {"uses": "test.safety_gate@1"},
            "transform": {"uses": "test.approved_operator@1"},
            "actuator": {"uses": "test.actuator@1"},
        },
        [
            {"from": "source.commands", "to": "gate.input"},
            {"from": "gate.commands", "to": "transform.input"},
            {"from": "transform.commands", "to": "actuator.commands"},
        ],
    )

    with pytest.raises(PipelineCompileError, match="safety gate directly"):
        compile_pipeline(config, "test", _registry())


@pytest.mark.parametrize(
    ("node_options", "message"),
    [
        ({"execution": {"mode": "thread"}}, "execution settings not implemented"),
        ({"resources": {"memory_mb": 256}}, "resource limits"),
    ],
)
def test_compile_rejects_unenforced_node_settings(
    node_options: dict[str, Any], message: str
) -> None:
    config = _config(
        {
            "source": {
                "uses": "test.text_source@1",
                **node_options,
            }
        },
        [],
    )

    with pytest.raises(PipelineCompileError, match=message):
        compile_pipeline(config, "test", _registry())


@pytest.mark.parametrize(
    ("field", "value"),
    [
        ("max_processes", 2),
        ("process_start_method", "forkserver"),
        ("health_bind", "127.0.0.1:8080"),
        ("reserved_memory_mb", 256),
    ],
)
def test_compile_rejects_reserved_runtime_fields(field: str, value: Any) -> None:
    config = load_config_data(
        {
            "api_version": "cmvr.edge.ai/v1",
            "runtime": {field: value},
            "pipelines": {
                "test": {"nodes": {"source": {"uses": "test.text_source@1"}}}
            },
        }
    )

    with pytest.raises(PipelineCompileError, match="reserved but not enforced"):
        compile_pipeline(config, "test", _registry())


def test_compile_rejects_pipeline_priority_without_scheduler() -> None:
    config = load_config_data(
        {
            "api_version": "cmvr.edge.ai/v1",
            "pipelines": {
                "test": {
                    "priority": "high",
                    "nodes": {"source": {"uses": "test.text_source@1"}},
                }
            },
        }
    )

    with pytest.raises(PipelineCompileError, match="no priority scheduler"):
        compile_pipeline(config, "test", _registry())
