# -*- coding: utf-8 -*-
import ctypes as C  # NOQA
import io
import multiprocessing
import os
import platform
import random
import sys
import warnings

import numpy as np

from obspy import Stream, Trace, UTCDateTime, read
from obspy.core.compatibility import from_buffer
from obspy.core.util import NamedTemporaryFile
from obspy.core.util.attribdict import AttribDict
from obspy.core.util.base import CatchAndAssertWarnings
from obspy.io.mseed import (InternalMSEEDError, InternalMSEEDWarning,
                            ObsPyMSEEDFilesizeTooSmallError)
from obspy.io.mseed import util
from obspy.io.mseed.core import _read_mseed, _write_mseed
from obspy.io.mseed.headers import clibmseed
from obspy.io.mseed.msstruct import _MSStruct
import pytest


# some Python version don't support negative timestamps
NO_NEGATIVE_TIMESTAMPS = False
try:
    UTCDateTime(-50000)
except Exception:
    NO_NEGATIVE_TIMESTAMPS = True


def _test_function(filename):
    """
    Internal function used by MSEEDSpecialIssueTestCase.test_infinite_loop
    """
    with warnings.catch_warnings(record=True):
        warnings.simplefilter("always")
        try:
            st = read(filename)  # noqa @UnusedVariable
        except (ValueError, InternalMSEEDError):
            # Should occur with broken files
            pass


class TestMSEEDSpecialIssue():
    """
    """
    @classmethod
    def setup_class(cls):
        # mseed steim compression is big endian
        if sys.byteorder == 'little':
            cls.swap = 1
        else:
            cls.swap = 0

    def test_invalid_record_length(self):
        """
        An invalid record length should raise an exception.
        """
        npts = 6000
        np.random.seed(815)  # make test reproducible
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            data = np.random.randint(-1000, 1000, npts).astype(np.int32)
            st = Stream([Trace(data=data)])
            # Writing should fail with invalid record lengths.
            # Not a power of 2.
            with pytest.raises(ValueError):
                _write_mseed(st, tempfile, format="MSEED", reclen=1000)
            # Too small.
            with pytest.raises(ValueError):
                _write_mseed(st, tempfile, format="MSEED", reclen=8)
            # Not a number.
            with pytest.raises(ValueError):
                _write_mseed(st, tempfile, format="MSEED", reclen='A')

    def test_invalid_encoding(self):
        """
        An invalid encoding should raise an exception.
        """
        npts = 6000
        np.random.seed(815)  # make test reproducible
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            data = np.random.randint(-1000, 1000, npts).astype(np.int32)
            st = Stream([Trace(data=data)])
            # Writing should fail with invalid record lengths.
            # Wrong number.
            with pytest.raises(ValueError):
                _write_mseed(st, tempfile, format="MSEED", encoding=2)
            # Wrong Text.
            with pytest.raises(ValueError):
                _write_mseed(st, tempfile, format="MSEED", encoding='FLOAT_64')

    def test_ctypes_arg_types(self):
        """
        Test that ctypes argtypes are set for type checking
        """
        argument_error = C.ArgumentError
        cl = clibmseed
        args = [C.pointer(C.pointer(C.c_int())), 'a', 1, 1.5, 1, 0, 0, 0, 0]
        with pytest.raises(argument_error):
            cl.ms_readtraces(*args)
        with pytest.raises(TypeError):
            cl.ms_readtraces(*args[:-1])
        with pytest.raises(argument_error):
            cl.ms_readmsr_r(*args)
        with pytest.raises(TypeError):
            cl.ms_readmsr_r(*args[:-1])
        with pytest.raises(argument_error):
            cl.mst_printtracelist(*args[:5])
        with pytest.raises(argument_error):
            cl.ms_detect(*args[:4])
        args.append(1)  # 10 argument function
        with pytest.raises(argument_error):
            cl.mst_packgroup(*args)
        args = ['hallo']  # one argument functions
        with pytest.raises(argument_error):
            cl.msr_starttime(*args)
        with pytest.raises(argument_error):
            cl.msr_endtime(*args)
        with pytest.raises(argument_error):
            cl.mst_init(*args)
        with pytest.raises(argument_error):
            cl.mst_free(*args)
        with pytest.raises(argument_error):
            cl.mst_initgroup(*args)
        with pytest.raises(argument_error):
            cl.mst_freegroup(*args)
        with pytest.raises(argument_error):
            cl.msr_init(*args)

    def test_broken_last_record(self, testdata):
        """
        Test if Libmseed is able to read files with broken last record. Use
        both methods, readMSTracesViaRecords and readMSTraces
        """
        file = testdata['brokenlastrecord.mseed']

        # independent reading of the data, 128 Bytes header
        d = np.fromfile(file, dtype=np.uint8)[128:]
        data = util._unpack_steim_2(d, 5980, swapflag=self.swap,
                                    verbose=0)

        # test readMSTraces. Will raise an internal warning.
        with CatchAndAssertWarnings() as w:
            data_record = _read_mseed(file)[0].data

        # This will raise 18 (!) warnings. It will skip 17 * 128 bytes due
        # to it not being a SEED records and then complain that the remaining
        # 30 bytes are not enough to constitute a full SEED record.
        assert len(w) == 18
        assert w[0].category == InternalMSEEDWarning

        # Test against reference data.
        assert len(data_record) == 5980
        last10samples = [2862, 2856, 2844, 2843, 2851,
                         2853, 2853, 2854, 2857, 2863]
        np.testing.assert_array_equal(data_record[-10:], last10samples)

        # Also test against independently unpacked data.
        np.testing.assert_allclose(data_record, data)

    def test_one_sample_overlap(self):
        """
        Both methods readMSTraces and readMSTracesViaRecords should recognize a
        single sample overlap.
        """
        # create a stream with one sample overlapping
        trace1 = Trace(data=np.zeros(1000))
        trace2 = Trace(data=np.zeros(10))
        trace2.stats.starttime = UTCDateTime(999)
        st = Stream([trace1, trace2])
        # write into MSEED
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            _write_mseed(st, tempfile, format="MSEED")
            # read it again
            new_stream = _read_mseed(tempfile)
            assert len(new_stream) == 2

    def test_bug_write_read_float32_seed_win32(self):
        """
        Test case for issue #64.
        """
        # create stream object
        data = np.array([395.07809448, 395.0782, 1060.28112793, -1157.37487793,
                         -1236.56237793, 355.07028198, -1181.42175293],
                        dtype=np.float32)
        st = Stream([Trace(data=data)])
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            _write_mseed(st, tempfile, format="MSEED")
            # read temp file directly without libmseed
            with open(tempfile, 'rb') as fp:
                fp.seek(56)
                dtype = np.dtype('>f4')
                bin_data = from_buffer(fp.read(7 * dtype.itemsize),
                                       dtype=dtype)
            np.testing.assert_array_equal(data, bin_data)
            # read via ObsPy
            st2 = _read_mseed(tempfile)
        # test results
        np.testing.assert_array_equal(data, st2[0].data)

    @pytest.mark.skipif(
        NO_NEGATIVE_TIMESTAMPS,
        reason='times before 1970 are not supported on this operation system')
    def test_write_with_date_time_before_1970(self):
        """
        Write an stream via libmseed with a datetime before 1970.

        This test depends on the platform specific localtime()/gmtime()
        function.
        """
        # create trace
        tr = Trace(data=np.empty(1000))
        tr.stats.starttime = UTCDateTime("1969-01-01T00:00:00")
        # write file
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            _write_mseed(Stream([tr]), tempfile, format="MSEED")
            # read again
            stream = _read_mseed(tempfile)
            stream.verify()

    def test_invalid_data_type(self):
        """
        Writing data of type int64 and int8 are not supported.

        int64 data can now be written since #2356 if it can be downcast to
        int32.
        """
        npts = 6000
        np.random.seed(815)  # make test reproducible
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            # int64
            data = np.random.randint(-1000, 1000, npts).astype(np.int64)
            # add a data point that can not be downcast (even though we have a
            # separate test for this)
            data[4] = np.iinfo(np.int32).max + 2
            st = Stream([Trace(data=data)])
            with pytest.raises(Exception):
                st.write(tempfile, format="MSEED")
            # int8
            data = np.random.randint(-1000, 1000, npts).astype(np.int8)
            st = Stream([Trace(data=data)])
            with pytest.raises(Exception):
                st.write(tempfile, format="MSEED")

    def test_write_wrong_encoding(self, testdata):
        """
        Test to write a floating point mseed file with encoding STEIM1.
        An exception should be raised.
        """
        file = testdata['BW.BGLD.__.EHE.D.2008.001.first_record']
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            # Read the data and convert them to float
            st = read(file)
            st[0].data = st[0].data.astype(np.float32) + .5
            # Type is not consistent float32 cannot be compressed with STEIM1,
            # therefore a exception should be raised.
            with pytest.raises(Exception):
                st.write(tempfile, format="MSEED", encoding=10)

    def test_write_wrong_encoding_via_mseed_stats(self, testdata):
        """
        Test to write a floating point mseed file with encoding STEIM1 with the
        encoding set in stats.mseed.encoding.
        This will just raise a warning.
        """
        file = testdata['BW.BGLD.__.EHE.D.2008.001.first_record']
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            # Read the data and convert them to float
            st = read(file)
            st[0].data = st[0].data.astype(np.float32) + .5
            # Type is not consistent float32 cannot be compressed with STEIM1,
            # therefore a warning should be raised.
            assert st[0].stats.mseed.encoding == 'STEIM1'
            with warnings.catch_warnings(record=True):
                warnings.simplefilter('error', UserWarning)
                with pytest.raises(UserWarning):
                    st.write(tempfile, format="MSEED")

    def test_wrong_record_length_as_argument(self, datapath):
        """
        Specifying a wrong record length should raise an error.
        """
        file = datapath / "encoding" / 'float32_Float32_bigEndian.mseed'
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter('always')
            # invalid reclen
            read(file, reclen=111)
            assert 'Invalid record length' in str(w[0].message)
            assert w[0].category == UserWarning
            # wrong reclen - raises and also displays a warning
            with pytest.raises(Exception):
                read(file, reclen=4096)
            assert 'reclen exceeds buflen' in str(w[1].message)
            assert w[1].category == InternalMSEEDWarning

    def test_read_with_missing_blockette010(self, testdata):
        """
        Reading a Full/Mini-SEED w/o blockette 010 but blockette 008.
        """
        # 1 - Mini-SEED
        file = testdata['blockette008.mseed']
        tr = read(file)[0]
        assert 'BW.PART..EHZ' == tr.id
        assert 1642 == tr.stats.npts
        # 2 - full SEED
        file = testdata['RJOB.BW.EHZ.D.300806.0000.fullseed']
        tr = read(file)[0]
        assert 'BW.RJOB..EHZ' == tr.id
        assert 412 == tr.stats.npts

    def test_issue160(self, testdata):
        """
        Tests issue #160.

        Reading the header of SEED file.
        """
        file = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']
        tr_one = read(file)[0]
        tr_two = read(file, headonly=True)[0]
        ms = AttribDict({'record_length': 512, 'encoding': 'STEIM1',
                         'filesize': 5120, 'dataquality': 'D',
                         'number_of_records': 10, 'byteorder': '>'})
        for tr in tr_one, tr_two:
            assert 'BW.BGLD..EHE' == tr.id
            assert ms == tr.stats.mseed
            assert 4120 == tr.stats.npts
            assert UTCDateTime(2008, 1, 1, 0, 0, 20, 510000) == \
                tr.stats.endtime

    def test_issue217(self, testdata):
        """
        Tests issue #217.

        Reading a MiniSEED file without sequence numbers and a record length of
        1024.
        """
        file = testdata['reclen_1024_without_sequence_numbers.mseed']
        tr = read(file)[0]
        ms = AttribDict({'record_length': 1024, 'encoding': 'STEIM1',
                         'filesize': 2048, 'dataquality': 'D',
                         'number_of_records': 2, 'byteorder': '>'})
        assert 'XX.STF1..HHN' == tr.id
        assert ms == tr.stats.mseed
        assert 932 == tr.stats.npts
        assert UTCDateTime(2007, 5, 31, 22, 45, 46, 720000) == tr.stats.endtime

    def test_issue296(self):
        """
        Tests issue #296.
        """
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            # 1 - transform to np.float64 values
            st = read()
            for tr in st:
                tr.data = tr.data.astype(np.float64)
            # write a single trace automatically detecting encoding
            st[0].write(tempfile, format="MSEED")
            # write a single trace automatically detecting encoding
            st.write(tempfile, format="MSEED")
            # write a single trace with encoding 5
            st[0].write(tempfile, format="MSEED", encoding=5)
            # write a single trace with encoding 5
            st.write(tempfile, format="MSEED", encoding=5)
            # 2 - transform to np.float32 values
            st = read()
            for tr in st:
                tr.data = tr.data.astype(np.float32)
            # write a single trace automatically detecting encoding
            st[0].write(tempfile, format="MSEED")
            # write a single trace automatically detecting encoding
            st.write(tempfile, format="MSEED")
            # write a single trace with encoding 4
            st[0].write(tempfile, format="MSEED", encoding=4)
            # write a single trace with encoding 4
            st.write(tempfile, format="MSEED", encoding=4)
            # 3 - transform to np.int32 values
            st = read()
            for tr in st:
                tr.data = tr.data.astype(np.int32)
            # write a single trace automatically detecting encoding
            st[0].write(tempfile, format="MSEED")
            # write a single trace automatically detecting encoding
            st.write(tempfile, format="MSEED")
            # write a single trace with encoding 3
            st[0].write(tempfile, format="MSEED", encoding=3)
            # write the whole stream with encoding 3
            st.write(tempfile, format="MSEED", encoding=3)
            # write a single trace with encoding 10
            st[0].write(tempfile, format="MSEED", encoding=10)
            # write the whole stream with encoding 10
            st.write(tempfile, format="MSEED", encoding=10)
            # write a single trace with encoding 11
            st[0].write(tempfile, format="MSEED", encoding=11)
            # write the whole stream with encoding 11
            st.write(tempfile, format="MSEED", encoding=11)
            # 4 - transform to np.int16 values
            st = read()
            for tr in st:
                tr.data = tr.data.astype(np.int16)
            # write a single trace automatically detecting encoding
            st[0].write(tempfile, format="MSEED")
            # write a single trace automatically detecting encoding
            st.write(tempfile, format="MSEED")
            # write a single trace with encoding 1
            st[0].write(tempfile, format="MSEED", encoding=1)
            # write the whole stream with encoding 1
            st.write(tempfile, format="MSEED", encoding=1)
            # 5 - transform to ASCII values
            st = read()
            for tr in st:
                tr.data = tr.data.astype('|S1')
            # write a single trace automatically detecting encoding
            st[0].write(tempfile, format="MSEED")
            # write a single trace automatically detecting encoding
            st.write(tempfile, format="MSEED")
            # write a single trace with encoding 0
            st[0].write(tempfile, format="MSEED", encoding=0)
            # write the whole stream with encoding 0
            st.write(tempfile, format="MSEED", encoding=0)

    def test_issue289(self, testdata):
        """
        Tests issue #289.

        Reading MiniSEED using start/end time outside of data should result in
        an empty Stream object.
        """
        # 1
        file = testdata['steim2.mseed']
        st = read(file, starttime=UTCDateTime() - 10, endtime=UTCDateTime())
        assert len(st) == 0
        # 2
        file = testdata['fullseed.mseed']
        st = read(file, starttime=UTCDateTime() - 10, endtime=UTCDateTime())
        assert len(st) == 0

    def test_issue312(self, testdata):
        """
        Tests issue #312

        The blkt_link struct was defined wrong.
        """
        filename = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']
        # start and end time
        ms = _MSStruct(str(filename))
        ms.read(-1, 0, 1, 0)
        blkt_link = ms.msr.contents.blkts.contents
        # The first blockette usually begins after 48 bytes. In the test file
        # it does.
        assert blkt_link.blktoffset == 48
        # The first blockette is blockette 1000 in this file.
        assert blkt_link.blkt_type == 1000
        # Only one blockette.
        assert blkt_link.next_blkt == 0
        # Blockette data is 8 bytes - 4 bytes for the blockette header.
        assert blkt_link.blktdatalen == 4
        del ms

    def test_issue272(self, testdata):
        """
        Tests issue #272

        Option headonly should not read the actual waveform data.
        """
        filename = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']
        # everything
        st = read(filename)
        assert st[0].stats.npts == 4120
        assert len(st[0].data) == 4120
        # headers only
        st = read(filename, headonly=True)
        assert st[0].stats.npts == 4120
        assert len(st[0].data) == 0

    def test_issue325(self, testdata):
        """
        Tests issue #325: Use selection with non default dataquality flag.
        """
        filename = testdata['dataquality-m.mseed']
        # 1 - read all
        st = read(filename)
        assert len(st) == 3
        t1 = st[0].stats.starttime
        t2 = st[0].stats.endtime
        # 2 - select full time window
        st2 = read(filename, starttime=t1, endtime=t2)
        assert len(st2) == 3
        for tr in st2:
            del tr.stats.processing
        assert st == st2
        # 3 - use selection
        st2 = read(filename, starttime=t1, endtime=t2, sourcename='*.*.*.*')
        assert len(st2) == 3
        for tr in st2:
            del tr.stats.processing
        assert st == st2
        st2 = read(filename, starttime=t1, endtime=t2, sourcename='*')
        assert len(st2) == 3
        for tr in st2:
            del tr.stats.processing
        assert st == st2
        # 4 - selection without times
        st2 = read(filename, sourcename='*.*.*.*')
        assert len(st2) == 3
        assert st == st2
        st2 = read(filename, sourcename='*')
        assert len(st2) == 3
        assert st == st2

    def test_issue332(self):
        """
        Tests issue #332

        Writing traces with wrong encoding in stats should raise only a user
        warning.
        """
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            st = read()
            tr = st[0]
            tr.data = tr.data.astype(np.float64) + .5
            tr.stats.mseed = {'encoding': 0}
            with warnings.catch_warnings(record=True):
                warnings.simplefilter('error', UserWarning)
                with pytest.raises(UserWarning):
                    st.write(tempfile, format="MSEED")

    def test_issue341(self):
        """
        Tests issue #341

        Read/write of MiniSEED files with huge sampling rates/delta values.
        """
        with NamedTemporaryFile() as tf:
            tempfile = tf.name
            # 1 - sampling rate
            st = read()
            tr = st[0]
            tr.stats.sampling_rate = 1000000000.0
            tr.write(tempfile, format="MSEED")
            # read again
            st = read(tempfile)
            assert st[0].stats.sampling_rate == 1000000000.0
            # 2 - delta
            st = read()
            tr = st[0]
            tr.stats.delta = 10000000.0
            tr.write(tempfile, format="MSEED")
            # read again
            st = read(tempfile)
            assert round(abs(st[0].stats.delta-10000000.0), 0) == 0

    def test_issue485(self, datapath):
        """
        Test reading floats and doubles, which are bytswapped nans
        """
        ref = [-1188.07800293, 638.16400146, 395.07809448, 1060.28112793]
        for filename in ('nan_float32.mseed', 'nan_float64.mseed'):
            filename = datapath / 'encoding' / filename
            data = read(filename)[0].data.tolist()
            np.testing.assert_array_almost_equal(
                data, ref, decimal=8, err_msg='Data of file %s not equal' %
                filename)

    def test_enforcing_reading_byteorder(self):
        """
        Tests if setting the byte order of the header for reading is passed to
        the C functions.

        Quite simple. It just checks if reading with the correct byte order
        works and reading with the wrong byte order fails.
        """
        tr = Trace(data=np.arange(10, dtype=np.int32))

        # Test with little endian.
        memfile = io.BytesIO()
        tr.write(memfile, format="mseed", byteorder="<")
        memfile.seek(0, 0)
        # Reading little endian should work just fine.
        tr2 = read(memfile, header_byteorder="<")[0]
        memfile.seek(0, 0)
        assert tr2.stats.mseed.byteorder == "<"
        # Remove the mseed specific header fields. These are obviously not
        # equal.
        del tr2.stats.mseed
        del tr2.stats._format
        assert tr == tr2
        # Wrong byte order raises.
        with pytest.raises(ValueError):
            read(memfile, header_byteorder=">")

        # Same test with big endian
        memfile = io.BytesIO()
        tr.write(memfile, format="mseed", byteorder=">")
        memfile.seek(0, 0)
        # Reading big endian should work just fine.
        tr2 = read(memfile, header_byteorder=">")[0]
        memfile.seek(0, 0)
        assert tr2.stats.mseed.byteorder == ">"
        # Remove the mseed specific header fields. These are obviously not
        # equal.
        del tr2.stats.mseed
        del tr2.stats._format
        assert tr == tr2
        # Wrong byte order raises.
        with pytest.raises(ValueError):
            read(memfile, header_byteorder="<")

    def test_long_year_range(self):
        """
        Tests reading and writing years 1900 to 2100.
        """
        tr = Trace(np.arange(5, dtype=np.float32))

        # Year 2056 is non-deterministic for days 1, 256 and 257. These three
        # dates are simply simply not supported right now. See the libmseed
        # documentation for more details.
        # Use every 5th year. Otherwise the test takes too long. Use 1901 as
        # start to get year 2056.
        years = range(1901, 2101, 5)
        for year in years:
            for byteorder in ["<", ">"]:
                memfile = io.BytesIO()
                # Get some random time with the year and the byte order as the
                # seed.
                random.seed(year + ord(byteorder))
                tr.stats.starttime = UTCDateTime(
                    year,
                    julday=random.randrange(1, 365),
                    hour=random.randrange(0, 24),
                    minute=random.randrange(0, 60),
                    second=random.randrange(0, 60))
                if year == 2056:
                    tr.stats.starttime = UTCDateTime(2056, 2, 1)
                tr.write(memfile, format="mseed")
                memfile.seek(0, 0)
                st2 = read(memfile)
                assert len(st2) == 1
                tr2 = st2[0]
                # Remove the mseed specific header fields. These are obviously
                # not equal.
                del tr2.stats.mseed
                del tr2.stats._format
                assert tr == tr2

    def test_full_seed_with_non_default_dataquality(self, testdata):
        """
        Tests the reading of full SEED files with dataqualities other then D.
        """
        # Test the normal one first.
        filename = testdata['fullseed.mseed']
        st = read(filename)
        assert st[0].stats.mseed.dataquality == "D"

        # Test the others. They should also have identical data.
        filename = testdata['fullseed_dataquality_M.mseed']
        st = read(filename)
        data_m = st[0].data
        assert len(st) == 1
        assert st[0].stats.mseed.dataquality == "M"

        filename = testdata['fullseed_dataquality_R.mseed']
        st = read(filename)
        data_r = st[0].data
        assert len(st) == 1
        assert st[0].stats.mseed.dataquality == "R"

        filename = testdata['fullseed_dataquality_Q.mseed']
        st = read(filename)
        data_q = st[0].data
        assert len(st) == 1
        assert st[0].stats.mseed.dataquality == "Q"

        # Assert that the data is the same.
        np.testing.assert_array_equal(data_m, data_r)
        np.testing.assert_array_equal(data_m, data_q)

    @pytest.mark.skipif(
        "CONDAFORGE" in os.environ and
        os.environ.get("APPVEYOR", "false").lower() == "true",
        reason='Test is known to fail when building conda package in '
               'Appveyor.')
    def test_infinite_loop(self, testdata):
        """
        Tests that libmseed doesn't enter an infinite loop on buggy files.
        """
        filename = testdata['infinite-loop.mseed']

        # Problem with overlap between obspy.signal and mp
        # so, try using 'fork' for unix systems
        if platform.system() == 'Windows':
            ctx = multiprocessing.get_context('spawn')
        else:
            ctx = multiprocessing.get_context('fork')

        process = ctx.Process(target=_test_function,
                              args=(filename, ))
        process.start()
        process.join(60)

        fail = process.is_alive()
        process.terminate()
        if process.is_alive():
            if platform.system() == 'Windows':
                # CTRL_BREAK_EVENT
                os.kill(process.pid, 1)
            else:
                # SKIGKILL
                os.kill(process.pid, 9)
        assert not fail

    def test_microsecond_accuracy_reading_and_writing_before_1970(self):
        """
        Tests that reading and writing data with microsecond accuracy and
        before 1970 works as expected.
        """
        # Test a couple of timestamps. Positive and negative ones.
        timestamps = [123456.789123, -123456.789123, 1.123400, 1.123412,
                      1.123449, 1.123450, 1.123499, -1.123400, -1.123412,
                      -1.123449, -1.123450, -1.123451, -1.123499]

        for timestamp in timestamps:
            starttime = UTCDateTime(timestamp)
            assert starttime.timestamp == timestamp

            tr = Trace(data=np.linspace(0, 100, 101))
            tr.stats.starttime = starttime

            with io.BytesIO() as fh:
                tr.write(fh, format="mseed")
                fh.seek(0, 0)
                tr2 = read(fh)[0]

            del tr2.stats.mseed
            del tr2.stats._format

            assert tr2.stats.starttime == starttime
            assert tr2 == tr

    def test_reading_noise_records(self, testdata):
        """
        Tests reading a noise record. See #1495.
        """
        file = testdata['single_record_plus_noise_record.mseed']
        st = read(file)
        assert len(st) == 1
        tr = st[0]
        assert tr.id == "IM.NV32..BHE"
        assert tr.stats.npts == 277
        assert tr.stats.sampling_rate == 40.0

    def test_read_file_with_various_noise_records(self, testdata):
        """
        Tests reading a custom made file with noise records.
        """
        # This file has the following layout:
        # 1. 256 byte NOISE record
        # 2. 512 byte normal record - station NV30
        # 3. 128 byte NOISE record
        # 4. 512 byte normal record - station NV31
        # 5. 512 byte NOISE record
        # 6. 512 byte NOISE record
        # 7. 512 byte normal record - station NV32
        # 8. 1024 byte NOISE record
        # 9. 512 byte normal record - station NV33
        file = testdata['various_noise_records.mseed']
        st = read(file)

        assert len(st), 4
        assert st[0].stats.station, "NV30"
        assert st[1].stats.station, "NV31"
        assert st[2].stats.station, "NV32"
        assert st[3].stats.station, "NV33"

        # Data is the same across all records.
        np.testing.assert_allclose(st[0].data, st[1].data)
        np.testing.assert_allclose(st[0].data, st[2].data)
        np.testing.assert_allclose(st[0].data, st[3].data)

    def test_mseed_zero_data_offset(self, datapath):
        """
        Tests that a data offset of zero in the fixed header does not
        confuse ObsPy.

        The file contains three records: a normal one, followed by one with
        a data-offset of zero, followed by another normal one.

        This currently results in three returned traces:

        * CH.PANIX..LHZ |
          2016-08-21T01:41:19.000000Z - 2016-08-21T01:45:30.000000Z |
          1.0 Hz, 252 samples
        * CH.PANIX..LHZ |
          2016-08-21T01:43:37.000000Z - 2016-08-21T01:43:37.000000Z |
          1.0 Hz, 0 samples
        * CH.PANIX..LHZ |
          2016-08-21T01:45:31.000000Z - 2016-08-21T01:49:52.000000Z |
          1.0 Hz, 262 samples
        """
        file = datapath / "bizarre" / "mseed_data_offset_0.mseed"
        st = read(file)

        assert len(st) == 3

        tr = st[0]
        assert tr.id == "CH.PANIX..LHZ"
        assert tr.stats.starttime == UTCDateTime("2016-08-21T01:41:19.000000Z")
        assert tr.stats.endtime == UTCDateTime("2016-08-21T01:45:30.000000Z")
        assert tr.stats.npts == len(tr.data)
        assert tr.stats.npts == 252

        tr = st[1]
        assert tr.id == "CH.PANIX..LHZ"
        assert tr.stats.starttime == UTCDateTime("2016-08-21T01:43:37.000000Z")
        assert tr.stats.endtime == UTCDateTime("2016-08-21T01:43:37.000000Z")
        assert tr.stats.npts == len(tr.data)
        assert tr.stats.npts == 0

        tr = st[2]
        assert tr.id == "CH.PANIX..LHZ"
        assert tr.stats.starttime == UTCDateTime("2016-08-21T01:45:31.000000Z")
        assert tr.stats.endtime == UTCDateTime("2016-08-21T01:49:52.000000Z")
        assert tr.stats.npts == len(tr.data)
        assert tr.stats.npts == 262

    def test_mseed_zero_data_headonly(self, testdata):
        """
        Tests that records with no data correctly work in headonly mode.
        """
        file = testdata['three_records_zero_data_in_middle.mseed']

        expected = [
            ("BW.BGLD..EHE", UTCDateTime("2007-12-31T23:59:59.765000Z"),
             UTCDateTime("2008-01-01T00:00:01.820000Z"), 200.0, 412),
            ("BW.BGLD..EHE", UTCDateTime("2008-01-01T00:00:01.825000Z"),
             UTCDateTime("2008-01-01T00:00:01.825000Z"), 200.0, 0),
            ("BW.BGLD..EHE", UTCDateTime("2008-01-01T00:00:03.885000Z"),
             UTCDateTime("2008-01-01T00:00:05.940000Z"), 200.0, 412)]

        # Default full read.
        st = read(file)
        assert len(st) == 3
        for tr, exp in zip(st, expected):
            assert tr.id == exp[0]
            assert tr.stats.starttime == exp[1]
            assert tr.stats.endtime == exp[2]
            assert tr.stats.sampling_rate == exp[3]
            assert tr.stats.npts == exp[4]

        # Headonly read.
        st = read(file, headonly=True)
        assert len(st) == 3
        for tr, exp in zip(st, expected):
            assert tr.id == exp[0]
            assert tr.stats.starttime == exp[1]
            assert tr.stats.endtime == exp[2]
            assert tr.stats.sampling_rate == exp[3]
            assert tr.stats.npts == exp[4]

    def test_read_file_with_microsecond_wrap(self, testdata):
        """
        This is not strictly valid but I encountered such a file in practice
        so I guess it happens. Libmseed can also correctly deal with it.

        The test file is a single record with the .0001 seconds field set to
        10000. SEED strictly allows only 0-9999 in this field.
        """
        file = testdata['microsecond_wrap.mseed']

        with warnings.catch_warnings(record=True) as w_1:
            warnings.simplefilter("always")
            info = util.get_record_information(file)

        assert w_1[0].message.args[0] == \
            ('Record contains a fractional seconds (.0001 secs) '
             'of 10000 - the maximum strictly allowed value is '
             '9999. It will be interpreted as one or more '
             'additional seconds.')

        with warnings.catch_warnings(record=True) as w_2:
            warnings.simplefilter("always")
            tr = read(file)[0]

        # First warning is identical.
        assert w_1[0].message.args[0] == w_2[0].message.args[0]
        # Second warning is raised by libmseed.
        assert w_2[1].message.args[0] == \
            ('readMSEEDBuffer(): Record with offset=0 has a '
             'fractional second (.0001 seconds) of 10000. This '
             'is not strictly valid but will be interpreted as '
             'one or more additional seconds.')

        # Make sure libmseed and the internal ObsPy record parser produce
        # the same result.
        assert info["starttime"] == tr.stats.starttime
        assert info["endtime"] == tr.stats.endtime

        # Read with a hex-editor.
        ref_time = UTCDateTime(year=2008, julday=8, hour=4, minute=58,
                               second=5 + 1)
        assert ref_time == info["starttime"]
        assert ref_time == tr.stats.starttime

    def test_reading_miniseed_with_no_blockette_1000(self, datapath):
        """
        Blockette 1000 was only introduced with SEED version 2.3.
        """
        file = datapath / "bizarre" / "mseed_no_blkt_1000.mseed"
        st = read(file)

        assert len(st) == 1
        tr = st[0]
        assert tr.stats.npts == 7536
        np.testing.assert_allclose(tr.stats.sampling_rate, 20.0)
        assert tr.stats.starttime == UTCDateTime(1976, 3, 10, 3, 28)
        assert tr.id == ".GRA1..BHZ"

        # Also test the data to make sure the unpacking was successful.
        np.testing.assert_equal(
            st[0].data[:10],
            [-185, -200, -209, -220, -228, -246, -252, -262, -262, -269])

    def test_writing_of_blockette_100(self):
        """
        Tests the writing of blockette 100 if necessary.
        """
        def has_blkt_100(filename):
            ms = _MSStruct(filename)
            ms.read(-1, 0, 1, 0)
            blkt_link = ms.msr.contents.blkts
            has_blkt_100 = False
            while True:
                try:
                    if blkt_link.contents.blkt_type == 100:
                        has_blkt_100 = True
                        break
                    blkt_link = blkt_link.contents.next
                except ValueError:
                    break
            del ms
            return has_blkt_100

        tr = read()[0]
        tr.data = tr.data[:10]

        with NamedTemporaryFile() as tf:
            tempfile = tf.name

            # A "clean" sampling rate does not require blockette 100.
            sr = 200.0
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

            # A more detailed one does.
            sr = 199.9997
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert has_blkt_100(tempfile)

            # A very large (but "clean") one does not need it.
            sr = 1E6
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

            # Same for a very small but "clean" one.
            sr = 1E-6
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

            # But small perturbations resulting in "unclean" sampling rates
            # will cause blockette 100 to be written.
            sr = 1E6 + 0.123456
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert has_blkt_100(tempfile)
            sr = 1E-6 + 0.325247 * 1E-7
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert has_blkt_100(tempfile)

            # Three more "clean" ones.
            sr = 1.0
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

            sr = 0.5
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

            sr = 0.1
            tr.stats.sampling_rate = sr
            tr.write(tempfile, format="mseed")
            assert np.float32(read(tempfile)[0].stats.sampling_rate) == \
                np.float32(sr)
            assert not has_blkt_100(tempfile)

    def test_reading_file_with_data_offset_of_48(self, testdata):
        """
        Tests reading a file which has a data offset of 48 bytes.

        It thus does not have a single blockette.
        """
        file = testdata['mseed_not_a_single_blkt_48byte_data_offset.mseed']
        st = read(file)

        assert len(st) == 1
        tr = st[0]
        assert tr.stats.npts == 3632
        np.testing.assert_allclose(tr.stats.sampling_rate, 20.0)
        assert tr.stats.starttime == UTCDateTime(1995, 9, 22, 0, 0, 18, 238400)
        assert tr.id == "XX.TEST..BHE"

        np.testing.assert_equal(
            st[0].data[:6],
            [337, 396, 454, 503, 547, 581])

    def test_reading_truncated_miniseed_files(self, testdata):
        """
        Regression test to guard against a segfault.
        """
        filename = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']

        with io.open(filename, 'rb') as fh:
            data = fh.read()

        data = data[:-257]
        # This is the offset for the record that later has to be recorded in
        # the warning.
        assert len(data) - 255 == 4608

        # The file now lacks information at the end. This will read the file
        # until that point and raise a warning that some things are missing.
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            with io.BytesIO(data) as buf:
                st = _read_mseed(buf)
        assert len(st) == 1
        assert len(w) == 1
        assert w[0].category is InternalMSEEDWarning
        assert w[0].message.args[0] == (
            "readMSEEDBuffer(): Unexpected end of file when "
            "parsing record starting at offset 4608. The rest of "
            "the file will not be read.")

    def test_reading_truncated_miniseed_files_case_2(self, testdata):
        """
        Second test in the same vain as
        test_reading_truncated_miniseed_files. Previously forgot a `<=` test.
        """
        filename = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']

        with io.open(filename, 'rb') as fh:
            data = fh.read()

        data = data[:-256]
        # This is the offset for the record that later has to be recorded in
        # the warning.
        assert len(data) - 256 == 4608

        # The file now lacks information at the end. This will read the file
        # until that point and raise a warning that some things are missing.
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            with io.BytesIO(data) as buf:
                st = _read_mseed(buf)
        assert len(st) == 1
        assert len(w) == 1
        assert w[0].category is InternalMSEEDWarning
        assert w[0].message.args[0] == (
            "readMSEEDBuffer(): Unexpected end of file when "
            "parsing record starting at offset 4608. The rest of "
            "the file will not be read.")

    def test_reading_less_than_128_bytes(self, testdata):
        """
        128 bytes is the smallest possible MiniSEED record.

        Reading anything smaller should result in an error.
        """
        filename = testdata['BW.BGLD.__.EHE.D.2008.001.first_10_records']

        with io.open(filename, 'rb') as fh:
            data = fh.read()

        # Reading at exactly 128 bytes offset will result in a truncation
        # warning.
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            with io.BytesIO(data[:128]) as buf:
                st = _read_mseed(buf)
        assert len(st) == 0  # nothing is read here.
        assert len(w) >= 1
        assert w[-1].category is InternalMSEEDWarning
        assert w[-1].message.args[0] == (
            "readMSEEDBuffer(): Unexpected end of file when "
            "parsing record starting at offset 0. The rest of "
            "the file will not be read.")

        # Reading anything less result in an exception.
        msg = ("The smallest possible mini-SEED record is made up of 128 "
               "bytes. The passed buffer or file contains only 127.")
        with pytest.raises(ObsPyMSEEDFilesizeTooSmallError, match=msg):
            with io.BytesIO(data[:127]) as buf:
                _read_mseed(buf)

    def test_read_file_with_non_valid_blocks_in_between(self, testdata):
        """
        Test reading MiniSEED files that have some non-valid blocks in-between.
        """
        # This file has two 4096 bytes records.
        filename = testdata['test.mseed']
        with io.open(filename, "rb") as fh:
            rec1 = fh.read(4096)
            rec2 = fh.read(4096)

        reference = _read_mseed(filename)
        del reference[0].stats.mseed

        # Fill with zero bytes.
        for length in (128, 256, 512, 1024, 2048, 4096, 8192):
            with io.BytesIO() as buf:
                buf.write(rec1)
                buf.write(b'\x00' * length)
                buf.write(rec2)
                buf.seek(0, 0)
                # This will raise 1 warning per 128 bytes.
                with CatchAndAssertWarnings() as w:
                    st = _read_mseed(buf)
                assert len(w) == length // 128

            # Also explicitly test the first warning message which should be
            # identical for all cases.
            assert str(w[0].message) == \
                "readMSEEDBuffer(): Not a SEED record. Will skip bytes " \
                "4096 to 4223."

            # Should always be two records.
            assert st[0].stats.mseed.number_of_records == 2

            # Remove things like file-size and what not.
            del st[0].stats.mseed
            assert reference == st

        # Try the same thing but fill with random bytes.
        # The seed is not really needed but hopefully guards against the
        # very very rare case of random bytes making up a valid SEED record.
        np.random.seed(34980)
        for length in (128, 256, 512, 1024, 2048, 4096, 8192):
            with io.BytesIO() as buf:
                buf.write(rec1)
                buf.write(np.random.bytes(length))
                buf.write(rec2)
                buf.seek(0, 0)
                # This will raise 1 warning per 128 bytes.
                with CatchAndAssertWarnings() as w:
                    st = _read_mseed(buf)
                assert len(w) == length // 128

            # Should always be two records.
            assert st[0].stats.mseed.number_of_records == 2

            # Remove things like file-size and what not.
            del st[0].stats.mseed
            assert reference == st

    def test_reading_files_with_non_ascii_headers(self, testdata):
        """
        Some dataloggers appear to do this.

        See #2177.
        """
        filename = testdata['gecko_non_ascii_header.ms']
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            st = read(filename)
        # Two identical messages will be raised (but in most common cases only
        # the first will be shown).
        assert len(w) == 2
        assert w[0].message.args[0].startswith(
            "Failed to decode location code as ASCII.")
        assert len(st) == 1
        assert st[0].id == '.GECKO.A.CNZ'
