import uuid

import numpy
import pytest
import sympy

from brian2.core.namespace import get_local_namespace
from brian2.core.variables import Constant
from brian2.groups.group import Group
from brian2.units import second, volt
from brian2.units.fundamentalunits import Unit
from brian2.units.stdunits import Hz, ms, mV
from brian2.units.unitsafefunctions import exp, log, sin
from brian2.utils.logger import catch_logs


# a simple Group for testing
class SimpleGroup(Group):
    def __init__(self, variables, namespace=None):
        self.variables = variables
        # We use a unique name to get repeated warnings
        Group.__init__(
            self,
            namespace=namespace,
            name=f"simplegroup_{str(uuid.uuid4()).replace('-', '_')}",
        )


def _assert_one_warning(l):
    assert len(l) == 1, f"expected one warning got {len(l)}"
    assert l[0][0] == "WARNING", f"expected a WARNING, got {l[0][0]} instead"


@pytest.mark.codegen_independent
def test_default_content():
    """
    Test that the default namespace contains standard units and functions.
    """
    group = Group()
    # Units
    assert group._resolve("second", {}).get_value_with_unit() == second
    assert group._resolve("volt", {}).get_value_with_unit() == volt
    assert group._resolve("ms", {}).get_value_with_unit() == ms
    assert group._resolve("Hz", {}).get_value_with_unit() == Hz
    assert group._resolve("mV", {}).get_value_with_unit() == mV

    # Functions
    assert group._resolve("sin", {}).pyfunc == sin
    assert group._resolve("log", {}).pyfunc == log
    assert group._resolve("exp", {}).pyfunc == exp

    # Constants
    assert group._resolve("e", {}).sympy_obj == sympy.E
    assert group._resolve("e", {}).get_value() == numpy.e
    assert group._resolve("pi", {}).sympy_obj == sympy.pi
    assert group._resolve("pi", {}).get_value() == numpy.pi
    assert group._resolve("inf", {}).sympy_obj == sympy.oo
    assert group._resolve("inf", {}).get_value() == numpy.inf


@pytest.mark.codegen_independent
def test_explicit_namespace():
    """Test resolution with an explicitly provided namespace"""
    group = SimpleGroup(namespace={"variable": 42}, variables={})

    # Explicitly provided
    with catch_logs() as l:
        assert group._resolve("variable", {}).get_value_with_unit() == 42
        assert len(l) == 0

    # Value from an explicit run namespace
    with catch_logs() as l:
        assert (
            group._resolve(
                "yet_another_var", run_namespace={"yet_another_var": 17}
            ).get_value_with_unit()
            == 17
        )
        assert len(l) == 0


@pytest.mark.codegen_independent
def test_errors():
    # No explicit namespace
    group = SimpleGroup(namespace=None, variables={})
    with pytest.raises(KeyError):
        group._resolve("nonexisting_variable", {})

    # Empty explicit namespace
    group = SimpleGroup(namespace={}, variables={})
    with pytest.raises(KeyError):
        group._resolve("nonexisting_variable", {})

    # Illegal name
    with pytest.raises(ValueError):
        SimpleGroup(namespace={"_illegal": 3.0}, variables={})


@pytest.mark.codegen_independent
def test_resolution():
    # implicit namespace
    tau = 10 * ms
    group = SimpleGroup(namespace=None, variables={})
    namespace = get_local_namespace(level=0)
    resolved = group.resolve_all(
        ["tau", "ms"], namespace, user_identifiers=["tau", "ms"]
    )
    assert len(resolved) == 2
    assert isinstance(resolved, dict)
    assert resolved["tau"].get_value_with_unit() == tau
    assert resolved["ms"].get_value_with_unit() == ms
    del tau

    # explicit namespace
    group = SimpleGroup(namespace={"tau": 20 * ms}, variables={})
    namespace = get_local_namespace(level=0)
    resolved = group.resolve_all(["tau", "ms"], namespace, ["tau", "ms"])
    assert len(resolved) == 2
    assert resolved["tau"].get_value_with_unit() == 20 * ms


@pytest.mark.codegen_independent
def test_warning():
    from brian2.core.functions import DEFAULT_FUNCTIONS
    from brian2.units.stdunits import cm as brian_cm

    # Name in external namespace clashes with unit/function name
    exp = 23
    cm = 42
    group = SimpleGroup(namespace=None, variables={})
    namespace = get_local_namespace(level=0)
    with catch_logs() as l:
        resolved = group.resolve_all(["exp"], namespace)["exp"]
        assert resolved == DEFAULT_FUNCTIONS["exp"]
        assert len(l) == 1, f"got warnings: {str(l)}"
        assert l[0][1].endswith(".resolution_conflict")
    with catch_logs() as l:
        resolved = group.resolve_all(["cm"], namespace)["cm"]
        assert resolved.get_value_with_unit() == brian_cm
        assert len(l) == 1, f"got warnings: {str(l)}"
        assert l[0][1].endswith(".resolution_conflict")


@pytest.mark.codegen_independent
def test_warning_internal_variables():
    group1 = SimpleGroup(namespace=None, variables={"N": Constant("N", 5)})
    group2 = SimpleGroup(namespace=None, variables={"N": Constant("N", 7)})
    with catch_logs() as l:
        group1.resolve_all(["N"], run_namespace={"N": 5})  # should not raise a warning
        assert len(l) == 0, f"got warnings: {str(l)}"
    with catch_logs() as l:
        group2.resolve_all(["N"], run_namespace={"N": 5})  # should raise a warning
        assert len(l) == 1, f"got warnings: {str(l)}"
        assert l[0][1].endswith(".resolution_conflict")


if __name__ == "__main__":
    test_default_content()
    test_explicit_namespace()
    test_errors()
    test_resolution()
    test_warning()
    test_warning_internal_variables()
