import contextlib
import os
import subprocess
import sys
import json
import types
import uuid
from concurrent.futures import Future
from pathlib import Path

from smooth_criminal.cli import (
    handle_jam_test,
    _import_callable,
    _import_module_from_path,
    _split_func_path,
)
from smooth_criminal.core import _module_search_paths


def test_jam_test_cli_output():
    repo_root = Path(__file__).resolve().parent.parent
    cmd = [
        sys.executable,
        "-m",
        "smooth_criminal.cli",
        "jam-test",
        "tests.sample_funcs:compute",
        "--workers",
        "2",
        "--reps",
        "1",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root)
    output = result.stdout + result.stderr
    assert "thread" in output
    assert "process" in output
    assert "async" in output
    assert result.returncode == 0


def test_jam_test_cli_silent_json():
    repo_root = Path(__file__).resolve().parent.parent
    cmd = [
        sys.executable,
        "-m",
        "smooth_criminal.cli",
        "jam-test",
        "tests.sample_funcs:compute",
        "--workers",
        "2",
        "--reps",
        "1",
        "--silent",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root)
    assert result.stderr == ""
    data = json.loads(result.stdout)
    assert set(data["averages"].keys()) == {"thread", "process", "async"}
    assert result.returncode == 0


def test_jam_test_cli_message_non_silent():
    repo_root = Path(__file__).resolve().parent.parent
    cmd = [
        sys.executable,
        "-m",
        "smooth_criminal.cli",
        "jam-test",
        "tests.sample_funcs:compute",
        "--workers",
        "2",
        "--reps",
        "1",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root)
    output = result.stdout + result.stderr
    assert "🎶 Just jammin' through those CPU cores! 🧠🕺" in output
    assert result.returncode == 0


def test_jam_test_accepts_file_path_with_relative_import(tmp_path, capsys):
    package_name = f"pkg_{uuid.uuid4().hex}"
    package_dir = tmp_path / package_name
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "helpers.py").write_text(
        "def helper(x):\n    return x * 2\n", encoding="utf-8"
    )
    module_path = package_dir / "module.py"
    module_path.write_text(
        "from .helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        handle_jam_test(f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False)
    finally:
        for key in list(sys.modules.keys()):
            if key.startswith(package_name):
                sys.modules.pop(key, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert data["fastest"] in {"thread", "process", "async"}


def test_jam_test_accepts_relative_file_path(tmp_path, capsys, monkeypatch):
    package_dir = tmp_path / "pkg_rel"
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "helpers.py").write_text(
        "def helper(x):\n    return x + 1\n", encoding="utf-8"
    )
    module_path = package_dir / "module.py"
    module_path.write_text(
        "from .helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        monkeypatch.chdir(tmp_path)
        relative_spec = "./" + module_path.relative_to(tmp_path).as_posix()
        handle_jam_test(
            f"{relative_spec}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for key in list(sys.modules.keys()):
            if key.startswith("pkg_rel"):
                sys.modules.pop(key, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert set(data["averages"].keys()) == {"thread", "process", "async"}


def test_jam_test_handles_stdlib_name_collision(tmp_path, capsys):
    package_dir = tmp_path / "json"
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "helpers.py").write_text(
        "def helper(x):\n    return x * 3\n", encoding="utf-8"
    )
    module_path = package_dir / "module.py"
    module_path.write_text(
        "from .helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        handle_jam_test(
            f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for key in list(sys.modules.keys()):
            module = sys.modules.get(key)
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(key, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert data["fastest"] in {"thread", "process", "async"}

    import json as std_json

    assert callable(std_json.loads)


def test_jam_test_supports_namespace_packages(tmp_path, capsys):
    package_dir = tmp_path / "ns_pkg"
    inner = package_dir / "inner"
    inner.mkdir(parents=True)

    (package_dir / "helpers.py").write_text(
        "def helper(value):\n    return value - 1\n", encoding="utf-8"
    )
    module_path = inner / "module.py"
    module_path.write_text(
        "from ..helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        handle_jam_test(
            f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for name, module in list(sys.modules.items()):
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(name, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert data["fastest"] in {"thread", "process", "async"}


def test_jam_test_supports_mixed_namespace_packages(tmp_path, capsys):
    package_dir = tmp_path / "pkg_ns"
    inner = package_dir / "inner"
    inner.mkdir(parents=True)

    (package_dir / "helpers.py").write_text(
        "def helper(value):\n    return value * 5\n", encoding="utf-8"
    )
    (inner / "__init__.py").write_text("", encoding="utf-8")

    module_path = inner / "module.py"
    module_path.write_text(
        "from ..helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        handle_jam_test(
            f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for name, module in list(sys.modules.items()):
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(name, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert set(data["averages"].keys()) == {"thread", "process", "async"}


def test_jam_test_supports_split_namespace_roots(tmp_path, capsys):
    left_root = tmp_path / "left"
    right_root = tmp_path / "right"
    left_root.mkdir()
    right_root.mkdir()

    (left_root / "split_pkg").mkdir()
    inner_right = right_root / "split_pkg" / "inner"
    inner_right.mkdir(parents=True)

    (left_root / "split_pkg" / "helpers.py").write_text(
        "def helper(value):\n    return value * 3\n", encoding="utf-8"
    )

    module_path = inner_right / "module.py"
    module_path.write_text(
        "from ..helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        handle_jam_test(
            f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for name, module in list(sys.modules.items()):
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(name, None)

    captured = capsys.readouterr()
    data = json.loads(captured.out)
    assert data["fastest"] in {"thread", "process", "async"}


def test_module_search_paths_include_all_namespace_roots(tmp_path):
    left_root = tmp_path / "left"
    right_root = tmp_path / "right"
    left_root.mkdir()
    right_root.mkdir()

    (left_root / "split_pkg").mkdir()
    (right_root / "split_pkg" / "inner").mkdir(parents=True)
    (left_root / "split_pkg" / "helpers.py").write_text(
        "def helper(value):\n    return value * 7\n", encoding="utf-8"
    )

    module_path = right_root / "split_pkg" / "inner" / "module.py"
    module_path.write_text(
        "from ..helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    try:
        with _import_module_from_path(str(module_path)) as module:
            paths = _module_search_paths(module)
            normalized = {Path(p) for p in paths}
            assert right_root in normalized
            assert left_root in normalized
    finally:
        for name, module in list(sys.modules.items()):
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(name, None)


def test_split_func_path_handles_windows_drive():
    module, attr = _split_func_path(r"C:\\ruta\\al\\modulo.py:target")
    assert module == r"C:\\ruta\\al\\modulo.py"
    assert attr == "target"


def test_import_callable_uses_full_windows_path(monkeypatch):
    captured: dict[str, str] = {}

    @contextlib.contextmanager
    def fake_import(module_path: str):
        captured["module_path"] = module_path
        yield types.SimpleNamespace(target="ok")

    monkeypatch.setattr("smooth_criminal.cli._import_module_from_path", fake_import)

    with _import_callable(r"C:\\tmp\\pkg\\module.py:target") as attr:
        assert attr == "ok"

    assert captured["module_path"] == r"C:\\tmp\\pkg\\module.py"


def test_import_module_from_path_preserves_package_name(tmp_path):
    package_root = tmp_path / "outer" / "inner" / "real_pkg"
    package_root.mkdir(parents=True)
    (package_root / "__init__.py").write_text("", encoding="utf-8")
    module_path = package_root / "mod.py"
    module_path.write_text("value = 7\n", encoding="utf-8")

    with _import_module_from_path(str(module_path)) as module:
        assert module.__name__ == "real_pkg.mod"
        assert module.value == 7


def test_import_module_from_path_supports_lazy_relative_imports(tmp_path):
    pkg_dir = tmp_path / "pkg"
    inner_dir = pkg_dir / "inner"
    pkg_dir.mkdir()
    inner_dir.mkdir()

    (pkg_dir / "__init__.py").write_text("", encoding="utf-8")
    (inner_dir / "__init__.py").write_text("", encoding="utf-8")
    (pkg_dir / "helpers.py").write_text(
        "def helper(value):\n    return value + 1\n", encoding="utf-8"
    )

    module_path = inner_dir / "module.py"
    module_path.write_text(
        "def compute(value):\n"
        "    from .. import helpers\n"
        "    return helpers.helper(value)\n",
        encoding="utf-8",
    )

    with _import_module_from_path(str(module_path)) as module:
        assert module.__name__ == "pkg.inner.module"
        assert module.compute(2) == 3


def test_import_module_from_path_handles_stdlib_name_collisions(tmp_path):
    import json.encoder as std_encoder

    package_dir = tmp_path / "json"
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "encoder.py").write_text(
        "def helper(x):\n    return x + 5\n", encoding="utf-8"
    )

    module_path = package_dir / "module.py"
    module_path.write_text(
        "from .encoder import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    with _import_module_from_path(str(module_path)) as module:
        assert module.__name__ == "json.module"
        assert module.target(3) == 8

    import json.encoder as restored_encoder

    assert restored_encoder is std_encoder
    assert not hasattr(restored_encoder, "helper")


def test_jam_test_process_backend_updates_pythonpath(tmp_path, capsys, monkeypatch):
    package_dir = tmp_path / "pkg_env"
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "helpers.py").write_text(
        "def helper(value):\n    return value + 1\n", encoding="utf-8"
    )
    module_path = package_dir / "mod.py"
    module_path.write_text(
        "from .helpers import helper\n\n\n"
        "def target(value):\n    return helper(value)\n",
        encoding="utf-8",
    )

    original_pythonpath = os.environ.get("PYTHONPATH")
    captured: dict[str, str | None] = {}

    class FakeExecutor:
        def __init__(self, *args, **kwargs):
            pass

        def __enter__(self):
            captured["pythonpath"] = os.environ.get("PYTHONPATH")

            class _Executor:
                def submit(self, fn, *args, **kwargs):
                    future: Future = Future()
                    future.set_result([])
                    return future

            return _Executor()

        def __exit__(self, exc_type, exc, tb):
            return False

    monkeypatch.setattr("smooth_criminal.core.ProcessPoolExecutor", FakeExecutor)
    monkeypatch.setattr("smooth_criminal.core.as_completed", lambda futures: futures)

    try:
        handle_jam_test(
            f"{module_path}:target", workers=1, reps=1, silent=True, mj_mode=False
        )
    finally:
        for name, module in list(sys.modules.items()):
            module_file = getattr(module, "__file__", "")
            if module_file and str(tmp_path) in module_file:
                sys.modules.pop(name, None)

    captured_output = capsys.readouterr()
    data = json.loads(captured_output.out)
    assert data["fastest"] in {"thread", "process", "async"}

    assert "pythonpath" in captured
    pythonpath_value = captured["pythonpath"] or ""
    env_paths = pythonpath_value.split(os.pathsep) if pythonpath_value else []
    assert str(tmp_path) in env_paths
    assert os.environ.get("PYTHONPATH") == original_pythonpath
