"""Tests for mining/_docker.py — Docker SDK orchestration via mocks."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest


def test_ensure_image_already_local():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake = MagicMock()
    fake.images.get.return_value = MagicMock(
        id="sha256:abc", tags=["openjarvis/pearl-miner:main"]
    )
    launcher = PearlDockerLauncher(client=fake)
    out = launcher.ensure_image("openjarvis/pearl-miner:main")
    assert out == "openjarvis/pearl-miner:main"
    fake.images.get.assert_called_once_with("openjarvis/pearl-miner:main")
    fake.images.pull.assert_not_called()


def test_ensure_image_pulls_if_published():
    from openjarvis.mining._docker import (
        APIError,
        ImageNotFound,
        NotFound,
        PearlDockerLauncher,
    )

    fake = MagicMock()
    fake.images.get.side_effect = ImageNotFound("nope")
    fake.images.pull.return_value = MagicMock(id="sha256:def")
    launcher = PearlDockerLauncher(client=fake)
    with patch(
        "openjarvis.mining._docker._docker_error_types",
        return_value=(ImageNotFound, NotFound, APIError),
    ):
        out = launcher.ensure_image("registry.example/pearl-miner:1.0")
    assert out == "registry.example/pearl-miner:1.0"
    fake.images.pull.assert_called_once_with("registry.example/pearl-miner:1.0")


def test_ensure_image_falls_back_to_build_for_default_tag():
    from openjarvis.mining._constants import PEARL_IMAGE_TAG
    from openjarvis.mining._docker import (
        APIError,
        ImageNotFound,
        NotFound,
        PearlDockerLauncher,
    )

    fake = MagicMock()
    fake.images.get.side_effect = ImageNotFound("nope")
    fake.images.pull.side_effect = NotFound("registry refused")
    launcher = PearlDockerLauncher(client=fake)
    with (
        patch.object(launcher, "_clone_pearl_repo") as clone,
        patch.object(launcher, "_docker_build") as build,
        patch(
            "openjarvis.mining._docker._docker_error_types",
            return_value=(ImageNotFound, NotFound, APIError),
        ),
    ):
        clone.return_value = "/tmp/pearl-cache"
        build.return_value = PEARL_IMAGE_TAG
        out = launcher.ensure_image(PEARL_IMAGE_TAG)
        assert out == PEARL_IMAGE_TAG
        clone.assert_called_once()
        build.assert_called_once()


def test_ensure_image_errors_when_non_default_tag_missing():
    import pytest

    from openjarvis.mining._docker import (
        APIError,
        ImageAcquisitionError,
        ImageNotFound,
        NotFound,
        PearlDockerLauncher,
    )

    fake = MagicMock()
    fake.images.get.side_effect = ImageNotFound("nope")
    fake.images.pull.side_effect = NotFound("registry refused")
    launcher = PearlDockerLauncher(client=fake)
    with (
        patch(
            "openjarvis.mining._docker._docker_error_types",
            return_value=(ImageNotFound, NotFound, APIError),
        ),
        pytest.raises(ImageAcquisitionError) as ei,
    ):
        launcher.ensure_image("user/custom-image:tag")
    assert "user/custom-image:tag" in str(ei.value)


def test_docker_build_raises_nofile_limit(tmp_path):
    from openjarvis.mining._docker import PearlDockerLauncher

    entrypoint = tmp_path / "miner" / "vllm-miner" / "entrypoint.sh"
    entrypoint.parent.mkdir(parents=True)
    entrypoint.write_text("#!/bin/sh\n")
    dockerfile = tmp_path / "miner" / "vllm-miner" / "Dockerfile"
    dockerfile.write_text("FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu24.04\n")
    launcher = PearlDockerLauncher(client=MagicMock())
    with patch("openjarvis.mining._docker.subprocess.run") as run:
        launcher._docker_build(tmp_path, "openjarvis/pearl-miner:test")

    cmd = run.call_args.args[0]
    assert "--ulimit" in cmd
    assert "nofile=1048576:1048576" in cmd


def test_patch_vllm_dockerfile_keeps_nvcc_runtime(tmp_path):
    from openjarvis.mining._docker import PearlDockerLauncher

    dockerfile = tmp_path / "miner" / "vllm-miner" / "Dockerfile"
    dockerfile.parent.mkdir(parents=True)
    dockerfile.write_text("FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu24.04\n")
    PearlDockerLauncher(client=MagicMock())._patch_vllm_dockerfile(tmp_path)
    text = dockerfile.read_text()
    assert "devel-ubuntu24.04" in text
    assert "runtime-ubuntu24.04" not in text


def test_patch_vllm_entrypoint_waits_for_gateway_socket(tmp_path):
    from openjarvis.mining._docker import PearlDockerLauncher

    entrypoint = tmp_path / "miner" / "vllm-miner" / "entrypoint.sh"
    entrypoint.parent.mkdir(parents=True)
    entrypoint.write_text(
        "# Wait until the gateway is ready\n"
        "curl -s http://localhost:8339/metrics --retry-delay 1 --retry 20 "
        "--retry-all-errors > /dev/null\n"
    )
    PearlDockerLauncher(client=MagicMock())._patch_vllm_entrypoint(tmp_path)
    text = entrypoint.read_text()
    assert "/tmp/pearlgw.sock" in text
    assert "localhost:8339/metrics" not in text


@pytest.fixture
def _env_password(monkeypatch):
    monkeypatch.setenv("PEARLD_RPC_PASSWORD", "secret123")


def test_launcher_start_calls_run_with_expected_kwargs(_env_password):
    from openjarvis.mining._docker import PearlDockerLauncher
    from openjarvis.mining._stubs import MiningConfig, SoloTarget

    fake = MagicMock()
    fake.containers.run.return_value = MagicMock(id="cid-1", status="running")
    launcher = PearlDockerLauncher(client=fake)
    cfg = MiningConfig(
        provider="vllm-pearl",
        wallet_address="prl1qaaa",
        submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
        extra={
            "docker_image_tag": "openjarvis/pearl-miner:main",
            "model": "pearl-ai/Llama-3.3-70B-Instruct-pearl",
            "vllm_port": 8000,
            "gpu_memory_utilization": 0.9,
            "max_model_len": 8192,
            "pearld_rpc_url": "http://localhost:44107",
            "pearld_rpc_user": "rpcuser",
            "pearld_rpc_password_env": "PEARLD_RPC_PASSWORD",
            "hf_token_env": "HF_TOKEN",
            "cuda_visible_devices": "0,1",
        },
    )
    container = launcher.start(cfg, image="openjarvis/pearl-miner:main")
    assert container.id == "cid-1"
    fake.containers.run.assert_called_once()
    kwargs = fake.containers.run.call_args.kwargs
    assert kwargs["image"] == "openjarvis/pearl-miner:main"
    assert kwargs["command"][0] == "pearl-ai/Llama-3.3-70B-Instruct-pearl"
    assert "--gpu-memory-utilization" in kwargs["command"]
    assert kwargs["restart_policy"]["Name"] == "unless-stopped"
    assert kwargs["environment"]["PEARLD_RPC_PASSWORD"] == "secret123"
    assert kwargs["environment"]["PEARLD_MINING_ADDRESS"] == "prl1qaaa"
    assert kwargs["environment"]["MINER_RPC_TRANSPORT"] == "uds"
    assert kwargs["environment"]["MINER_RPC_SOCKET_PATH"] == "/tmp/pearlgw.sock"
    assert kwargs["environment"]["CUDA_VISIBLE_DEVICES"] == "0,1"
    assert kwargs["environment"]["NVIDIA_VISIBLE_DEVICES"] == "0,1"
    assert "device_requests" in kwargs
    if kwargs["device_requests"] is not None:
        assert kwargs["device_requests"][0].device_ids == ["0", "1"]


def test_launcher_start_maps_host_gpu_ids_to_container_local_cuda_ids(
    _env_password,
) -> None:
    from openjarvis.mining._docker import PearlDockerLauncher
    from openjarvis.mining._stubs import MiningConfig, SoloTarget

    fake = MagicMock()
    fake.containers.run.return_value = MagicMock(id="cid-1", status="running")
    launcher = PearlDockerLauncher(client=fake)
    cfg = MiningConfig(
        provider="vllm-pearl",
        wallet_address="prl1qaaa",
        submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
        extra={
            "pearld_rpc_password_env": "PEARLD_RPC_PASSWORD",
            "cuda_visible_devices": "1",
        },
    )

    launcher.start(cfg, image="openjarvis/pearl-miner:main")

    kwargs = fake.containers.run.call_args.kwargs
    assert kwargs["environment"]["NVIDIA_VISIBLE_DEVICES"] == "1"
    assert kwargs["environment"]["CUDA_VISIBLE_DEVICES"] == "0"
    if kwargs["device_requests"] is not None:
        assert kwargs["device_requests"][0].device_ids == ["1"]


def test_launcher_start_mounts_local_model_path(_env_password, tmp_path):
    from openjarvis.mining._docker import LOCAL_MODEL_BIND_PATH, PearlDockerLauncher
    from openjarvis.mining._stubs import MiningConfig, SoloTarget

    local_model = tmp_path / "llama31-pearl"
    local_model.mkdir()
    fake = MagicMock()
    fake.containers.run.return_value = MagicMock(id="cid-1", status="running")
    launcher = PearlDockerLauncher(client=fake)
    cfg = MiningConfig(
        provider="vllm-pearl",
        wallet_address="prl1qaaa",
        submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
        extra={
            "model": "pearl-ai/Llama-3.1-8B-Instruct-pearl",
            "local_model_path": str(local_model),
            "pearld_rpc_password_env": "PEARLD_RPC_PASSWORD",
            "vllm_args": ["--language-model-only", "--skip-mm-profiling"],
        },
    )

    launcher.start(cfg, image="openjarvis/pearl-miner:main")

    kwargs = fake.containers.run.call_args.kwargs
    assert kwargs["command"][0] == LOCAL_MODEL_BIND_PATH
    assert "--served-model-name" in kwargs["command"]
    assert "pearl-ai/Llama-3.1-8B-Instruct-pearl" in kwargs["command"]
    assert "--language-model-only" in kwargs["command"]
    assert "--skip-mm-profiling" in kwargs["command"]
    assert kwargs["volumes"][str(local_model.resolve())] == {
        "bind": LOCAL_MODEL_BIND_PATH,
        "mode": "ro",
    }


def test_launcher_stop_calls_container_stop_and_remove():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock()
    launcher = PearlDockerLauncher(client=fake_client)
    launcher._container = fake_container
    launcher.stop()
    fake_container.stop.assert_called_once()
    fake_container.remove.assert_called_once()
    # Reference cleared unconditionally so a future start() isn't blocked.
    assert launcher._container is None


def test_launcher_stop_finds_named_container_without_in_memory_reference():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock()
    fake_client.containers.get.return_value = fake_container
    launcher = PearlDockerLauncher(client=fake_client)
    launcher.stop()
    fake_client.containers.get.assert_called_once_with("openjarvis-pearl-miner")
    fake_container.stop.assert_called_once()
    fake_container.remove.assert_called_once()
    assert launcher._container is None


def test_launcher_is_running_when_container_running():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock(status="running")
    fake_container.reload.return_value = None
    launcher = PearlDockerLauncher(client=fake_client)
    launcher._container = fake_container
    assert launcher.is_running() is True


def test_launcher_is_running_finds_named_container():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock(status="running")
    fake_container.reload.return_value = None
    fake_client.containers.get.return_value = fake_container
    launcher = PearlDockerLauncher(client=fake_client)
    assert launcher.is_running() is True
    fake_client.containers.get.assert_called_once_with("openjarvis-pearl-miner")


def test_launcher_is_running_false_when_container_exited():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock()
    fake_container.reload.return_value = None
    fake_container.status = "exited"
    launcher = PearlDockerLauncher(client=fake_client)
    launcher._container = fake_container
    assert launcher.is_running() is False


def test_launcher_get_logs_returns_decoded_string():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock()
    fake_container.logs.return_value = b"hello\nworld\n"
    launcher = PearlDockerLauncher(client=fake_client)
    launcher._container = fake_container
    assert "hello" in launcher.get_logs(tail=100)


def test_launcher_get_logs_redacts_rpc_passwords():
    from openjarvis.mining._docker import PearlDockerLauncher

    fake_client = MagicMock()
    fake_container = MagicMock()
    fake_container.logs.return_value = (
        b"PearlNodeClient initialized with rpc_password: secret123\n"
        b"PEARLD_RPC_PASSWORD=secret123\n"
        b'{"PEARLD_RPC_PASSWORD": "secret123"}\n'
    )
    launcher = PearlDockerLauncher(client=fake_client)
    launcher._container = fake_container
    logs = launcher.get_logs(tail=100)
    assert "secret123" not in logs
    assert logs.count("[REDACTED]") == 3


def test_launcher_start_errors_when_password_env_missing():
    from openjarvis.mining._docker import (
        ConfigurationError,
        PearlDockerLauncher,
    )
    from openjarvis.mining._stubs import MiningConfig, SoloTarget

    fake = MagicMock()
    launcher = PearlDockerLauncher(client=fake)
    cfg = MiningConfig(
        provider="vllm-pearl",
        wallet_address="prl1qaaa",
        submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
        extra={
            "docker_image_tag": "openjarvis/pearl-miner:main",
            "model": "pearl-ai/Llama-3.3-70B-Instruct-pearl",
            "vllm_port": 8000,
            "gpu_memory_utilization": 0.9,
            "pearld_rpc_url": "http://localhost:44107",
            "pearld_rpc_user": "rpcuser",
            "pearld_rpc_password_env": "DOES_NOT_EXIST_IN_ENV",
        },
    )
    with pytest.raises(ConfigurationError) as ei:
        launcher.start(cfg, image="openjarvis/pearl-miner:main")
    assert "DOES_NOT_EXIST_IN_ENV" in str(ei.value)
