# -*- coding: utf-8 -*-
# Copyright (C) Duncan Macleod (2014-2020)
#
# This file is part of GWpy.
#
# GWpy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GWpy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GWpy.  If not, see <http://www.gnu.org/licenses/>.

"""Tests for :mod:`gwpy.segments`
"""

import os.path
import re
import tempfile
from io import BytesIO
from ssl import SSLError

from six.moves.urllib.error import (URLError, HTTPError)

import pytest

import h5py

from matplotlib import rc_context

from ...plot import SegmentAxes
from ...segments import (Segment, SegmentList,
                         DataQualityFlag, DataQualityDict)
from ...testing import (mocks, utils)
from ...testing.compat import mock
from ...utils.misc import null_context

__author__ = 'Duncan Macleod <duncan.macleod@ligo.org>'


# -- veto definer fixture -----------------------------------------------------

VETO_DEFINER_FILE = """<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE LIGO_LW SYSTEM "http://ldas-sw.ligo.caltech.edu/doc/ligolwAPI/html/ligolw_dtd.txt">
<LIGO_LW>
    <Table Name="veto_definer:table">
        <Column Type="int_4s" Name="veto_definer:category"/>
        <Column Type="lstring" Name="veto_definer:comment"/>
        <Column Type="int_4s" Name="veto_definer:end_pad"/>
        <Column Type="ilwd:char" Name="veto_definer:process_id"/>
        <Column Type="lstring" Name="veto_definer:name"/>
        <Column Type="int_4s" Name="veto_definer:version"/>
        <Column Type="int_4s" Name="veto_definer:start_pad"/>
        <Column Type="int_4s" Name="veto_definer:start_time"/>
        <Column Type="lstring" Name="veto_definer:ifo"/>
        <Column Type="int_4s" Name="veto_definer:end_time"/>
        <Stream Delimiter="," Type="Local" Name="veto_definer:table">
            1,"Test flag 1",2,,"TEST-FLAG",1,-1,100,"X1",0,
            2,"Test flag 1",2,,"TEST-FLAG_2",1,1,100,"X1",200,
            2,"Test flag 1",2,,"TEST-FLAG_2",2,-2,200,"X1",0,
            2,"Test flag 1",2,,"TEST-FLAG_2",2,-2,100,"Y1",0,
        </Stream>
    </Table>
</LIGO_LW>
"""  # noqa: E501


@pytest.fixture(scope='module')
def veto_definer():
    with tempfile.NamedTemporaryFile(suffix='.xml', mode='w',
                                     delete=False) as f:
        f.write(VETO_DEFINER_FILE)
        f.seek(0)
        yield f.name
    if os.path.isfile(f.name):
        os.remove(f.name)


# -- test data ----------------------------------------------------------------

def _as_segmentlist(*segments):
    return SegmentList([Segment(a, b) for a, b in segments])


NAME = 'X1:TEST-FLAG_NAME:0'

# simple list of 'known' segments
KNOWN = _as_segmentlist(
    (0, 3), (6, 7))

# simple  list of 'active' segments
ACTIVE = _as_segmentlist(
    (1, 2), (3, 4), (5, 7))

# intersection of above 'known' and 'active' segments
KNOWNACTIVE = _as_segmentlist(
    (1, 2), (6, 7))

# 'active' set contracted by 0.1 seconds
ACTIVE_CONTRACTED = _as_segmentlist(
    (1.1, 1.9), (3.1, 3.9), (5.1, 6.9))

# 'active' seg protracted by 0.1 seconts
ACTIVE_PROTRACTED = _as_segmentlist(
    (.9, 2.1), (2.9, 4.1), (4.9, 7.1))

# some more segments
KNOWN2 = _as_segmentlist(
    (100, 150))

ACTIVE2 = _as_segmentlist(
    (100, 101), (110, 120))

# padding
PADDING = (-0.5, 1)

# padded version of above 'known' segments
KNOWNPAD = _as_segmentlist(
    (-.5, 4), (5.5, 8))

# padded version of above 'active' segments
ACTIVEPAD = _as_segmentlist(
    (.5, 3), (2.5, 5), (4.5, 8))

# padded, coalesed version of above 'active' segments
ACTIVEPADC = _as_segmentlist(
    (.5, 4), (5.5, 8))


# -- query helpers ------------------------------------------------------------

QUERY_FLAGS = ['X1:TEST-FLAG:1', 'Y1:TEST-FLAG2:4']

QUERY_RESULT = DataQualityDict()

QUERY_RESULT['X1:TEST-FLAG:1'] = DataQualityFlag(
    'X1:TEST-FLAG:1',
    known=[(0, 10)],
    active=[(0, 1), (1, 2), (3, 4), (6, 9)])

QUERY_RESULT['Y1:TEST-FLAG2:4'] = DataQualityFlag(
    'Y1:TEST-FLAG2:4',
    known=[(0, 5), (9, 10)],
    active=[])

QUERY_RESULTC = type(QUERY_RESULT)({x: y.copy().coalesce() for
                                    x, y in QUERY_RESULT.items()})


def query_segdb(query_func, *args, **kwargs):
    """Mock a query to an S6-style DB2 database
    """
    try:
        with mock.patch('glue.segmentdb.segmentdb_utils.setup_database'), \
             mock.patch('glue.segmentdb.segmentdb_utils.expand_version_number',
                        mocks.segdb_expand_version_number(1, 4)), \
             mock.patch('glue.segmentdb.segmentdb_utils.query_segments',
                        mocks.segdb_query_segments(QUERY_RESULT)):
            return query_func(*args, **kwargs)
    except ImportError as e:
        pytest.skip(str(e))


def segdb_expand_version_number(min_, max_):
    def expand_version_number(engine, segdef):
        ifo, name, version, start_time, end_time, start_pad, end_pad = segdef
        if version != '*':
            return [segdef]
        return [[ifo, name, v, start_time, end_time, start_pad, end_pad] for
                v in range(min_, max_+1)[::-1]]

    return expand_version_number


def segdb_query_segments(result):
    def query_segments(engine, tablename, segdefs):
        out = []
        for ifo, flag, version, start, end, startpad, endpad in segdefs:
            flag = '%s:%s:%d' % (ifo, flag, version)
            if flag not in result:
                out.append([])
            if tablename == 'segment':
                out.append(result[flag].active)
            else:
                out.append(result[flag].known)
        return out
    return query_segments


def dqsegdb2_query_segments(result, deactivated=False,
                            active_indicates_ifo_badness=False, **kwargs):
    """Build a mock of `dqsegdb2.query.query_segments` for testing
    """
    def query_segments(flag, start, end, host=None):
        try:
            ifo, name, version = flag.split(':')
            version = int(version)
        except ValueError:
            ifo, name = flag.split(':', 1)
            version = None
        span = SegmentList([Segment(start, end)])
        reflag = re.compile(flag)
        try:
            actual = list(filter(reflag.match, result))[0]
        except IndexError:
            raise HTTPError('test-url/', 404, 'Not found', None, None)
        return {
            'ifo': ifo,
            'name': name,
            'version': version,
            'known': list(map(tuple, result[actual].known & span)),
            'active': list(map(tuple, result[actual].active & span)),
            'query_information': {},
            'metadata': kwargs,
        }

    return query_segments


# -- DataQualityFlag ----------------------------------------------------------

class TestDataQualityFlag(object):
    TEST_CLASS = DataQualityFlag

    @classmethod
    def create(cls, name=NAME, known=KNOWN, active=ACTIVE, **kwargs):
        return cls.TEST_CLASS(name=name, known=known, active=active, **kwargs)

    @classmethod
    @pytest.fixture()
    def flag(cls):
        return cls.create()

    @classmethod
    @pytest.fixture()
    def empty(cls):
        return cls.TEST_CLASS()

    # -- test attributes ------------------------

    def test_name(self, empty, flag):
        assert empty.name is None

        assert flag.name == NAME
        assert flag.ifo == NAME.split(':')[0]
        assert flag.tag == NAME.split(':')[1]
        assert flag.version == int(NAME.split(':')[2])

    def test_known(self, empty, flag):
        assert isinstance(empty.known, SegmentList)
        assert empty.known == []

        utils.assert_segmentlist_equal(flag.known, KNOWN)

        new = self.TEST_CLASS()
        new.known = [(1, 2), (3, 4)]
        assert isinstance(empty.known, SegmentList)

    def test_active(self, empty, flag):
        assert isinstance(empty.active, SegmentList)
        assert empty.active == []

        utils.assert_segmentlist_equal(flag.active, ACTIVE)

        new = self.TEST_CLASS()
        new.active = [(1, 2), (3, 4)]
        assert isinstance(empty.active, SegmentList)

    def test_texname(self, empty, flag):
        assert empty.texname is None
        assert flag.texname == NAME.replace('_', r'\_')

    def test_extent(self, empty, flag):
        assert flag.extent == (KNOWN[0][0], KNOWN[-1][1])
        with pytest.raises(ValueError):
            empty.extent

    def test_livetime(self, empty, flag):
        assert empty.livetime == 0
        assert flag.livetime == abs(ACTIVE)

    def test_regular(self, empty, flag):
        assert empty.regular is True
        assert flag.regular is False

    def test_padding(self, flag):
        assert flag.padding == (0, 0)

        flag.padding = [-1, 2]
        assert isinstance(flag.padding, tuple)
        assert flag.padding == (-1, 2)

        flag.padding = None
        assert flag.padding == (0, 0)

        del flag.padding
        assert flag.padding == (0, 0)

    # -- test methods ---------------------------

    def test_parse_name(self):
        flag = self.TEST_CLASS(None)
        assert flag.name is None
        assert flag.ifo is None
        assert flag.tag is None
        assert flag.version is None

        flag = self.TEST_CLASS('test')
        assert flag.name == 'test'
        assert flag.ifo is None
        assert flag.tag is None
        assert flag.version is None

        flag = self.TEST_CLASS('L1:test')
        assert flag.name == 'L1:test'
        assert flag.ifo == 'L1'
        assert flag.tag == 'test'
        assert flag.version is None

        flag = self.TEST_CLASS('L1:test:1')
        assert flag.name == 'L1:test:1'
        assert flag.ifo == 'L1'
        assert flag.tag == 'test'
        assert isinstance(flag.version, int)
        assert flag.version == 1

        flag = self.TEST_CLASS('test:1')
        assert flag.name == 'test:1'
        assert flag.ifo is None
        assert flag.tag == 'test'
        assert flag.version == 1

    def test_plot(self, flag):
        flag.label = 'Test label'
        with rc_context(rc={'text.usetex': False}):
            plot = flag.plot(figsize=(6.4, 3.8))
            assert isinstance(plot.gca(), SegmentAxes)
            assert plot.gca().get_epoch() == flag.known[0][0]
            assert len(plot.gca().collections) == 2
            assert len(plot.gca().collections[1].get_paths()) == len(KNOWN)
            assert len(plot.gca().collections[0].get_paths()) == len(ACTIVE)
            assert plot.gca().collections[0].get_label() == flag.label

            plot.save(BytesIO(), format='png')
            plot.close()

        flag.label = None
        with rc_context(rc={'text.usetex': True}):
            plot = flag.plot(figsize=(6.4, 3.8))
            assert plot.gca().collections[0].get_label() == flag.texname
            plot.close()

    def test_math(self):
        a = self.TEST_CLASS(active=ACTIVE[:2], known=KNOWN)
        b = self.TEST_CLASS(active=ACTIVE[2:], known=KNOWN)

        # and
        x = a & b
        utils.assert_segmentlist_equal(x.active, a.active & b.active)
        utils.assert_segmentlist_equal(x.known, a.known & b.known)

        # sub
        x = a - b
        utils.assert_segmentlist_equal(x.known, a.known & b.known)
        utils.assert_segmentlist_equal(x.active,
                                       (a.active - b.active) & x.known)

        # or
        x = a | b
        utils.assert_segmentlist_equal(x.active, a.active | b.active)
        utils.assert_segmentlist_equal(x.known, a.known | b.known)

        # invert
        x = ~a
        utils.assert_segmentlist_equal(x.active, a.known & ~a.active)
        utils.assert_segmentlist_equal(x.known, a.known)

    def test_coalesce(self):
        flag = self.create()
        flag.coalesce()
        utils.assert_segmentlist_equal(flag.known, KNOWN)
        utils.assert_segmentlist_equal(flag.active, KNOWNACTIVE)
        assert flag.regular is True

    def test_contract(self):
        flag = self.create()
        flag.contract(.1)
        utils.assert_segmentlist_equal(flag.known, KNOWN)
        utils.assert_segmentlist_equal(flag.active, ACTIVE_CONTRACTED)

    def test_protract(self):
        flag = self.create(active=ACTIVE_CONTRACTED)
        flag.protract(.1)
        utils.assert_segmentlist_equal(flag.known, KNOWN)
        utils.assert_segmentlist_equal(flag.active, ACTIVE)

    @pytest.mark.parametrize('contract, active', [
        (False, ACTIVE_CONTRACTED),
        (True, ACTIVE_PROTRACTED),
    ])
    def test_round(self, contract, active):
        flag = self.create(active=active)
        r = flag.round(contract=contract)
        utils.assert_segmentlist_equal(r.known, KNOWN)
        utils.assert_segmentlist_equal(r.active, KNOWNACTIVE)

    def test_pad(self, flag):
        # test with no arguments (and no padding)
        padded = flag.pad()
        utils.assert_flag_equal(flag, padded)

        # test with padding
        flag.padding = PADDING
        padded = flag.pad()
        utils.assert_segmentlist_equal(padded.known, KNOWNPAD)
        utils.assert_segmentlist_equal(padded.active, ACTIVEPAD)

        # test with arguments
        flag.padding = None
        padded = flag.pad(*PADDING)
        utils.assert_segmentlist_equal(padded.known, KNOWNPAD)
        utils.assert_segmentlist_equal(padded.active, ACTIVEPAD)

        # test in-place
        padded = flag.pad(*PADDING)
        assert padded is not flag
        padded = flag.pad(*PADDING, inplace=True)
        assert padded is flag
        utils.assert_segmentlist_equal(flag.known, KNOWNPAD)
        utils.assert_segmentlist_equal(flag.active, ACTIVEPAD)

        # check that other keyword arguments get rejected appropriately
        with pytest.raises(TypeError):
            flag.pad(*PADDING, kwarg='test')

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    def test_from_veto_def(self):
        from ligo.lw.lsctables import VetoDef

        def veto_def(ifo, name, version, **kwargs):
            vdef = VetoDef()
            kwargs['ifo'] = ifo
            kwargs['name'] = name
            kwargs['version'] = version
            for key in VetoDef.__slots__:
                setattr(vdef, key, kwargs.get(key, None))
            return vdef

        a = veto_def('X1', 'TEST-FLAG', 1, start_time=0, end_time=0,
                     start_pad=-2, end_pad=2, comment='Comment')
        f = self.TEST_CLASS.from_veto_def(a)
        assert f.name == 'X1:TEST-FLAG:1'
        assert f.category is None
        assert f.padding == (-2, 2)
        assert f.description == 'Comment'
        utils.assert_segmentlist_equal(f.known, [(0, float('inf'))])

        a = veto_def('X1', 'TEST-FLAG', None, start_time=0, end_time=1)
        f = self.TEST_CLASS.from_veto_def(a)
        assert f.name == 'X1:TEST-FLAG'
        assert f.version is None

    def test_populate(self):
        name = QUERY_FLAGS[0]
        flag = self.TEST_CLASS(name, known=QUERY_RESULT[name].known)

        with mock.patch('gwpy.segments.flag.query_segments',
                        dqsegdb2_query_segments(QUERY_RESULT)):
            flag.populate()
        utils.assert_flag_equal(flag, QUERY_RESULTC[name])

    # -- test I/O -------------------------------

    @pytest.mark.parametrize('format, ext, rw_kwargs, simple', [
        ('hdf5', 'hdf5', {'path': 'test-dqflag'}, False),
        ('hdf5', 'h5', {'path': 'test-dqflag'}, False),
        ('json', 'json', {}, True),
    ])
    def test_read_write(self, flag, format, ext, rw_kwargs, simple):
        # simplify calling read/write tester
        def _read_write(**kwargs):
            read_kw = rw_kwargs.copy()
            read_kw.update(kwargs.pop('read_kw', {}))
            write_kw = rw_kwargs.copy()
            write_kw.update(kwargs.pop('write_kw', {}))
            return utils.test_read_write(flag, format, extension=ext,
                                         assert_equal=utils.assert_flag_equal,
                                         read_kw=read_kw, write_kw=write_kw,
                                         **kwargs)

        # perform simple test
        if simple:
            _read_write()

        # perform complicated test
        else:
            _read_write(autoidentify=False)
            with pytest.raises(IOError):
                _read_write(autoidentify=True)
            _read_write(autoidentify=True, write_kw={'overwrite': True})

    def test_read_write_hdf5(self, flag):
        with utils.TemporaryFilename(suffix=".h5") as fp:
            flag.write(fp, path="test")
            f2 = self.TEST_CLASS.read(fp, path="test", format="hdf5")
            utils.assert_flag_equal(f2, flag)

            # test direct access from dataset
            with h5py.File(fp, "r") as h5f:
                f2 = self.TEST_CLASS.read(h5f["test"])
                utils.assert_flag_equal(f2, flag)

            # test auto-discover of single dataset
            f2 = self.TEST_CLASS.read(fp)
            utils.assert_flag_equal(f2, flag)

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    @pytest.mark.parametrize("ilwdchar_compat", [False, True])
    def test_read_write_ligolw(self, flag, ilwdchar_compat):
        utils.test_read_write(
            flag, "ligolw", extension="xml",
            assert_equal=utils.assert_flag_equal,
            autoidentify=False,
            read_kw={}, write_kw={"ilwdchar_compat": ilwdchar_compat},
        )

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    def test_write_ligolw_attrs(self, flag):
        from gwpy.io.ligolw import read_table
        with tempfile.NamedTemporaryFile(suffix='.xml') as f:
            flag.write(
                f,
                format='ligolw',
                attrs={'process_id': 100},
                ilwdchar_compat=False,
            )
            segdeftab = read_table(f, 'segment_definer')
            assert int(segdeftab[0].process_id) == 100

    # -- test queries ---------------------------

    def test_query(self):
        with mock.patch('gwpy.segments.flag.query_segments',
                        dqsegdb2_query_segments(QUERY_RESULT)):
            result = self.TEST_CLASS.query(QUERY_FLAGS[0], 0, 10)

        assert isinstance(result, self.TEST_CLASS)
        RESULT = QUERY_RESULT[QUERY_FLAGS[0]].copy().coalesce()
        utils.assert_segmentlist_equal(result.known, RESULT.known)
        utils.assert_segmentlist_equal(result.active, RESULT.active)

    def test_query_segdb(self):
        with pytest.deprecated_call():
            result = query_segdb(self.TEST_CLASS.query_segdb,
                                 QUERY_FLAGS[0], 0, 10)
        RESULT = QUERY_RESULT[QUERY_FLAGS[0]]

        assert isinstance(result, self.TEST_CLASS)
        utils.assert_segmentlist_equal(result.known, RESULT.known)
        utils.assert_segmentlist_equal(result.active, RESULT.active)

        with pytest.deprecated_call():
            result2 = query_segdb(self.TEST_CLASS.query_segdb,
                                  QUERY_FLAGS[0], (0, 10))
        utils.assert_flag_equal(result, result2)

        with pytest.deprecated_call():
            result2 = query_segdb(self.TEST_CLASS.query_segdb,
                                  QUERY_FLAGS[0], SegmentList([(0, 10)]))
        utils.assert_flag_equal(result, result2)

        with pytest.deprecated_call():
            with pytest.raises(ValueError):
                self.TEST_CLASS.query_segdb(QUERY_FLAGS[0], 1, 2, 3)
            with pytest.raises(ValueError):
                self.TEST_CLASS.query_segdb(QUERY_FLAGS[0], (1, 2, 3))

    @pytest.mark.parametrize('name, flag', [
        (QUERY_FLAGS[0], QUERY_FLAGS[0]),  # regular query
        (QUERY_FLAGS[0].rsplit(':', 1)[0], QUERY_FLAGS[0]),  # versionless
    ])
    @mock.patch('gwpy.segments.flag.query_segments',
                dqsegdb2_query_segments(QUERY_RESULT))
    def test_query_dqsegdb(self, name, flag):
        # standard query
        result = self.TEST_CLASS.query_dqsegdb(name, 0, 10)
        RESULT = QUERY_RESULTC[flag]
        assert isinstance(result, self.TEST_CLASS)
        utils.assert_segmentlist_equal(result.known, RESULT.known)
        utils.assert_segmentlist_equal(result.active, RESULT.active)

        # segment as tuple
        result2 = self.TEST_CLASS.query_dqsegdb(name, (0, 10))
        utils.assert_flag_equal(result, result2)

        # segmentlist
        result2 = self.TEST_CLASS.query_dqsegdb(name, SegmentList([(0, 10)]))
        utils.assert_flag_equal(result, result2)

        # flag name malformed
        with pytest.raises(ValueError):
            self.TEST_CLASS.query_dqsegdb('BAD-FLAG_NAME',
                                          SegmentList([(0, 10)]))

        # flag not in database
        with pytest.raises(HTTPError) as exc:
            self.TEST_CLASS.query_dqsegdb('X1:GWPY-TEST:0', 0, 10)
        assert str(exc.value) == 'HTTP Error 404: Not found [X1:GWPY-TEST:0]'

        # bad syntax
        with pytest.raises(ValueError):
            self.TEST_CLASS.query_dqsegdb(QUERY_FLAGS[0], 1, 2, 3)
        with pytest.raises(ValueError):
            self.TEST_CLASS.query_dqsegdb(QUERY_FLAGS[0], (1, 2, 3))

    @mock.patch('gwpy.segments.flag.query_segments',
                dqsegdb2_query_segments(QUERY_RESULT))
    def test_query_dqsegdb_multi(self):
        segs = SegmentList([Segment(0, 2), Segment(8, 10)])
        result = self.TEST_CLASS.query_dqsegdb(QUERY_FLAGS[0], segs)
        RESULT = QUERY_RESULTC[QUERY_FLAGS[0]]

        assert isinstance(result, self.TEST_CLASS)
        utils.assert_segmentlist_equal(result.known, RESULT.known & segs)
        utils.assert_segmentlist_equal(result.active, RESULT.active & segs)

    def test_fetch_open_data(self):
        try:
            segs = self.TEST_CLASS.fetch_open_data(
                'H1_DATA', 946339215, 946368015)
        except (URLError, SSLError) as exc:
            pytest.skip(str(exc))
        assert segs.ifo == 'H1'
        assert segs.name == 'H1:DATA'
        assert segs.label == 'H1_DATA'
        utils.assert_segmentlist_equal(segs.known, [(946339215, 946368015)])
        utils.assert_segmentlist_equal(segs.active, [
            (946340946, 946351799),
            (946356479, 946360619),
            (946362652, 946368015),
        ])


# -- DataQualityDict ----------------------------------------------------------

class TestDataQualityDict(object):
    TEST_CLASS = DataQualityDict
    ENTRY_CLASS = DataQualityFlag

    @classmethod
    def create(cls):
        flgd = cls.TEST_CLASS()
        flgd['X1:TEST-FLAG:1'] = cls.ENTRY_CLASS(name='X1:TEST-FLAG:1',
                                                 active=ACTIVE, known=KNOWN)
        flgd['Y1:TEST-FLAG:2'] = cls.ENTRY_CLASS(name='Y1:TEST-FLAG:2',
                                                 active=ACTIVE2, known=KNOWN2)
        return flgd

    @classmethod
    @pytest.fixture()
    def instance(cls):
        return cls.create()

    @classmethod
    @pytest.fixture()
    def reverse(cls):
        inst = cls.create()
        rev = type(inst)()
        keys = list(inst.keys())
        rev[keys[0]] = inst[keys[1]]
        rev[keys[1]] = inst[keys[0]]
        return rev

    # -- test logic -----------------------------

    def test_iand(self, instance, reverse):
        a = instance.copy()
        a &= reverse
        keys = list(a.keys())
        utils.assert_flag_equal(a[keys[0]],
                                instance[keys[0]] & reverse[keys[1]])

    def test_and(self, instance, reverse):
        a = instance.copy()
        a &= reverse
        utils.assert_dict_equal(a, instance & reverse, utils.assert_flag_equal)

    def test_ior(self, instance, reverse):
        a = instance.copy()
        a |= reverse
        keys = list(a.keys())
        utils.assert_flag_equal(a[keys[0]],
                                instance[keys[0]] | reverse[keys[1]])

    def test_or(self, instance, reverse):
        a = instance.copy()
        a |= reverse
        utils.assert_dict_equal(a, instance | reverse, utils.assert_flag_equal)

    def test_isub(self, instance, reverse):
        a = instance.copy()
        a -= reverse
        keys = list(a.keys())
        utils.assert_flag_equal(a[keys[0]],
                                instance[keys[0]] - reverse[keys[1]])

    def test_sub(self, instance, reverse):
        a = instance.copy(deep=True)
        a -= reverse
        utils.assert_dict_equal(a, instance - reverse, utils.assert_flag_equal)

    def test_xor(self, instance, reverse):
        a = instance.copy(deep=True)
        a ^= reverse
        utils.assert_dict_equal(a, instance ^ reverse, utils.assert_flag_equal)

    def test_invert(self, instance):
        inverse = type(instance)()
        for key in instance:
            inverse[key] = ~instance[key]
        utils.assert_dict_equal(~instance, inverse, utils.assert_flag_equal)

    # -- test methods ---------------------------

    def test_union(self, instance):
        union = instance.union()
        assert isinstance(union, self.ENTRY_CLASS)
        utils.assert_segmentlist_equal(union.known, KNOWN + KNOWN2)
        utils.assert_segmentlist_equal(union.active, ACTIVE + ACTIVE2)

    def test_intersection(self, instance):
        intersection = instance.intersection()
        assert isinstance(intersection, self.ENTRY_CLASS)
        utils.assert_segmentlist_equal(intersection.known, KNOWN & KNOWN2)
        utils.assert_segmentlist_equal(intersection.active, ACTIVE & ACTIVE2)

    def test_plot(self, instance):
        with rc_context(rc={'text.usetex': False}):
            plot = instance.plot(figsize=(6.4, 3.8))
            assert isinstance(plot.gca(), SegmentAxes)
            plot.save(BytesIO(), format='png')
            plot.close()

    # -- test I/O -------------------------------

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    def test_from_veto_definer_file(self, veto_definer):
        # read veto definer
        vdf = self.TEST_CLASS.from_veto_definer_file(veto_definer)
        assert len(vdf.keys()) == 4

        # test one flag to make sure it is well read
        name = 'X1:TEST-FLAG:1'
        assert name in vdf
        utils.assert_segmentlist_equal(vdf[name].known,
                                       [(100, float('inf'))])
        assert vdf[name].category == 1
        assert vdf[name].padding == (-1, 2)

        # test ifo kwarg
        vdf = self.TEST_CLASS.from_veto_definer_file(veto_definer, ifo='X1')
        assert len(vdf.keys()) == 3
        assert 'Y1:TEST-FLAG_2:2' not in vdf

        # test start and end kwargs
        vdf = self.TEST_CLASS.from_veto_definer_file(veto_definer,
                                                     start=200, end=300)
        assert len(vdf.keys()) == 3
        assert 'X1:TEST-FLAG_2:1' not in vdf

    @pytest.mark.parametrize('format, ext, dep, rw_kwargs', [
        ('hdf5', 'hdf5', 'h5py', {}),
        ('hdf5', 'h5', 'h5py', {}),
        ('hdf5', 'hdf5', 'h5py', {'path': 'test-dqdict'}),
    ])
    def test_read_write(self, instance, format, ext, dep, rw_kwargs):
        # define assertion
        def _assert(a, b):
            return utils.assert_dict_equal(a, b, utils.assert_flag_equal)

        # simplify calling read/write tester
        def _read_write(**kwargs):
            read_kw = rw_kwargs.copy()
            read_kw.update(kwargs.pop('read_kw', {}))
            write_kw = rw_kwargs.copy()
            write_kw.update(kwargs.pop('write_kw', {}))
            return utils.test_read_write(instance, format, extension=ext,
                                         assert_equal=_assert,
                                         read_kw=read_kw, write_kw=write_kw,
                                         **kwargs)

        _read_write(autoidentify=False)
        with pytest.raises(IOError):
            _read_write(autoidentify=True)
        _read_write(autoidentify=True, write_kw={'overwrite': True})

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    @pytest.mark.parametrize("ilwdchar_compat", [False, True])
    def test_read_write_ligolw(self, instance, ilwdchar_compat):
        def _assert(a, b):
            return utils.assert_dict_equal(a, b, utils.assert_flag_equal)
        utils.test_read_write(
            instance, "ligolw", extension="xml",
            assert_equal=_assert,
            autoidentify=False,
            read_kw={},
            write_kw={"ilwdchar_compat": ilwdchar_compat},
        )

    def test_read_on_missing(self, instance):
        with h5py.File(
                'test',
                mode='w-',
                driver='core',
                backing_store=False,
        ) as h5f:
            instance.write(h5f)
            names = ['randomname']

            def _read(**kwargs):
                return self.TEST_CLASS.read(h5f, names=names, format='hdf5',
                                            **kwargs)

            # check on_missing='error' (default) raises ValueError
            with pytest.raises(ValueError) as exc:
                _read()
            assert str(exc.value) == ('\'randomname\' not found in any input '
                                      'file')

            # check on_missing='warn' prints warning
            with pytest.warns(UserWarning):
                _read(on_missing='warn')

            # check on_missing='ignore' does nothing
            with pytest.warns(None) as record:
                _read(on_missing='ignore')
            assert not record.list

            # check on_missing=<anything else> raises exception
            with pytest.raises(ValueError) as exc:
                _read(on_missing='blah')

    @utils.skip_missing_dependency('ligo.lw.lsctables')
    @pytest.mark.parametrize("ilwdchar_compat", [None, False, True])
    def test_to_ligolw_tables(self, instance, ilwdchar_compat):
        if ilwdchar_compat is None:
            ctx = pytest.warns(PendingDeprecationWarning)
        else:
            ctx = null_context()
        with ctx:
            tables = instance.to_ligolw_tables(
                ilwdchar_compat=ilwdchar_compat,
            )

        if ilwdchar_compat is False:
            mod = "ligo.lw.lsctables"
        else:  # True or None
            mod = "glue.ligolw.lsctables"
        for tab in tables:
            assert type(tab).__module__ == mod
        assert len(tables[0]) == len(instance)  # segdef
        assert len(tables[1]) == sum(len(x.known) for x in instance.values())
        assert len(tables[2]) == sum(len(x.active) for x in instance.values())

    # -- test queries ---------------------------

    @mock.patch('gwpy.segments.flag.query_segments',
                dqsegdb2_query_segments(QUERY_RESULT))
    def test_query(self):
        result = self.TEST_CLASS.query(QUERY_FLAGS, 0, 10)
        RESULT = QUERY_RESULT.copy().coalesce()

        assert isinstance(result, self.TEST_CLASS)
        utils.assert_dict_equal(result, RESULT, utils.assert_flag_equal)

    @mock.patch('gwpy.segments.flag.query_segments',
                dqsegdb2_query_segments(QUERY_RESULT))
    def test_query_dqsegdb(self):
        result = self.TEST_CLASS.query_dqsegdb(QUERY_FLAGS, 0, 10)
        RESULT = QUERY_RESULTC
        assert isinstance(result, self.TEST_CLASS)
        utils.assert_dict_equal(result, RESULT, utils.assert_flag_equal)

        # check all values of on_error
        with pytest.warns(UserWarning) as record:
            result = self.TEST_CLASS.query_dqsegdb(
                QUERY_FLAGS + ['X1:BLAHBLAH:1'], 0, 10, on_error='warn')
            result = self.TEST_CLASS.query_dqsegdb(
                QUERY_FLAGS + ['X1:BLAHBLAH:1'], 0, 10, on_error='ignore')
        utils.assert_dict_equal(result, RESULT, utils.assert_flag_equal)
        assert len(record) == 1  # check on_error='ignore' didn't warn
        with pytest.raises(ValueError):
            self.TEST_CLASS.query_dqsegdb(QUERY_FLAGS, 0, 10, on_error='blah')

    def test_query_segdb(self):
        with pytest.deprecated_call():
            result = query_segdb(self.TEST_CLASS.query_segdb,
                                 QUERY_FLAGS, 0, 10)
        assert isinstance(result, self.TEST_CLASS)
        utils.assert_dict_equal(result, QUERY_RESULT, utils.assert_flag_equal)

    def test_populate(self):
        def fake():
            return self.TEST_CLASS({
                x: self.ENTRY_CLASS(name=x, known=y.known) for
                x, y in QUERY_RESULT.items()})

        # build fake veto definer file
        vdf = fake()
        vdf2 = fake()
        vdf3 = fake()

        flag = QUERY_FLAGS[0]
        vdf2[flag].padding = (-1, 1)

        span = SegmentList([Segment(0, 2)])

        # and populate using a mocked query
        with mock.patch('gwpy.segments.flag.query_segments',
                        dqsegdb2_query_segments(QUERY_RESULT)):
            vdf.populate()
            vdf2.populate()
            vdf3.populate(segments=span)

            # test warnings on bad entries
            vdf['TEST'] = self.ENTRY_CLASS('X1:BLAHBLAHBLAH:1', known=[(0, 1)])
            with pytest.warns(UserWarning) as record:
                vdf.populate(on_error='warn')
                vdf.populate(on_error='ignore')
            assert len(record) == 1
            vdf.pop('TEST')

            with pytest.raises(ValueError):
                vdf.populate(on_error='blah')

        # check basic populate worked
        utils.assert_dict_equal(vdf, QUERY_RESULTC, utils.assert_flag_equal)

        # check padded populate worked
        utils.assert_flag_equal(vdf2[flag], QUERY_RESULTC[flag].pad(-1, 1))

        # check segment-restricted populate worked
        for flag in vdf3:
            utils.assert_segmentlist_equal(
                vdf3[flag].known, QUERY_RESULTC[flag].known & span)
            utils.assert_segmentlist_equal(
                vdf3[flag].active, QUERY_RESULTC[flag].active & span)

    def test_coalesce(self):
        instance = self.create()
        instance.coalesce()
        value = instance['X1:TEST-FLAG:1']
        utils.assert_segmentlist_equal(value.active, KNOWNACTIVE)
