import pytest
import subprocess
import typer
from unittest.mock import MagicMock, patch
from pathlib import Path
from armorer.installers.dynamic import DynamicInstaller
from armorer.config import ConfigManager
from armorer.registry import AppMetadata
from armorer.registry import AppRegistry
from armorer.profiles import RiskProfile, NetworkPolicy, FilesystemPolicy

@pytest.fixture
def mock_profile():
    return RiskProfile(
        name="TEST_PROFILE",
        description="Test",
        network_policy=NetworkPolicy.OPEN,
        filesystem_policy=FilesystemPolicy.SCOPED
    )


@pytest.fixture(autouse=True)
def isolate_armorer_state(tmp_path, monkeypatch):
    armorer_home = tmp_path / ".armorer"
    monkeypatch.setattr(ConfigManager, "CONFIG_DIR", armorer_home)
    monkeypatch.setattr(ConfigManager, "VAULT_FILE", armorer_home / "vault.json")
    monkeypatch.setattr(AppRegistry, "REGISTRY_FILE", armorer_home / "registry.json")

def test_dynamic_installer_init(mock_profile):
    installer = DynamicInstaller(
        app_name="test-app",
        profile=mock_profile,
        mount_paths=["/tmp"],
        options={"confirm": False}
    )
    assert installer.app_name == "test-app"
    assert "test-app" in str(installer.install_dir)

@patch("armorer.installers.dynamic.subprocess.run")
def test_ensure_docker_running(mock_run, mock_profile):
    installer = DynamicInstaller("test", mock_profile, [])
    mock_run.side_effect = [
        MagicMock(returncode=0),  # docker info
        MagicMock(returncode=0),  # docker compose version
    ]
    installer._ensure_docker_running()
    assert any(call.args[0] == ["docker", "info"] for call in mock_run.call_args_list)
    assert any(call.args[0] == ["docker", "compose", "version"] for call in mock_run.call_args_list)


def test_ensure_docker_running_fails_when_docker_missing(monkeypatch, mock_profile):
    installer = DynamicInstaller("test", mock_profile, [], options={"confirm": False})
    monkeypatch.setattr(installer, "_docker_info_reachable", lambda: False)
    monkeypatch.setattr(installer, "_docker_compose_available", lambda: False)
    monkeypatch.setattr("armorer.installers.dynamic.shutil.which", lambda name: None if name == "docker" else f"/usr/bin/{name}")

    with pytest.raises(typer.Exit):
        installer._ensure_docker_running()


def test_ensure_docker_running_fails_when_compose_missing(monkeypatch, mock_profile):
    installer = DynamicInstaller("test", mock_profile, [], options={"confirm": False})

    monkeypatch.setattr(installer, "_docker_info_reachable", lambda: False)
    monkeypatch.setattr(installer, "_docker_compose_available", lambda: False)
    monkeypatch.setattr("armorer.installers.dynamic.shutil.which", lambda name: "/usr/bin/docker" if name == "docker" else f"/usr/bin/{name}")

    with pytest.raises(typer.Exit):
        installer._ensure_docker_running()


def test_ensure_docker_running_runs_openclaw_preflight(monkeypatch, mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    seen = {"called": False}

    monkeypatch.setattr(installer, "_docker_info_reachable", lambda: True)
    monkeypatch.setattr(installer, "_docker_compose_available", lambda: True)
    monkeypatch.setattr(installer, "_ensure_openclaw_build_prerequisites", lambda: seen.__setitem__("called", True))

    installer._ensure_docker_running()

    assert seen["called"] is True


def test_run_with_live_logs_uses_docker_runner_streaming(monkeypatch, mock_profile, tmp_path):
    from armorer import docker_runner

    installer = DynamicInstaller("test", mock_profile, [], options={"confirm": False, "live_ui": True})

    # Force LiveLogPanel enabled but no-op to keep test quiet.
    class DummyPanel:
        enabled = True

        def __init__(self, *a, **k):
            pass

        def start(self):
            return None

        def stop(self):
            return None

        def set_header(self, *a, **k):
            return None

        def add_line(self, *a, **k):
            return None

    # `DynamicInstaller` is loaded from the dynamically-loaded monolith entrypoint.
    # Patch the `_LiveLogPanel` symbol in that module's namespace.
    import sys

    dyn_mod = sys.modules[DynamicInstaller.__module__]
    monkeypatch.setattr(dyn_mod, "_LiveLogPanel", DummyPanel)

    called = {"hit": False}

    class Proc:
        def __init__(self):
            self.returncode = 0
            self.stdout = iter(["ok\n"])

        def wait(self):
            return 0

    def fake_run_streaming(*args, **kwargs):
        called["hit"] = True
        return Proc()

    monkeypatch.setattr(docker_runner, "run_streaming", fake_run_streaming)

    res = installer._run_with_live_logs(["echo", "hi"], label="x", cwd=tmp_path)
    assert called["hit"] is True
    assert res.returncode == 0


def test_openclaw_preflight_fails_when_docker_memory_too_low(monkeypatch, mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})

    monkeypatch.setattr(installer, "_docker_info_json", lambda: {"OperatingSystem": "Docker Desktop"})
    monkeypatch.setattr(installer, "_docker_runtime_memory_bytes", lambda info: 8 * 1024**3)
    monkeypatch.setattr(installer, "_docker_runtime_free_disk_bytes", lambda info: (50 * 1024**3, Path("/tmp")))

    with pytest.raises(typer.Exit):
        installer._ensure_openclaw_build_prerequisites()


def test_openclaw_preflight_fails_when_free_disk_too_low(monkeypatch, mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})

    monkeypatch.setattr(installer, "_docker_info_json", lambda: {"OperatingSystem": "Docker Desktop"})
    monkeypatch.setattr(installer, "_docker_runtime_memory_bytes", lambda info: 16 * 1024**3)
    monkeypatch.setattr(installer, "_docker_runtime_free_disk_bytes", lambda info: (4 * 1024**3, Path("/tmp")))

    with pytest.raises(typer.Exit):
        installer._ensure_openclaw_build_prerequisites()


def test_openclaw_preflight_passes_with_sufficient_resources(monkeypatch, mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})

    monkeypatch.setattr(installer, "_docker_info_json", lambda: {"OperatingSystem": "Docker Desktop"})
    monkeypatch.setattr(installer, "_docker_runtime_memory_bytes", lambda info: 16 * 1024**3)
    monkeypatch.setattr(installer, "_docker_runtime_free_disk_bytes", lambda info: (20 * 1024**3, Path("/tmp")))

    installer._ensure_openclaw_build_prerequisites()


def test_is_git_url(mock_profile):
    installer = DynamicInstaller("test", mock_profile, [])
    assert installer._is_git_url("https://github.com/user/repo.git") is True
    assert installer._is_git_url("my-app") is False


def test_register_persists_host_path_when_running_in_docker(mock_profile, tmp_path, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer.instance_name = "openclaw"
    installer.install_dir = Path("/app/installed/openclaw")
    installer.registry = MagicMock()

    host_root = tmp_path / "project"
    host_install = host_root / "installed" / "openclaw"

    monkeypatch.setenv("HOST_PROJECT_ROOT", str(host_root))
    monkeypatch.setattr("armorer.installers.dynamic.os.path.exists", lambda p: p == "/.dockerenv")
    monkeypatch.setattr(installer, "_discover_config_structure", lambda: {"env": ".env", "main": None, "monitor_targets": []})

    installer._register(run_commands=[{"label": "Status", "service": "openclaw-cli", "command": "status"}])

    metadata = installer.registry.add_app.call_args.args[0]
    assert isinstance(metadata, AppMetadata)
    assert metadata.install_path == str(host_install)
    assert (host_install / ".armorer_monitor.json").exists()


def test_detect_post_config_sequence_uses_playbook_post_commands(mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})

    steps = installer._detect_post_config_sequence()

    assert any(step["label"] == "Lock down OpenClaw permissions" for step in steps)
    step = next(step for step in steps if step["label"] == "Lock down OpenClaw permissions")
    assert "chmod 700 .openclaw" in step["command"]
    assert "chmod 600 .openclaw/openclaw.json" in step["command"]


def test_docker_compose_env_translates_container_paths_back_to_host(mock_profile, tmp_path, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer.install_dir = Path("/app/installed/openclaw")
    installer._compose_dir = installer.install_dir

    host_root = tmp_path / "project"
    host_compose_dir = host_root / "installed" / "openclaw"
    host_compose_dir.mkdir(parents=True)
    (host_compose_dir / ".env").write_text(
        "OPENCLAW_CONFIG_DIR=./.openclaw\n"
        "OPENCLAW_WORKSPACE_DIR=./workspace\n"
    )
    monkeypatch.setenv("HOST_PROJECT_ROOT", str(host_root))
    monkeypatch.setattr("armorer.installers.dynamic.os.path.exists", lambda p: p == "/.dockerenv")

    env = installer._docker_compose_env()

    assert env["OPENCLAW_CONFIG_DIR"] == str(host_root / "installed" / "openclaw" / ".openclaw")
    assert env["OPENCLAW_WORKSPACE_DIR"] == str(host_root / "installed" / "openclaw" / "workspace")


@patch("armorer.installers.dynamic.subprocess.run")
def test_compose_run_uses_translated_env(mock_run, mock_profile, tmp_path, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer.install_dir = Path("/app/installed/openclaw")
    installer._compose_dir = installer.install_dir

    host_root = tmp_path / "project"
    host_compose_dir = host_root / "installed" / "openclaw"
    host_compose_dir.mkdir(parents=True)
    (host_compose_dir / ".env").write_text("OPENCLAW_WORKSPACE_DIR=./workspace\n")
    monkeypatch.setenv("HOST_PROJECT_ROOT", str(host_root))
    monkeypatch.setattr("armorer.installers.dynamic.os.path.exists", lambda p: p == "/.dockerenv")
    mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")

    installer._compose_run(["config"], capture_output=True, text=True)

    called_env = mock_run.call_args.kwargs["env"]
    assert called_env["OPENCLAW_WORKSPACE_DIR"] == str(host_root / "installed" / "openclaw" / "workspace")


def test_docker_compose_env_preserves_scalar_openclaw_image_from_env(mock_profile, tmp_path):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer.install_dir = tmp_path
    installer._compose_dir = tmp_path
    (tmp_path / ".env").write_text("OPENCLAW_IMAGE=openclaw:local\n")

    env = installer._docker_compose_env()

    assert env["OPENCLAW_IMAGE"] == "openclaw:local"


def test_maybe_configure_env_bootstraps_openclaw_in_host_visible_dir(mock_profile, tmp_path, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer.install_dir = Path("/app/installed/openclaw_2")
    installer._compose_dir = installer.install_dir

    host_root = tmp_path / "project"
    host_compose_dir = host_root / "installed" / "openclaw_2"
    host_compose_dir.mkdir(parents=True)
    (host_compose_dir / "docker-compose.yml").write_text(
        """
services:
  openclaw-gateway:
    command: ["node", "dist/index.js", "gateway"]
    ports:
      - ${OPENCLAW_GATEWAY_PORT:-18789}:18789
"""
    )

    monkeypatch.setenv("HOST_PROJECT_ROOT", str(host_root))
    monkeypatch.setattr("armorer.installers.dynamic.os.path.exists", lambda p: p == "/.dockerenv")
    monkeypatch.setattr("armorer.installers.dynamic.resolve_playbook", lambda name: MagicMock(id="openclaw"))
    bootstrap_calls = []
    validate_calls = []

    def _bootstrap(path):
        bootstrap_calls.append(path)
        return {}

    def _validate(path):
        validate_calls.append(path)
        return []

    monkeypatch.setattr("armorer.installers.dynamic.ensure_openclaw_runtime_bootstrap", _bootstrap)
    monkeypatch.setattr("armorer.installers.dynamic.validate_openclaw_runtime_bootstrap", _validate)

    installer._maybe_configure_env()

    assert bootstrap_calls == [host_compose_dir]
    assert validate_calls == [host_compose_dir]


def test_execute_setup_sequence_reuses_existing_built_image_on_failure(mock_profile, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    monkeypatch.setattr(installer, "_run_with_live_logs", lambda *args, **kwargs: MagicMock(returncode=1, stdout="", stderr=""))
    monkeypatch.setattr(installer, "_docker_build_target_image", lambda cmd, shell=False: "openclaw:local")
    monkeypatch.setattr(installer, "_docker_image_exists", lambda image: image == "openclaw:local")

    installer._execute_setup_sequence(
        [
            {
                "type": "command",
                "label": "Build local image",
                "command": "docker build -t openclaw:local .",
                "cwd": ".",
                "required": True,
            }
        ]
    )


def test_ensure_local_compose_images_raises_when_build_still_missing(mock_profile, tmp_path, monkeypatch):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer._compose_dir = tmp_path
    (tmp_path / "docker-compose.yml").write_text(
        """
services:
  openclaw-gateway:
    image: openclaw:local
    build: .
"""
    )
    monkeypatch.setattr(installer, "_run_with_live_logs", lambda *args, **kwargs: MagicMock(returncode=1, stdout="", stderr=""))
    monkeypatch.setattr(installer, "_docker_image_exists", lambda image: False)
    monkeypatch.setattr(
        "armorer.installers.dynamic.subprocess.run",
        lambda *args, **kwargs: MagicMock(returncode=1, stdout="", stderr=""),
    )

    with pytest.raises(typer.Exit):
        installer._ensure_local_compose_images()


def test_verify_run_commands_skips_execution_probe_for_interactive_shortcuts(mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer._compose_run = MagicMock()

    commands = [
        {
            "label": "Setup (configure)",
            "service": "openclaw-cli",
            "command": "configure",
        }
    ]

    verified = installer._verify_run_commands(commands)

    assert verified == commands
    installer._compose_run.assert_not_called()


def test_verify_run_commands_keeps_command_when_help_probe_times_out(mock_profile):
    installer = DynamicInstaller("openclaw", mock_profile, [], options={"confirm": False})
    installer._compose_run = MagicMock(
        side_effect=[
            MagicMock(returncode=1, stdout="", stderr=""),
            subprocess.TimeoutExpired(cmd=["docker", "compose"], timeout=20),
        ]
    )

    commands = [
        {
            "label": "Status",
            "service": "openclaw-cli",
            "command": "status",
        }
    ]

    verified = installer._verify_run_commands(commands)

    assert verified == commands
    assert installer._compose_run.call_count == 2
