"""
A module containing unit tests for the `bitmask` module.

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

"""

import warnings

import numpy as np
import pytest

from astropy.nddata import bitmask

MAX_INT_TYPE = np.int64
MAX_UINT_TYPE = np.uint64
MAX_UINT_FLAG = np.left_shift(
    MAX_UINT_TYPE(1), MAX_UINT_TYPE(np.iinfo(MAX_UINT_TYPE).bits - 1)
)
MAX_INT_FLAG = np.left_shift(
    MAX_INT_TYPE(1), MAX_INT_TYPE(np.iinfo(MAX_INT_TYPE).bits - 2)
)
SUPER_LARGE_FLAG = 1 << np.iinfo(MAX_UINT_TYPE).bits
EXTREME_TEST_DATA = np.array(
    [
        0,
        1,
        1 + 1 << 2,
        MAX_INT_FLAG,
        ~0,
        MAX_INT_TYPE(MAX_UINT_FLAG),
        1 + MAX_INT_TYPE(MAX_UINT_FLAG),
    ],
    dtype=MAX_INT_TYPE,
)


@pytest.mark.parametrize("flag", [0, -1])
def test_nonpositive_not_a_bit_flag(flag):
    assert not bitmask._is_bit_flag(n=flag)


@pytest.mark.parametrize(
    "flag", [1, MAX_UINT_FLAG, int(MAX_UINT_FLAG), SUPER_LARGE_FLAG]
)
def test_is_bit_flag(flag):
    assert bitmask._is_bit_flag(n=flag)


@pytest.mark.parametrize("number", [0, 1, MAX_UINT_FLAG, SUPER_LARGE_FLAG])
def test_is_int(number):
    assert bitmask._is_int(number)


@pytest.mark.parametrize("number", ["1", True, 1.0])
def test_nonint_is_not_an_int(number):
    assert not bitmask._is_int(number)


@pytest.mark.parametrize(
    "flag,flip,expected",
    [
        (3, None, 3),
        (3, True, -4),
        (3, False, 3),
        ([1, 2], False, 3),
        ([1, 2], True, -4),
    ],
)
def test_interpret_valid_int_bit_flags(flag, flip, expected):
    assert bitmask.interpret_bit_flags(bit_flags=flag, flip_bits=flip) == expected


@pytest.mark.parametrize("flag", [None, " ", "None", "Indef"])
def test_interpret_none_bit_flags_as_None(flag):
    assert bitmask.interpret_bit_flags(bit_flags=flag) is None


@pytest.mark.parametrize(
    "flag,expected",
    [
        ("1", 1),
        ("~-1", ~(-1)),
        ("~1", ~1),
        ("1,2", 3),
        ("1|2", 3),
        ("1+2", 3),
        ("(1,2)", 3),
        ("(1+2)", 3),
        ("~1,2", ~3),
        ("~1+2", ~3),
        ("~(1,2)", ~3),
        ("~(1+2)", ~3),
    ],
)
def test_interpret_valid_str_bit_flags(flag, expected):
    assert bitmask.interpret_bit_flags(bit_flags=flag) == expected


@pytest.mark.parametrize(
    "flag,expected",
    [
        ("CR", 1),
        ("~CR", ~1),
        ("CR|HOT", 3),
        ("CR,HOT", 3),
        ("CR+HOT", 3),
        (["CR", "HOT"], 3),
        ("(CR,HOT)", 3),
        ("(HOT+CR)", 3),
        ("~HOT,CR", ~3),
        ("~CR+HOT", ~3),
        ("~(HOT,CR)", ~3),
        ("~(HOT|CR)", ~3),
        ("~(CR+HOT)", ~3),
        ("CR | HOT", 3),
        ("CR , HOT", 3),
        ("CR + HOT", 3),
        ("(CR , HOT)", 3),
        ("(HOT + CR)", 3),
        ("~HOT , CR", ~3),
        ("~CR + HOT", ~3),
        ("~(HOT , CR)", ~3),
        ("~(HOT | CR)", ~3),
        ("~(CR + HOT)", ~3),
        (" CR ", 1),
        (" CR , HOT ", 3),
        ("~ CR + HOT ", ~3),
        ("~ ( CR | HOT ) ", ~3),
    ],
)
def test_interpret_valid_mnemonic_bit_flags(flag, expected):
    flagmap = bitmask.extend_bit_flag_map("DetectorMap", CR=1, HOT=2)

    assert (
        bitmask.interpret_bit_flags(bit_flags=flag, flag_name_map=flagmap) == expected
    )


@pytest.mark.parametrize(
    "flag,flip",
    [
        (None, True),
        (" ", True),
        ("None", True),
        ("Indef", True),
        (None, False),
        (" ", False),
        ("None", False),
        ("Indef", False),
        ("1", True),
        ("1", False),
    ],
)
def test_interpret_None_or_str_and_flip_incompatibility(flag, flip):
    with pytest.raises(TypeError):
        bitmask.interpret_bit_flags(bit_flags=flag, flip_bits=flip)


@pytest.mark.parametrize("flag", [True, 1.0, [1.0], object])
def test_interpret_wrong_flag_type(flag):
    with pytest.raises(TypeError):
        bitmask.interpret_bit_flags(bit_flags=flag)


@pytest.mark.parametrize("flag", ["SOMETHING", "1.0,2,3"])
def test_interpret_wrong_string_int_format(flag):
    with pytest.raises(ValueError):
        bitmask.interpret_bit_flags(bit_flags=flag)


def test_interpret_duplicate_flag_warning():
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        assert bitmask.interpret_bit_flags([2, 4, 4]) == 6
        assert len(w)
        assert issubclass(w[-1].category, UserWarning)
        assert "Duplicate" in str(w[-1].message)


@pytest.mark.parametrize("flag", [[1, 2, 3], "1, 2, 3"])
def test_interpret_non_flag(flag):
    with pytest.raises(ValueError):
        bitmask.interpret_bit_flags(bit_flags=flag)


def test_interpret_allow_single_value_str_nonflags():
    assert bitmask.interpret_bit_flags(bit_flags=str(3)) == 3


@pytest.mark.parametrize(
    "flag",
    ["~", "( )", "(~1,2)", "~(1,2", "1,~2", "1,(2,4)", "1,2+4", "1+4,2", "1|4+2"],
)
def test_interpret_bad_str_syntax(flag):
    with pytest.raises(ValueError):
        bitmask.interpret_bit_flags(bit_flags=flag)


def test_bitfield_must_be_integer_check():
    with pytest.raises(TypeError):
        bitmask.bitfield_to_boolean_mask(1.0, 1)


@pytest.mark.parametrize(
    "data,flags,flip,goodval,dtype,ref",
    [
        (EXTREME_TEST_DATA, None, None, True, np.bool_, EXTREME_TEST_DATA.size * [1]),
        (EXTREME_TEST_DATA, None, None, False, np.bool_, EXTREME_TEST_DATA.size * [0]),
        (
            EXTREME_TEST_DATA,
            [1, MAX_UINT_FLAG],
            False,
            True,
            np.bool_,
            [1, 1, 0, 0, 0, 1, 1],
        ),
        (
            EXTREME_TEST_DATA,
            [1, MAX_UINT_FLAG],
            False,
            False,
            np.bool_,
            [0, 0, 1, 1, 1, 0, 0],
        ),
        (
            EXTREME_TEST_DATA,
            [1, MAX_UINT_FLAG],
            True,
            True,
            np.int8,
            [1, 0, 1, 1, 0, 0, 0],
        ),
    ],
)
def test_bitfield_to_boolean_mask(data, flags, flip, goodval, dtype, ref):
    mask = bitmask.bitfield_to_boolean_mask(
        bitfield=data,
        ignore_flags=flags,
        flip_bits=flip,
        good_mask_value=goodval,
        dtype=dtype,
    )

    assert mask.dtype == dtype
    assert np.all(mask == ref)


@pytest.mark.parametrize("flag", [(4, "flag1"), 8])
def test_bitflag(flag):
    f = bitmask.BitFlag(flag)
    if isinstance(flag, tuple):
        assert f == flag[0]
        assert f.__doc__ == flag[1]

        f = bitmask.BitFlag(*flag)
        assert f == flag[0]
        assert f.__doc__ == flag[1]

    else:
        assert f == flag


def test_bitflag_docs2():
    with pytest.raises(ValueError):
        bitmask.BitFlag((1, "docs1"), "docs2")


@pytest.mark.parametrize("flag", [0, 3])
def test_bitflag_not_pow2(flag):
    with pytest.raises(bitmask.InvalidBitFlag):
        bitmask.BitFlag(flag, "custom flag")


@pytest.mark.parametrize("flag", [0.0, True, "1"])
def test_bitflag_not_int_flag(flag):
    with pytest.raises(bitmask.InvalidBitFlag):
        bitmask.BitFlag((flag, "custom flag"))


@pytest.mark.parametrize("caching", [True, False])
def test_basic_map(monkeypatch, caching):
    monkeypatch.setattr(bitmask, "_ENABLE_BITFLAG_CACHING", False)

    class ObservatoryDQMap(bitmask.BitFlagNameMap):
        _not_a_flag = 1
        CR = 1, "cosmic ray"
        HOT = 2
        DEAD = 4

    class DetectorMap(ObservatoryDQMap):
        __version__ = "1.0"
        _not_a_flag = 181
        READOUT_ERR = 16

    assert ObservatoryDQMap.cr == 1
    assert ObservatoryDQMap.cr.__doc__ == "cosmic ray"
    assert DetectorMap.READOUT_ERR == 16


@pytest.mark.parametrize("caching", [True, False])
def test_extend_map(monkeypatch, caching):
    monkeypatch.setattr(bitmask, "_ENABLE_BITFLAG_CACHING", caching)

    class ObservatoryDQMap(bitmask.BitFlagNameMap):
        CR = 1
        HOT = 2
        DEAD = 4

    DetectorMap = bitmask.extend_bit_flag_map(
        "DetectorMap", ObservatoryDQMap, __version__="1.0", DEAD=4, READOUT_ERR=16
    )

    assert DetectorMap.CR == 1
    assert DetectorMap.readout_err == 16


@pytest.mark.parametrize("caching", [True, False])
def test_extend_map_redefine_flag(monkeypatch, caching):
    monkeypatch.setattr(bitmask, "_ENABLE_BITFLAG_CACHING", caching)

    class ObservatoryDQMap(bitmask.BitFlagNameMap):
        CR = 1
        HOT = 2
        DEAD = 4

    with pytest.raises(AttributeError):
        bitmask.extend_bit_flag_map(
            "DetectorMap", ObservatoryDQMap, __version__="1.0", DEAD=32
        )

    with pytest.raises(AttributeError):
        bitmask.extend_bit_flag_map(
            "DetectorMap", ObservatoryDQMap, __version__="1.0", DEAD=32, dead=64
        )


@pytest.mark.parametrize("caching", [True, False])
def test_map_redefine_flag(monkeypatch, caching):
    monkeypatch.setattr(bitmask, "_ENABLE_BITFLAG_CACHING", caching)

    class ObservatoryDQMap(bitmask.BitFlagNameMap):
        _not_a_flag = 8
        CR = 1
        HOT = 2
        DEAD = 4

    with pytest.raises(AttributeError):

        class DetectorMap1(ObservatoryDQMap):
            __version__ = "1.0"
            CR = 16

    with pytest.raises(AttributeError):

        class DetectorMap2(ObservatoryDQMap):
            SHADE = 8
            _FROZEN = 16

        DetectorMap2.novel = 32

    with pytest.raises(AttributeError):
        bitmask.extend_bit_flag_map(
            "DetectorMap", ObservatoryDQMap, READOUT_ERR=16, SHADE=32, readout_err=128
        )


def test_map_cant_modify_version():
    class ObservatoryDQMap(bitmask.BitFlagNameMap):
        __version__ = "1.2.3"
        CR = 1

    assert ObservatoryDQMap.__version__ == "1.2.3"
    assert ObservatoryDQMap.CR == 1

    with pytest.raises(AttributeError):
        ObservatoryDQMap.__version__ = "3.2.1"


@pytest.mark.parametrize("flag", [0, 3])
def test_map_not_bit_flag(flag):
    with pytest.raises(ValueError):
        bitmask.extend_bit_flag_map("DetectorMap", DEAD=flag)

    with pytest.raises(ValueError):

        class DetectorMap(bitmask.BitFlagNameMap):
            DEAD = flag


@pytest.mark.parametrize("flag", [0.0, True, "1"])
def test_map_not_int_flag(flag):
    with pytest.raises(bitmask.InvalidBitFlag):
        bitmask.extend_bit_flag_map("DetectorMap", DEAD=flag)

    with pytest.raises(bitmask.InvalidBitFlag):

        class ObservatoryDQMap(bitmask.BitFlagNameMap):
            CR = flag


def test_map_access_undefined_flag():
    DetectorMap = bitmask.extend_bit_flag_map("DetectorMap", DEAD=1)

    with pytest.raises(AttributeError):
        DetectorMap.DEAD1

    with pytest.raises(AttributeError):
        DetectorMap["DEAD1"]


def test_map_delete_flag():
    DetectorMap = bitmask.extend_bit_flag_map("DetectorMap", DEAD=1)

    with pytest.raises(AttributeError):
        del DetectorMap.DEAD1

    with pytest.raises(AttributeError):
        del DetectorMap["DEAD1"]


def test_map_repr():
    DetectorMap = bitmask.extend_bit_flag_map("DetectorMap", DEAD=1)
    assert repr(DetectorMap) == "<BitFlagNameMap 'DetectorMap'>"


def test_map_add_flags():
    map1 = bitmask.extend_bit_flag_map("DetectorMap", CR=1)

    map2 = map1 + {"HOT": 2, "DEAD": (4, "a really dead pixel")}
    assert map2.CR == 1
    assert map2.HOT == 2
    assert map2.DEAD.__doc__ == "a really dead pixel"
    assert map2.DEAD == 4

    map2 = map1 + [("HOT", 2), ("DEAD", 4)]
    assert map2.CR == 1
    assert map2.HOT == 2

    map2 = map1 + ("HOT", 2)
    assert map2.CR == 1
    assert map2.HOT == 2
