# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

import os
import platform
import re
from functools import partial
from pathlib import Path
from urllib.error import URLError

import pytest

import mne
import mne.utils.config
from mne.utils import (
    ClosingStringIO,
    _get_stim_channel,
    get_config,
    get_config_path,
    get_subjects_dir,
    requires_good_network,
    set_config,
    set_memmap_min_size,
    sys_info,
)


def test_config(tmp_path):
    """Test mne-python config file support."""
    tempdir = str(tmp_path)
    key = "_MNE_PYTHON_CONFIG_TESTING"
    value = "123456"
    value2 = "123"
    value3 = Path("/foo/bar")

    old_val = os.getenv(key, None)
    os.environ[key] = value
    assert get_config(key) == value
    del os.environ[key]
    # catch the warning about it being a non-standard config key
    known_config_keys = get_config("")
    assert len(known_config_keys) > 10  # dict of valid keys
    for k, val in known_config_keys.items():
        assert isinstance(k, str)
        assert isinstance(val, str), k
        assert len(val) > 0, k
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, None, home_dir=tempdir, set_env=False)
    assert get_config(key, home_dir=tempdir) is None
    pytest.raises(KeyError, get_config, key, raise_error=True)
    assert key not in os.environ
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, value, home_dir=tempdir, set_env=True)
    assert key in os.environ
    assert get_config(key, home_dir=tempdir) == value
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, None, home_dir=tempdir, set_env=True)
    assert key not in os.environ
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, None, home_dir=tempdir, set_env=True)
    assert key not in os.environ
    if old_val is not None:
        os.environ[key] = old_val

    # Check serialization from Path to string
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, value3, home_dir=tempdir)

    # Check if get_config with key=None returns all config
    key = "MNE_PYTHON_TESTING_KEY"
    assert key not in get_config(home_dir=tempdir)
    with pytest.warns(RuntimeWarning, match="non-standard"):
        set_config(key, value, home_dir=tempdir)
    assert get_config(home_dir=tempdir)[key] == value
    old_val = os.environ.get(key)
    try:  # os.environ should take precedence over config file
        os.environ[key] = value2
        assert get_config(home_dir=tempdir)[key] == value2
    finally:  # reset os.environ
        if old_val is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = old_val
    # Check what happens when we use a corrupted file
    json_fname = get_config_path(home_dir=tempdir)
    with open(json_fname, "w") as fid:
        fid.write("foo{}")
    with pytest.warns(RuntimeWarning, match="not a valid JSON"):
        assert key not in get_config(home_dir=tempdir)
    with pytest.warns(RuntimeWarning, match="non-standard"):
        pytest.raises(RuntimeError, set_config, key, "true", home_dir=tempdir)

    # degenerate conditions
    with pytest.raises(TypeError, match="must be an instance"):
        set_memmap_min_size(1)
    pytest.raises(ValueError, set_memmap_min_size, "foo")
    pytest.raises(TypeError, get_config, 1)
    pytest.raises(TypeError, set_config, 1)
    pytest.raises(TypeError, set_config, "foo", 1)
    pytest.raises(TypeError, _get_stim_channel, 1, None)
    pytest.raises(TypeError, _get_stim_channel, [1], None)


def test_sys_info_basic():
    """Test info-showing utility."""
    out = ClosingStringIO()
    sys_info(fid=out, check_version=False)
    out = out.getvalue()
    assert "numpy" in out
    # replace all in-line whitespace with single space
    out = "\n".join(" ".join(o.split()) for o in out.splitlines())
    assert "? GiB" not in out
    if platform.system() == "Darwin":
        assert "Platform macOS-" in out
    elif platform.system() == "Linux":
        assert "Platform Linux" in out


def test_sys_info_complete():
    """Test that sys_info is sufficiently complete."""
    tomllib = pytest.importorskip("tomllib")  # python 3.11+
    pyproject = Path(__file__).parents[3] / "pyproject.toml"
    if not pyproject.is_file():
        pytest.skip("Does not appear to be a dev installation")
    out = ClosingStringIO()
    sys_info(fid=out, check_version=False, dependencies="developer")
    out = out.getvalue()
    pyproject = tomllib.loads(pyproject.read_text("utf-8"))
    deps = pyproject["project"]["optional-dependencies"]["test_extra"]
    for dep in deps:
        dep = dep.split("[")[0].split(">")[0].strip()
        assert f" {dep}" in out, f"Missing in dev config: {dep}"


def test_sys_info_qt_browser():
    """Test if mne_qt_browser is correctly detected."""
    pytest.importorskip("mne_qt_browser")
    out = ClosingStringIO()
    sys_info(fid=out, check_version=False)
    out = out.getvalue()
    assert "mne-qt-browser" in out


def test_get_subjects_dir(tmp_path, monkeypatch):
    """Test get_subjects_dir()."""
    subjects_dir = tmp_path / "foo"
    subjects_dir.mkdir()

    # String
    assert get_subjects_dir(str(subjects_dir)) == subjects_dir

    # Path
    assert get_subjects_dir(subjects_dir) == subjects_dir

    # `None`
    monkeypatch.setenv("_MNE_FAKE_HOME_DIR", str(tmp_path))
    monkeypatch.delenv("SUBJECTS_DIR", raising=False)
    assert get_subjects_dir() is None

    # Expand `~`
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("USERPROFILE", str(tmp_path))  # Windows
    assert str(get_subjects_dir("~/foo")) == str(subjects_dir)

    monkeypatch.setenv("SUBJECTS_DIR", str(tmp_path / "doesntexist"))
    with pytest.warns(RuntimeWarning, match="MNE-Python config"):
        get_subjects_dir()
    with pytest.raises(FileNotFoundError, match="MNE-Python config"):
        get_subjects_dir(raise_error=True)


@pytest.mark.slowtest
@requires_good_network
def test_sys_info_check_outdated(monkeypatch):
    """Test sys info checking."""
    # Old (actually ping GitHub)
    monkeypatch.setattr(mne, "__version__", "0.1")
    out = ClosingStringIO()
    sys_info(fid=out, check_version=10)
    out = out.getvalue()
    assert "(outdated, release " in out
    assert "updating.html" in out

    # Timeout (will call urllib.open)
    out = ClosingStringIO()
    sys_info(fid=out, check_version=1e-12)
    out = out.getvalue()
    assert re.match(".*unable to check.*timeout.*", out, re.DOTALL) is not None
    assert "updating.html" not in out


def test_sys_info_check_other(monkeypatch):
    """Test other failure modes of the sys info check."""

    def bad_open(url, timeout, msg):
        raise URLError(msg)

    # SSL error
    out = ClosingStringIO()
    with monkeypatch.context() as m:
        m.setattr(mne.utils.config, "urlopen", partial(bad_open, msg="SSL: CERT"))
        sys_info(fid=out)
    out = out.getvalue()
    assert re.match(".*unable to check.*SSL.*", out, re.DOTALL) is not None

    # Other error
    out = ClosingStringIO()
    with monkeypatch.context() as m:
        m.setattr(mne.utils.config, "urlopen", partial(bad_open, msg="foo bar"))
        sys_info(fid=out)
    out = out.getvalue()
    match = re.match(".*unable to .*unknown error: .*foo bar.*", out, re.DOTALL)
    assert match is not None

    # Match
    monkeypatch.setattr(
        mne.utils.config,
        "_get_latest_version",
        lambda timeout: "1.5.1",
    )
    monkeypatch.setattr(mne, "__version__", "1.5.1")
    out = ClosingStringIO()
    sys_info(fid=out)
    out = out.getvalue()
    assert " 1.5.1 (latest release)" in out

    # Devel
    monkeypatch.setattr(mne, "__version__", "1.6.dev0")
    out = ClosingStringIO()
    sys_info(fid=out)
    out = out.getvalue()
    assert "devel, " in out
    assert "updating.html" not in out
