from __future__ import annotations

import copy
import inspect
from unittest.mock import patch

import pytest

from hessian_recipes import agent_rl_tasks as tasks
from hessian_recipes.gym_client import (GymInfrastructureError, RemoteGymEnv,
                                       _base_url, fetch_gym_identity)


def task():
    return tasks.generate_tasks("train", 1, seed=42)[0]["task"]


def test_only_policy_tools_and_reset_are_public_methods():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    methods = {name for name, member in inspect.getmembers(env, inspect.ismethod)
               if not name.startswith("_")}
    assert methods == {"reset", "lookup", "calculator"}
    for name in ("lookup", "calculator"):
        assert inspect.signature(getattr(env, name)) == inspect.signature(getattr(tasks.GermanAgentEnv(), name))
        assert getattr(env, name).__doc__ == getattr(tasks.GermanAgentEnv(), name).__doc__


@pytest.mark.parametrize("url", ["https://127.0.0.1:8000", "http://example.com:80",
                                 "http://127.0.0.1", "http://user@localhost:8000",
                                 "http://localhost:8000/path", "http://localhost:8000?x=y"])
def test_remote_or_ambiguous_service_origins_are_rejected(url):
    with pytest.raises(ValueError, match="loopback"):
        _base_url(url)


def test_wrong_native_gym_revision_is_rejected():
    with patch("hessian_recipes.gym_client._request", return_value={"backend": "fixture"}):
        with pytest.raises(GymInfrastructureError, match="identity mismatch"):
            fetch_gym_identity("http://127.0.0.1:8000")


def test_tool_rpc_error_remains_fatal_when_tool_loop_catches_it():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    with patch("hessian_recipes.gym_client._request", return_value={"session_id": "episode-one"}):
        env.reset(task=task())
    with patch("hessian_recipes.gym_client._request", side_effect=GymInfrastructureError("service unavailable")):
        with pytest.raises(GymInfrastructureError):
            env.lookup(task()["record_id"])
    with pytest.raises(GymInfrastructureError, match="service unavailable"):
        env._score("<answer>1</answer>")
    with pytest.raises(GymInfrastructureError, match="service unavailable"):
        env.reset(task=task())


def test_forged_observation_is_rejected_by_independent_replay():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    with patch("hessian_recipes.gym_client._request", return_value={"session_id": "episode-one"}):
        env.reset(task=task())
    with patch("hessian_recipes.gym_client._request", return_value={"observation": '{"quantity": 999}'}):
        env.lookup(task()["record_id"])
    with patch("hessian_recipes.gym_client._request", return_value={"score": {}}):
        with pytest.raises(GymInfrastructureError, match="observation differs"):
            env._score("<answer>1</answer>")


def test_forged_reward_cannot_override_incomplete_workflow():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    local = tasks.GermanAgentEnv()
    local.reset(task=task())
    score = tasks.score_episode(local, "<answer>1</answer>")
    forged = copy.deepcopy(score) | {"success": True, "reward": 1.0}
    with patch("hessian_recipes.gym_client._request", return_value={"session_id": "episode-one"}):
        env.reset(task=task())
    with patch("hessian_recipes.gym_client._request", return_value={"score": forged}):
        with pytest.raises(GymInfrastructureError, match="verifier differs"):
            env._score("<answer>1</answer>")


def test_normal_scored_tool_error_stays_a_task_failure():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    local = tasks.GermanAgentEnv()
    local.reset(task=task())
    with patch("hessian_recipes.gym_client._request", return_value={"session_id": "episode-one"}):
        env.reset(task=task())
    observation = local.lookup("wrong-record")
    with patch("hessian_recipes.gym_client._request", return_value={"observation": observation}):
        assert env.lookup("wrong-record") == observation
    score = tasks.score_episode(local, "<answer>1</answer>")
    with patch("hessian_recipes.gym_client._request", return_value={"score": score}):
        assert env._score("<answer>1</answer>")["reward"] == 0
    assert env._infrastructure_error is None


def test_close_after_failed_tool_call_preserves_primary_failure():
    env = RemoteGymEnv("http://127.0.0.1:8000", {})
    with patch("hessian_recipes.gym_client._request", return_value={"session_id": "episode-one"}):
        env.reset(task=task())
    primary = GymInfrastructureError("lost tool reply")
    env._infrastructure_error = primary
    with patch("hessian_recipes.gym_client._request", side_effect=GymInfrastructureError("close unavailable")):
        with pytest.raises(GymInfrastructureError) as error:
            env._close()
    assert error.value is primary
