"""Tests for orchestrator/target_spec.py — target VM spec loader
(PIPELINE.md §4.2 / §4.13)."""

from __future__ import annotations

from pathlib import Path

import pytest

from orchestrator.target_spec import (
    TargetSpecError, list_target_specs, load_target_spec,
)


REPO_ROOT = Path(__file__).resolve().parent.parent

VALID_SPEC = """
name = "fixture-target"
description = "fixture for spec validation"
base_image = "alpine-3.21-virt"

[promises]
cve = "CVE-2014-6271"
service_name = "apache"
service_port = 80
service_proto = "tcp"
vulnerable_software = "bash"
vulnerable_version = "4.2"

[containment]
upstream_egress = false
shared_filesystem = false
unprivileged_qemu = true
fresh_snapshot_per_episode = true
"""


def _write_spec(repo: Path, name: str, body: str) -> None:
    target = repo / "vm" / "targets" / name
    target.mkdir(parents=True, exist_ok=True)
    (target / "spec.toml").write_text(body)


def test_valid_spec_loads(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC)
    s = load_target_spec(tmp_path, "fixture-target")
    assert s.name == "fixture-target"
    assert s.promises.cve == "CVE-2014-6271"
    assert s.promises.service_port == 80
    assert s.containment.upstream_egress is False
    assert s.containment.shared_filesystem is False
    assert s.containment.unprivileged_qemu is True
    assert s.containment.fresh_snapshot_per_episode is True


def test_missing_spec_raises(tmp_path: Path) -> None:
    with pytest.raises(TargetSpecError, match="not found"):
        load_target_spec(tmp_path, "no-such-target")


def test_name_must_match_directory(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        'name = "fixture-target"', 'name = "different-name"'
    ))
    with pytest.raises(TargetSpecError, match="doesn't match directory"):
        load_target_spec(tmp_path, "fixture-target")


def test_upstream_egress_must_be_false(tmp_path: Path) -> None:
    """§4.13: containment regression rejected at spec load."""
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        "upstream_egress = false", "upstream_egress = true"
    ))
    with pytest.raises(TargetSpecError, match="upstream_egress.*must be false"):
        load_target_spec(tmp_path, "fixture-target")


def test_shared_filesystem_must_be_false(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        "shared_filesystem = false", "shared_filesystem = true"
    ))
    with pytest.raises(TargetSpecError, match="shared_filesystem.*must be false"):
        load_target_spec(tmp_path, "fixture-target")


def test_unprivileged_qemu_must_be_true(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        "unprivileged_qemu = true", "unprivileged_qemu = false"
    ))
    with pytest.raises(TargetSpecError, match="unprivileged_qemu.*must be true"):
        load_target_spec(tmp_path, "fixture-target")


def test_fresh_snapshot_must_be_true(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        "fresh_snapshot_per_episode = true",
        "fresh_snapshot_per_episode = false",
    ))
    with pytest.raises(TargetSpecError,
                       match="fresh_snapshot_per_episode.*must be true"):
        load_target_spec(tmp_path, "fixture-target")


def test_invalid_proto_rejected(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        'service_proto = "tcp"', 'service_proto = "icmp"'
    ))
    with pytest.raises(TargetSpecError, match="service_proto"):
        load_target_spec(tmp_path, "fixture-target")


def test_out_of_range_port_rejected(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-target", VALID_SPEC.replace(
        "service_port = 80", "service_port = 99999"
    ))
    with pytest.raises(TargetSpecError, match="service_port out of range"):
        load_target_spec(tmp_path, "fixture-target")


def test_missing_required_field_rejected(tmp_path: Path) -> None:
    body = VALID_SPEC.replace('cve = "CVE-2014-6271"\n', "")
    _write_spec(tmp_path, "fixture-target", body)
    with pytest.raises(TargetSpecError, match="cve"):
        load_target_spec(tmp_path, "fixture-target")


def test_to_meta_round_trips_to_json(tmp_path: Path) -> None:
    import json
    _write_spec(tmp_path, "fixture-target", VALID_SPEC)
    s = load_target_spec(tmp_path, "fixture-target")
    decoded = json.loads(json.dumps(s.to_meta()))
    assert decoded["name"] == "fixture-target"
    assert decoded["containment"]["upstream_egress"] is False


def test_list_target_specs_finds_all_valid(tmp_path: Path) -> None:
    _write_spec(tmp_path, "fixture-a", VALID_SPEC.replace(
        'name = "fixture-target"', 'name = "fixture-a"'
    ))
    _write_spec(tmp_path, "fixture-b", VALID_SPEC.replace(
        'name = "fixture-target"', 'name = "fixture-b"'
    ))
    specs = list_target_specs(tmp_path)
    assert {s.name for s in specs} == {"fixture-a", "fixture-b"}


# ---------------------------------------------------------------------
# Repo-level invariant: every spec in vm/targets/ must validate.
# This is the production tripwire — adding a target with broken
# containment fails this test before any build runs.
# ---------------------------------------------------------------------


def test_every_repo_target_spec_loads_cleanly() -> None:
    """All shipped target specs must validate. A new target that
    weakens §4.13 containment fails this assertion before its
    image even gets built (PIPELINE.md §4.2 / §4.13)."""
    specs = list_target_specs(REPO_ROOT)
    # No assertion on count — empty is fine (shipping no targets is
    # the honest interim state). What matters: every spec that IS
    # there validates.
    for s in specs:
        # Containment posture re-asserted explicitly so a future drift
        # in the validator doesn't silently accept a regression.
        assert s.containment.upstream_egress is False, s.name
        assert s.containment.shared_filesystem is False, s.name
        assert s.containment.unprivileged_qemu is True, s.name
        assert s.containment.fresh_snapshot_per_episode is True, s.name
