"""R4 (partial) — forwarder containment-guard acceptance check.

Acceptance predicate (instrument-validation gate item 5, the offline-verifiable
half of the roadmap R4 check): a SOR forwarder is isolated-engine-only —
``assert engine != local`` or it refuses to run. This suite pins exactly that:
a plan/prefix for ``local`` (or any non-isolated / unknown engine, or a missing
container) is refused; an isolated engine kind is accepted and yields the
correct isolation exec prefix.

The traffic-moving half of R4 (gate item 1: 3-hop e2e delivery + per-hop pcaps
across the grid) is deliberately NOT covered here — it is HELD for a live
isolated engine + grid. This guard moves no traffic and spawns no engine, so it
is fully validatable offline.
"""

import pytest

from cmd_chat.sor.forwarder import (
    ISOLATED_ENGINES,
    LOCAL_ENGINE,
    ContainmentError,
    ForwarderPlan,
    assert_isolated,
    isolation_prefix,
)


def test_local_engine_is_refused():
    # The core containment invariant: never the host.
    with pytest.raises(ContainmentError):
        assert_isolated(LOCAL_ENGINE)
    with pytest.raises(ContainmentError):
        ForwarderPlan(engine="local", container="c0")
    with pytest.raises(ContainmentError):
        isolation_prefix("local", "c0")


@pytest.mark.parametrize("engine", ["", "host", "podman", "vmware", "LOCAL", "docker "])
def test_unknown_or_non_isolated_engines_refused(engine):
    # Anything not on the manifest allow-list is refused, not assumed safe.
    with pytest.raises(ContainmentError):
        assert_isolated(engine)
    with pytest.raises(ContainmentError):
        ForwarderPlan(engine=engine, container="c0")


@pytest.mark.parametrize("engine", list(ISOLATED_ENGINES))
def test_isolated_engines_are_accepted(engine):
    # An isolated engine passes the gate and builds a plan.
    assert_isolated(engine)  # does not raise
    plan = ForwarderPlan(engine=engine, container="node-1")
    prefix = plan.exec_prefix()
    assert isinstance(prefix, list) and prefix
    # The container name is present as its own argv item (no shell interpolation).
    assert "node-1" in prefix


def test_exec_prefix_shapes_match_the_isolation_convention():
    assert isolation_prefix("docker", "n") == ["docker", "exec", "-i", "n"]
    assert isolation_prefix("multipass", "n") == ["multipass", "exec", "n", "--"]


def test_missing_container_is_refused():
    with pytest.raises(ContainmentError):
        ForwarderPlan(engine="docker", container="")
    with pytest.raises(ContainmentError):
        isolation_prefix("docker", "")


def test_no_prefix_path_ever_targets_the_host():
    # Structural check: for every isolated engine, the exec prefix's leading
    # argv is the engine tool itself (docker/multipass), never a bare host shell.
    for engine in ISOLATED_ENGINES:
        prefix = isolation_prefix(engine, "c")
        assert prefix[0] == engine
        assert prefix != []  # a host-shell prefix (empty) is never produced
