use std::path::PathBuf;
use std::time::Duration;

use python_exec_mcp::executor::{self, ExecConfig};

fn config() -> ExecConfig {
    ExecConfig {
        binary: "python3".to_string(),
        timeout: Duration::from_secs(10),
        workdir: None,
        deny_modules: vec![],
        restrict_path: false,
        deny_paths: vec![],
    }
}

fn micropython_config() -> ExecConfig {
    ExecConfig {
        binary: "micropython".to_string(),
        timeout: Duration::from_secs(10),
        workdir: None,
        deny_modules: vec![],
        restrict_path: false,
        deny_paths: vec![],
    }
}

// =============================================================================
// Basic execution
// =============================================================================

#[tokio::test]
async fn execute_simple_print() {
    let result = executor::execute("print('hello')", &config())
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "hello");
    assert_eq!(result.exit_code, 0);
}

#[tokio::test]
async fn execute_arithmetic() {
    let result = executor::execute("print(2 + 3)", &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "5");
}

#[tokio::test]
async fn execute_multiline() {
    let script = r#"
x = [1, 2, 3]
y = [i * 2 for i in x]
print(y)
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "[2, 4, 6]");
}

#[tokio::test]
async fn execute_no_output() {
    let result = executor::execute("x = 42", &config()).await.unwrap();
    assert!(result.stdout.is_empty());
    assert_eq!(result.exit_code, 0);
}

// =============================================================================
// Regex (python3 vs micropython difference)
// =============================================================================

#[tokio::test]
async fn execute_regex_findall() {
    let script = r#"
import re
print(re.findall(r'\d+', 'a1b22c333'))
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "['1', '22', '333']");
    assert_eq!(result.exit_code, 0);
}

// =============================================================================
// Error handling
// =============================================================================

#[tokio::test]
async fn execute_syntax_error() {
    let result = executor::execute("def f(", &config()).await.unwrap();
    assert_ne!(result.exit_code, 0);
    assert!(result.stderr.contains("SyntaxError"));
}

#[tokio::test]
async fn execute_runtime_error() {
    let result = executor::execute("raise ValueError('boom')", &config())
        .await
        .unwrap();
    assert_ne!(result.exit_code, 0);
    assert!(result.stderr.contains("ValueError"));
    assert!(result.stderr.contains("boom"));
}

#[tokio::test]
async fn execute_stderr_output() {
    let script = "import sys; print('err', file=sys.stderr)";
    let result = executor::execute(script, &config()).await.unwrap();
    assert!(result.stderr.contains("err"));
}

// =============================================================================
// Sandbox: os dangerous attrs removed
// =============================================================================

#[tokio::test]
async fn sandbox_os_system_removed() {
    let script = "import os; print(hasattr(os, 'system'))";
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "False");
}

#[tokio::test]
async fn sandbox_os_popen_removed() {
    let script = "import os; print(hasattr(os, 'popen'))";
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "False");
}

#[tokio::test]
async fn sandbox_os_fork_removed() {
    let script = "import os; print(hasattr(os, 'fork'))";
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "False");
}

#[tokio::test]
async fn sandbox_os_kill_removed() {
    let script = "import os; print(hasattr(os, 'kill'))";
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "False");
}

// =============================================================================
// Sandbox: module blocking
// =============================================================================

#[tokio::test]
async fn sandbox_subprocess_blocked() {
    let script = r#"
try:
    import subprocess
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn sandbox_shutil_blocked() {
    let script = r#"
try:
    import shutil
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn sandbox_custom_deny() {
    let mut cfg = config();
    cfg.deny_modules = vec!["urllib".to_string(), "http".to_string()];

    let script = r#"
try:
    import urllib
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn sandbox_ctypes_blocked() {
    let script = r#"
try:
    import ctypes
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn sandbox_multiprocessing_blocked() {
    let script = r#"
try:
    import multiprocessing
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn sandbox_signal_blocked() {
    let script = r#"
try:
    import signal
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

// =============================================================================
// Bypass prevention: importlib, __import__, reload
// =============================================================================

#[tokio::test]
async fn bypass_importlib_import_module() {
    let script = r#"
try:
    import importlib
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn bypass_importlib_subprocess() {
    let script = r#"
try:
    __builtins__.__import__('importlib').import_module('subprocess')
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn bypass_builtins_import_subprocess() {
    let script = r#"
try:
    __builtins__.__import__('subprocess')
    print("LEAKED")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn bypass_reload_os() {
    // importlib itself is blocked, so reload is unreachable
    let script = r#"
try:
    import importlib
    import os
    importlib.reload(os)
    print("LEAKED" if hasattr(os, 'system') else "SAFE")
except (ImportError, ModuleNotFoundError):
    print("BLOCKED")
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn safe_import_still_works() {
    // Non-denied modules must still be importable
    let script = r#"
import json
print(json.dumps({"ok": True}))
"#;
    let result = executor::execute(script, &config()).await.unwrap();
    assert_eq!(result.stdout.trim(), r#"{"ok": true}"#);
}

// =============================================================================
// Sandbox: NOT applied to micropython
// =============================================================================

#[tokio::test]
async fn micropython_no_sandbox_preamble() {
    // micropython doesn't have subprocess, so this just verifies
    // the preamble itself doesn't cause errors on micropython
    let result = executor::execute("print('ok')", &micropython_config()).await;
    match result {
        Ok(r) => assert_eq!(r.stdout.trim(), "ok"),
        Err(e) => {
            // micropython may not be installed — skip
            let msg = e.to_string();
            assert!(msg.contains("not found"), "unexpected error: {msg}");
        }
    }
}

// =============================================================================
// Timeout
// =============================================================================

#[tokio::test]
async fn execute_timeout() {
    let mut cfg = config();
    cfg.timeout = Duration::from_secs(2);

    let script = "import time; time.sleep(10)";
    let result = executor::execute(script, &cfg).await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("timed out"));
}

// =============================================================================
// Workdir
// =============================================================================

#[tokio::test]
async fn execute_with_workdir() {
    let dir = tempfile::tempdir().unwrap();
    let mut cfg = config();
    cfg.workdir = Some(dir.path().to_path_buf());

    let script = "import os; print(os.getcwd())";
    let result = executor::execute(script, &cfg).await.unwrap();
    // tempdir may have symlinks, compare canonical
    let expected = std::fs::canonicalize(dir.path()).unwrap();
    let actual = PathBuf::from(result.stdout.trim());
    let actual_canon = std::fs::canonicalize(&actual).unwrap();
    assert_eq!(actual_canon, expected);
}

// =============================================================================
// execute_file
// =============================================================================

#[tokio::test]
async fn execute_file_basic() {
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("test.py");
    std::fs::write(&script_path, "print('from file')").unwrap();

    let result = executor::execute_file(&script_path, &config())
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "from file");
}

#[tokio::test]
async fn execute_file_sandbox_applied() {
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("test.py");
    std::fs::write(&script_path, "import os; print(hasattr(os, 'system'))").unwrap();

    let result = executor::execute_file(&script_path, &config())
        .await
        .unwrap();
    assert_eq!(result.stdout.trim(), "False");
}

#[tokio::test]
async fn execute_file_not_found() {
    let result = executor::execute_file(&PathBuf::from("/nonexistent.py"), &config()).await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("file not found"));
}

// =============================================================================
// restrict_path
// =============================================================================

#[tokio::test]
async fn restrict_path_allows_file_inside_workdir() {
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("ok.py");
    std::fs::write(&script_path, "print('inside')").unwrap();

    let mut cfg = config();
    cfg.workdir = Some(dir.path().to_path_buf());
    cfg.restrict_path = true;

    let result = executor::execute_file(&script_path, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "inside");
}

#[tokio::test]
async fn restrict_path_blocks_file_outside_workdir() {
    let workdir = tempfile::tempdir().unwrap();
    let other = tempfile::tempdir().unwrap();
    let script_path = other.path().join("evil.py");
    std::fs::write(&script_path, "print('escaped')").unwrap();

    let mut cfg = config();
    cfg.workdir = Some(workdir.path().to_path_buf());
    cfg.restrict_path = true;

    let result = executor::execute_file(&script_path, &cfg).await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("outside workdir"));
}

#[tokio::test]
async fn restrict_path_off_allows_any_path() {
    let workdir = tempfile::tempdir().unwrap();
    let other = tempfile::tempdir().unwrap();
    let script_path = other.path().join("ok.py");
    std::fs::write(&script_path, "print('free')").unwrap();

    let mut cfg = config();
    cfg.workdir = Some(workdir.path().to_path_buf());
    cfg.restrict_path = false;

    let result = executor::execute_file(&script_path, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "free");
}

#[tokio::test]
async fn restrict_path_without_workdir_errors() {
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("test.py");
    std::fs::write(&script_path, "print('x')").unwrap();

    let mut cfg = config();
    cfg.workdir = None;
    cfg.restrict_path = true;

    let result = executor::execute_file(&script_path, &cfg).await;
    assert!(result.is_err());
    assert!(
        result
            .unwrap_err()
            .to_string()
            .contains("workdir is not set")
    );
}

// =============================================================================
// deny_paths
// =============================================================================

#[tokio::test]
async fn deny_paths_blocks_read() {
    let dir = tempfile::tempdir().unwrap();
    let secret = dir.path().join("secret.txt");
    std::fs::write(&secret, "top secret").unwrap();

    let mut cfg = config();
    cfg.deny_paths = vec![dir.path().to_path_buf()];

    let script = format!(
        "try:\n    open('{}')\n    print('LEAKED')\nexcept PermissionError:\n    print('BLOCKED')",
        secret.display()
    );
    let result = executor::execute(&script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn deny_paths_blocks_write() {
    let dir = tempfile::tempdir().unwrap();
    let target = dir.path().join("output.txt");

    let mut cfg = config();
    cfg.deny_paths = vec![dir.path().to_path_buf()];

    let script = format!(
        "try:\n    open('{}', 'w')\n    print('LEAKED')\nexcept PermissionError:\n    print('BLOCKED')",
        target.display()
    );
    let result = executor::execute(&script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn deny_paths_allows_non_blocked_path() {
    let blocked = tempfile::tempdir().unwrap();
    let allowed = tempfile::tempdir().unwrap();
    let file = allowed.path().join("ok.txt");
    std::fs::write(&file, "hello").unwrap();

    let mut cfg = config();
    cfg.deny_paths = vec![blocked.path().to_path_buf()];

    let script = format!("print(open('{}').read())", file.display());
    let result = executor::execute(&script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "hello");
}

#[tokio::test]
async fn deny_paths_prefix_match() {
    let dir = tempfile::tempdir().unwrap();
    let subdir = dir.path().join("sub");
    std::fs::create_dir(&subdir).unwrap();
    let file = subdir.join("nested.txt");
    std::fs::write(&file, "data").unwrap();

    let mut cfg = config();
    cfg.deny_paths = vec![dir.path().to_path_buf()];

    let script = format!(
        "try:\n    open('{}')\n    print('LEAKED')\nexcept PermissionError:\n    print('BLOCKED')",
        file.display()
    );
    let result = executor::execute(&script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "BLOCKED");
}

#[tokio::test]
async fn deny_paths_empty_no_guard() {
    // With no deny_paths, open() works normally
    let dir = tempfile::tempdir().unwrap();
    let file = dir.path().join("normal.txt");
    std::fs::write(&file, "content").unwrap();

    let cfg = config(); // deny_paths is empty

    let script = format!("print(open('{}').read())", file.display());
    let result = executor::execute(&script, &cfg).await.unwrap();
    assert_eq!(result.stdout.trim(), "content");
}
