# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
#
# SPDX-License-Identifier: MIT
"""
Tests for vdkr - Docker CLI for cross-architecture emulation.

These tests verify vdkr functionality including:
- Memory resident mode (memres)
- Image management (images, pull, import, save, load)
- Container execution (vrun)
- System commands (system df, system prune)
- Storage management (vstorage list, path, df, clean)

Tests use a separate state directory (~/.vdkr-test/) to avoid
interfering with user's images in ~/.vdkr/.

Run with:
    pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vdkr-standalone

Run with memres already started (faster):
    ./tests/memres-test.sh start --vdkr-dir /tmp/vdkr-standalone
    pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vdkr-standalone --skip-destructive

Run with OCI image for import tests:
    pytest tests/test_vdkr.py -v --vdkr-dir /tmp/vdkr-standalone --oci-image /path/to/container-oci
"""

import pytest
import json
import os


class TestMemresBasic:
    """Test memory resident mode basic operations.

    These tests use a separate state directory (~/.vdkr-test/) so they
    don't interfere with user's memres in ~/.vdkr/.
    """

    def test_memres_start(self, vdkr):
        """Test starting memory resident mode."""
        # Stop first if running
        vdkr.memres_stop()

        result = vdkr.memres_start(timeout=180)
        assert result.returncode == 0, f"memres start failed: {result.stderr}"

    def test_memres_status(self, vdkr):
        """Test checking memory resident status."""
        if not vdkr.is_memres_running():
            vdkr.memres_start(timeout=180)

        result = vdkr.memres_status()
        assert result.returncode == 0
        assert "running" in result.stdout.lower() or "started" in result.stdout.lower()

    def test_memres_stop(self, vdkr):
        """Test stopping memory resident mode."""
        # Ensure running first
        if not vdkr.is_memres_running():
            vdkr.memres_start(timeout=180)

        result = vdkr.memres_stop()
        assert result.returncode == 0

        # Verify stopped
        status = vdkr.memres_status()
        assert status.returncode != 0 or "not running" in status.stdout.lower()

    def test_memres_restart(self, vdkr):
        """Test restarting memory resident mode."""
        result = vdkr.run("memres", "restart", timeout=180)
        assert result.returncode == 0

        # Verify running
        assert vdkr.is_memres_running()


class TestPortForwarding:
    """Test port forwarding with memres.

    Port forwarding allows access to services running in containers from the host.
    Docker bridge networking (docker0, 172.17.0.0/16) is used by default.
    Each container gets its own IP, enabling multiple containers to listen
    on the same internal port with different host port mappings.
    """

    @pytest.mark.network
    @pytest.mark.slow
    def test_port_forward_nginx(self, vdkr):
        """Test port forwarding with nginx using bridge networking.

        This test:
        1. Starts memres (no static port forwards needed)
        2. Runs nginx with -p 8080:80 (Docker bridge + iptables NAT)
        3. Verifies nginx is accessible from host via curl
        """
        import subprocess
        import time

        # Stop any running memres first
        vdkr.memres_stop()

        # Start memres (no static port forwards needed - use dynamic via -p on run)
        result = vdkr.memres_start(timeout=180)
        assert result.returncode == 0, f"memres start failed: {result.stderr}"

        try:
            # Pull nginx:alpine if not present
            vdkr.run("pull", "nginx:alpine", timeout=300)

            # Run nginx with port forward - Docker sets up iptables for bridge networking
            result = vdkr.run("run", "-d", "--rm", "-p", "8080:80", "nginx:alpine", timeout=60)
            assert result.returncode == 0, f"nginx run failed: {result.stderr}"

            # Give nginx time to start
            time.sleep(3)

            # Test access from host
            curl_result = subprocess.run(
                ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080"],
                capture_output=True,
                text=True,
                timeout=10
            )
            assert curl_result.stdout == "200", f"Expected HTTP 200, got {curl_result.stdout}"

        finally:
            # Clean up: stop all containers
            vdkr.run("ps", "-q", check=False)
            ps_result = vdkr.run("ps", "-q", check=False)
            if ps_result.stdout.strip():
                for container_id in ps_result.stdout.strip().split('\n'):
                    vdkr.run("stop", container_id, timeout=30, check=False)

            # Stop memres
            vdkr.memres_stop()

    @pytest.mark.network
    @pytest.mark.slow
    def test_multiple_containers_same_internal_port(self, vdkr):
        """Test multiple containers listening on same internal port.

        This tests the key benefit of bridge networking:
        - nginx1 listens on container port 80, mapped to host port 8080
        - nginx2 listens on container port 80, mapped to host port 8081
        - Both should work simultaneously (impossible with --network=host)
        """
        import subprocess
        import time

        # Stop any running memres first
        vdkr.memres_stop()

        # Start memres
        result = vdkr.memres_start(timeout=180)
        assert result.returncode == 0, f"memres start failed: {result.stderr}"

        try:
            # Pull nginx:alpine if not present
            vdkr.run("pull", "nginx:alpine", timeout=300)

            # Run first nginx on host:8080 -> container:80
            result1 = vdkr.run("run", "-d", "--name", "nginx1", "-p", "8080:80",
                               "nginx:alpine", timeout=60)
            assert result1.returncode == 0, f"nginx1 run failed: {result1.stderr}"

            # Run second nginx on host:8081 -> container:80 (same internal port!)
            result2 = vdkr.run("run", "-d", "--name", "nginx2", "-p", "8081:80",
                               "nginx:alpine", timeout=60)
            assert result2.returncode == 0, f"nginx2 run failed: {result2.stderr}"

            # Give nginx time to start
            time.sleep(3)

            # Test both are accessible
            curl1 = subprocess.run(
                ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                 "http://localhost:8080"],
                capture_output=True, text=True, timeout=10
            )
            assert curl1.stdout == "200", f"nginx1: Expected HTTP 200, got {curl1.stdout}"

            curl2 = subprocess.run(
                ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                 "http://localhost:8081"],
                capture_output=True, text=True, timeout=10
            )
            assert curl2.stdout == "200", f"nginx2: Expected HTTP 200, got {curl2.stdout}"

            # Verify ps shows both containers with their port mappings
            ps_result = vdkr.run("ps")
            assert "nginx1" in ps_result.stdout
            assert "nginx2" in ps_result.stdout
            assert "8080" in ps_result.stdout
            assert "8081" in ps_result.stdout

        finally:
            # Clean up
            vdkr.run("stop", "nginx1", timeout=30, check=False)
            vdkr.run("stop", "nginx2", timeout=30, check=False)
            vdkr.run("rm", "-f", "nginx1", check=False)
            vdkr.run("rm", "-f", "nginx2", check=False)
            vdkr.memres_stop()

    @pytest.mark.network
    @pytest.mark.slow
    def test_network_host_backward_compat(self, vdkr):
        """Test --network=host backward compatibility.

        This tests that the old host networking mode still works when explicitly
        specified. With --network=host, containers share the VM's network stack
        (10.0.2.15), so static port forwarding at memres start is required.

        Note: With bridge networking as default, static port forwards now map
        host_port -> host_port on VM (Docker -p handles container port mapping).
        For --network=host, use matching ports (e.g., 8082:8082) since the
        container binds directly to VM ports.
        """
        import subprocess
        import time

        # Stop any running memres first
        vdkr.memres_stop()

        # Start memres with static port forward (required for --network=host)
        # Use matching ports since container binds directly to VM network
        result = vdkr.memres_start(timeout=180, port_forwards=["8082:8082"])
        assert result.returncode == 0, f"memres start failed: {result.stderr}"

        try:
            # Use busybox httpd (configurable port) instead of nginx (fixed port 80)
            vdkr.run("pull", "busybox:latest", timeout=300, check=False)

            # Run httpd with --network=host on port 8082
            # With host networking, httpd binds directly to VM:8082
            # Static forward maps host:8082 -> VM:8082
            result = vdkr.run("run", "-d", "--rm", "--name", "httpd-host",
                              "--network=host", "busybox:latest",
                              "httpd", "-f", "-p", "8082", timeout=60)
            assert result.returncode == 0, f"httpd run failed: {result.stderr}"

            # Give httpd time to start
            time.sleep(2)

            # Test access from host via static port forward
            # Note: busybox httpd returns 404 for /, but that's still a valid HTTP response
            curl_result = subprocess.run(
                ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                 "http://localhost:8082/"],
                capture_output=True,
                text=True,
                timeout=10
            )
            # Accept 200, 404, or other HTTP codes - we just need connectivity
            http_code = curl_result.stdout
            assert http_code.isdigit() and int(http_code) > 0, \
                f"Expected HTTP response, got {http_code}"

        finally:
            # Clean up
            ps_result = vdkr.run("ps", "-q", check=False)
            if ps_result.stdout.strip():
                for container_id in ps_result.stdout.strip().split('\n'):
                    if container_id.strip():
                        vdkr.run("stop", container_id, timeout=30, check=False)
            vdkr.memres_stop()


class TestImages:
    """Test image management commands."""

    def test_images_list(self, memres_session):
        """Test images command."""
        vdkr = memres_session
        vdkr.ensure_memres()
        result = vdkr.images()
        assert result.returncode == 0
        # Should have header line at minimum
        assert "REPOSITORY" in result.stdout or "IMAGE" in result.stdout

    @pytest.mark.network
    def test_pull_alpine(self, memres_session):
        """Test pulling alpine image from registry."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Pull alpine (small image)
        result = vdkr.pull("alpine:latest", timeout=300)
        assert result.returncode == 0

        # Verify it appears in images
        images = vdkr.images()
        assert "alpine" in images.stdout

    def test_rmi(self, memres_session):
        """Test removing an image."""
        vdkr = memres_session

        # Ensure we have alpine to test with
        vdkr.ensure_alpine()

        # Force remove to handle containers using the image
        result = vdkr.run("rmi", "-f", "alpine:latest", check=False)
        assert result.returncode == 0


class TestVimport:
    """Test vimport command for OCI image import."""

    def test_vimport_oci(self, memres_session, oci_image):
        """Test importing an OCI directory."""
        if oci_image is None:
            pytest.skip("No OCI image provided (use --oci-image)")

        vdkr = memres_session
        vdkr.ensure_memres()
        result = vdkr.vimport(oci_image, "test-import:latest", timeout=180)
        assert result.returncode == 0

        # Verify it appears in images
        images = vdkr.images()
        assert "test-import" in images.stdout


class TestSaveLoad:
    """Test save and load commands."""

    def test_save_and_load(self, memres_session, temp_dir):
        """Test saving and loading an image."""
        vdkr = memres_session

        # Ensure we have alpine
        vdkr.ensure_alpine()

        tar_path = temp_dir / "test-save.tar"

        # Save
        result = vdkr.save(tar_path, "alpine:latest", timeout=180)
        assert result.returncode == 0
        assert tar_path.exists()
        assert tar_path.stat().st_size > 0

        # Remove the image
        vdkr.run("rmi", "-f", "alpine:latest", check=False)

        # Load
        result = vdkr.load(tar_path, timeout=180)
        assert result.returncode == 0

        # Verify it's back
        images = vdkr.images()
        assert "alpine" in images.stdout


class TestVrun:
    """Test vrun command for container execution."""

    def test_vrun_echo(self, memres_session):
        """Test running echo command in a container."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        result = vdkr.vrun("alpine:latest", "/bin/echo", "hello", "world")
        assert result.returncode == 0
        assert "hello world" in result.stdout

    def test_vrun_uname(self, memres_session, arch):
        """Test running uname to verify architecture."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        result = vdkr.vrun("alpine:latest", "/bin/uname", "-m")
        assert result.returncode == 0

        # Check architecture matches
        expected_arch = "x86_64" if arch == "x86_64" else "aarch64"
        assert expected_arch in result.stdout

    def test_vrun_exit_code(self, memres_session):
        """Test container command execution."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Run command that exits with code 1 (false command)
        result = vdkr.run("vrun", "alpine:latest", "/bin/false",
                          check=False, timeout=60)
        # Container exit codes may or may not be propagated depending on vdkr implementation
        # At minimum, verify the command ran (no crash/timeout)
        # Note: exit code propagation is a future enhancement
        assert result.returncode in [0, 1], f"Unexpected return code: {result.returncode}"


class TestInspect:
    """Test inspect command."""

    def test_inspect_image(self, memres_session):
        """Test inspecting an image."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        result = vdkr.inspect("alpine:latest")
        assert result.returncode == 0

        # Should be valid JSON
        data = json.loads(result.stdout)
        assert isinstance(data, list)
        assert len(data) > 0


class TestHistory:
    """Test history command."""

    def test_history(self, memres_session):
        """Test showing image history."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        result = vdkr.run("history", "alpine:latest")
        assert result.returncode == 0
        assert "IMAGE" in result.stdout or "CREATED" in result.stdout


class TestClean:
    """Test clean command."""

    def test_clean(self, vdkr, request):
        """Test cleaning state directory."""
        if request.config.getoption("--skip-destructive"):
            pytest.skip("Skipped with --skip-destructive")

        # Stop memres first
        vdkr.memres_stop()

        result = vdkr.clean()
        assert result.returncode == 0


class TestFallbackMode:
    """Test fallback to regular QEMU mode when memres not running."""

    @pytest.mark.slow
    def test_images_without_memres(self, vdkr, request):
        """Test images command works without memres (slower)."""
        if request.config.getoption("--skip-destructive"):
            pytest.skip("Skipped with --skip-destructive")

        # Ensure memres is stopped
        vdkr.memres_stop()

        # This should still work, just slower
        result = vdkr.images(timeout=120)
        assert result.returncode == 0


class TestContainerLifecycle:
    """Test container lifecycle commands."""

    @pytest.mark.slow
    def test_run_detached_and_manage(self, memres_session):
        """Test running a detached container and managing it."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Run a container in detached mode
        # Note: vdkr run auto-prepends "docker run", so just pass the docker run args
        result = vdkr.run("run", "-d", "--name", "test-container", "alpine:latest", "sleep", "300",
                          timeout=60, check=False)
        if result.returncode != 0:
            # Show error for debugging
            print(f"Failed to start detached container: {result.stderr}")
            pytest.skip("Could not start detached container")

        try:
            # List containers
            ps_result = vdkr.run("ps")
            assert "test-container" in ps_result.stdout

            # Stop container
            stop_result = vdkr.run("stop", "test-container", timeout=30)
            assert stop_result.returncode == 0

            # Remove container
            rm_result = vdkr.run("rm", "test-container")
            assert rm_result.returncode == 0

        finally:
            # Cleanup
            vdkr.run("rm", "-f", "test-container", check=False)


class TestVolumeMounts:
    """Test volume mount functionality.

    Volume mounts require memres to be running.
    """

    def test_volume_mount_read_file(self, memres_session, temp_dir):
        """Test mounting a host directory and reading a file from it."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Create a test file on host
        test_file = temp_dir / "testfile.txt"
        test_content = "Hello from host volume!"
        test_file.write_text(test_content)

        # Run container with volume mount and read the file
        result = vdkr.run("vrun", "-v", f"{temp_dir}:/data", "alpine:latest",
                          "cat", "/data/testfile.txt", timeout=60)
        assert result.returncode == 0
        assert test_content in result.stdout

    def test_volume_mount_write_file(self, memres_session, temp_dir):
        """Test writing a file in a mounted volume."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Create a script that writes to a file - avoids shell metacharacter issues
        # when passing through multiple shells (host -> vdkr -> runner -> guest -> container)
        # Include sync to ensure write is flushed to host via 9p/virtio-fs
        script = temp_dir / "write.sh"
        script.write_text("#!/bin/sh\necho 'Created in container' > /data/output.txt\nsync\n")
        script.chmod(0o755)

        # Run the script inside the container
        result = vdkr.run("vrun", "-v", f"{temp_dir}:/data", "alpine:latest",
                          "/data/write.sh", timeout=60)
        assert result.returncode == 0

        # Verify the file was synced back to host
        output_file = temp_dir / "output.txt"
        assert output_file.exists(), "Output file should be synced back to host"
        assert "Created in container" in output_file.read_text()

    def test_volume_mount_read_only(self, memres_session, temp_dir):
        """Test read-only volume mount."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Create a test file
        test_file = temp_dir / "readonly.txt"
        test_file.write_text("Read-only content")

        # Can read from ro mount
        result = vdkr.run("vrun", "-v", f"{temp_dir}:/data:ro", "alpine:latest",
                          "cat", "/data/readonly.txt", timeout=60)
        assert result.returncode == 0
        assert "Read-only content" in result.stdout

    def test_volume_mount_multiple(self, memres_session, temp_dir):
        """Test multiple volume mounts."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Create two directories with test files
        dir1 = temp_dir / "dir1"
        dir2 = temp_dir / "dir2"
        dir1.mkdir()
        dir2.mkdir()

        (dir1 / "file1.txt").write_text("Content from dir1")
        (dir2 / "file2.txt").write_text("Content from dir2")

        # Create a script to avoid shell metacharacter issues with ';' or '&&'
        script = temp_dir / "read_both.sh"
        script.write_text("#!/bin/sh\ncat /data1/file1.txt\ncat /data2/file2.txt\n")
        script.chmod(0o755)

        # Mount both directories plus the script
        result = vdkr.run("vrun",
                          "-v", f"{temp_dir}:/scripts",
                          "-v", f"{dir1}:/data1",
                          "-v", f"{dir2}:/data2",
                          "alpine:latest",
                          "/scripts/read_both.sh",
                          timeout=60)
        assert result.returncode == 0
        assert "Content from dir1" in result.stdout
        assert "Content from dir2" in result.stdout

    def test_volume_mount_with_run_command(self, memres_session, temp_dir):
        """Test volume mount with run command (not vrun)."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Create a test file
        test_file = temp_dir / "runtest.txt"
        test_file.write_text("Testing run command volumes")

        # Use run command with volume
        result = vdkr.run("run", "--rm", "-v", f"{temp_dir}:/data",
                          "alpine:latest", "cat", "/data/runtest.txt",
                          timeout=60)
        assert result.returncode == 0
        assert "Testing run command volumes" in result.stdout

    def test_volume_mount_requires_memres(self, vdkr, temp_dir, request):
        """Test that volume mounts fail gracefully without memres."""
        if request.config.getoption("--skip-destructive"):
            pytest.skip("Skipped with --skip-destructive")

        # Ensure memres is stopped
        vdkr.memres_stop()

        # Create a test file
        test_file = temp_dir / "test.txt"
        test_file.write_text("test")

        # Try to use volume mount without memres - should fail with clear message
        result = vdkr.run("vrun", "-v", f"{temp_dir}:/data", "alpine:latest",
                          "cat", "/data/test.txt", check=False, timeout=30)

        # Should fail because memres is not running
        assert result.returncode != 0
        assert "memres" in result.stderr.lower() or "daemon" in result.stderr.lower()


class TestSystem:
    """Test system commands (run inside VM)."""

    def test_system_df(self, memres_session):
        """Test system df command."""
        vdkr = memres_session
        vdkr.ensure_memres()

        result = vdkr.run("system", "df")
        assert result.returncode == 0
        # Should show images, containers, volumes headers
        assert "IMAGES" in result.stdout.upper() or "TYPE" in result.stdout.upper()

    def test_system_df_verbose(self, memres_session):
        """Test system df -v command."""
        vdkr = memres_session
        vdkr.ensure_memres()

        result = vdkr.run("system", "df", "-v")
        assert result.returncode == 0
        # Verbose mode shows more details
        assert "IMAGES" in result.stdout.upper() or "TYPE" in result.stdout.upper()

    def test_system_prune_dry_run(self, memres_session):
        """Test system prune with dry run (doesn't actually delete)."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Just verify the command runs (don't actually prune in tests)
        # Add -f to skip confirmation prompt
        result = vdkr.run("system", "prune", "-f", check=False)
        # Command may return 0 even with nothing to prune
        assert result.returncode == 0

    def test_system_without_subcommand(self, memres_session):
        """Test system command without subcommand shows error."""
        vdkr = memres_session
        vdkr.ensure_memres()

        result = vdkr.run("system", check=False)
        assert result.returncode != 0
        assert "subcommand" in result.stderr.lower() or "requires" in result.stderr.lower()


class TestVstorage:
    """Test vstorage commands (host-side storage management).

    These commands run on the host and don't require memres.
    """

    def test_vstorage_list(self, vdkr):
        """Test vstorage list command."""
        # Ensure there's something to list by starting memres briefly
        vdkr.ensure_memres()

        result = vdkr.run("vstorage", "list", check=False)
        # vstorage list is an alias for vstorage
        assert result.returncode == 0
        assert "storage" in result.stdout.lower() or "path" in result.stdout.lower()

    def test_vstorage_default(self, vdkr):
        """Test vstorage with no subcommand (defaults to list)."""
        vdkr.ensure_memres()

        result = vdkr.run("vstorage", check=False)
        assert result.returncode == 0
        # Should show storage info
        assert "storage" in result.stdout.lower() or "vdkr" in result.stdout.lower()

    def test_vstorage_path(self, vdkr, arch):
        """Test vstorage path command."""
        result = vdkr.run("vstorage", "path", check=False)
        assert result.returncode == 0
        # Output should contain the architecture or .vdkr path
        assert arch in result.stdout or ".vdkr" in result.stdout

    def test_vstorage_path_specific_arch(self, vdkr):
        """Test vstorage path with specific architecture."""
        # Use the same arch as the runner to avoid cross-arch issues
        arch = vdkr.arch
        result = vdkr.run("vstorage", "path", arch, check=False)
        assert result.returncode == 0
        assert arch in result.stdout

    def test_vstorage_df(self, vdkr):
        """Test vstorage df command."""
        # Ensure there's something to show
        vdkr.ensure_memres()

        result = vdkr.run("vstorage", "df", check=False)
        assert result.returncode == 0
        # Should show size information (may be empty if no state yet)

    def test_vstorage_shows_memres_status(self, vdkr):
        """Test that vstorage list shows memres running status."""
        vdkr.ensure_memres()

        result = vdkr.run("vstorage", "list", check=False)
        assert result.returncode == 0
        # Should show running status when memres is active
        assert "running" in result.stdout.lower() or "memres" in result.stdout.lower() \
            or "status" in result.stdout.lower()

    def test_vstorage_clean_current_arch(self, vdkr, request):
        """Test vstorage clean for current architecture."""
        if request.config.getoption("--skip-destructive"):
            pytest.skip("Skipped with --skip-destructive")

        # Ensure there's something to clean
        vdkr.ensure_memres()
        vdkr.memres_stop()

        result = vdkr.run("vstorage", "clean", check=False)
        assert result.returncode == 0
        assert "clean" in result.stdout.lower()

    def test_vstorage_unknown_subcommand(self, vdkr):
        """Test vstorage with unknown subcommand shows error."""
        result = vdkr.run("vstorage", "invalid", check=False)
        assert result.returncode != 0
        assert "unknown" in result.stderr.lower() or "usage" in result.stderr.lower()


class TestRun:
    """Test run command with docker run options."""

    def test_run_with_entrypoint(self, memres_session):
        """Test run command with --entrypoint override."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        result = vdkr.run("run", "--rm", "--entrypoint", "/bin/echo",
                          "alpine:latest", "hello", "from", "entrypoint")
        assert result.returncode == 0
        assert "hello from entrypoint" in result.stdout

    def test_run_with_env_var(self, memres_session):
        """Test run command with environment variable."""
        vdkr = memres_session
        vdkr.ensure_alpine()

        # Use printenv instead of echo $MY_VAR to avoid shell quoting issues
        result = vdkr.run("run", "--rm", "-e", "MY_VAR=test_value",
                          "alpine:latest", "printenv", "MY_VAR")
        assert result.returncode == 0
        assert "test_value" in result.stdout


class TestRemoteFetchAndCrossInstall:
    """Test remote container fetch and cross-install workflow.

    These tests verify the full workflow for bundling containers into images:
    1. Pull container from remote registry
    2. Verify container is functional
    3. Export container storage (simulates cross-install bundle)
    4. Import storage into fresh state (simulates target boot)
    5. Verify container works after import

    Requires network access - use @pytest.mark.network marker.
    """

    @pytest.mark.network
    def test_pull_busybox(self, memres_session):
        """Test pulling busybox image from registry."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Pull busybox (very small image, faster than alpine for this test)
        result = vdkr.pull("busybox:latest", timeout=300)
        assert result.returncode == 0

        # Verify it appears in images
        images = vdkr.images()
        assert "busybox" in images.stdout

    @pytest.mark.network
    def test_pull_and_run(self, memres_session):
        """Test that pulled container can be executed."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Ensure we have busybox
        images = vdkr.images()
        if "busybox" not in images.stdout:
            vdkr.pull("busybox:latest", timeout=300)

        # Run a command in the pulled container
        result = vdkr.vrun("busybox:latest", "/bin/echo", "remote_fetch_works")
        assert result.returncode == 0
        assert "remote_fetch_works" in result.stdout

    @pytest.mark.network
    def test_cross_install_workflow(self, memres_session, temp_dir):
        """Test full cross-install workflow: pull -> export -> import -> run.

        This simulates:
        1. Build host: pull container from registry
        2. Build host: export Docker storage to tar (for bundling into image)
        3. Target boot: import storage tar
        4. Target: run the container

        This is the core workflow for container-cross-install.
        """
        vdkr = memres_session
        vdkr.ensure_memres()

        # Step 1: Pull container from remote registry
        images = vdkr.images()
        if "busybox" not in images.stdout:
            result = vdkr.pull("busybox:latest", timeout=300)
            assert result.returncode == 0

        # Step 2: Save container to tar (simulates bundle export)
        bundle_tar = temp_dir / "cross-install-bundle.tar"
        result = vdkr.save(bundle_tar, "busybox:latest", timeout=180)
        assert result.returncode == 0
        assert bundle_tar.exists()
        assert bundle_tar.stat().st_size > 0

        # Step 3: Remove original image (simulates fresh target state)
        vdkr.run("rmi", "-f", "busybox:latest", check=False)
        images = vdkr.images()
        # Verify removed (may still show if other tags exist)

        # Step 4: Load from bundle tar (simulates target importing bundled storage)
        result = vdkr.load(bundle_tar, timeout=180)
        assert result.returncode == 0

        # Step 5: Verify container works after import
        images = vdkr.images()
        assert "busybox" in images.stdout

        result = vdkr.vrun("busybox:latest", "/bin/echo", "cross_install_success")
        assert result.returncode == 0
        assert "cross_install_success" in result.stdout

    @pytest.mark.network
    def test_pull_verify_architecture(self, memres_session, arch):
        """Test that pulled container matches target architecture."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Ensure we have busybox
        images = vdkr.images()
        if "busybox" not in images.stdout:
            vdkr.pull("busybox:latest", timeout=300)

        # Run uname to verify architecture inside container
        result = vdkr.vrun("busybox:latest", "/bin/uname", "-m")
        assert result.returncode == 0

        # Check architecture matches target
        expected_arch = "x86_64" if arch == "x86_64" else "aarch64"
        assert expected_arch in result.stdout, \
            f"Architecture mismatch: expected {expected_arch}, got {result.stdout.strip()}"

    @pytest.mark.network
    def test_multiple_containers_bundle(self, memres_session, temp_dir):
        """Test bundling multiple containers (simulates multi-container image)."""
        vdkr = memres_session
        vdkr.ensure_memres()

        containers = ["busybox:latest", "alpine:latest"]
        bundle_tars = []

        # Pull and save each container
        for container in containers:
            name = container.split(":")[0]
            images = vdkr.images()
            if name not in images.stdout:
                result = vdkr.pull(container, timeout=300)
                assert result.returncode == 0

            tar_path = temp_dir / f"{name}-bundle.tar"
            result = vdkr.save(tar_path, container, timeout=180)
            assert result.returncode == 0
            bundle_tars.append((container, tar_path))

        # Remove all containers
        for container, _ in bundle_tars:
            vdkr.run("rmi", "-f", container, check=False)

        # Load all bundles (simulates target with multiple bundled containers)
        for container, tar_path in bundle_tars:
            result = vdkr.load(tar_path, timeout=180)
            assert result.returncode == 0

        # Verify all containers work
        images = vdkr.images()
        for container, _ in bundle_tars:
            name = container.split(":")[0]
            assert name in images.stdout, f"{name} not found after load"

        # Run a command in each
        result = vdkr.vrun("busybox:latest", "/bin/echo", "busybox_ok")
        assert "busybox_ok" in result.stdout

        result = vdkr.vrun("alpine:latest", "/bin/echo", "alpine_ok")
        assert "alpine_ok" in result.stdout


class TestAutoStartDaemon:
    """Test auto-start daemon behavior.

    When auto-daemon is enabled (default), vmemres starts automatically
    on the first command and stops after idle timeout.
    """

    def test_auto_start_on_first_command(self, vdkr):
        """Test that daemon auto-starts on first command."""
        # Stop daemon if running
        vdkr.memres_stop()
        assert not vdkr.is_memres_running(), "Daemon should be stopped"

        # Run a command - daemon should auto-start
        result = vdkr.images(timeout=180)
        assert result.returncode == 0

        # Verify daemon is now running
        assert vdkr.is_memres_running(), "Daemon should have auto-started"

    def test_no_daemon_flag(self, vdkr):
        """Test --no-daemon runs without starting daemon."""
        # Stop daemon if running
        vdkr.memres_stop()
        assert not vdkr.is_memres_running(), "Daemon should be stopped"

        # Run with --no-daemon - should use ephemeral mode
        result = vdkr.run("--no-daemon", "images", timeout=180)
        assert result.returncode == 0

        # Daemon should NOT be running
        assert not vdkr.is_memres_running(), "Daemon should not have started with --no-daemon"

    def test_vconfig_auto_daemon(self, vdkr):
        """Test vconfig auto-daemon setting."""
        # Check current value
        result = vdkr.run("vconfig", "auto-daemon")
        assert result.returncode == 0
        assert "true" in result.stdout.lower() or "auto-daemon" in result.stdout

        # Test setting to false
        result = vdkr.run("vconfig", "auto-daemon", "false")
        assert result.returncode == 0

        # Reset to default
        result = vdkr.run("vconfig", "auto-daemon", "--reset")
        assert result.returncode == 0

    def test_vconfig_idle_timeout(self, vdkr):
        """Test vconfig idle-timeout setting."""
        # Check current value
        result = vdkr.run("vconfig", "idle-timeout")
        assert result.returncode == 0

        # Test setting value
        result = vdkr.run("vconfig", "idle-timeout", "3600")
        assert result.returncode == 0

        # Reset to default
        result = vdkr.run("vconfig", "idle-timeout", "--reset")
        assert result.returncode == 0


class TestDynamicPortForwarding:
    """Test dynamic port forwarding via QMP.

    Port forwards can be added dynamically when running detached containers,
    without needing to specify them at vmemres start time.
    """

    @pytest.mark.network
    @pytest.mark.slow
    def test_dynamic_port_forward_run(self, vdkr):
        """Test that run -d -p adds port forward dynamically."""
        import subprocess
        import time

        # Ensure memres is running (without static port forwards)
        vdkr.memres_stop()
        vdkr.memres_start(timeout=180)
        assert vdkr.is_memres_running()

        try:
            # Pull nginx:alpine if not present
            vdkr.run("pull", "nginx:alpine", timeout=300)

            # Run with dynamic port forward
            result = vdkr.run("run", "-d", "--name", "nginx-test", "-p", "8888:80",
                              "nginx:alpine", timeout=60)
            assert result.returncode == 0, f"nginx run failed: {result.stderr}"

            # Give nginx time to start
            time.sleep(3)

            # Test access from host
            curl_result = subprocess.run(
                ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                 "http://localhost:8888"],
                capture_output=True,
                text=True,
                timeout=10
            )
            assert curl_result.stdout == "200", \
                f"Expected HTTP 200, got {curl_result.stdout}"

            # Check ps shows port forwards
            ps_result = vdkr.run("ps")
            assert ps_result.returncode == 0
            assert "8888" in ps_result.stdout or "Port Forwards" in ps_result.stdout

        finally:
            # Clean up
            vdkr.run("stop", "nginx-test", timeout=30, check=False)
            vdkr.run("rm", "-f", "nginx-test", check=False)

    def test_port_forward_cleanup_on_stop(self, memres_session):
        """Test that port forwards are cleaned up when container stops."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Ensure we have busybox
        vdkr.ensure_busybox()

        # Run a container with port forward
        result = vdkr.run("run", "-d", "--name", "port-test", "-p", "9999:80",
                          "busybox:latest", "sleep", "300", timeout=60, check=False)

        if result.returncode == 0:
            # Stop the container (docker stop has 10s grace period, so need longer timeout)
            vdkr.run("stop", "port-test", timeout=30, check=False)

            # Check ps - port forward should be removed
            ps_result = vdkr.run("ps")
            assert "9999" not in ps_result.stdout or "port-test" not in ps_result.stdout

            # Clean up
            vdkr.run("rm", "-f", "port-test", check=False)

    def test_port_forward_cleanup_on_rm(self, memres_session):
        """Test that port forwards are cleaned up when container is removed."""
        vdkr = memres_session
        vdkr.ensure_memres()

        # Ensure we have busybox
        vdkr.ensure_busybox()

        # Run a container with port forward
        result = vdkr.run("run", "-d", "--name", "rm-test", "-p", "7777:80",
                          "busybox:latest", "sleep", "300", timeout=60, check=False)

        if result.returncode == 0:
            # Force remove the container
            vdkr.run("rm", "-f", "rm-test", timeout=10, check=False)

            # Check ps - port forward should be removed
            ps_result = vdkr.run("ps")
            assert "7777" not in ps_result.stdout or "rm-test" not in ps_result.stdout

    @pytest.mark.network
    @pytest.mark.slow
    def test_multiple_dynamic_port_forwards(self, vdkr):
        """Test multiple containers with different dynamic port forwards."""
        import time

        vdkr.memres_stop()
        vdkr.memres_start(timeout=180)
        assert vdkr.is_memres_running()

        try:
            # Pull busybox
            vdkr.run("pull", "busybox:latest", timeout=300, check=False)

            # Run first container with port forward
            result1 = vdkr.run("run", "-d", "--name", "http1", "-p", "8001:80",
                               "busybox:latest", "httpd", "-f", "-p", "80",
                               timeout=60, check=False)

            # Run second container with different port forward
            result2 = vdkr.run("run", "-d", "--name", "http2", "-p", "8002:80",
                               "busybox:latest", "httpd", "-f", "-p", "80",
                               timeout=60, check=False)

            time.sleep(2)

            # Check ps shows both port forwards
            ps_result = vdkr.run("ps")
            # Note: May show in port forwards section, not PORTS column
            assert ps_result.returncode == 0

            # Stop first - second should still work
            vdkr.run("stop", "http1", timeout=30, check=False)

            # Check ps - only second port forward should remain
            ps_result = vdkr.run("ps")
            # http1's port should be cleaned up, http2 should remain

        finally:
            vdkr.run("stop", "http1", timeout=30, check=False)
            vdkr.run("stop", "http2", timeout=30, check=False)
            vdkr.run("rm", "-f", "http1", check=False)
            vdkr.run("rm", "-f", "http2", check=False)


class TestPortForwardRegistry:
    """Test port forward registry cleanup."""

    def test_port_forward_cleared_on_memres_stop(self, vdkr):
        """Test that port forward registry is cleared when memres stops."""
        import os

        # Start memres
        vdkr.memres_stop()
        vdkr.memres_start(timeout=180)
        assert vdkr.is_memres_running()

        # Get state dir path
        result = vdkr.run("vstorage", "path")
        if result.returncode == 0:
            state_dir = result.stdout.strip()
            pf_file = os.path.join(state_dir, "port-forwards.txt")

            # Run a container with port forward to create registry entry
            vdkr.ensure_busybox()
            vdkr.run("run", "-d", "--name", "pf-test", "-p", "6666:80",
                     "busybox:latest", "sleep", "60", timeout=60, check=False)

            # Stop memres - should clear port forward file
            vdkr.memres_stop()

            # Port forward file should not exist or be empty
            if os.path.exists(pf_file):
                with open(pf_file, 'r') as f:
                    content = f.read()
                assert content.strip() == "", "Port forward file should be empty"
