"""Tests for proxym.virt._lifecycle — VM creation, start, stop, pause, resume."""

from __future__ import annotations

import time
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch, PropertyMock

import pytest

from proxym.virt._config import VMConfig
from proxym.virt._enums import VMBackend, VMState


# Minimal concrete class that pulls in all mixins like VMOrchestrator
# but without needing libvirt or real filesystem.
class _FakeOrchestrator:
    """Stub orchestrator with just enough wiring for lifecycle tests."""

    def __init__(self, tmp_path: Path):
        self.vms: dict[str, VMConfig] = {}
        self.projects_root = tmp_path / "projects"
        self.projects_root.mkdir()
        self.config_dir = tmp_path / "config"
        self.config_dir.mkdir()
        self.overlays_dir = tmp_path / "overlays"
        self.overlays_dir.mkdir()
        self.images_dir = tmp_path / "images"
        self.images_dir.mkdir()
        self.profiles_dir = tmp_path / "profiles"
        self.profiles_dir.mkdir()
        self._clonebox = None
        self._vm_key_registry: dict = {}

    def _save_config(self, config):
        pass

    def _run(self, cmd, check=True):
        return SimpleNamespace(returncode=0, stdout="", stderr="")

    def _get_clonebox(self):
        return self._clonebox

    def _generate_domain_xml(self, config):
        pass

    def _inject_vm_config(self, config):
        return {}

    def _generate_vm_key(self, vm_id, account_id=""):
        return f"vm-key-{vm_id}-test"

    def _save_vm_key_mapping(self, vm_key, config):
        pass


# Attach lifecycle mixin methods
from proxym.virt._lifecycle import _LifecycleMixin

for name in dir(_LifecycleMixin):
    if not name.startswith("__"):
        method = getattr(_LifecycleMixin, name)
        if callable(method):
            setattr(_FakeOrchestrator, name, method)


@pytest.fixture
def orch(tmp_path):
    return _FakeOrchestrator(tmp_path)


class TestCreateVM:
    def test_creates_vm_with_defaults(self, orch):
        config = VMConfig(name="test-vm")
        result = orch.create_vm(config)
        assert result.id.startswith("proxym-")
        assert result.state == VMState.STOPPED
        assert result.id in orch.vms

    def test_assigns_code_mount_from_projects_root(self, orch):
        config = VMConfig(name="test-vm")
        result = orch.create_vm(config)
        assert result.code_mount_host == str(orch.projects_root)

    def test_preserves_existing_id(self, orch):
        config = VMConfig(name="test-vm", id="my-custom-id")
        result = orch.create_vm(config)
        assert result.id == "my-custom-id"

    def test_virsh_creates_overlay_from_base(self, orch):
        base = orch.images_dir / "fedora-40-dev.qcow2"
        base.write_text("fake-image")
        config = VMConfig(name="test-vm", base_image="fedora-40-dev")
        result = orch.create_vm(config)
        assert result.overlay_path is not None

    def test_virsh_creates_empty_overlay_when_no_base(self, orch):
        config = VMConfig(name="test-vm", base_image="nonexistent")
        result = orch.create_vm(config)
        assert result.overlay_path is not None


class TestCreateVMClonebox:
    def test_clonebox_not_available_raises(self, orch):
        orch._clonebox = None
        config = VMConfig(name="test-vm", id="test-1")
        with pytest.raises(RuntimeError, match="CloneBox not available"):
            orch._create_vm_clonebox(config)

    def test_clonebox_success(self, orch):
        cb = MagicMock()
        cb.get_profile_path.return_value = None
        cb.create_vm.return_value = SimpleNamespace(success=True, stderr="")
        orch._clonebox = cb
        config = VMConfig(name="test-vm", id="test-2", memory_mb=4096, vcpus=2)
        orch._create_vm_clonebox(config)
        cb.create_vm.assert_called_once()
        call_kwargs = cb.create_vm.call_args
        assert call_kwargs[0][0] == "test-2"

    def test_clonebox_failure_logs_error(self, orch):
        cb = MagicMock()
        cb.get_profile_path.return_value = None
        cb.create_vm.return_value = SimpleNamespace(success=False, stderr="disk full")
        orch._clonebox = cb
        config = VMConfig(name="test-vm", id="test-3")
        # Should not raise, just log
        orch._create_vm_clonebox(config)


class TestPrepareCloneboxArgs:
    def test_builds_extra_args(self, orch):
        cb = MagicMock()
        cb.get_profile_path.return_value = "/profiles/windsurf"
        config = VMConfig(name="x", id="y", tool="windsurf", memory_mb=8192, vcpus=4)
        profile_path, extra = orch._prepare_clonebox_args(config, cb)
        assert profile_path == "/profiles/windsurf"
        assert extra["memory"] == "8192"
        assert extra["vcpus"] == "4"

    def test_no_tool_gives_none_profile(self, orch):
        cb = MagicMock()
        config = VMConfig(name="x", id="y")
        profile_path, extra = orch._prepare_clonebox_args(config, cb)
        assert profile_path is None


class TestWriteCloudInitFile:
    def test_writes_yaml_file(self, orch):
        cloud_init = {"write_files": [{"path": "/tmp/test", "content": "hello"}]}
        result = orch._write_cloud_init_file(cloud_init)
        assert result is not None
        assert result.exists()
        content = result.read_text()
        assert "write_files" in content
        result.unlink()

    def test_returns_none_without_pyyaml(self, orch):
        with patch.dict("sys.modules", {"yaml": None}):
            import importlib
            # Force ImportError for yaml
            cloud_init = {"write_files": []}
            # This is tricky to test without actually removing yaml.
            # We trust the except ImportError branch.


class TestStartVM:
    def test_start_virsh_success(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.STOPPED, backend="virsh")
        orch.vms["vm-1"] = config
        assert orch.start_vm("vm-1") is True
        assert config.state == VMState.RUNNING
        assert config.last_started_at > 0

    def test_start_nonexistent_returns_false(self, orch):
        assert orch.start_vm("nonexistent") is False

    def test_start_virsh_failure(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.STOPPED, backend="virsh")
        orch.vms["vm-1"] = config
        # First call is dominfo (success), second is start (failure)
        call_count = [0]
        def mock_run(cmd, check=True):
            call_count[0] += 1
            if "dominfo" in cmd:
                return SimpleNamespace(returncode=0, stderr="")
            return SimpleNamespace(returncode=1, stderr="error")
        orch._run = mock_run
        assert orch.start_vm("vm-1") is False


class TestStopVM:
    def test_stop_virsh_success(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.RUNNING, backend="virsh")
        orch.vms["vm-1"] = config
        assert orch.stop_vm("vm-1") is True
        assert config.state == VMState.STOPPED

    def test_stop_nonexistent_returns_false(self, orch):
        assert orch.stop_vm("nonexistent") is False

    def test_force_stop_uses_destroy(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.RUNNING, backend="virsh")
        orch.vms["vm-1"] = config
        commands = []
        orch._run = lambda cmd, check=True: (commands.append(cmd), SimpleNamespace(returncode=0, stderr=""))[1]
        orch.stop_vm("vm-1", force=True)
        assert "destroy" in commands[0]


class TestPauseResumeVM:
    def test_pause_success(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.RUNNING, backend="virsh")
        orch.vms["vm-1"] = config
        assert orch.pause_vm("vm-1") is True
        assert config.state == VMState.PAUSED

    def test_resume_success(self, orch):
        config = VMConfig(name="test", id="vm-1", state=VMState.PAUSED, backend="virsh")
        orch.vms["vm-1"] = config
        assert orch.resume_vm("vm-1") is True
        assert config.state == VMState.RUNNING
