from unittest.mock import MagicMock, patch, ANY
from pathlib import Path
import subprocess
import pytest
from armorer.installers.dynamic import DynamicInstaller
from armorer.profiles import RiskProfile, NetworkPolicy, FilesystemPolicy
from armorer.registry import AppRegistry, AppMetadata
from armorer.installers.schemas import DeploymentPlan, ExecutionResult
from armorer.agent_sdk import Status
from armorer.catalog import resolve_playbook

from armorer.installers.schemas import DeploymentPlan, ExecutionResult, AppStructure

@patch("armorer.knowledge.KnowledgeBase")
@patch("armorer.installers.dynamic.SearchTool")
@patch("armorer.installers.dynamic.ArmorerAgent")
@patch("armorer.installers.graph.BaseAgent")
@patch("subprocess.run")
def test_dynamic_install_flow(mock_subprocess, mock_graph_agent, mock_dynamic_agent, mock_search_class, mock_kb_class, tmp_path, monkeypatch):
    mock_kb = MagicMock()
    mock_kb.get.return_value = ""
    mock_kb_class.return_value = mock_kb
    # Setup Mocks
    mock_search = MagicMock()
    mock_search.search.return_value = "Run docker run redis"
    mock_search_class.return_value = mock_search

    mock_agent_instance = MagicMock()
    
    # DeploymentPlan for the planner
    plan = DeploymentPlan(
        docker_compose="version: '3'\nservices:\n  redis:\n    image: redis:alpine\n",
        intent_checklist=["Setup redis"]
    )
    
    # AppStructure for discovery
    app_struct = AppStructure(
        env=".env",
        main=None,
        monitor_targets=[]
    )
    
    # ExecutionResult for the triage/stabilizer
    exec_res = ExecutionResult(success=True, message="Installed successfully")
    
    # Mock execute calls for the graph nodes
    mock_agent_instance.execute.side_effect = [
        "Repository map", # 1. repo_mapper
        plan,             # 2. compose_planner
        plan,             # 3. hardening_reviewer
        exec_res,         # 5. triage (first attempt success)
    ]
    
    # Mock run calls for DynamicInstaller methods (like discovery)
    mock_agent_instance.run.side_effect = [
        app_struct,       # 4. discovery (inside _register)
    ]
    
    # Both patches should return the same mock instance
    mock_graph_agent.return_value = mock_agent_instance
    mock_dynamic_agent.return_value = mock_agent_instance




    # Mock subprocess.run to return successful for docker commands
    def sub_run_side_effect(cmd, **kwargs):
        m = MagicMock()
        m.returncode = 0
        m.stdout = ""
        m.stderr = ""
        # Simulate successful config and pull
        return m
    mock_subprocess.side_effect = sub_run_side_effect

    # Setup Installer
    profile = RiskProfile("TEST", "Test Profile", NetworkPolicy.OPEN, FilesystemPolicy.NONE, False)
    monkeypatch.setattr(DynamicInstaller, "_start_and_stabilize", lambda self, max_attempts=10: None)
    
    # Use a temporary registry location
    with patch("armorer.registry.AppRegistry.REGISTRY_FILE", tmp_path / "registry.json"):
        installer = DynamicInstaller("test-redis", profile, [], options={"confirm": False})
        
        # Override install_dir to tmp_path
        installer.install_dir = tmp_path / "test-redis"
        installer.install_dir.mkdir(parents=True, exist_ok=True) # Ensure it exists for markers
        
        # Execute
        installer.install()

        # Verify Armorer was utilized
        assert mock_agent_instance.execute.called
        
        # Verify File Creation
        assert (tmp_path / "test-redis" / "docker-compose.yml").exists()
        
        # Verify Subprocess Execution
        mock_subprocess.assert_called()
        
        # Verify Registry
        import json
        with open(tmp_path / "registry.json", "r") as f:
            data = json.load(f)
            assert "test-redis" in data


def _make_installer_for_env(tmp_path):
    with patch("armorer.knowledge.KnowledgeBase"), \
         patch("armorer.installers.dynamic.SearchTool"), \
         patch("armorer.installers.dynamic.ArmorerAgent"), \
         patch("armorer.registry.AppRegistry.REGISTRY_FILE", tmp_path / "registry.json"):
        profile = RiskProfile("TEST", "Test Profile", NetworkPolicy.OPEN, FilesystemPolicy.NONE, False)
        installer = DynamicInstaller("test-app", profile, [], options={"confirm": False})
        installer.install_dir = tmp_path
        installer._compose_dir = tmp_path
        return installer


def test_register_tolerates_monitor_sidecar_write_failure(tmp_path, monkeypatch):
    installer = _make_installer_for_env(tmp_path)
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True, exist_ok=True)
    installer.app_name = "openclaw"
    installer.instance_name = "openclaw"
    installer.install_dir = install_dir
    installer._compose_dir = install_dir
    monkeypatch.setattr(
        installer,
        "_discover_config_structure",
        lambda: {"env": ".env", "main": None, "monitor_targets": ["sessions/**/*.json"], "monitoring_mode": "hybrid"},
    )

    original_write_text = Path.write_text

    def _write_text(self, data, *args, **kwargs):
        if self.name == ".armorer_monitor.json":
            raise PermissionError("read-only monitor file")
        return original_write_text(self, data, *args, **kwargs)

    monkeypatch.setattr(Path, "write_text", _write_text, raising=True)

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

    app_meta = installer.registry.get_app("openclaw")
    assert app_meta is not None
    assert app_meta.install_path == str(install_dir)
    assert not (install_dir / ".armorer_monitor.json").exists()


def test_register_openclaw_tolerates_registry_save_failure(tmp_path, monkeypatch):
    installer = _make_installer_for_env(tmp_path)
    install_dir = tmp_path / "installed" / "openclaw"
    install_dir.mkdir(parents=True, exist_ok=True)
    installer.app_name = "openclaw"
    installer.instance_name = "openclaw"
    installer.install_dir = install_dir
    installer._compose_dir = install_dir
    installer._active_playbook = MagicMock(id="openclaw")
    monkeypatch.setattr(
        installer,
        "_discover_config_structure",
        lambda: {"env": ".env", "main": None, "monitor_targets": ["sessions/**/*.json"], "monitoring_mode": "hybrid"},
    )
    monkeypatch.setattr(
        installer.registry,
        "add_app",
        lambda app: (_ for _ in ()).throw(PermissionError("read-only registry")),
    )

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

    assert installer.registry.get_app("openclaw") is None
    assert (install_dir / ".armorer_monitor.json").exists()


def test_compose_runtime_env_defaults_openclaw_image_to_remote(tmp_path):
    installer = _make_installer_for_env(tmp_path)
    installer.app_name = "openclaw"
    installer._compose_dir = tmp_path

    with patch("armorer.openclaw_config._openclaw_local_image_exists", return_value=False):
        env = installer._compose_runtime_env()

    assert env["OPENCLAW_IMAGE"] == "ghcr.io/openclaw/openclaw:main"


def test_ensure_local_compose_images_uses_runtime_env_image_override(tmp_path, monkeypatch):
    installer = _make_installer_for_env(tmp_path)
    installer.app_name = "openclaw"
    installer.instance_name = "openclaw"
    (tmp_path / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-gateway:\n"
        "    image: ${OPENCLAW_IMAGE:-openclaw:local}\n"
        "    build: .\n"
    )

    inspected_images: list[str] = []

    def _fake_run(cmd, **kwargs):
        if isinstance(cmd, list) and cmd[:3] == ["docker", "image", "inspect"]:
            inspected_images.append(str(cmd[2]))
        return MagicMock(returncode=0, stdout="", stderr="")

    monkeypatch.setattr("armorer.installers.dynamic.subprocess.run", _fake_run)

    installer._ensure_local_compose_images({"OPENCLAW_IMAGE": "ghcr.io/openclaw/openclaw:main"})

    assert inspected_images == []


def test_openclaw_playbook_defaults_new_env_to_ghcr_main(tmp_path):
    playbook = resolve_playbook("openclaw")
    assert playbook is not None

    default_step = next(
        cmd for cmd in (playbook.commands or [])
        if cmd.label == "Set secure OpenClaw defaults in .env"
    )
    (tmp_path / ".env.example").write_text("# example\n")

    result = subprocess.run(
        default_step.command,
        cwd=tmp_path,
        shell=True,
        capture_output=True,
        text=True,
    )

    assert result.returncode == 0
    env_text = (tmp_path / ".env").read_text()
    assert "OPENCLAW_IMAGE=ghcr.io/openclaw/openclaw:main" in env_text
    assert "OPENCLAW_IMAGE=openclaw:local" not in env_text


@patch("armorer.docker_runner.compose_run")
def test_openclaw_security_gate_uses_resolved_runtime_env(mock_compose_run, tmp_path, monkeypatch):
    installer = _make_installer_for_env(tmp_path)
    installer.app_name = "openclaw"
    installer.instance_name = "openclaw"
    installer._compose_dir = tmp_path
    (tmp_path / "docker-compose.yml").write_text(
        "services:\n"
        "  openclaw-cli:\n"
        "    image: ${OPENCLAW_IMAGE:-openclaw:local}\n"
    )
    (tmp_path / ".env").write_text("OPENCLAW_IMAGE=ghcr.io/openclaw/openclaw:main\n")
    mock_compose_run.return_value = MagicMock(returncode=0, stdout="Summary: 0 critical\n", stderr="")
    monkeypatch.setattr(installer, "_resolve_openclaw_config_dir", lambda: tmp_path / ".openclaw")

    installer._run_openclaw_post_start_security_gate()

    assert mock_compose_run.call_count == 1
    assert mock_compose_run.call_args.kwargs["env"]["OPENCLAW_IMAGE"] == "ghcr.io/openclaw/openclaw:main"


def test_maybe_configure_env_merges_missing_values_into_existing_env_template(tmp_path, monkeypatch):
    installer = _make_installer_for_env(tmp_path)
    (tmp_path / "docker-compose.yml").write_text(
        "services:\n"
        "  app:\n"
        "    environment:\n"
        "      - OPENAI_API_KEY=${OPENAI_API_KEY}\n"
        "      - MODEL_PROVIDER=${MODEL_PROVIDER}\n"
        "      - Armorer_MODEL=${Armorer_MODEL}\n"
    )
    (tmp_path / ".env").write_text(
        "# copied template\n"
        "OPENAI_API_KEY=\n"
        "MODEL_PROVIDER=ollama\n"
        "EXISTING=value\n"
    )

    from armorer import config as config_mod

    class _FakeConfigManager:
        def find_vault_key(self, key):
            return {
                "OPENAI_API_KEY": "sk-test-openai",
                "MODEL_PROVIDER": "openai",
                "Armorer_MODEL": "gpt-4.1-mini",
            }.get(key)

        def is_secret(self, key):
            return key.endswith("_KEY")

    monkeypatch.setattr(config_mod, "ConfigManager", _FakeConfigManager, raising=True)

    installer._maybe_configure_env()

    env_text = (tmp_path / ".env").read_text()
    assert "OPENAI_API_KEY=sk-test-openai" in env_text
    assert "MODEL_PROVIDER=ollama" in env_text
    assert "Armorer_MODEL=gpt-4.1-mini" in env_text
    assert "EXISTING=value" in env_text
