"""Tests for ieum.tools.basic -- Python REPL and SSRF protection.

Covers:
- Python REPL security (blocked patterns: os.system, subprocess, etc.)
- Python REPL safe code execution
- Python REPL output size limit
- Python REPL state persistence and reset
- SSRF protection via is_safe_url
- _check_repl_code_safety (pure function tests)
"""

import socket
from unittest.mock import patch

import pytest

from ieum.tools.basic import (
    _REPL_BLOCKED_PATTERNS,
    _REPL_MAX_OUTPUT_BYTES,
    _check_repl_code_safety,
    is_safe_url,
    python_repl,
)


# ---------------------------------------------------------------------------
# _check_repl_code_safety -- pure function
# ---------------------------------------------------------------------------


class TestCheckReplCodeSafety:
    """Unit tests for the code safety checker."""

    @pytest.mark.parametrize(
        "code",
        [
            "os.system('rm -rf /')",
            "os .system('ls')",
            "os. system('whoami')",
        ],
    )
    def test_os_system_blocked(self, code):
        reason = _check_repl_code_safety(code)
        assert reason is not None
        assert "os.system" in reason

    @pytest.mark.parametrize(
        "code",
        [
            "subprocess.run(['ls'])",
            "subprocess.Popen(['bash'])",
            "subprocess.call(['echo', 'hi'])",
            "subprocess.check_call(['make'])",
            "subprocess.check_output(['git', 'status'])",
        ],
    )
    def test_subprocess_calls_blocked(self, code):
        reason = _check_repl_code_safety(code)
        assert reason is not None
        assert "subprocess" in reason

    def test_eval_input_blocked(self):
        reason = _check_repl_code_safety("eval(input())")
        assert reason is not None
        assert "eval(input())" in reason

    def test_exec_open_blocked(self):
        reason = _check_repl_code_safety("exec(open('malicious.py'))")
        assert reason is not None
        assert "exec(open())" in reason

    def test_shutil_rmtree_root_blocked(self):
        reason = _check_repl_code_safety("shutil.rmtree('/')")
        assert reason is not None

    @pytest.mark.parametrize(
        "code",
        [
            "print('hello')",
            "import json",
            "x = 1 + 2",
            "data = [i**2 for i in range(10)]",
            "open('test.txt', 'r').read()",
            "os.path.exists('/tmp')",
            "result = os.getcwd()",
        ],
    )
    def test_safe_code_allowed(self, code):
        reason = _check_repl_code_safety(code)
        assert reason is None, f"Expected '{code}' to be allowed but got: {reason}"


# ---------------------------------------------------------------------------
# python_repl -- integration tests
# ---------------------------------------------------------------------------


class TestPythonRepl:
    """Integration tests for the Python REPL tool."""

    def test_simple_expression(self):
        result = python_repl("1 + 2")
        assert result["success"] is True
        assert result["result"] == "3"

    def test_print_output(self):
        result = python_repl("print('hello world')")
        assert result["success"] is True
        assert "hello world" in result["output"]

    def test_blocked_os_system(self):
        result = python_repl("os.system('ls')")
        assert result["success"] is False
        assert "Security policy" in result["error"]

    def test_blocked_subprocess_run(self):
        result = python_repl("subprocess.run(['ls'])")
        assert result["success"] is False
        assert "Security policy" in result["error"]

    def test_reset_clears_state(self):
        # Set a variable
        python_repl("test_var_reset = 42")
        # Reset
        result = python_repl("", reset=True)
        assert result["success"] is True
        assert "reset" in result.get("message", "").lower()
        # Variable should not exist
        result = python_repl("test_var_reset")
        assert result["success"] is False  # NameError expected

    def test_state_persists_between_calls(self):
        python_repl("", reset=True)  # clean slate
        python_repl("persistent_val = 99")
        result = python_repl("persistent_val + 1")
        assert result["success"] is True
        assert result["result"] == "100"
        python_repl("", reset=True)  # cleanup

    def test_empty_code_returns_success(self):
        result = python_repl("   ")
        assert result["success"] is True

    def test_syntax_error_reported(self):
        result = python_repl("def (broken")
        assert result["success"] is False
        assert "SyntaxError" in result["error"]

    def test_output_truncation(self):
        code = f"print('x' * {_REPL_MAX_OUTPUT_BYTES + 1000})"
        result = python_repl(code)
        assert result["success"] is True
        assert "truncated" in result["output"].lower()

    def test_markdown_fences_stripped(self):
        code = "```python\nprint('from_fence')\n```"
        result = python_repl(code)
        assert result["success"] is True
        assert "from_fence" in result["output"]

    def test_multiline_code(self):
        python_repl("", reset=True)
        code = """
def add(a, b):
    return a + b
"""
        result = python_repl(code)
        assert result["success"] is True
        result2 = python_repl("add(3, 4)")
        assert result2["result"] == "7"
        python_repl("", reset=True)


# ---------------------------------------------------------------------------
# is_safe_url -- SSRF protection
# ---------------------------------------------------------------------------


class TestIsSafeUrl:
    """Test the SSRF protection function."""

    def test_empty_url(self):
        assert is_safe_url("") is False

    def test_no_hostname(self):
        assert is_safe_url("just-a-string") is False

    @pytest.mark.parametrize(
        "url",
        [
            "http://127.0.0.1/admin",
            "http://localhost/secret",
            "http://10.0.0.1/internal",
            "http://172.16.0.1/private",
            "http://192.168.1.1/router",
        ],
    )
    def test_private_addresses_blocked(self, url):
        """Private/loopback addresses must be blocked."""
        # Mock socket.getaddrinfo to return predictable results
        ip = url.split("//")[1].split("/")[0]
        # For localhost, map to 127.0.0.1
        if ip == "localhost":
            ip = "127.0.0.1"

        mock_result = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (ip, 0))]
        with patch("ieum.tools.basic.socket.getaddrinfo", return_value=mock_result):
            assert is_safe_url(url) is False

    def test_public_address_allowed(self):
        """Public addresses should be allowed."""
        mock_result = [
            (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("93.184.216.34", 0))
        ]
        with patch("ieum.tools.basic.socket.getaddrinfo", return_value=mock_result):
            assert is_safe_url("http://example.com") is True

    def test_dns_failure_blocked(self):
        """DNS resolution failure should block the URL."""
        with patch(
            "ieum.tools.basic.socket.getaddrinfo",
            side_effect=socket.gaierror("DNS lookup failed"),
        ):
            assert is_safe_url("http://nonexistent.invalid") is False
