"""Tests for the code execution engine (executor.py).

Tests cover:
- build_python_harness: harness structure and correct execution behaviour
- parse_execution_result: all result variants (success, timeout, error, no JSON)
- detect_runtime: python detection and fallback logic
- run_code_local: integration via actual subprocess (python3 must be available)
"""

import json
import subprocess
import sys
from unittest.mock import patch

import pytest

from skilark_cli.executor import (
    build_python_harness,
    detect_runtime,
    parse_execution_result,
    run_code_local,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_SIMPLE_PROBLEM = {
    "function_name": "add",
    "test_cases": [
        # expected_output stores native Python types (int here) so the harness
        # can compare directly without any string coercion.
        {"input": {"a": 1, "b": 2}, "expected_output": 3, "hidden": False},
        {"input": {"a": 10, "b": 20}, "expected_output": 30, "hidden": False},
    ],
}

_CORRECT_SOLUTION = "def add(a, b):\n    return a + b\n"
_WRONG_SOLUTION = "def add(a, b):\n    return a - b\n"
_CRASHING_SOLUTION = "def add(a, b):\n    raise ValueError('oops')\n"


# ---------------------------------------------------------------------------
# build_python_harness
# ---------------------------------------------------------------------------


class TestBuildPythonHarness:
    def test_returns_string(self):
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        assert isinstance(harness, str)

    def test_harness_contains_user_code(self):
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        assert "def add(a, b):" in harness

    def test_harness_contains_json_output(self):
        """Harness must print JSON so parse_execution_result can read it."""
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        # The harness should use json.dumps somewhere
        assert "json" in harness.lower()

    def test_harness_is_valid_python(self):
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        # Should compile without syntax error
        compile(harness, "<harness>", "exec")

    def test_harness_executes_correct_answer(self):
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = subprocess.run(
            [sys.executable, "-c", harness],
            capture_output=True,
            text=True,
            timeout=10,
        )
        data = json.loads(result.stdout)
        assert data["all_passed"] is True
        assert data["passed_count"] == 2
        assert data["total_count"] == 2

    def test_harness_detects_wrong_answer(self):
        harness = build_python_harness(
            _WRONG_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = subprocess.run(
            [sys.executable, "-c", harness],
            capture_output=True,
            text=True,
            timeout=10,
        )
        data = json.loads(result.stdout)
        assert data["all_passed"] is False
        assert data["passed_count"] == 0

    def test_harness_catches_exception_per_case(self):
        harness = build_python_harness(
            _CRASHING_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = subprocess.run(
            [sys.executable, "-c", harness],
            capture_output=True,
            text=True,
            timeout=10,
        )
        data = json.loads(result.stdout)
        assert data["all_passed"] is False
        # Each result should have passed=False
        for case_result in data["results"]:
            assert case_result["passed"] is False

    def test_harness_marks_hidden_test_cases(self):
        test_cases_with_hidden = [
            {"input": {"a": 1, "b": 2}, "expected_output": 3, "hidden": False},
            {"input": {"a": 5, "b": 5}, "expected_output": 10, "hidden": True},
        ]
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            "add",
            test_cases_with_hidden,
        )
        result = subprocess.run(
            [sys.executable, "-c", harness],
            capture_output=True,
            text=True,
            timeout=10,
        )
        data = json.loads(result.stdout)
        assert data["results"][0]["hidden"] is False
        assert data["results"][1]["hidden"] is True

    def test_harness_result_shape(self):
        """Each result dict must have the expected keys."""
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = subprocess.run(
            [sys.executable, "-c", harness],
            capture_output=True,
            text=True,
            timeout=10,
        )
        data = json.loads(result.stdout)
        for case_result in data["results"]:
            assert "case" in case_result
            assert "passed" in case_result
            assert "expected" in case_result
            assert "actual" in case_result
            assert "time_ms" in case_result
            assert "hidden" in case_result


# ---------------------------------------------------------------------------
# parse_execution_result
# ---------------------------------------------------------------------------


class TestParseExecutionResult:
    def test_parses_valid_json_stdout(self):
        payload = {
            "results": [{"case": 0, "passed": True, "expected": "3", "actual": "3", "time_ms": 0.5, "hidden": False}],
            "passed_count": 1,
            "total_count": 1,
            "all_passed": True,
        }
        stdout = json.dumps(payload)
        result = parse_execution_result(stdout, "", 0)
        assert result["all_passed"] is True
        assert result["passed_count"] == 1

    def test_timeout_returns_error_result(self):
        result = parse_execution_result("", "", 0, timed_out=True)
        assert result["all_passed"] is False
        assert "timed out" in result.get("error", "").lower()

    def test_nonzero_return_code_with_no_json(self):
        """A compile error produces no JSON stdout; should return an error dict."""
        result = parse_execution_result("", "SyntaxError: invalid syntax", 1)
        assert result["all_passed"] is False
        assert result.get("error") is not None

    def test_no_json_in_stdout(self):
        """If stdout has no JSON, treat as execution error."""
        result = parse_execution_result("not json at all", "", 0)
        assert result["all_passed"] is False
        assert result.get("error") is not None

    def test_valid_result_preserves_results_list(self):
        payload = {
            "results": [
                {"case": 0, "passed": True, "expected": "1", "actual": "1", "time_ms": 1.0, "hidden": False},
                {"case": 1, "passed": False, "expected": "2", "actual": "0", "time_ms": 0.8, "hidden": False},
            ],
            "passed_count": 1,
            "total_count": 2,
            "all_passed": False,
        }
        result = parse_execution_result(json.dumps(payload), "", 0)
        assert len(result["results"]) == 2
        assert result["total_count"] == 2


# ---------------------------------------------------------------------------
# detect_runtime
# ---------------------------------------------------------------------------


class TestDetectRuntime:
    def test_python_detects_local(self):
        # Python should always be available in the test environment
        runtime = detect_runtime("python")
        assert runtime in ("local", "piston", "unavailable")

    def test_python_with_available_interpreter(self):
        # patch shutil.which to simulate python3 available
        with patch("shutil.which", return_value="/usr/bin/python3"):
            runtime = detect_runtime("python")
        assert runtime == "local"

    def test_python_always_local(self):
        # Python detection now uses sys.executable (always valid) rather than
        # shutil.which("python3"), so it returns 'local' regardless of PATH.
        with patch("shutil.which", return_value=None):
            runtime = detect_runtime("python")
        assert runtime == "local"

    def test_java_without_jvm_falls_back(self):
        with patch("shutil.which", return_value=None):
            runtime = detect_runtime("java")
        assert runtime in ("piston", "unavailable")

    def test_unknown_language_returns_unavailable_without_piston(self):
        with patch("shutil.which", return_value=None):
            runtime = detect_runtime("cobol")
        assert runtime == "unavailable"


# ---------------------------------------------------------------------------
# run_code_local (integration — requires python3)
# ---------------------------------------------------------------------------


class TestRunCodeLocal:
    def test_runs_correct_python_solution(self):
        harness = build_python_harness(
            _CORRECT_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = run_code_local("python", harness)
        assert result["all_passed"] is True

    def test_runs_wrong_python_solution(self):
        harness = build_python_harness(
            _WRONG_SOLUTION,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = run_code_local("python", harness)
        assert result["all_passed"] is False

    def test_handles_timeout(self):
        infinite_code = "def add(a, b):\n    while True: pass\n"
        harness = build_python_harness(
            infinite_code,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = run_code_local("python", harness, timeout=1)
        assert result["all_passed"] is False
        assert "timed out" in result.get("error", "").lower()

    def test_handles_syntax_error(self):
        bad_code = "def add(a, b):\n    return a +\n"
        harness = build_python_harness(
            bad_code,
            _SIMPLE_PROBLEM["function_name"],
            _SIMPLE_PROBLEM["test_cases"],
        )
        result = run_code_local("python", harness)
        assert result["all_passed"] is False
        assert result.get("error") is not None

    def test_harness_with_special_chars_in_input(self):
        """Inputs containing quotes and backslashes must not break the harness."""
        user_code = 'def fn(s):\n    return len(s)\n'
        test_cases = [
            {"input": {"s": 'he said "hello"'}, "expected_output": 15, "hidden": False},
            {"input": {"s": "back\\slash"}, "expected_output": 10, "hidden": False},
        ]
        harness = build_python_harness(user_code, "fn", test_cases)
        result = run_code_local("python", harness, timeout=5)
        assert result["all_passed"] is True

    def test_unsupported_language_returns_error(self):
        """Unsupported language returns error dict."""
        result = run_code_local("ruby", "puts 'hello'")
        assert result["all_passed"] is False
        assert "Unsupported" in result["error"]

    def test_clean_env_hides_ambient_vars(self):
        """Child process cannot see ambient environment variables."""
        import os
        sentinel = "SKILARK_TEST_SENTINEL_12345"
        os.environ[sentinel] = "secret"
        try:
            # Use harness: function checks if sentinel is absent from env
            user_code = f"import os\ndef check_env():\n    return os.environ.get('{sentinel}', 'not_found')\n"
            test_cases = [{"input": {}, "expected_output": "not_found", "hidden": False}]
            harness = build_python_harness(user_code, "check_env", test_cases)
            result = run_code_local("python", harness)
            assert result["all_passed"] is True, f"Sentinel var leaked to child: {result}"
        finally:
            del os.environ[sentinel]

    def test_injection_safe_base64_encoding(self):
        """Base64 encoding prevents injection from test case content."""
        cases = [
            {"input": {"s": 'he said "hello"'}, "expected_output": True, "hidden": False},
        ]
        harness = build_python_harness("def f(s): return True", "f", cases)
        assert '"hello"' not in harness.split("# ---- end user code ----")[1]
