"""
Tests for SafeExecutor (PR3): path validation, env filtering, subprocess mocking, timeout, exit_code.
All tests use monkeypatched subprocess — no real scripts executed.
"""
from __future__ import annotations

import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest


def test_path_traversal_rejected():
    """Script path with .. is rejected."""
    import app.safe_executor as se
    with pytest.raises(se.ScriptNotAllowedError, match="traversal"):
        se._validate_script_path("ops/../etc/passwd")


def test_absolute_path_rejected():
    """Absolute script path is rejected."""
    import app.safe_executor as se
    with pytest.raises(se.ScriptNotAllowedError, match="Absolute"):
        se._validate_script_path("/bin/bash")


def test_non_allowlisted_script_rejected():
    """Unknown script path is rejected even if no traversal."""
    import app.safe_executor as se
    with pytest.raises(se.ScriptNotAllowedError, match="allowlist"):
        se._validate_script_path("ops/custom_hack.sh")


def test_allowlisted_script_resolves():
    """Allowlisted script resolves to a Path under REPO_ROOT without error."""
    import app.safe_executor as se
    resolved = se._validate_script_path("ops/preflight_sofiia_console.sh")
    assert resolved.name == "preflight_sofiia_console.sh"
    assert str(se._REPO_ROOT) in str(resolved)


def test_env_filter_drops_non_allowlisted_keys():
    """Non-allowlisted env keys are dropped and recorded."""
    import app.safe_executor as se
    filtered, dropped = se._filter_env({
        "STRICT": "1",
        "LD_PRELOAD": "/evil.so",
        "PYTHONPATH": "/hack",
        "SOFIIA_URL": "http://localhost:8002",
    })
    assert "STRICT" in filtered
    assert "SOFIIA_URL" in filtered
    assert "LD_PRELOAD" not in filtered
    assert "PYTHONPATH" not in filtered
    assert "LD_PRELOAD" in dropped
    assert "PYTHONPATH" in dropped


def test_env_filter_empty():
    """Empty env dict produces empty filtered + no dropped."""
    import app.safe_executor as se
    filtered, dropped = se._filter_env({})
    assert filtered == {}
    assert dropped == []


def _fake_proc(returncode: int = 0, stdout: bytes = b"ok", stderr: bytes = b"") -> MagicMock:
    """Return a mock process where communicate() returns (stdout, stderr)."""
    proc = MagicMock()
    proc.returncode = returncode
    proc.kill = MagicMock()
    proc.wait = AsyncMock(return_value=None)

    async def _communicate():
        return stdout, stderr

    proc.communicate = _communicate
    return proc


def test_run_script_success(sofiia_module, monkeypatch):
    """Allowlisted script with exit_code=0 returns ok=True, exit_code=0, stdout_tail."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()

    async def run():
        with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
            mock_exec.return_value = _fake_proc(returncode=0, stdout=b"PASS\n", stderr=b"")
            return await se.run_script("ops/preflight_sofiia_console.sh", env={"STRICT": "1"})

    result = loop.run_until_complete(run())
    assert result["ok"] is True
    assert result["exit_code"] == 0
    assert "PASS" in result["stdout_tail"]
    assert result["timeout"] is False


def test_run_script_nonzero_exit(sofiia_module, monkeypatch):
    """Script with exit_code=1 returns ok=False, exit_code=1."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()

    async def run():
        with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
            mock_exec.return_value = _fake_proc(returncode=1, stderr=b"FAIL\n")
            return await se.run_script("ops/preflight_sofiia_console.sh")

    result = loop.run_until_complete(run())
    assert result["ok"] is False
    assert result["exit_code"] == 1
    assert result["timeout"] is False


def test_run_script_timeout(sofiia_module, monkeypatch):
    """Script that hangs beyond timeout returns timeout=True, ok=False."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()

    async def run():
        async def slow_proc(*args, **kwargs):
            proc = MagicMock()
            proc.kill = MagicMock()
            proc.wait = AsyncMock(return_value=None)

            async def _hang():
                await asyncio.sleep(9999)
                return b"", b""

            proc.communicate = _hang
            return proc

        with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
            mock_exec.side_effect = slow_proc
            return await se.run_script("ops/preflight_sofiia_console.sh", timeout_s=1)

    result = loop.run_until_complete(run())
    assert result["timeout"] is True
    assert result["ok"] is False


def test_run_script_timeout_clamped(sofiia_module, monkeypatch):
    """Timeout values above _TIMEOUT_MAX_S are clamped."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()
    captured_timeout: list = []

    async def run():
        async def fake_wait_for(coro, timeout):
            captured_timeout.append(timeout)
            return await asyncio.wait_for(coro, timeout=999)

        with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
            mock_exec.return_value = _fake_proc(returncode=0)
            with patch("asyncio.wait_for", side_effect=fake_wait_for):
                return await se.run_script(
                    "ops/preflight_sofiia_console.sh",
                    timeout_s=9999,
                )

    loop.run_until_complete(run())
    if captured_timeout:
        assert captured_timeout[0] <= se._TIMEOUT_MAX_S


def test_run_script_env_key_not_in_allowlist_dropped(sofiia_module, monkeypatch):
    """Non-allowlisted env keys are dropped; result still has dropped_env_keys."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()

    async def run():
        with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
            mock_exec.return_value = _fake_proc(returncode=0)
            return await se.run_script(
                "ops/preflight_sofiia_console.sh",
                env={"STRICT": "1", "EVIL_KEY": "bad"},
            )

    result = loop.run_until_complete(run())
    assert result.get("dropped_env_keys") is not None
    assert "EVIL_KEY" in result["dropped_env_keys"]


def test_run_script_non_allowlisted_returns_error(sofiia_module):
    """Non-allowlisted script returns ok=False without executing anything."""
    import app.safe_executor as se

    loop = asyncio.get_event_loop()

    async def run():
        return await se.run_script("ops/some_random_script.sh")

    result = loop.run_until_complete(run())
    assert result["ok"] is False
    assert "allowlist" in result.get("error", "").lower()
