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

import os
import pathlib
import warnings
import io

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

from astropy.io import fits
from astropy import units as u
from astropy.table import Table
from astropy.io.fits import printdiff
from astropy.io.fits.column import ColumnAttribute
from astropy.io.fits.connect import REMOVE_KEYWORDS
from astropy.utils.exceptions import AstropyUserWarning

from astropy.io.fits.tests.test_table import _assert_attr_col
from . import FitsTestCase


class TestConvenience(FitsTestCase):

    def test_resource_warning(self):
        warnings.simplefilter('always', ResourceWarning)
        _ = fits.getdata(self.data('test0.fits'))
        _ = fits.getheader(self.data('test0.fits'))

    def test_fileobj_not_closed(self):
        """
        Tests that file-like objects are not closed after being passed
        to convenience functions.

        Regression test for https://github.com/astropy/astropy/issues/5063
        """

        f = open(self.data('test0.fits'), 'rb')
        _ = fits.getdata(f)
        assert not f.closed

        f.seek(0)
        _ = fits.getheader(f)
        assert not f.closed

        f.close()  # Close it now

    def test_table_to_hdu(self):
        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
                      names=['a', 'b', 'c'], dtype=['i', 'U1', 'f'])
        table['a'].unit = 'm/s'
        table['b'].unit = 'not-a-unit'

        with pytest.warns(u.UnitsWarning, match="'not-a-unit' did not parse as"
                          " fits unit") as w:
            hdu = fits.table_to_hdu(table)
        assert len(w) == 1

        # Check that TUNITn cards appear in the correct order
        # (https://github.com/astropy/astropy/pull/5720)
        assert hdu.header.index('TUNIT1') < hdu.header.index('TTYPE2')

        assert isinstance(hdu, fits.BinTableHDU)
        filename = self.temp('test_table_to_hdu.fits')
        hdu.writeto(filename, overwrite=True)

    def test_masked_table_to_hdu(self):
        i = np.ma.MaskedArray([1, 2, 3], mask=[True, False, False])
        s = np.ma.MaskedArray(['a', 'b', 'c'], mask=[False, True, True])
        c = np.ma.MaskedArray([2.3+1j, 4.5+0j, 6.7-1j], mask=[True, False, True])
        f = np.ma.MaskedArray([2.3, 4.5, 6.7], mask=[True, False, True])
        table = Table([i, s, c, f], names=['i', 's', 'c', 'f'])
        # Check that FITS standard is used in replacing masked values.
        hdu = fits.table_to_hdu(table)
        assert isinstance(hdu, fits.BinTableHDU)
        assert hdu.header['TNULL1'] == i.fill_value
        assert_array_equal(hdu.data['i'], i.filled())
        assert_array_equal(hdu.data['s'], s.filled(''))
        assert_array_equal(hdu.data['c'], c.filled(np.nan))
        assert_array_equal(hdu.data['c'].real, c.real.filled(np.nan))
        assert_array_equal(hdu.data['c'].imag, c.imag.filled(np.nan))
        assert_array_equal(hdu.data['c'], c.filled(complex(np.nan, np.nan)))
        assert_array_equal(hdu.data['f'], f.filled(np.nan))
        filename = self.temp('test_table_to_hdu.fits')
        hdu.writeto(filename, overwrite=True)

    def test_table_non_stringifyable_unit_to_hdu(self):
        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
                      names=['a', 'b', 'c'], dtype=['i', 'U1', 'f'])
        table['a'].unit = u.core.IrreducibleUnit("test")

        with pytest.warns(AstropyUserWarning, match="The unit 'test' could not be saved") as w:
            fits.table_to_hdu(table)
        assert len(w) == 1

    def test_table_to_hdu_convert_comment_convention(self):
        """
        Regression test for https://github.com/astropy/astropy/issues/6079
        """
        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
                      names=['a', 'b', 'c'], dtype=['i', 'U1', 'f'])
        table.meta['comments'] = ['This', 'is', 'a', 'comment']
        hdu = fits.table_to_hdu(table)

        assert hdu.header.get('comment') == ['This', 'is', 'a', 'comment']
        with pytest.raises(ValueError):
            hdu.header.index('comments')

    def test_table_to_hdu_filter_reserved(self):
        """
        Regression test for https://github.com/astropy/astropy/issues/9387
        """
        diag = 'be ignored since it conflicts with a FITS reserved keyword'
        ins_cards = {'EXPTIME': 32.1, 'XTENSION': 'NEWTABLE',
                     'NAXIS': 1, 'NAXIS1': 3, 'NAXIS2': 9,
                     'PCOUNT': 42, 'OBSERVER': 'Adams'}
        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
                      names=['a', 'b', 'c'], dtype=['i4', 'U1', 'f8'])
        table.meta.update(ins_cards)

        with pytest.warns(AstropyUserWarning,
                          match=rf'Meta-data keyword \w+ will {diag}') as w:
            hdu = fits.table_to_hdu(table)

        # This relies on the warnings being raised in the order of the
        # meta dict (note that the first and last card are legitimate keys)
        assert len(w) == len(ins_cards) - 2
        for i, key in enumerate(list(ins_cards)[1:-1]):
            assert f'Meta-data keyword {key}' in str(w[i].message)

        assert hdu.header.get('XTENSION') == 'BINTABLE'
        assert hdu.header.get('NAXIS') == 2
        assert hdu.header.get('NAXIS1') == 13
        assert hdu.header.get('NAXIS2') == 3
        assert hdu.header.get('PCOUNT') == 0
        np.testing.assert_almost_equal(hdu.header.get('EXPTIME'), 3.21e1)

    @pytest.mark.parametrize('card', REMOVE_KEYWORDS)
    def test_table_to_hdu_warn_reserved(self, card):
        """
        Test warning for each keyword in ..connect.REMOVE_KEYWORDS, 1 by 1
        """
        diag = 'be ignored since it conflicts with a FITS reserved keyword'
        res_cards = {'XTENSION': 'BINTABLE', 'BITPIX': 8,
                     'NAXIS': 2, 'NAXIS1': 12, 'NAXIS2': 3,
                     'PCOUNT': 0, 'GCOUNT': 1, 'TFIELDS': 2, 'THEAP': None}
        ins_cards = {'XTENSION': 'TABLE', 'BITPIX': 16,
                     'NAXIS': 1, 'NAXIS1': 2, 'NAXIS2': 6,
                     'PCOUNT': 2, 'GCOUNT': 2, 'TFIELDS': 4, 'THEAP': 36}

        table = Table([[1.0, 2.0, 3.0], [2.3, 4.5, 6.7]],
                      names=['wavelength', 'flux'], dtype=['f8', 'f4'])
        table.meta['ORIGIN'] = 'Min.Silly Walks'
        table.meta[card] = ins_cards[card]
        assert table.meta.get(card) != res_cards[card]

        with pytest.warns(AstropyUserWarning,
                          match=f'Meta-data keyword {card} will {diag}'):
            hdu = fits.table_to_hdu(table)

        assert hdu.header.get(card) == res_cards[card]
        assert hdu.header.get('ORIGIN') == 'Min.Silly Walks'

    def test_table_to_hdu_filter_incompatible(self):
        """
        Test removal of unsupported data types from header
        """
        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
                      names=['a', 'b', 'c'], dtype=['i4', 'U1', 'f8'])
        table.meta.update({'OBSDATE': '2001-05-26', 'RAMP': np.arange(5),
                           'TARGETS': {'PRIMARY': 1, 'SECONDAR': 3}})
        with pytest.warns(AstropyUserWarning, match=r'Attribute \S+ of type '
                          r'.+ cannot be added to FITS Header - skipping'):
            hdu = fits.table_to_hdu(table)

        assert hdu.header.get('OBSDATE') == '2001-05-26'
        assert 'RAMP' not in hdu.header
        assert 'TARGETS' not in hdu.header

    def test_table_writeto_header(self):
        """
        Regression test for https://github.com/astropy/astropy/issues/5988
        """
        data = np.zeros((5, ), dtype=[('x', float), ('y', int)])
        h_in = fits.Header()
        h_in['ANSWER'] = (42.0, 'LTU&E')
        filename = self.temp('tabhdr42.fits')
        fits.writeto(filename, data=data, header=h_in, overwrite=True)
        h_out = fits.getheader(filename, ext=1)
        assert h_out['ANSWER'] == 42

    def test_image_extension_update_header(self):
        """
        Test that _makehdu correctly includes the header. For example in the
        fits.update convenience function.
        """
        filename = self.temp('twoextension.fits')

        hdus = [fits.PrimaryHDU(np.zeros((10, 10))),
                fits.ImageHDU(np.zeros((10, 10)))]

        fits.HDUList(hdus).writeto(filename)

        fits.update(filename,
                    np.zeros((10, 10)),
                    header=fits.Header([('WHAT', 100)]),
                    ext=1)
        h_out = fits.getheader(filename, ext=1)
        assert h_out['WHAT'] == 100

    def test_printdiff(self):
        """
        Test that FITSDiff can run the different inputs without crashing.
        """

        # Testing different string input options
        assert printdiff(self.data('arange.fits'),
                         self.data('blank.fits')) is None
        assert printdiff(self.data('arange.fits'),
                         self.data('blank.fits'), ext=0) is None
        assert printdiff(self.data('o4sp040b0_raw.fits'),
                         self.data('o4sp040b0_raw.fits'),
                         extname='sci') is None

        # This may seem weird, but check printdiff to see, need to test
        # incorrect second file
        with pytest.raises(OSError):
            printdiff('o4sp040b0_raw.fits', 'fakefile.fits', extname='sci')

        # Test HDU object inputs
        with fits.open(self.data('stddata.fits'), mode='readonly') as in1:
            with fits.open(self.data('checksum.fits'), mode='readonly') as in2:

                assert printdiff(in1[0], in2[0]) is None

                with pytest.raises(ValueError):
                    printdiff(in1[0], in2[0], ext=0)

                assert printdiff(in1, in2) is None

                with pytest.raises(NotImplementedError):
                    printdiff(in1, in2, 0)

    def test_tabledump(self):
        """
        A simple test of the dump method.
        Also regression test for https://github.com/astropy/astropy/issues/6937
        """
        datastr = (
            '"                    1" "abc" "     3.70000007152557" "                    0"\n'
            '"                    2" "xy " "     6.69999971389771" "                    1"\n'
        )
        cdstr = (
            'c1 1J I11              ""               ""'
            '               -2147483647      ""               ""              \n'
            'c2 3A A3               ""               ""'
            '               ""               ""               ""              \n'
            'c3 1E G15.7            ""               ""'
            '               ""               3                0.4             \n'
            'c4 1L L6               ""               ""'
            '               ""               ""               ""              \n'
        )
        # copy fits file to the temp directory
        self.copy_file('tb.fits')

        # test without datafile
        fits.tabledump(self.temp('tb.fits'))
        assert os.path.isfile(self.temp('tb_1.txt'))

        # test with datafile
        fits.tabledump(self.temp('tb.fits'), datafile=self.temp('test_tb.txt'))
        assert os.path.isfile(self.temp('test_tb.txt'))

        # test with datafile and cdfile
        datafile = self.temp('data.txt')
        cdfile = self.temp('coldefs.txt')
        fits.tabledump(self.temp('tb.fits'), datafile, cdfile)
        assert os.path.isfile(datafile)
        with open(datafile) as data:
            assert data.read() == datastr
        with open(cdfile) as coldefs:
            assert coldefs.read() == cdstr

    @pytest.mark.parametrize('tablename', ['table.fits', 'tb.fits'])
    def test_dump_load_round_trip(self, tablename):
        """
        A simple test of the dump/load methods; dump the data, column, and
        header files and try to reload the table from them.
        """

        # copy fits file to the temp directory
        self.copy_file(tablename)

        datafile = self.temp('data.txt')
        cdfile = self.temp('coldefs.txt')
        hfile = self.temp('header.txt')
        fits.tabledump(self.temp(tablename), datafile, cdfile, hfile)

        new_tbhdu = fits.tableload(datafile, cdfile, hfile)

        with fits.open(self.temp(tablename)) as hdul:
            _assert_attr_col(new_tbhdu, hdul[1])

    def test_append_filename(self):
        """
        Test fits.append with a filename argument.
        """
        data = np.arange(6)
        testfile = self.temp('test_append_1.fits')

        # Test case 1: creation of file
        fits.append(testfile, data=data, checksum=True)

        # Test case 2: append to existing file, with verify=True
        # Also test that additional keyword can be passed to fitsopen
        fits.append(testfile, data=data * 2, checksum=True, ignore_blank=True)

        # Test case 3: append to existing file, with verify=False
        fits.append(testfile, data=data * 3, checksum=True, verify=False)

        with fits.open(testfile, checksum=True) as hdu1:
            np.testing.assert_array_equal(hdu1[0].data, data)
            np.testing.assert_array_equal(hdu1[1].data, data * 2)
            np.testing.assert_array_equal(hdu1[2].data, data * 3)

    @pytest.mark.parametrize('mode', ['wb', 'wb+', 'ab', 'ab+'])
    def test_append_filehandle(self, tmpdir, mode):
        """
        Test fits.append with a file handle argument.
        """
        append_file = tmpdir.join('append.fits')
        with append_file.open(mode) as handle:
            fits.append(filename=handle, data=np.ones((4, 4)))

    def test_append_with_header(self):
        """
        Test fits.append with a fits Header, which triggers detection of the
        HDU class. Regression test for
        https://github.com/astropy/astropy/issues/8660
        """
        testfile = self.temp('test_append_1.fits')
        with fits.open(self.data('test0.fits')) as hdus:
            for hdu in hdus:
                fits.append(testfile, hdu.data, hdu.header, checksum=True)

        with fits.open(testfile, checksum=True) as hdus:
            assert len(hdus) == 5

    def test_pathlib(self):
        testfile = pathlib.Path(self.temp('test.fits'))
        data = np.arange(10)
        hdulist = fits.HDUList([fits.PrimaryHDU(data)])
        hdulist.writeto(testfile)

        with fits.open(testfile) as hdul:
            np.testing.assert_array_equal(hdul[0].data, data)

    def test_getdata_ext_given(self):
        prihdu = fits.PrimaryHDU(data=np.zeros((5, 5), dtype=int))
        exthdu1 = fits.ImageHDU(data=np.ones((5, 5), dtype=int))
        exthdu2 = fits.ImageHDU(data=2 * np.ones((5, 5), dtype=int))
        hdulist = fits.HDUList([prihdu, exthdu1, exthdu2])
        buf = io.BytesIO()
        hdulist.writeto(buf)

        for ext in [0, 1, 2]:
            buf.seek(0)
            data = fits.getdata(buf, ext=ext)
            assert data[0, 0] == ext

    def test_getdata_ext_given_nodata(self):
        prihdu = fits.PrimaryHDU(data=np.zeros((5, 5), dtype=int))
        exthdu1 = fits.ImageHDU(data=np.ones((5, 5), dtype=int))
        exthdu2 = fits.ImageHDU(data=None)
        hdulist = fits.HDUList([prihdu, exthdu1, exthdu2])
        buf = io.BytesIO()
        hdulist.writeto(buf)
        buf.seek(0)

        with pytest.raises(IndexError, match="No data in HDU #2."):
            fits.getdata(buf, ext=2)

    def test_getdata_ext_not_given_with_data_in_primary(self):
        prihdu = fits.PrimaryHDU(data=np.zeros((5, 5), dtype=int))
        exthdu1 = fits.ImageHDU(data=None)
        exthdu2 = fits.ImageHDU(data=None)
        hdulist = fits.HDUList([prihdu, exthdu1, exthdu2])
        buf = io.BytesIO()
        hdulist.writeto(buf)
        buf.seek(0)

        data = fits.getdata(buf)
        assert data[0, 0] == 0

    def test_getdata_ext_not_given_with_data_in_ext(self):
        # tests fallback mechanism
        prihdu = fits.PrimaryHDU(data=None)
        exthdu1 = fits.ImageHDU(data=np.ones((5, 5), dtype=int))
        exthdu2 = fits.ImageHDU(data=None)
        hdulist = fits.HDUList([prihdu, exthdu1, exthdu2])
        buf = io.BytesIO()
        hdulist.writeto(buf)
        buf.seek(0)

        data = fits.getdata(buf)
        assert data[0, 0] == 1

    def test_getdata_ext_not_given_nodata_any(self):
        # tests exception raised when there is no data in either
        # Primary HDU or first extension HDU
        prihdu = fits.PrimaryHDU(data=None)
        exthdu1 = fits.ImageHDU(data=None)
        exthdu2 = fits.ImageHDU(data=np.ones((5, 5), dtype=int))
        hdulist = fits.HDUList([prihdu, exthdu1, exthdu2])
        buf = io.BytesIO()
        hdulist.writeto(buf)
        buf.seek(0)

        with pytest.raises(
            IndexError,
            match="No data in either Primary or first extension HDUs."
        ):
            fits.getdata(buf)

    def test_getdata_ext_not_given_nodata_noext(self):
        # tests exception raised when there is no data in the
        # Primary HDU and there are no extension HDUs
        prihdu = fits.PrimaryHDU(data=None)
        hdulist = fits.HDUList([prihdu])
        buf = io.BytesIO()
        hdulist.writeto(buf)
        buf.seek(0)

        with pytest.raises(
            IndexError,
            match="No data in Primary HDU and no extension HDU found."
        ):
            fits.getdata(buf)
