from __future__ import annotations

import hashlib
import json
from pathlib import Path

import pytest
import yaml
from typer.testing import CliRunner

from k1link.simulation import (
    CheckStatus,
    DoctorVerdict,
    S0ProfileError,
    load_s0_profile,
    run_s0_doctor,
)
from k1link.simulation.cli import app

PROFILE = Path(__file__).resolve().parents[1] / "simulation" / "s0" / "qualification-profile.yaml"
runner = CliRunner()


def _profile_document() -> dict[str, object]:
    loaded = yaml.safe_load(PROFILE.read_text(encoding="utf-8"))
    assert isinstance(loaded, dict)
    return loaded


def _write_profile(tmp_path: Path, document: dict[str, object]) -> Path:
    path = tmp_path / "profile.yaml"
    path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8")
    return path


def test_canonical_s0_profile_is_strict_and_safe() -> None:
    profile = load_s0_profile(PROFILE)

    assert profile.profile_id == "ai-worker-px4-gazebo-v1"
    assert profile.target_host.wsl_distribution == "MissionCore-Sim"
    assert profile.target_host.container_runtime == "none"
    assert profile.storage.windows_root.drive.upper() == "D:"
    assert profile.storage.wsl_root.as_posix() == "/mnt/d/NDC_MISSIONCORE"
    assert profile.clock.authority == "gazebo:/clock"
    assert profile.clock.ros_use_sim_time is True
    assert profile.clock.px4_uxrce_dds_sync_enabled is False
    assert profile.authority.simulation_or_shadow_only is True
    assert profile.authority.actuator_authority is False
    assert profile.authority.navigation_or_safety_accepted is False
    assert profile.authority.direct_actuator_setpoints_allowed is False
    assert profile.ros.domain_id == 0
    assert profile.runtime_acceptance.world == "rover"
    assert profile.runtime_acceptance.model == "rover_ackermann_0"
    assert profile.runtime_acceptance.physics_step_ns == 2_000_000
    assert profile.runtime_acceptance.pause_max_advance_ns == 0
    assert profile.runtime_acceptance.speed_factors == (1, 2)
    assert profile.runtime_acceptance.rtf_tolerance_percent == 20
    assert "docker" not in profile.required_tools
    assert {item.identifier for item in profile.components} >= {
        "px4-autopilot",
        "px4-msgs",
        "gazebo",
        "nav2",
    }
    components = {item.identifier: item for item in profile.components}
    assert components["px4-gazebo-models"].resolved_commit == (
        "b6127f4ec20de867e215fb5f78ae88b80f371909"
    )
    assert {component.qualification_state for component in components.values()} == {"accepted"}
    micro_xrce = next(item for item in profile.ports if item.identifier == "micro-xrce-dds")
    assert micro_xrce.host_scope == "loopback-only-netns"
    assert micro_xrce.bind == "0.0.0.0"
    assert {item.identifier for item in profile.processes} == {
        "simulation-orchestrator",
        "gazebo-server",
        "px4-sitl",
        "micro-xrce-dds-agent",
        "ros2-bridge",
        "nav2",
    }


def test_profile_rejects_real_actuator_authority(tmp_path: Path) -> None:
    document = _profile_document()
    authority = document["authority"]
    assert isinstance(authority, dict)
    authority["actuator_authority"] = True

    with pytest.raises(S0ProfileError, match="cannot grant real"):
        load_s0_profile(_write_profile(tmp_path, document))


def test_profile_rejects_mutable_c_drive_path(tmp_path: Path) -> None:
    document = _profile_document()
    storage = document["storage"]
    assert isinstance(storage, dict)
    mutable = storage["mutable_paths"]
    assert isinstance(mutable, list)
    first = mutable[0]
    assert isinstance(first, dict)
    first["windows_path"] = r"C:\NDC_MISSIONCORE\wsl"

    with pytest.raises(S0ProfileError, match="escapes D-only root"):
        load_s0_profile(_write_profile(tmp_path, document))


def test_profile_rejects_duplicate_scoped_port(tmp_path: Path) -> None:
    document = _profile_document()
    ports = document["ports"]
    assert isinstance(ports, list)
    duplicate = dict(ports[0])
    duplicate["id"] = "duplicate-port"
    duplicate["owner"] = "simulation-orchestrator"
    ports.append(duplicate)

    with pytest.raises(S0ProfileError, match="duplicates a scoped binding"):
        load_s0_profile(_write_profile(tmp_path, document))


def test_profile_rejects_wildcard_bind_outside_loopback_namespace(
    tmp_path: Path,
) -> None:
    document = _profile_document()
    ports = document["ports"]
    assert isinstance(ports, list)
    first = ports[0]
    assert isinstance(first, dict)
    first["host_scope"] = "worker-wsl"

    with pytest.raises(S0ProfileError, match="wildcard bind requires loopback-only-netns"):
        load_s0_profile(_write_profile(tmp_path, document))


def test_profile_rejects_runtime_speed_contract_drift(tmp_path: Path) -> None:
    document = _profile_document()
    runtime = document["runtime_acceptance"]
    assert isinstance(runtime, dict)
    runtime["speed_factors"] = [1, 4]

    with pytest.raises(S0ProfileError, match="exactly the 1x and 2x"):
        load_s0_profile(_write_profile(tmp_path, document))


def test_doctor_never_claims_go_without_target_evidence(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        "k1link.simulation.s0._target_host_match",
        lambda target: (False, "test host is not target"),
    )

    report = run_s0_doctor(PROFILE)

    assert report.verdict is DoctorVerdict.INCOMPLETE
    assert report.target_host_match is False
    assert any(
        check.identifier == "factual-evidence" and check.status is CheckStatus.UNKNOWN
        for check in report.checks
    )
    payload = report.to_dict()
    assert payload["authority"] == {
        "simulation_or_shadow_only": True,
        "actuator_authority": False,
        "navigation_or_safety_accepted": False,
        "direct_actuator_setpoints_allowed": False,
    }


def test_doctor_validates_digest_bound_evidence(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setattr(
        "k1link.simulation.s0._target_host_match",
        lambda target: (False, "test host is not target"),
    )
    profile = load_s0_profile(PROFILE)
    checks: list[dict[str, object]] = []
    for identifier in profile.required_evidence:
        artifact = tmp_path / f"{identifier}.txt"
        artifact.write_text(f"evidence for {identifier}\n", encoding="utf-8")
        checks.append(
            {
                "id": identifier,
                "status": "pass",
                "artifact": artifact.name,
                "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
                "note": "synthetic unit-test evidence",
            }
        )
    evidence = tmp_path / "evidence.yaml"
    evidence.write_text(
        yaml.safe_dump(
            {
                "schema_version": "missioncore.simulation-s0-evidence/v1",
                "profile_sha256": hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
                "checks": checks,
            },
            sort_keys=False,
        ),
        encoding="utf-8",
    )

    report = run_s0_doctor(PROFILE, evidence_path=evidence)

    assert report.verdict is DoctorVerdict.INCOMPLETE
    assert any(
        check.identifier == "factual-evidence" and check.status is CheckStatus.PASS
        for check in report.checks
    )


def test_doctor_rejects_changed_evidence_artifact(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setattr(
        "k1link.simulation.s0._target_host_match",
        lambda target: (False, "test host is not target"),
    )
    profile = load_s0_profile(PROFILE)
    checks: list[dict[str, object]] = []
    artifacts: list[Path] = []
    for identifier in profile.required_evidence:
        artifact = tmp_path / f"{identifier}.txt"
        artifact.write_text("original\n", encoding="utf-8")
        artifacts.append(artifact)
        checks.append(
            {
                "id": identifier,
                "status": "pass",
                "artifact": artifact.name,
                "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
                "note": "synthetic unit-test evidence",
            }
        )
    evidence = tmp_path / "evidence.yaml"
    evidence.write_text(
        yaml.safe_dump(
            {
                "schema_version": "missioncore.simulation-s0-evidence/v1",
                "profile_sha256": hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
                "checks": checks,
            },
            sort_keys=False,
        ),
        encoding="utf-8",
    )
    artifacts[0].write_text("changed\n", encoding="utf-8")

    report = run_s0_doctor(PROFILE, evidence_path=evidence)

    assert report.verdict is DoctorVerdict.BLOCKED
    assert any(
        check.identifier == "factual-evidence"
        and check.status is CheckStatus.FAIL
        and "digest changed" in check.detail
        for check in report.checks
    )


def test_simulation_doctor_cli_emits_json_without_side_effects() -> None:
    result = runner.invoke(app, ["s0", "doctor", "--profile", str(PROFILE), "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    assert payload["schema_version"] == "missioncore.simulation-s0-doctor/v1"
    assert payload["profile_id"] == "ai-worker-px4-gazebo-v1"
    assert payload["verdict"] in {"incomplete", "blocked"}
    assert payload["authority"]["actuator_authority"] is False
