import copy
import json
import subprocess
import sys
from pathlib import Path

import pytest

import launch_controller_v4 as controller
from launch_controller_v4 import BOUNDARIES, Blocked, BoundaryModel


def valid_record():
    modules = {
        name: {
            "origin": str(controller.SITE_PACKAGES / member),
            "loaded_sha256": "a" * 64,
            "wheel_sha256": "a" * 64,
        }
        for name, member in controller.MODULE_MEMBERS.items()
    }
    return {
        "executable": str(controller.PYTHON),
        "prefix": str(controller.HERMES / ".venv"),
        "telegram_origin": str(controller.SITE_PACKAGES / "telegram/__init__.py"),
        "execstart_python": str(controller.PYTHON),
        "service_virtual_env": str(controller.HERMES / ".venv"),
        "working_directory": str(controller.PROFILE),
        "modules": modules,
        "hermes_wheel_sha256": controller.HERMES_SHA,
        "profile_wheel_sha256": controller.PROFILE_SHA,
        "v3_controller_seal_sha256": controller.V3_CONTROLLER_SEAL_SHA,
        "v3_clean_run_seal_sha256": controller.V3_CLEAN_RUN_SEAL_SHA,
    }


def test_happy_order_exactly_one():
    model = BoundaryModel()
    for boundary in BOUNDARIES:
        model.cross(boundary)
    assert model.sent == 1 and model.attempted


def test_every_order_violation_fails():
    for index in range(len(BOUNDARIES) - 1):
        model = BoundaryModel()
        with pytest.raises(Blocked):
            model.cross(BOUNDARIES[index + 1])


@pytest.mark.parametrize("where", range(len(BOUNDARIES)))
@pytest.mark.parametrize("side", ("before", "after"))
def test_crash_each_boundary_never_duplicates(where, side):
    model = BoundaryModel()
    for boundary in BOUNDARIES[:where]:
        model.cross(boundary)
    with pytest.raises(RuntimeError):
        model.cross(BOUNDARIES[where], **{f"crash_{side}": True})
    assert model.sent <= 1


def test_duplicate_invocation_and_unknown_outcome_are_fail_closed():
    model = BoundaryModel()
    for boundary in BOUNDARIES[:-2]:
        model.cross(boundary)
    model.cross("invite_intent")
    with pytest.raises(Blocked):
        model.cross("invite_intent")
    with pytest.raises(Blocked):
        model.cross("invite_result", unknown=True)
    assert model.sent == 0


def test_valid_interpreter_record_passes():
    controller.validate_interpreter_record(valid_record())


@pytest.mark.parametrize(
    ("mutation", "message"),
    [
        (lambda row: row.update(executable="/usr/bin/python3"), "wrong interpreter"),
        (lambda row: row.update(telegram_origin=None), "telegram missing"),
        (lambda row: row["modules"]["checkin_cli.customer_admin"].update(origin="/tmp/drift.py"), "import path drift"),
        (lambda row: row["modules"]["gateway.platforms.telegram"].update(loaded_sha256="b" * 64), "import byte drift"),
    ],
)
def test_interpreter_failures_are_pre_mutation(monkeypatch, tmp_path, mutation, message):
    record = copy.deepcopy(valid_record())
    mutation(record)
    counters = {"prepare": 0, "start": 0}
    monkeypatch.setattr(controller, "prepare", lambda *_: counters.__setitem__("prepare", counters["prepare"] + 1))
    monkeypatch.setattr(controller, "start_runtime", lambda *_: counters.__setitem__("start", counters["start"] + 1))
    with pytest.raises(Blocked, match=message):
        controller.validate_interpreter_record(record)
    assert counters == {"prepare": 0, "start": 0}
    assert not (tmp_path / "customers").exists()


def test_missing_telegram_is_pre_mutation(monkeypatch):
    real_import = controller.importlib.import_module
    counters = {"prepare": 0, "start": 0}
    monkeypatch.setattr(controller.sys, "executable", str(controller.PYTHON))
    monkeypatch.setattr(controller.importlib, "import_module", lambda name: (_ for _ in ()).throw(ImportError(name)) if name == "telegram" else real_import(name))
    monkeypatch.setattr(controller, "prepare", lambda *_: counters.__setitem__("prepare", 1))
    monkeypatch.setattr(controller, "start_runtime", lambda *_: counters.__setitem__("start", 1))
    with pytest.raises(Blocked, match="telegram unavailable"):
        controller.interpreter_proof()
    assert counters == {"prepare": 0, "start": 0}


def test_usr_bin_python_dry_run_rejected_without_root(tmp_path):
    root = tmp_path / "must-not-exist"
    command = [
        "/usr/bin/python3",
        str(Path(controller.__file__).resolve()),
        "dry-run",
        "--root",
        str(root),
        "--permission",
        str(tmp_path / "not-read.json"),
    ]
    result = subprocess.run(command, text=True, capture_output=True, check=False)
    assert result.returncode == 2
    assert "wrong interpreter: /usr/bin/python3" in result.stderr
    assert not root.exists()
