import numpy as np
import pytest
from numpy.testing import assert_allclose

from astropy import units as u
from astropy.coordinates.spectral_quantity import SpectralQuantity
from astropy.tests.helper import assert_quantity_allclose

SPECTRAL_UNITS = (u.GHz, u.micron, u.keV, (1 / u.nm).unit, u.km / u.s)


class TestSpectralQuantity:
    @pytest.mark.parametrize("unit", SPECTRAL_UNITS)
    def test_init_value(self, unit):
        SpectralQuantity(1, unit=unit)

    @pytest.mark.parametrize("unit", SPECTRAL_UNITS)
    def test_init_quantity(self, unit):
        SpectralQuantity(1 * unit)

    @pytest.mark.parametrize("unit", SPECTRAL_UNITS)
    def test_init_spectralquantity(self, unit):
        SpectralQuantity(SpectralQuantity(1, unit=unit))

    @pytest.mark.parametrize("unit", (u.kg, u.byte))
    def test_init_invalid(self, unit):
        with pytest.raises(
            u.UnitsError, match="SpectralQuantity instances require units"
        ):
            SpectralQuantity(1, unit=unit)
        with pytest.raises(
            u.UnitsError, match="SpectralQuantity instances require units"
        ):
            SpectralQuantity(1 * unit)

    @pytest.mark.parametrize(
        ("unit1", "unit2"), list(zip(SPECTRAL_UNITS, SPECTRAL_UNITS))
    )
    def test_spectral_conversion(self, unit1, unit2):
        sq1 = SpectralQuantity(1 * unit1)
        sq2 = sq1.to(unit2)
        sq3 = sq2.to(str(unit1))  # check that string units work
        assert isinstance(sq2, SpectralQuantity)
        assert isinstance(sq3, SpectralQuantity)
        assert_quantity_allclose(sq1, sq3)

    def test_doppler_conversion(self):
        sq1 = SpectralQuantity(
            1 * u.km / u.s, doppler_convention="optical", doppler_rest=500 * u.nm
        )

        sq2 = sq1.to(u.m / u.s)
        assert_allclose(sq2.value, 1000)

        sq3 = sq1.to(u.m / u.s, doppler_convention="radio")
        assert_allclose(sq3.value, 999.996664)

        sq4 = sq1.to(u.m / u.s, doppler_convention="relativistic")
        assert_allclose(sq4.value, 999.998332)

        sq5 = sq1.to(u.m / u.s, doppler_rest=499.9 * u.nm)
        assert_allclose(sq5.value, 60970.685737)

        val5 = sq1.to_value(u.m / u.s, doppler_rest=499.9 * u.nm)
        assert_allclose(val5, 60970.685737)

    def test_doppler_conversion_validation(self):
        sq1 = SpectralQuantity(1 * u.GHz)
        sq2 = SpectralQuantity(1 * u.km / u.s)

        with pytest.raises(
            ValueError,
            match="doppler_convention not set, cannot convert to/from velocities",
        ):
            sq1.to(u.km / u.s)

        with pytest.raises(
            ValueError,
            match="doppler_convention not set, cannot convert to/from velocities",
        ):
            sq2.to(u.GHz)

        with pytest.raises(
            ValueError, match="doppler_rest not set, cannot convert to/from velocities"
        ):
            sq1.to(u.km / u.s, doppler_convention="radio")

        with pytest.raises(
            ValueError, match="doppler_rest not set, cannot convert to/from velocities"
        ):
            sq2.to(u.GHz, doppler_convention="radio")

        with pytest.raises(
            u.UnitsError,
            match="Argument 'doppler_rest' to function 'to' must be in units",
        ):
            sq1.to(u.km / u.s, doppler_convention="radio", doppler_rest=5 * u.kg)

        with pytest.raises(
            u.UnitsError,
            match="Argument 'doppler_rest' to function 'to' must be in units",
        ):
            sq2.to(u.GHz, doppler_convention="radio", doppler_rest=5 * u.kg)

        with pytest.raises(
            ValueError,
            match="doppler_convention should be one of optical/radio/relativistic",
        ):
            sq1.to(u.km / u.s, doppler_convention="banana", doppler_rest=5 * u.GHz)

        with pytest.raises(
            ValueError,
            match="doppler_convention should be one of optical/radio/relativistic",
        ):
            sq2.to(u.GHz, doppler_convention="banana", doppler_rest=5 * u.GHz)

        with pytest.raises(ValueError, match="Original doppler_convention not set"):
            sq2.to(u.km / u.s, doppler_convention="radio")

        with pytest.raises(ValueError, match="Original doppler_rest not set"):
            sq2.to(u.km / u.s, doppler_rest=5 * u.GHz)

    def test_doppler_set_parameters(self):
        sq1 = SpectralQuantity(1 * u.km / u.s)

        with pytest.raises(
            ValueError,
            match="doppler_convention should be one of optical/radio/relativistic",
        ):
            sq1.doppler_convention = "banana"

        assert sq1.doppler_convention is None

        sq1.doppler_convention = "radio"

        assert sq1.doppler_convention == "radio"

        with pytest.raises(
            AttributeError,
            match="doppler_convention has already been set, and cannot be changed",
        ):
            sq1.doppler_convention = "optical"

        assert sq1.doppler_convention == "radio"

        with pytest.raises(
            u.UnitsError,
            match="Argument 'value' to function 'doppler_rest' must be in units",
        ):
            sq1.doppler_rest = 5 * u.kg

        sq1.doppler_rest = 5 * u.GHz

        assert_quantity_allclose(sq1.doppler_rest, 5 * u.GHz)

        with pytest.raises(
            AttributeError,
            match="doppler_rest has already been set, and cannot be changed",
        ):
            sq1.doppler_rest = 4 * u.GHz

        assert_quantity_allclose(sq1.doppler_rest, 5 * u.GHz)

    def test_arithmetic(self):
        # Checks for arithmetic - some operations should return SpectralQuantity,
        # while some should just return plain Quantity

        # First, operations that should return SpectralQuantity

        sq1 = SpectralQuantity(10 * u.AA)
        sq2 = sq1 * 2
        assert isinstance(sq2, SpectralQuantity)
        assert sq2.value == 20
        assert sq2.unit == u.AA

        sq2 = sq1 / 2
        assert isinstance(sq2, SpectralQuantity)
        assert sq2.value == 5
        assert sq2.unit == u.AA

        sq3 = SpectralQuantity(10 * u.AA)
        sq3 *= 2
        assert isinstance(sq3, SpectralQuantity)
        assert sq3.value == 20
        assert sq3.unit == u.AA

        sq4 = SpectralQuantity(10 * u.AA)
        sq4 /= 2
        assert isinstance(sq4, SpectralQuantity)
        assert sq4.value == 5
        assert sq4.unit == u.AA

        sq5 = SpectralQuantity(10 * u.AA)
        with pytest.raises(
            TypeError,
            match="Cannot store the result of this operation in SpectralQuantity",
        ):
            sq5 += 10 * u.AA

        # Note different order to sq2
        sq6 = SpectralQuantity(10 * u.AA)
        sq6 = 2 * sq1
        assert isinstance(sq6, SpectralQuantity)
        assert sq6.value == 20
        assert sq6.unit == u.AA

        # Next, operations that should return Quantity

        q1 = sq1 / u.s
        assert isinstance(q1, u.Quantity) and not isinstance(q1, SpectralQuantity)
        assert q1.value == 10
        assert q1.unit.is_equivalent(u.AA / u.s)

        q2 = sq1 / u.kg
        assert isinstance(q2, u.Quantity) and not isinstance(q2, SpectralQuantity)
        assert q2.value == 10
        assert q2.unit.is_equivalent(u.AA / u.kg)

        q3 = sq1 + 10 * u.AA
        assert isinstance(q3, u.Quantity) and not isinstance(q3, SpectralQuantity)
        assert q3.value == 20
        assert q3.unit == u.AA

        q4 = sq1 / SpectralQuantity(5 * u.AA)
        assert isinstance(q4, u.Quantity) and not isinstance(q4, SpectralQuantity)
        assert q4.value == 2
        assert q4.unit == u.one

    def test_ufuncs(self):
        # Checks for ufuncs - some operations should return SpectralQuantity,
        # while some should just return plain Quantity

        # First, operations that should return SpectralQuantity

        sq1 = SpectralQuantity([10, 20, 30] * u.AA)
        for ufunc in (np.min, np.max):
            sq2 = ufunc(sq1)
            assert isinstance(sq2, SpectralQuantity)
            assert sq2.value == ufunc(sq1.value)
            assert sq2.unit == u.AA

    def test_functions(self):
        # Checks for other functions - some operations should return SpectralQuantity,
        # while some should just return plain Quantity

        # First, operations that should return SpectralQuantity

        sq1 = SpectralQuantity([10, 20, 30] * u.AA)
        for func in (np.nanmin, np.nanmax):
            sq2 = func(sq1)
            assert isinstance(sq2, SpectralQuantity)
            assert sq2.value == func(sq1.value)
            assert sq2.unit == u.AA

        # Next, operations that should return Quantity

        for func in (np.sum,):
            q3 = func(sq1)
            assert isinstance(q3, u.Quantity) and not isinstance(q3, SpectralQuantity)
            assert q3.value == func(sq1.value)
            assert q3.unit == u.AA

    @pytest.mark.xfail
    def test_functions_std(self):
        # np.std should return a Quantity but it returns a SpectralQuantity. We
        # make this a separate xfailed test for now, but once this passes,
        # np.std could also just be added to the main test_functions test.
        # See https://github.com/astropy/astropy/issues/10245 for more details.

        # Checks for other functions - some operations should return SpectralQuantity,
        # while some should just return plain Quantity

        # First, operations that should return SpectralQuantity

        sq1 = SpectralQuantity([10, 20, 30] * u.AA)
        q1 = np.std(sq1)
        assert isinstance(q1, u.Quantity) and not isinstance(q1, SpectralQuantity)
        assert q1.value == np.sum(sq1.value)
        assert q1.unit == u.AA
