from __future__ import annotations

from types import SimpleNamespace

import evaluation.dual_sandbox_evaluator as dse_mod
from evaluation.dual_sandbox_evaluator import DualSandboxEvaluator
from evaluation.models import CaseResult, CaseStatus, PhaseConfig, PhaseStatus, RuntimeConfig, UserSubmission


def _cfg(**kwargs) -> PhaseConfig:
    base = {
        "phase_order": 1,
        "phase_level": "Easy",
        "num_cases": 2,
        "parallel_cases": 2,
        "artifact_url": "http://artifact",
        "artifact_checksum": "",
        "sandbox_timeout": 20,
        "user_bridge": "sandbox/_user_bridge.py",
        "adapter_preset": "maze",
        "solve_attr_name": "solve",
    }
    base.update(kwargs)
    return PhaseConfig(**base)


def _submission(cfg: PhaseConfig, files=None) -> UserSubmission:
    return UserSubmission(
        submit_id=1,
        user_id=2,
        phase_id=3,
        code_url="http://code",
        code_checksum="",
        code_files=files or {"requirements.txt": "pytest\n", "solution.py": "def solve(x): return x"},
        phase_config=cfg,
        runtime_config=RuntimeConfig(),
        phase_type="agent",
    )


def test_cleanup_swallow_destroy_errors(monkeypatch) -> None:
    ev = DualSandboxEvaluator(_cfg())
    ev._judge_sandbox = object()
    ev._user_sandbox = object()
    monkeypatch.setattr(dse_mod, "destroy_sandbox", lambda sb: (_ for _ in ()).throw(RuntimeError("boom")))
    ev.cleanup()
    assert ev._judge_sandbox is None
    assert ev._user_sandbox is None


def test_run_parallel_pairs_handles_group_failure(monkeypatch) -> None:
    ev = DualSandboxEvaluator(_cfg(num_cases=3, parallel_cases=2))
    sub = _submission(ev.config)

    calls = {"n": 0}

    def _run_pair(**kwargs):
        calls["n"] += 1
        if calls["n"] == 1:
            raise RuntimeError("group fail")
        return [CaseResult(case_index=1, status=CaseStatus.PASSED, score=1)]

    monkeypatch.setattr(ev, "_run_sandbox_pair", _run_pair)
    out = ev._run_parallel_pairs(
        submission=sub,
        gateway_token=None,
        artifact_files={"sandbox/run.py": b"print(1)"},
        user_files_bytes={"solution.py": b"ok"},
        user_req_path="",
        num_cases=3,
        parallel=2,
        on_case_start=None,
        on_case_end=None,
        deadline=10**9,
        track_per_case_usage=False,
    )
    assert len(out) == 1
    assert out[0].case_index == 1


def test_evaluate_additional_branches(monkeypatch) -> None:
    cfg = _cfg(user_bridge="sandbox/_user_bridge.py", allowed_packages=["pytest"], parallel_cases=2, num_cases=2)
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg)

    def _create_token(*args, **kwargs):  # type: ignore[no-untyped-def]
        ev._gateway_token_info = {"token": "tok"}
        return "tok"

    monkeypatch.setattr(dse_mod, "runtime_create_gateway_token_for_user", _create_token)
    monkeypatch.setattr(dse_mod, "runtime_download_artifact", lambda *args, **kwargs: b"zip")
    monkeypatch.setattr(
        dse_mod,
        "runtime_extract_artifact",
        lambda data: {
            "sandbox/run.py": b"print(1)",
            "sandbox/_user_bridge.py": b"def serve(): pass",
            "sandbox/user_adapter.py": b"class X: pass\n",
        },
    )
    monkeypatch.setattr(dse_mod, "runtime_filter_requirements", lambda req, allow: req)
    monkeypatch.setattr(
        ev,
        "_run_parallel_pairs",
        lambda **kwargs: [CaseResult(case_index=0, status=CaseStatus.PASSED, score=1)],
    )
    monkeypatch.setattr(ev, "_get_client", lambda: SimpleNamespace(get_gateway_token_usage=lambda submit_id: {"used_chars": 12, "used_requests": 3}))
    revoked = {"ok": False}
    monkeypatch.setattr(dse_mod, "runtime_revoke_gateway_token", lambda *args, **kwargs: revoked.__setitem__("ok", True))

    result = ev.evaluate(sub, parallel_cases=2)
    assert result.status == PhaseStatus.ERROR  # missing case backfill path
    assert result.total_chars == 12
    assert result.total_requests == 3
    assert revoked["ok"] is True


def test_run_sandbox_pair_uses_handle_process_contract(monkeypatch) -> None:
    cfg = _cfg(num_cases=1, parallel_cases=1)
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg)
    captured = {"deps": None}

    def _capture_run_pair_session(**kwargs):  # type: ignore[no-untyped-def]
        captured["deps"] = kwargs["deps"]
        return []

    monkeypatch.setattr(dse_mod, "run_sandbox_pair_session", _capture_run_pair_session)

    out = ev._run_sandbox_pair(
        submission=sub,
        gateway_token=None,
        artifact_files={"sandbox/run.py": b"print(1)"},
        user_files_bytes={"solution.py": b"def solve(x): return x"},
        user_req_path="",
        case_provider=lambda: None,
        track_per_case_usage=False,
        on_case_start=None,
        on_case_end=None,
        deadline=10**9,
    )
    assert out == []
    deps = captured["deps"]
    assert deps is not None
    assert callable(deps.stop_process)
    assert callable(deps.is_process_alive)
    assert callable(deps.describe_process)
    # Anti-regression: pid-era hooks must not be present anymore.
    assert not hasattr(deps, "kill_pid")
    assert not hasattr(deps, "is_pid_alive")


def test_evaluate_downloads_code_when_submission_files_empty(monkeypatch) -> None:
    cfg = _cfg(num_cases=1, parallel_cases=1, allowed_packages=["pytest"])
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg, files={})

    captured: dict[str, object] = {}
    revoked = {"ok": False}

    monkeypatch.setattr(dse_mod, "runtime_create_gateway_token_for_user", lambda *args, **kwargs: None)
    monkeypatch.setattr(dse_mod, "runtime_download_artifact", lambda *args, **kwargs: b"zip")
    monkeypatch.setattr(
        dse_mod,
        "runtime_extract_artifact",
        lambda data: {
            "sandbox/run.py": b"print(1)",
            "sandbox/_user_bridge.py": b"def serve(): pass",
            "sandbox/user_adapter.py": b"class X: pass\n",
        },
    )
    monkeypatch.setattr(dse_mod, "runtime_filter_requirements", lambda req, allow: "pytest\n")
    monkeypatch.setattr(
        ev,
        "_get_client",
        lambda: SimpleNamespace(
            download_code=lambda code_url, expected_checksum="": {
                "requirements.txt": "pytest\n",
                "solution.py": "def solve(x): return x",
            }
        ),
    )

    def _run_parallel_pairs(**kwargs):  # type: ignore[no-untyped-def]
        captured["user_files_bytes"] = kwargs["user_files_bytes"]
        captured["user_req_path"] = kwargs["user_req_path"]
        return [CaseResult(case_index=0, status=CaseStatus.PASSED, score=1, time_used=10)]

    monkeypatch.setattr(ev, "_run_parallel_pairs", _run_parallel_pairs)
    monkeypatch.setattr(
        dse_mod,
        "runtime_revoke_gateway_token",
        lambda *args, **kwargs: revoked.__setitem__("ok", True),
    )

    result = ev.evaluate(sub, parallel_cases=1)
    assert result.status == PhaseStatus.SUCCESS
    assert result.total_cases == 1
    assert result.passed_cases == 1
    assert result.total_time == 10
    assert revoked["ok"] is True

    user_files_bytes = captured["user_files_bytes"]
    assert isinstance(user_files_bytes, dict)
    assert user_files_bytes["requirements.txt"] == b"pytest\n"
    assert "_agent_wrapper.py" in user_files_bytes
    assert "_user_bridge.py" in user_files_bytes
    assert captured["user_req_path"] == "requirements.txt"


def test_evaluate_returns_error_when_requirements_missing(monkeypatch) -> None:
    cfg = _cfg(num_cases=1, parallel_cases=1)
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg, files={"solution.py": "def solve(x): return x"})

    monkeypatch.setattr(dse_mod, "runtime_create_gateway_token_for_user", lambda *args, **kwargs: None)
    monkeypatch.setattr(dse_mod, "runtime_download_artifact", lambda *args, **kwargs: b"zip")
    monkeypatch.setattr(
        dse_mod,
        "runtime_extract_artifact",
        lambda data: {
            "sandbox/run.py": b"print(1)",
            "sandbox/_user_bridge.py": b"def serve(): pass",
            "sandbox/user_adapter.py": b"class X: pass\n",
        },
    )
    monkeypatch.setattr(dse_mod, "runtime_revoke_gateway_token", lambda *args, **kwargs: None)

    result = ev.evaluate(sub, parallel_cases=1)
    assert result.status == PhaseStatus.ERROR
    assert "Missing requirements.txt" in (result.error or "")


def test_evaluate_uses_generated_bridge_from_config(monkeypatch) -> None:
    cfg = _cfg(num_cases=1, parallel_cases=1, adapter_preset="maze", solve_attr_name="solve")
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg)

    monkeypatch.setattr(dse_mod, "runtime_create_gateway_token_for_user", lambda *args, **kwargs: None)
    monkeypatch.setattr(dse_mod, "runtime_download_artifact", lambda *args, **kwargs: b"zip")
    monkeypatch.setattr(
        dse_mod,
        "runtime_extract_artifact",
        lambda data: {
            "sandbox/run.py": b"print(1)",
            # Artifact bridge has mismatched content, but evaluator should ignore it
            # and generate _user_bridge.py from phase_config.
            "sandbox/_user_bridge.py": b"def serve():\n    raise RuntimeError('wrong bridge')\n",
            "sandbox/user_adapter.py": b"class X: pass\n",
        },
    )
    monkeypatch.setattr(
        ev,
        "_run_parallel_pairs",
        lambda **kwargs: [CaseResult(case_index=0, status=CaseStatus.PASSED, score=1, time_used=10)],
    )
    monkeypatch.setattr(dse_mod, "runtime_revoke_gateway_token", lambda *args, **kwargs: None)

    result = ev.evaluate(sub, parallel_cases=1)
    assert result.status == PhaseStatus.SUCCESS


def test_evaluate_returns_error_when_bridge_uses_runtime_without_user_adapter(monkeypatch) -> None:
    cfg = _cfg(num_cases=1, parallel_cases=1, adapter_preset="maze", solve_attr_name="solve")
    ev = DualSandboxEvaluator(cfg)
    sub = _submission(cfg)

    monkeypatch.setattr(dse_mod, "runtime_create_gateway_token_for_user", lambda *args, **kwargs: None)
    monkeypatch.setattr(dse_mod, "runtime_download_artifact", lambda *args, **kwargs: b"zip")
    monkeypatch.setattr(
        dse_mod,
        "runtime_extract_artifact",
        lambda data: {
            "sandbox/run.py": b"print(1)",
            "sandbox/_user_bridge.py": b"def serve(): pass\n",
        },
    )
    monkeypatch.setattr(dse_mod, "runtime_revoke_gateway_token", lambda *args, **kwargs: None)

    result = ev.evaluate(sub, parallel_cases=1)
    assert result.status == PhaseStatus.ERROR
    assert "Missing sandbox/user_adapter.py" in (result.error or "")

