import copy
import json
import subprocess
from pathlib import Path
from types import SimpleNamespace

import pytest
import yaml

import launch_controller_v5 as controller
from launch_controller_v5 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, "v4_controller_seal_sha256": controller.V4_CONTROLLER_SEAL_SHA, "v4_clean_run_seal_sha256": controller.V4_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_and_unknown_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_no_config_token_supported_resolver_succeeds():
    raw = yaml.safe_load((controller.PROFILE / "config.yaml").read_text())
    assert "token" not in raw["platforms"]["telegram"]
    platform = SimpleNamespace(TELEGRAM="telegram")
    config = SimpleNamespace(platforms={"telegram": SimpleNamespace(token="memory-only-secret")})
    assert controller.extract_telegram_credential(config, platform) == "memory-only-secret"


def test_missing_credential_is_pre_mutation(monkeypatch, tmp_path):
    counters = {"prepare": 0, "start": 0}
    monkeypatch.setattr(controller, "prepare", lambda *_: counters.__setitem__("prepare", 1))
    monkeypatch.setattr(controller, "start_runtime", lambda *_: counters.__setitem__("start", 1))
    with pytest.raises(Blocked, match="credential unavailable"):
        controller.extract_telegram_credential(SimpleNamespace(platforms={}), SimpleNamespace(TELEGRAM="telegram"))
    assert counters == {"prepare": 0, "start": 0}
    assert not (tmp_path / "run").exists()


def test_missing_resolver_is_pre_mutation(monkeypatch, tmp_path):
    real_import = controller.importlib.import_module
    counters = {"prepare": 0, "start": 0}
    monkeypatch.setattr(controller, "prepare", lambda *_: counters.__setitem__("prepare", 1))
    monkeypatch.setattr(controller, "start_runtime", lambda *_: counters.__setitem__("start", 1))
    monkeypatch.setattr(controller.importlib, "import_module", lambda name: SimpleNamespace(__file__=__file__) if name == "hermes_cli.env_loader" else real_import(name))
    with pytest.raises(Blocked, match="resolver unavailable"):
        controller.resolver_contract()
    assert counters == {"prepare": 0, "start": 0}
    assert not (tmp_path / "run").exists()


@pytest.mark.parametrize("identity,readiness", [(SimpleNamespace(id=1, username="wrong_bot"), 1), (SimpleNamespace(id=1, username=controller.BOT_USERNAME), 2), (SimpleNamespace(id=None, username=controller.BOT_USERNAME), None)])
def test_wrong_identity_or_admin_readiness_is_pre_mutation(monkeypatch, tmp_path, identity, readiness):
    counters = {"prepare": 0, "start": 0}
    monkeypatch.setattr(controller, "prepare", lambda *_: counters.__setitem__("prepare", 1))
    monkeypatch.setattr(controller, "start_runtime", lambda *_: counters.__setitem__("start", 1))
    with pytest.raises(Blocked, match="wrong Telegram bot identity"):
        controller.validate_network_identity(identity, readiness)
    assert counters == {"prepare": 0, "start": 0}
    assert not (tmp_path / "run").exists()


def test_valid_identity_and_admin_readiness():
    assert controller.validate_network_identity(SimpleNamespace(id=42, username=controller.BOT_USERNAME), 42) == (42, controller.BOT_USERNAME)


def test_secret_never_appears_in_receipts_or_logs(tmp_path):
    secret = "123456:strict-secret-value"
    (tmp_path / "receipt.json").write_text(json.dumps({"credential_resolved": True, "credential_stored": False}))
    (tmp_path / "journal.txt").write_text("Telegram preflight passed\n")
    controller.assert_secret_absent(tmp_path, secret)
    (tmp_path / "bad.log").write_text("leak=" + secret)
    with pytest.raises(Blocked, match="credential leaked"):
        controller.assert_secret_absent(tmp_path, secret)


def test_interpreter_drift_rejected():
    row = copy.deepcopy(valid_record())
    row["executable"] = "/usr/bin/python3"
    with pytest.raises(Blocked, match="wrong interpreter"):
        controller.validate_interpreter_record(row)


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