# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Regression tests for the units.format package
"""
import warnings
from contextlib import nullcontext
from fractions import Fraction

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

from astropy import units as u
from astropy.constants import si
from astropy.units import PrefixUnit, Unit, UnitBase, UnitsWarning, dex
from astropy.units import format as u_format
from astropy.units.utils import is_effectively_unity


@pytest.mark.parametrize(
    "strings, unit",
    [
        (["m s", "m*s", "m.s"], u.m * u.s),
        (["m/s", "m*s**-1", "m /s", "m / s", "m/ s"], u.m / u.s),
        (["m**2", "m2", "m**(2)", "m**+2", "m+2", "m^(+2)"], u.m**2),
        (["m**-3", "m-3", "m^(-3)", "/m3"], u.m**-3),
        (["m**(1.5)", "m(3/2)", "m**(3/2)", "m^(3/2)"], u.m**1.5),
        (["2.54 cm"], u.Unit(u.cm * 2.54)),
        (["10+8m"], u.Unit(u.m * 1e8)),
        # This is the VOUnits documentation, but doesn't seem to follow the
        # unity grammar (["3.45 10**(-4)Jy"], 3.45 * 1e-4 * u.Jy)
        (["sqrt(m)"], u.m**0.5),
        (["dB(mW)", "dB (mW)"], u.DecibelUnit(u.mW)),
        (["mag"], u.mag),
        (["mag(ct/s)"], u.MagUnit(u.ct / u.s)),
        (["dex"], u.dex),
        (["dex(cm s**-2)", "dex(cm/s2)"], u.DexUnit(u.cm / u.s**2)),
    ],
)
def test_unit_grammar(strings, unit):
    for s in strings:
        print(s)
        unit2 = u_format.Generic.parse(s)
        assert unit2 == unit


@pytest.mark.parametrize(
    "string", ["sin( /pixel /s)", "mag(mag)", "dB(dB(mW))", "dex()"]
)
def test_unit_grammar_fail(string):
    with pytest.raises(ValueError):
        print(string)
        u_format.Generic.parse(string)


@pytest.mark.parametrize(
    "strings, unit",
    [
        (["0.1nm"], u.AA),
        (["mW/m2"], u.Unit(u.erg / u.cm**2 / u.s)),
        (["mW/(m2)"], u.Unit(u.erg / u.cm**2 / u.s)),
        (["km/s", "km.s-1"], u.km / u.s),
        (["km/s/Mpc"], u.km / u.s / u.Mpc),
        (["km/(s.Mpc)"], u.km / u.s / u.Mpc),
        (["10+3J/m/s/kpc2"], u.Unit(1e3 * u.W / (u.m * u.kpc**2))),
        (["10pix/nm"], u.Unit(10 * u.pix / u.nm)),
        (["1.5x10+11m"], u.Unit(1.5e11 * u.m)),
        (["1.5×10+11/m"], u.Unit(1.5e11 / u.m)),
        (["/s"], u.s**-1),
        (["m2"], u.m**2),
        (["10+21m"], u.Unit(u.m * 1e21)),
        (["2.54cm"], u.Unit(u.cm * 2.54)),
        (["20%"], 0.20 * u.dimensionless_unscaled),
        (["10+9"], 1.0e9 * u.dimensionless_unscaled),
        (["2x10-9"], 2.0e-9 * u.dimensionless_unscaled),
        (["---"], u.dimensionless_unscaled),
        (["ma"], u.ma),
        (["mAU"], u.mAU),
        (["uarcmin"], u.uarcmin),
        (["uarcsec"], u.uarcsec),
        (["kbarn"], u.kbarn),
        (["Gbit"], u.Gbit),
        (["Gibit"], 2**30 * u.bit),
        (["kbyte"], u.kbyte),
        (["mRy"], 0.001 * u.Ry),
        (["mmag"], u.mmag),
        (["Mpc"], u.Mpc),
        (["Gyr"], u.Gyr),
        (["°"], u.degree),
        (["°/s"], u.degree / u.s),
        (["Å"], u.AA),
        (["Å/s"], u.AA / u.s),
        (["\\h"], si.h),
        (["[cm/s2]"], dex(u.cm / u.s**2)),
        (["[K]"], dex(u.K)),
        (["[-]"], dex(u.dimensionless_unscaled)),
    ],
)
def test_cds_grammar(strings, unit):
    for s in strings:
        print(s)
        unit2 = u_format.CDS.parse(s)
        assert unit2 == unit


@pytest.mark.parametrize(
    "string",
    [
        "0.1 nm",
        "solMass(3/2)",
        "km / s",
        "km s-1",
        "km/s.Mpc-1",
        "/s.Mpc",
        "pix0.1nm",
        "pix/(0.1nm)",
        "km*s",
        "km**2",
        "5x8+3m",
        "0.1---",
        "---m",
        "m---",
        "--",
        "0.1-",
        "-m",
        "m-",
        "mag(s-1)",
        "dB(mW)",
        "dex(cm s-2)",
        "[--]",
    ],
)
def test_cds_grammar_fail(string):
    with pytest.raises(ValueError):
        print(string)
        u_format.CDS.parse(string)


def test_cds_dimensionless():
    assert u.Unit("---", format="cds") == u.dimensionless_unscaled
    assert u.dimensionless_unscaled.to_string(format="cds") == "---"


def test_cds_log10_dimensionless():
    assert u.Unit("[-]", format="cds") == u.dex(u.dimensionless_unscaled)
    assert u.dex(u.dimensionless_unscaled).to_string(format="cds") == "[-]"


# These examples are taken from the EXAMPLES section of
# https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/
@pytest.mark.parametrize(
    "strings, unit",
    [
        (
            ["count /s", "count/s", "count s**(-1)", "count / s", "count /s "],
            u.count / u.s,
        ),
        (
            ["/pixel /s", "/(pixel * s)"],
            (u.pixel * u.s) ** -1,
        ),
        (
            [
                "count /m**2 /s /eV",
                "count m**(-2) * s**(-1) * eV**(-1)",
                "count /(m**2 * s * eV)",
            ],
            u.count * u.m**-2 * u.s**-1 * u.eV**-1,
        ),
        (
            ["erg /pixel /s /GHz", "erg /s /GHz /pixel", "erg /pixel /(s * GHz)"],
            u.erg / (u.s * u.GHz * u.pixel),
        ),
        (
            ["keV**2 /yr /angstrom", "10**(10) keV**2 /yr /m"],
            # Though this is given as an example, it seems to violate the rules
            # of not raising scales to powers, so I'm just excluding it
            # "(10**2 MeV)**2 /yr /m"
            u.keV**2 / (u.yr * u.angstrom),
        ),
        (
            [
                "10**(46) erg /s",
                "10**46 erg /s",
                "10**(39) J /s",
                "10**(39) W",
                "10**(15) YW",
                "YJ /fs",
            ],
            10**46 * u.erg / u.s,
        ),
        (
            [
                "10**(-7) J /cm**2 /MeV",
                "10**(-9) J m**(-2) eV**(-1)",
                "nJ m**(-2) eV**(-1)",
                "nJ /m**2 /eV",
            ],
            10**-7 * u.J * u.cm**-2 * u.MeV**-1,
        ),
        (
            [
                "sqrt(erg /pixel /s /GHz)",
                "(erg /pixel /s /GHz)**(0.5)",
                "(erg /pixel /s /GHz)**(1/2)",
                "erg**(0.5) pixel**(-0.5) s**(-0.5) GHz**(-0.5)",
            ],
            (u.erg * u.pixel**-1 * u.s**-1 * u.GHz**-1) ** 0.5,
        ),
        (
            [
                "(count /s) (/pixel /s)",
                "(count /s) * (/pixel /s)",
                "count /pixel /s**2",
            ],
            (u.count / u.s) * (1.0 / (u.pixel * u.s)),
        ),
    ],
)
def test_ogip_grammar(strings, unit):
    for s in strings:
        print(s)
        unit2 = u_format.OGIP.parse(s)
        assert unit2 == unit


@pytest.mark.parametrize(
    "string",
    [
        "log(photon /m**2 /s /Hz)",
        "sin( /pixel /s)",
        "log(photon /cm**2 /s /Hz) /(sin( /pixel /s))",
        "log(photon /cm**2 /s /Hz) (sin( /pixel /s))**(-1)",
        "dB(mW)",
        "dex(cm/s**2)",
    ],
)
def test_ogip_grammar_fail(string):
    with pytest.raises(ValueError):
        print(string)
        u_format.OGIP.parse(string)


class RoundtripBase:
    deprecated_units = set()

    def check_roundtrip(self, unit, output_format=None):
        if output_format is None:
            output_format = self.format_
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")  # Same warning shows up multiple times
            s = unit.to_string(output_format)

        if s in self.deprecated_units:
            with pytest.warns(UnitsWarning, match="deprecated") as w:
                a = Unit(s, format=self.format_)
            assert len(w) == 1
        else:
            a = Unit(s, format=self.format_)  # No warning

        assert_allclose(a.decompose().scale, unit.decompose().scale, rtol=1e-9)

    def check_roundtrip_decompose(self, unit):
        ud = unit.decompose()
        s = ud.to_string(self.format_)
        assert "  " not in s
        a = Unit(s, format=self.format_)
        assert_allclose(a.decompose().scale, ud.scale, rtol=1e-5)


class TestRoundtripGeneric(RoundtripBase):
    format_ = "generic"

    @pytest.mark.parametrize(
        "unit",
        [
            unit
            for unit in u.__dict__.values()
            if (isinstance(unit, UnitBase) and not isinstance(unit, PrefixUnit))
        ],
    )
    def test_roundtrip(self, unit):
        self.check_roundtrip(unit)
        self.check_roundtrip(unit, output_format="unicode")
        self.check_roundtrip_decompose(unit)


class TestRoundtripVOUnit(RoundtripBase):
    format_ = "vounit"
    deprecated_units = u_format.VOUnit._deprecated_units

    @pytest.mark.parametrize(
        "unit",
        [
            unit
            for unit in u_format.VOUnit._units.values()
            if (isinstance(unit, UnitBase) and not isinstance(unit, PrefixUnit))
        ],
    )
    def test_roundtrip(self, unit):
        self.check_roundtrip(unit)
        if unit not in (u.mag, u.dB):
            self.check_roundtrip_decompose(unit)


class TestRoundtripFITS(RoundtripBase):
    format_ = "fits"
    deprecated_units = u_format.Fits._deprecated_units

    @pytest.mark.parametrize(
        "unit",
        [
            unit
            for unit in u_format.Fits._units.values()
            if (isinstance(unit, UnitBase) and not isinstance(unit, PrefixUnit))
        ],
    )
    def test_roundtrip(self, unit):
        self.check_roundtrip(unit)


class TestRoundtripCDS(RoundtripBase):
    format_ = "cds"

    @pytest.mark.parametrize(
        "unit",
        [
            unit
            for unit in u_format.CDS._units.values()
            if (isinstance(unit, UnitBase) and not isinstance(unit, PrefixUnit))
        ],
    )
    def test_roundtrip(self, unit):
        self.check_roundtrip(unit)
        if unit == u.mag:
            # Skip mag: decomposes into dex, which is unknown to CDS.
            return

        self.check_roundtrip_decompose(unit)

    @pytest.mark.parametrize(
        "unit", [u.dex(unit) for unit in (u.cm / u.s**2, u.K, u.Lsun)]
    )
    def test_roundtrip_dex(self, unit):
        string = unit.to_string(format="cds")
        recovered = u.Unit(string, format="cds")
        assert recovered == unit


class TestRoundtripOGIP(RoundtripBase):
    format_ = "ogip"
    deprecated_units = u_format.OGIP._deprecated_units | {"d"}

    @pytest.mark.parametrize(
        "unit",
        [
            unit
            for unit in u_format.OGIP._units.values()
            if (isinstance(unit, UnitBase) and not isinstance(unit, PrefixUnit))
        ],
    )
    def test_roundtrip(self, unit):
        if str(unit) in ("d", "0.001 Crab"):
            # Special-case day, which gets auto-converted to hours, and mCrab,
            # which the default check does not recognize as a deprecated unit.
            with pytest.warns(UnitsWarning):
                s = unit.to_string(self.format_)
                a = Unit(s, format=self.format_)
            assert_allclose(a.decompose().scale, unit.decompose().scale, rtol=1e-9)
        else:
            self.check_roundtrip(unit)
        if str(unit) in ("mag", "byte", "Crab"):
            # Skip mag and byte, which decompose into dex and bit, resp.,
            # both of which are unknown to OGIP, as well as Crab, which does
            # not decompose, and thus gives a deprecated unit warning.
            return

        power_of_ten = np.log10(unit.decompose().scale)
        if abs(power_of_ten - round(power_of_ten)) > 1e-3:
            ctx = pytest.warns(UnitsWarning, match="power of 10")
        elif str(unit) == "0.001 Crab":
            ctx = pytest.warns(UnitsWarning, match="deprecated")
        else:
            ctx = nullcontext()
        with ctx:
            self.check_roundtrip_decompose(unit)


def test_fits_units_available():
    u_format.Fits._units


def test_vo_units_available():
    u_format.VOUnit._units


def test_cds_units_available():
    u_format.CDS._units


def test_cds_non_ascii_unit():
    """Regression test for #5350.  This failed with a decoding error as
    μas could not be represented in ascii."""
    from astropy.units import cds

    with cds.enable():
        u.radian.find_equivalent_units(include_prefix_units=True)


def test_latex():
    fluxunit = u.erg / (u.cm**2 * u.s)
    assert fluxunit.to_string("latex") == r"$\mathrm{\frac{erg}{s\,cm^{2}}}$"


def test_new_style_latex():
    fluxunit = u.erg / (u.cm**2 * u.s)
    assert f"{fluxunit:latex}" == r"$\mathrm{\frac{erg}{s\,cm^{2}}}$"


def test_latex_scale():
    fluxunit = u.Unit(1.0e-24 * u.erg / (u.cm**2 * u.s * u.Hz))
    latex = r"$\mathrm{1 \times 10^{-24}\,\frac{erg}{Hz\,s\,cm^{2}}}$"
    assert fluxunit.to_string("latex") == latex


def test_latex_inline_scale():
    fluxunit = u.Unit(1.0e-24 * u.erg / (u.cm**2 * u.s * u.Hz))
    latex_inline = r"$\mathrm{1 \times 10^{-24}\,erg\,Hz^{-1}\,s^{-1}\,cm^{-2}}$"
    assert fluxunit.to_string("latex_inline") == latex_inline


@pytest.mark.parametrize(
    "format_spec, string, decomposed",
    [
        ("generic", "erg / (Angstrom s cm2)", "1e+07 kg / (m s3)"),
        ("s", "erg / (Angstrom s cm2)", "1e+07 kg / (m s3)"),
        ("console", "erg Angstrom^-1 s^-1 cm^-2", "10000000 kg m^-1 s^-3"),
        (
            "latex",
            r"$\mathrm{\frac{erg}{\mathring{A}\,s\,cm^{2}}}$",
            r"$\mathrm{10000000\,\frac{kg}{m\,s^{3}}}$",
        ),
        (
            "latex_inline",
            r"$\mathrm{erg\,\mathring{A}^{-1}\,s^{-1}\,cm^{-2}}$",
            r"$\mathrm{10000000\,kg\,m^{-1}\,s^{-3}}$",
        ),
        ("unicode", "erg Å⁻¹ s⁻¹ cm⁻²", "10000000 kg m⁻¹ s⁻³"),
        (">25s", "   erg / (Angstrom s cm2)", "        1e+07 kg / (m s3)"),
        ("cds", "erg.Angstrom-1.s-1.cm-2", "10000000kg.m-1.s-3"),
        ("ogip", "10 erg / (nm s cm**2)", "1e+07 kg / (m s**3)"),
        ("fits", "erg Angstrom-1 s-1 cm-2", "10**7 kg m-1 s-3"),
        ("vounit", "erg.Angstrom**-1.s**-1.cm**-2", "10000000kg.m**-1.s**-3"),
        # TODO: make fits and vounit less awful!
    ],
)
def test_format_styles(format_spec, string, decomposed):
    fluxunit = u.erg / (u.cm**2 * u.s * u.Angstrom)
    if format_spec == "vounit":
        # erg and Angstrom are deprecated in vounit.
        with pytest.warns(UnitsWarning, match="deprecated"):
            formatted = format(fluxunit, format_spec)
    else:
        formatted = format(fluxunit, format_spec)
    assert formatted == string
    # Decomposed mostly to test that scale factors are dealt with properly
    # in the various formats.
    assert format(fluxunit.decompose(), format_spec) == decomposed


@pytest.mark.parametrize(
    "format_spec, fraction, string, decomposed",
    [
        ("generic", False, "erg s-1 cm-2", "0.001 kg s-3"),
        (
            "console",
            "multiline",
            " erg  \n------\ns cm^2",
            "      kg \n0.001 ---\n      s^3",
        ),
        ("console", "inline", "erg / (s cm^2)", "0.001 kg / s^3"),
        ("unicode", "multiline", " erg \n─────\ns cm²", "      kg\n0.001 ──\n      s³"),
        ("unicode", "inline", "erg / (s cm²)", "0.001 kg / s³"),
        (
            "latex",
            False,
            r"$\mathrm{erg\,s^{-1}\,cm^{-2}}$",
            r"$\mathrm{0.001\,kg\,s^{-3}}$",
        ),
        (
            "latex",
            "inline",
            r"$\mathrm{erg / (s\,cm^{2})}$",
            r"$\mathrm{0.001\,kg / s^{3}}$",
        ),
        # TODO: make generic with fraction=False less awful!
    ],
)
def test_format_styles_non_default_fraction(format_spec, fraction, string, decomposed):
    fluxunit = u.erg / (u.cm**2 * u.s)
    assert fluxunit.to_string(format_spec, fraction=fraction) == string
    assert fluxunit.decompose().to_string(format_spec, fraction=fraction) == decomposed


@pytest.mark.parametrize("format_spec", ["generic", "cds", "fits", "ogip", "vounit"])
def test_no_multiline_fraction(format_spec):
    fluxunit = u.W / u.m**2
    with pytest.raises(ValueError, match="only supports.*not fraction='multiline'"):
        fluxunit.to_string(format_spec, fraction="multiline")


@pytest.mark.parametrize(
    "format_spec",
    ["generic", "cds", "fits", "ogip", "vounit", "latex", "console", "unicode"],
)
def test_unknown_fraction_style(format_spec):
    fluxunit = u.W / u.m**2
    with pytest.raises(ValueError, match="only supports.*parrot"):
        fluxunit.to_string(format_spec, fraction="parrot")


def test_flatten_to_known():
    myunit = u.def_unit("FOOBAR_One", u.erg / u.Hz)
    assert myunit.to_string("fits") == "erg Hz-1"
    myunit2 = myunit * u.bit**3
    assert myunit2.to_string("fits") == "bit3 erg Hz-1"


def test_flatten_impossible():
    myunit = u.def_unit("FOOBAR_Two")
    with u.add_enabled_units(myunit), pytest.raises(ValueError):
        myunit.to_string("fits")


def test_console_out():
    """
    Issue #436.
    """
    u.Jy.decompose().to_string("console")


@pytest.mark.parametrize(
    "format,string",
    [
        ("generic", "10"),
        ("console", "10"),
        ("unicode", "10"),
        ("cds", "10"),
        ("latex", r"$\mathrm{10}$"),
    ],
)
def test_scale_only(format, string):
    unit = u.Unit(10)
    assert unit.to_string(format) == string


def test_flexible_float():
    assert u.min._represents.to_string("latex") == r"$\mathrm{60\,s}$"


def test_fits_to_string_function_error():
    """Test function raises TypeError on bad input.

    This instead of returning None, see gh-11825.
    """

    with pytest.raises(TypeError, match="unit argument must be"):
        u_format.Fits.to_string(None)


def test_fraction_repr():
    area = u.cm**2.0
    assert "." not in area.to_string("latex")

    fractional = u.cm**2.5
    assert "5/2" in fractional.to_string("latex")

    assert fractional.to_string("unicode") == "cm⁵⸍²"


def test_scale_effectively_unity():
    """Scale just off unity at machine precision level is OK.
    Ensures #748 does not recur
    """
    a = (3.0 * u.N).cgs
    assert is_effectively_unity(a.unit.scale)
    assert len(a.__repr__().split()) == 3


def test_percent():
    """Test that the % unit is properly recognized.  Since % is a special
    symbol, this goes slightly beyond the round-tripping tested above."""
    assert u.Unit("%") == u.percent == u.Unit(0.01)

    assert u.Unit("%", format="cds") == u.Unit(0.01)
    assert u.Unit(0.01).to_string("cds") == "%"

    with pytest.raises(ValueError):
        u.Unit("%", format="fits")

    with pytest.raises(ValueError):
        u.Unit("%", format="vounit")


def test_scaled_dimensionless():
    """Test that scaled dimensionless units are properly recognized in generic
    and CDS, but not in fits and vounit."""
    assert u.Unit("0.1") == u.Unit(0.1) == 0.1 * u.dimensionless_unscaled
    assert u.Unit("1.e-4") == u.Unit(1.0e-4)

    assert u.Unit("10-4", format="cds") == u.Unit(1.0e-4)
    assert u.Unit("10+8").to_string("cds") == "10+8"

    with pytest.raises(ValueError):
        u.Unit(0.15).to_string("fits")

    assert u.Unit(0.1).to_string("fits") == "10**-1"

    with pytest.raises(ValueError):
        u.Unit(0.1).to_string("vounit")


def test_deprecated_did_you_mean_units():
    with pytest.raises(ValueError) as exc_info:
        u.Unit("ANGSTROM", format="fits")
    assert "Did you mean Angstrom or angstrom?" in str(exc_info.value)

    with pytest.raises(ValueError) as exc_info:
        u.Unit("crab", format="ogip")
    assert "Crab (deprecated)" in str(exc_info.value)
    assert "mCrab (deprecated)" in str(exc_info.value)

    with pytest.warns(
        UnitsWarning,
        match=r".* Did you mean 0\.1nm, Angstrom "
        r"\(deprecated\) or angstrom \(deprecated\)\?",
    ) as w:
        u.Unit("ANGSTROM", format="vounit")
    assert len(w) == 1
    assert str(w[0].message).count("0.1nm") == 1

    with pytest.warns(UnitsWarning, match=r".* 0\.1nm\.") as w:
        u.Unit("angstrom", format="vounit")
    assert len(w) == 1


@pytest.mark.parametrize("string", ["mag(ct/s)", "dB(mW)", "dex(cm s**-2)"])
def test_fits_function(string):
    # Function units cannot be written, so ensure they're not parsed either.
    with pytest.raises(ValueError):
        print(string)
        u_format.Fits().parse(string)


@pytest.mark.parametrize("string", ["mag(ct/s)", "dB(mW)", "dex(cm s**-2)"])
def test_vounit_function(string):
    # Function units cannot be written, so ensure they're not parsed either.
    with pytest.raises(ValueError), warnings.catch_warnings():
        # ct, dex also raise warnings - irrelevant here.
        warnings.simplefilter("ignore")
        u_format.VOUnit().parse(string)


def test_vounit_binary_prefix():
    assert u.Unit("KiB", format="vounit") == u.Unit("1024 B")
    assert u.Unit("Kibyte", format="vounit") == u.Unit("1024 B")
    assert u.Unit("Kibit", format="vounit") == u.Unit("128 B")
    with pytest.warns(UnitsWarning) as w:
        u.Unit("kibibyte", format="vounit")
    assert len(w) == 1


def test_vounit_unknown():
    assert u.Unit("unknown", format="vounit") is None
    assert u.Unit("UNKNOWN", format="vounit") is None
    assert u.Unit("", format="vounit") is u.dimensionless_unscaled


def test_vounit_details():
    assert u.Unit("Pa", format="vounit") is u.Pascal
    assert u.Unit("ka", format="vounit") == u.Unit("1000 yr")
    assert u.Unit("pix", format="vounit") == u.Unit("pixel", format="vounit")

    # The da- prefix is not allowed, and the d- prefix is discouraged
    assert u.dam.to_string("vounit") == "10m"
    assert u.Unit("dam dag").to_string("vounit") == "100g.m"

    # Parse round-trip
    with pytest.warns(UnitsWarning, match="deprecated"):
        flam = u.erg / u.cm / u.cm / u.s / u.AA
        x = u.format.VOUnit.to_string(flam)
        assert x == "erg.Angstrom**-1.s**-1.cm**-2"
        new_flam = u.format.VOUnit.parse(x)
        assert new_flam == flam


@pytest.mark.parametrize(
    "unit, vounit, number, scale, voscale",
    [
        ("nm", "nm", 0.1, "10^-1", "0.1"),
        ("fm", "fm", 100.0, "10+2", "100"),
        ("m^2", "m**2", 100.0, "100.0", "100"),
        ("cm", "cm", 2.54, "2.54", "2.54"),
        ("kg", "kg", 1.898124597e27, "1.898124597E27", "1.8981246e+27"),
        ("m/s", "m.s**-1", 299792458.0, "299792458", "2.9979246e+08"),
        ("cm2", "cm**2", 1.0e-20, "10^(-20)", "1e-20"),
    ],
)
def test_vounit_scale_factor(unit, vounit, number, scale, voscale):
    x = u.Unit(f"{scale} {unit}")
    assert x == number * u.Unit(unit)
    assert x.to_string(format="vounit") == voscale + vounit


@pytest.mark.parametrize(
    "unit, vounit",
    [
        ("m s^-1", "m/s"),
        ("s^-1", "1/s"),
        ("100 s^-2", "100/s**2"),
        ("kg m-1 s-2", "kg/(m.s**2)"),
    ],
)
@pytest.mark.parametrize("fraction", [True, "inline"])
def test_vounit_fraction(unit, vounit, fraction):
    x = u.Unit(unit)
    assert x.to_string(format="vounit", fraction=fraction) == vounit


@pytest.mark.parametrize(
    "unit, vounit",
    [
        ("m^2", "m**2"),
        ("s^-1", "s**-1"),
        ("s(0.333)", "s**(0.333)"),
        ("s(-0.333)", "s**(-0.333)"),
        ("s(1/3)", "s**(1/3)"),
        ("s(-1/3)", "s**(-1/3)"),
    ],
)
def test_vounit_power(unit, vounit):
    x = u.Unit(unit)
    assert x.to_string(format="vounit") == vounit


def test_vounit_custom():
    x = u.Unit("'foo' m", format="vounit")
    x_vounit = x.to_string("vounit")
    assert x_vounit == "'foo'.m"
    x_string = x.to_string()
    assert x_string == "foo m"

    x = u.Unit("m'foo' m", format="vounit")
    assert x.bases[1]._represents.scale == 0.001
    x_vounit = x.to_string("vounit")
    assert x_vounit == "m.m'foo'"
    x_string = x.to_string()
    assert x_string == "m mfoo"


def test_vounit_implicit_custom():
    # Yikes, this becomes "femto-urlong"...  But at least there's a warning.
    with pytest.warns(UnitsWarning) as w:
        x = u.Unit("furlong/week", format="vounit")
    assert x.bases[0]._represents.scale == 1e-15
    assert x.bases[0]._represents.bases[0].name == "urlong"
    assert len(w) == 2
    assert "furlong" in str(w[0].message)
    assert "week" in str(w[1].message)


@pytest.mark.parametrize(
    "scale, number, string",
    [
        ("10+2", 100, "10**2"),
        ("10(+2)", 100, "10**2"),
        ("10**+2", 100, "10**2"),
        ("10**(+2)", 100, "10**2"),
        ("10^+2", 100, "10**2"),
        ("10^(+2)", 100, "10**2"),
        ("10**2", 100, "10**2"),
        ("10**(2)", 100, "10**2"),
        ("10^2", 100, "10**2"),
        ("10^(2)", 100, "10**2"),
        ("10-20", 10 ** (-20), "10**-20"),
        ("10(-20)", 10 ** (-20), "10**-20"),
        ("10**-20", 10 ** (-20), "10**-20"),
        ("10**(-20)", 10 ** (-20), "10**-20"),
        ("10^-20", 10 ** (-20), "10**-20"),
        ("10^(-20)", 10 ** (-20), "10**-20"),
    ],
)
def test_fits_scale_factor(scale, number, string):
    x = u.Unit(scale + " erg/(s cm**2 Angstrom)", format="fits")
    assert x == number * (u.erg / u.s / u.cm**2 / u.Angstrom)
    assert x.to_string(format="fits") == string + " erg Angstrom-1 s-1 cm-2"

    x = u.Unit(scale + "*erg/(s cm**2 Angstrom)", format="fits")
    assert x == number * (u.erg / u.s / u.cm**2 / u.Angstrom)
    assert x.to_string(format="fits") == string + " erg Angstrom-1 s-1 cm-2"


def test_fits_scale_factor_errors():
    with pytest.raises(ValueError):
        x = u.Unit("1000 erg/(s cm**2 Angstrom)", format="fits")

    with pytest.raises(ValueError):
        x = u.Unit("12 erg/(s cm**2 Angstrom)", format="fits")

    x = u.Unit(1.2 * u.erg)
    with pytest.raises(ValueError):
        x.to_string(format="fits")

    x = u.Unit(100.0 * u.erg)
    assert x.to_string(format="fits") == "10**2 erg"


def test_double_superscript():
    """Regression test for #5870, #8699, #9218; avoid double superscripts."""
    assert (u.deg).to_string("latex") == r"$\mathrm{{}^{\circ}}$"
    assert (u.deg**2).to_string("latex") == r"$\mathrm{deg^{2}}$"
    assert (u.arcmin).to_string("latex") == r"$\mathrm{{}^{\prime}}$"
    assert (u.arcmin**2).to_string("latex") == r"$\mathrm{arcmin^{2}}$"
    assert (u.arcsec).to_string("latex") == r"$\mathrm{{}^{\prime\prime}}$"
    assert (u.arcsec**2).to_string("latex") == r"$\mathrm{arcsec^{2}}$"
    assert (u.hourangle).to_string("latex") == r"$\mathrm{{}^{h}}$"
    assert (u.hourangle**2).to_string("latex") == r"$\mathrm{hourangle^{2}}$"
    assert (u.electron).to_string("latex") == r"$\mathrm{e^{-}}$"
    assert (u.electron**2).to_string("latex") == r"$\mathrm{electron^{2}}$"


def test_no_prefix_superscript():
    """Regression test for gh-911 and #14419."""
    assert u.mdeg.to_string("latex") == r"$\mathrm{mdeg}$"
    assert u.narcmin.to_string("latex") == r"$\mathrm{narcmin}$"
    assert u.parcsec.to_string("latex") == r"$\mathrm{parcsec}$"
    assert u.mdeg.to_string("unicode") == "mdeg"
    assert u.narcmin.to_string("unicode") == "narcmin"
    assert u.parcsec.to_string("unicode") == "parcsec"


@pytest.mark.parametrize(
    "power,expected",
    (
        (1.0, "m"),
        (2.0, "m2"),
        (-10, "1 / m10"),
        (1.5, "m(3/2)"),
        (2 / 3, "m(2/3)"),
        (7 / 11, "m(7/11)"),
        (-1 / 64, "1 / m(1/64)"),
        (1 / 100, "m(1/100)"),
        (2 / 101, "m(0.019801980198019802)"),
        (Fraction(2, 101), "m(2/101)"),
    ),
)
def test_powers(power, expected):
    """Regression test for #9279 - powers should not be oversimplified."""
    unit = u.m**power
    s = unit.to_string()
    assert s == expected
    assert unit == s


@pytest.mark.parametrize(
    "string,unit",
    [
        ("\N{MICRO SIGN}g", u.microgram),
        ("\N{GREEK SMALL LETTER MU}g", u.microgram),
        ("g\N{MINUS SIGN}1", u.g ** (-1)),
        ("m\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT ONE}", 1 / u.m),
        ("m s\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT ONE}", u.m / u.s),
        ("m\N{SUPERSCRIPT TWO}", u.m**2),
        ("m\N{SUPERSCRIPT PLUS SIGN}\N{SUPERSCRIPT TWO}", u.m**2),
        ("m\N{SUPERSCRIPT THREE}", u.m**3),
        ("m\N{SUPERSCRIPT ONE}\N{SUPERSCRIPT ZERO}", u.m**10),
        ("\N{GREEK CAPITAL LETTER OMEGA}", u.ohm),
        ("\N{OHM SIGN}", u.ohm),  # deprecated but for compatibility
        ("\N{MICRO SIGN}\N{GREEK CAPITAL LETTER OMEGA}", u.microOhm),
        ("\N{ANGSTROM SIGN}", u.Angstrom),
        ("\N{ANGSTROM SIGN} \N{OHM SIGN}", u.Angstrom * u.Ohm),
        ("\N{LATIN CAPITAL LETTER A WITH RING ABOVE}", u.Angstrom),
        ("\N{LATIN CAPITAL LETTER A}\N{COMBINING RING ABOVE}", u.Angstrom),
        ("m\N{ANGSTROM SIGN}", u.milliAngstrom),
        ("°C", u.deg_C),
        ("°", u.deg),
        ("M⊙", u.Msun),  # \N{CIRCLED DOT OPERATOR}
        ("L☉", u.Lsun),  # \N{SUN}
        ("M⊕", u.Mearth),  # normal earth symbol = \N{CIRCLED PLUS}
        ("M♁", u.Mearth),  # be generous with \N{EARTH}
        ("R♃", u.Rjup),  # \N{JUPITER}
        ("′", u.arcmin),  # \N{PRIME}
        ("R∞", u.Ry),
        ("Mₚ", u.M_p),
    ],
)
def test_unicode(string, unit):
    assert u_format.Generic.parse(string) == unit
    assert u.Unit(string) == unit


@pytest.mark.parametrize(
    "string",
    [
        "g\N{MICRO SIGN}",
        "g\N{MINUS SIGN}",
        "m\N{SUPERSCRIPT MINUS}1",
        "m+\N{SUPERSCRIPT ONE}",
        "m\N{MINUS SIGN}\N{SUPERSCRIPT ONE}",
        "k\N{ANGSTROM SIGN}",
    ],
)
def test_unicode_failures(string):
    with pytest.raises(ValueError):
        u.Unit(string)


@pytest.mark.parametrize("format_", ("unicode", "latex", "latex_inline"))
def test_parse_error_message_for_output_only_format(format_):
    with pytest.raises(NotImplementedError, match="not parse"):
        u.Unit("m", format=format_)


def test_unknown_parser():
    with pytest.raises(ValueError, match=r"Unknown.*unicode'\] for output only"):
        u.Unit("m", format="foo")


def test_celsius_fits():
    assert u.Unit("Celsius", format="fits") == u.deg_C
    assert u.Unit("deg C", format="fits") == u.deg_C

    # check that compounds do what we expect: what do we expect?
    assert u.Unit("deg C kg-1", format="fits") == u.C * u.deg / u.kg
    assert u.Unit("Celsius kg-1", format="fits") == u.deg_C / u.kg
    assert u.deg_C.to_string("fits") == "Celsius"


@pytest.mark.parametrize(
    "format_spec, string",
    [
        ("generic", "dB(1 / m)"),
        ("latex", r"$\mathrm{dB}$$\mathrm{\left( \mathrm{\frac{1}{m}} \right)}$"),
        ("latex_inline", r"$\mathrm{dB}$$\mathrm{\left( \mathrm{m^{-1}} \right)}$"),
        ("console", "dB(m^-1)"),
        ("unicode", "dB(m⁻¹)"),
    ],
)
def test_function_format_styles(format_spec, string):
    dbunit = u.decibel(u.m**-1)
    assert dbunit.to_string(format_spec) == string
    assert f"{dbunit:{format_spec}}" == string


@pytest.mark.parametrize(
    "format_spec, fraction, string",
    [
        ("console", "multiline", "   1\ndB(-)\n   m"),
        ("console", "inline", "dB(1 / m)"),
        ("unicode", "multiline", "   1\ndB(─)\n   m"),
        ("unicode", "inline", "dB(1 / m)"),
        ("latex", False, r"$\mathrm{dB}$$\mathrm{\left( \mathrm{m^{-1}} \right)}$"),
        ("latex", "inline", r"$\mathrm{dB}$$\mathrm{\left( \mathrm{1 / m} \right)}$"),
    ],
)
def test_function_format_styles_non_default_fraction(format_spec, fraction, string):
    dbunit = u.decibel(u.m**-1)
    assert dbunit.to_string(format_spec, fraction=fraction) == string
