# Licensed under a 3-clause BSD style license - see LICENSE.rst

import pytest

from astropy.cosmology import Cosmology
from astropy.cosmology._src.core import _COSMOLOGY_CLASSES
from astropy.cosmology._src.io.builtin.table import from_table, to_table
from astropy.table import QTable, Table, vstack

from .base import ToFromDirectTestBase, ToFromTestMixinBase

###############################################################################


class ToFromTableTestMixin(ToFromTestMixinBase):
    """
    Tests for a Cosmology[To/From]Format with ``format="astropy.table"``.
    This class will not be directly called by :mod:`pytest` since its name does
    not begin with ``Test``. To activate the contained tests this class must
    be inherited in a subclass. Subclasses must define a :func:`pytest.fixture`
    ``cosmo`` that returns/yields an instance of a |Cosmology|.
    See ``TestCosmology`` for an example.
    """

    def test_to_table_bad_index(self, from_format, to_format):
        """Test if argument ``index`` is incorrect"""
        tbl = to_format("astropy.table")

        # single-row table and has a non-0/None index
        with pytest.raises(IndexError, match="index 2 out of range"):
            from_format(tbl, index=2, format="astropy.table")

        # string index where doesn't match
        with pytest.raises(KeyError, match="No matches found for key"):
            from_format(tbl, index="row 0", format="astropy.table")

    # -----------------------

    def test_to_table_failed_cls(self, to_format):
        """Test failed table type."""
        with pytest.raises(TypeError, match="'cls' must be"):
            to_format("astropy.table", cls=list)

    @pytest.mark.parametrize("tbl_cls", [QTable, Table])
    def test_to_table_cls(self, to_format, tbl_cls):
        tbl = to_format("astropy.table", cls=tbl_cls)
        assert isinstance(tbl, tbl_cls)  # test type

    # -----------------------

    @pytest.mark.parametrize("in_meta", [True, False])
    def test_to_table_in_meta(self, cosmo_cls, to_format, in_meta):
        """Test where the cosmology class is placed."""
        tbl = to_format("astropy.table", cosmology_in_meta=in_meta)

        # if it's in metadata, it's not a column. And vice versa.
        if in_meta:
            assert tbl.meta["cosmology"] == cosmo_cls.__qualname__
            assert "cosmology" not in tbl.colnames  # not also a column
        else:
            assert tbl["cosmology"][0] == cosmo_cls.__qualname__
            assert "cosmology" not in tbl.meta

    # -----------------------

    def test_to_table(self, cosmo_cls, cosmo, to_format):
        """Test cosmology -> astropy.table."""
        tbl = to_format("astropy.table")

        # Test properties of Table.
        assert isinstance(tbl, QTable)
        assert tbl.meta["cosmology"] == cosmo_cls.__qualname__
        assert tbl["name"] == cosmo.name
        assert tbl.indices  # indexed

        # Test each Parameter column has expected information.
        for n, P in cosmo_cls.parameters.items():
            col = tbl[n]  # Column

            # Compare the two
            assert col.info.name == P.name
            assert col.info.description == P.__doc__
            assert col.info.meta == (cosmo.meta.get(n) or {})

    # -----------------------

    def test_from_not_table(self, cosmo, from_format):
        """Test not passing a Table to the Table parser."""
        with pytest.raises((TypeError, ValueError)):
            from_format("NOT A TABLE", format="astropy.table")

    def test_tofrom_table_instance(self, cosmo_cls, cosmo, from_format, to_format):
        """Test cosmology -> astropy.table -> cosmology."""
        tbl = to_format("astropy.table")

        # add information
        tbl["mismatching"] = "will error"

        # tests are different if the last argument is a **kwarg
        if cosmo._init_has_kwargs:
            got = from_format(tbl, format="astropy.table")

            assert got.__class__ is cosmo_cls
            assert got.name == cosmo.name
            assert "mismatching" not in got.meta

            return  # don't continue testing

        # read with mismatching parameters errors
        with pytest.raises(TypeError, match="there are unused parameters"):
            from_format(tbl, format="astropy.table")

        # unless mismatched are moved to meta
        got = from_format(tbl, format="astropy.table", move_to_meta=True)
        assert got == cosmo
        assert got.meta["mismatching"] == "will error"

        # it won't error if everything matches up
        tbl.remove_column("mismatching")
        got = from_format(tbl, format="astropy.table")
        assert got == cosmo

        # and it will also work if the cosmology is a class
        # Note this is not the default output of ``to_format``.
        tbl.meta["cosmology"] = _COSMOLOGY_CLASSES[tbl.meta["cosmology"]]
        got = from_format(tbl, format="astropy.table")
        assert got == cosmo

        # also it auto-identifies 'format'
        got = from_format(tbl)
        assert got == cosmo

    def test_fromformat_table_subclass_partial_info(
        self, cosmo_cls, cosmo, from_format, to_format
    ):
        """
        Test writing from an instance and reading from that class.
        This works with missing information.
        """
        # test to_format
        tbl = to_format("astropy.table")
        assert isinstance(tbl, QTable)

        # partial information
        tbl.meta.pop("cosmology", None)
        del tbl["Tcmb0"]

        # read with the same class that wrote fills in the missing info with
        # the default value
        got = cosmo_cls.from_format(tbl, format="astropy.table")
        got2 = from_format(tbl, format="astropy.table", cosmology=cosmo_cls)
        got3 = from_format(
            tbl, format="astropy.table", cosmology=cosmo_cls.__qualname__
        )

        assert (got == got2) and (got2 == got3)  # internal consistency

        # not equal, because Tcmb0 is changed, which also changes m_nu
        assert got != cosmo
        assert got.Tcmb0 == cosmo_cls.parameters["Tcmb0"].default
        assert got.clone(name=cosmo.name, Tcmb0=cosmo.Tcmb0, m_nu=cosmo.m_nu) == cosmo
        # but the metadata is the same
        assert got.meta == cosmo.meta

    @pytest.mark.parametrize("add_index", [True, False])
    def test_tofrom_table_mutlirow(self, cosmo_cls, cosmo, from_format, add_index):
        """Test if table has multiple rows."""
        # ------------
        # To Table

        cosmo1 = cosmo.clone(name="row 0")
        cosmo2 = cosmo.clone(name="row 2")
        tbl = vstack(
            [c.to_format("astropy.table") for c in (cosmo1, cosmo, cosmo2)],
            metadata_conflicts="silent",
        )

        assert isinstance(tbl, QTable)
        assert tbl.meta["cosmology"] == cosmo_cls.__qualname__
        assert tbl[1]["name"] == cosmo.name

        # whether to add an index. `from_format` can work with or without.
        if add_index:
            tbl.add_index("name", unique=True)

        # ------------
        # From Table

        # it will error on a multi-row table
        with pytest.raises(ValueError, match="need to select a specific row"):
            from_format(tbl, format="astropy.table")

        # unless the index argument is provided
        got = from_format(tbl, index=1, format="astropy.table")
        assert got == cosmo

        # the index can be a string
        got = from_format(tbl, index=cosmo.name, format="astropy.table")
        assert got == cosmo

        # when there's more than one cosmology found
        tbls = vstack([tbl, tbl], metadata_conflicts="silent")
        with pytest.raises(ValueError, match="more than one"):
            from_format(tbls, index=cosmo.name, format="astropy.table")

    def test_tofrom_table_rename(self, cosmo, to_format, from_format):
        """Test renaming columns in row."""
        rename = {"name": "cosmo_name"}
        table = to_format("astropy.table", rename=rename)

        assert "name" not in table.colnames
        assert "cosmo_name" in table.colnames

        # Error if just reading
        with pytest.raises(TypeError, match="there are unused parameters"):
            from_format(table)

        # Roundtrip
        inv_rename = {v: k for k, v in rename.items()}
        got = from_format(table, rename=inv_rename)
        assert got == cosmo

    def test_from_table_renamed_index_column(self, cosmo, to_format, from_format):
        """Test reading from a table with a renamed index column."""
        cosmo1 = cosmo.clone(name="row 0")
        cosmo2 = cosmo.clone(name="row 2")
        tbl = vstack(
            [c.to_format("astropy.table") for c in (cosmo1, cosmo, cosmo2)],
            metadata_conflicts="silent",
        )
        tbl.rename_column("name", "cosmo_name")

        inv_rename = {"cosmo_name": "name"}
        newcosmo = from_format(
            tbl, index="row 0", rename=inv_rename, format="astropy.table"
        )
        assert newcosmo == cosmo1

    @pytest.mark.parametrize("format", [True, False, None, "astropy.table"])
    def test_is_equivalent_to_table(self, cosmo, to_format, format):
        """Test :meth:`astropy.cosmology.Cosmology.is_equivalent`.

        This test checks that Cosmology equivalency can be extended to any
        Python object that can be converted to a Cosmology -- in this case
        a |Table|.
        """
        obj = to_format("astropy.table")
        assert not isinstance(obj, Cosmology)

        is_equiv = cosmo.is_equivalent(obj, format=format)
        assert is_equiv is (format is not False)


class TestToFromTable(ToFromDirectTestBase, ToFromTableTestMixin):
    """Directly test ``to/from_table``."""

    def setup_class(self):
        self.functions = {"to": to_table, "from": from_table}
