import operator
import re

import numpy as np
import pytest

import pandas as pd
import pandas._testing as tm
from pandas.core import roperator
from pandas.core.arrays.sparse import SparseArray


@pytest.fixture(params=["integer", "block"])
def kind(request):
    """kind kwarg to pass to SparseArray"""
    return request.param


@pytest.fixture(params=[True, False])
def mix(request):
    """
    Fixture returning True or False, determining whether to operate
    op(sparse, dense) instead of op(sparse, sparse)
    """
    return request.param


class TestSparseArrayArithmetics:
    def _assert(self, a, b):
        tm.assert_numpy_array_equal(a, b)

    def _assert_canonical(self, res):
        # GH#45126 the result stores only positions that differ from its fill value.
        #  A stored -0.0 would read as non-canonical here; no case below makes one
        expected = SparseArray(res.to_dense(), fill_value=res.fill_value)
        assert res.npoints == expected.npoints

    def _check_numeric_ops(self, a, b, a_dense, b_dense, mix: bool, op):
        # Check that arithmetic behavior matches non-Sparse Series arithmetic

        if isinstance(a_dense, np.ndarray):
            expected = op(pd.Series(a_dense), b_dense).values
        elif isinstance(b_dense, np.ndarray):
            expected = op(a_dense, pd.Series(b_dense)).values
        else:
            raise NotImplementedError

        with np.errstate(invalid="ignore", divide="ignore"):
            res = op(a, b_dense) if mix else op(a, b)

        self._assert_canonical(res)
        self._assert(res.to_dense(), expected)

    def _check_bool_result(self, res):
        assert isinstance(res, SparseArray)
        assert isinstance(res.dtype, pd.SparseDtype)
        assert res.dtype.subtype == np.bool_
        assert isinstance(res.fill_value, bool)

    def _check_comparison_ops(self, a, b, a_dense, b_dense):
        with np.errstate(invalid="ignore"):
            # Unfortunately, trying to wrap the computation of each expected
            # value is with np.errstate() is too tedious.
            #
            # sparse & sparse
            self._check_bool_result(a == b)
            self._assert((a == b).to_dense(), a_dense == b_dense)

            self._check_bool_result(a != b)
            self._assert((a != b).to_dense(), a_dense != b_dense)

            self._check_bool_result(a >= b)
            self._assert((a >= b).to_dense(), a_dense >= b_dense)

            self._check_bool_result(a <= b)
            self._assert((a <= b).to_dense(), a_dense <= b_dense)

            self._check_bool_result(a > b)
            self._assert((a > b).to_dense(), a_dense > b_dense)

            self._check_bool_result(a < b)
            self._assert((a < b).to_dense(), a_dense < b_dense)

            # sparse & dense
            self._check_bool_result(a == b_dense)
            self._assert((a == b_dense).to_dense(), a_dense == b_dense)

            self._check_bool_result(a != b_dense)
            self._assert((a != b_dense).to_dense(), a_dense != b_dense)

            self._check_bool_result(a >= b_dense)
            self._assert((a >= b_dense).to_dense(), a_dense >= b_dense)

            self._check_bool_result(a <= b_dense)
            self._assert((a <= b_dense).to_dense(), a_dense <= b_dense)

            self._check_bool_result(a > b_dense)
            self._assert((a > b_dense).to_dense(), a_dense > b_dense)

            self._check_bool_result(a < b_dense)
            self._assert((a < b_dense).to_dense(), a_dense < b_dense)

    def _check_logical_ops(self, a, b, a_dense, b_dense):
        # sparse & sparse
        self._check_bool_result(a & b)
        self._assert((a & b).to_dense(), a_dense & b_dense)

        self._check_bool_result(a | b)
        self._assert((a | b).to_dense(), a_dense | b_dense)
        # sparse & dense
        self._check_bool_result(a & b_dense)
        self._assert((a & b_dense).to_dense(), a_dense & b_dense)

        self._check_bool_result(a | b_dense)
        self._assert((a | b_dense).to_dense(), a_dense | b_dense)

    @pytest.mark.parametrize("scalar", [0, 1, 3])
    @pytest.mark.parametrize("fill_value", [None, 0, 2])
    def test_float_scalar(
        self, kind, mix, all_arithmetic_functions, fill_value, scalar, request
    ):
        op = all_arithmetic_functions
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        a = SparseArray(values, kind=kind, fill_value=fill_value)
        self._check_numeric_ops(a, scalar, values, scalar, mix, op)

    def test_float_scalar_comparison(self, kind):
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])

        a = SparseArray(values, kind=kind)
        self._check_comparison_ops(a, 1, values, 1)
        self._check_comparison_ops(a, 0, values, 0)
        self._check_comparison_ops(a, 3, values, 3)

        a = SparseArray(values, kind=kind, fill_value=0)
        self._check_comparison_ops(a, 1, values, 1)
        self._check_comparison_ops(a, 0, values, 0)
        self._check_comparison_ops(a, 3, values, 3)

        a = SparseArray(values, kind=kind, fill_value=2)
        self._check_comparison_ops(a, 1, values, 1)
        self._check_comparison_ops(a, 0, values, 0)
        self._check_comparison_ops(a, 3, values, 3)

    def test_float_same_index_without_nans(self, kind, mix, all_arithmetic_functions):
        # when sp_index are the same
        op = all_arithmetic_functions

        values = np.array([0.0, 1.0, 2.0, 6.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0])
        rvalues = np.array([0.0, 2.0, 3.0, 4.0, 0.0, 0.0, 1.0, 3.0, 2.0, 0.0])

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_float_same_index_with_nans(
        self, kind, mix, all_arithmetic_functions, request
    ):
        # when sp_index are the same
        op = all_arithmetic_functions
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([np.nan, 2, 3, 4, np.nan, 0, 1, 3, 2, np.nan])

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_float_same_index_comparison(self, kind):
        # when sp_index are the same
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([np.nan, 2, 3, 4, np.nan, 0, 1, 3, 2, np.nan])

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        self._check_comparison_ops(a, b, values, rvalues)

        values = np.array([0.0, 1.0, 2.0, 6.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0])
        rvalues = np.array([0.0, 2.0, 3.0, 4.0, 0.0, 0.0, 1.0, 3.0, 2.0, 0.0])

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        self._check_comparison_ops(a, b, values, rvalues)

    def test_float_array(self, kind, mix, all_arithmetic_functions):
        op = all_arithmetic_functions

        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([2, np.nan, 2, 3, np.nan, 0, 1, 5, 2, np.nan])

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)
        self._check_numeric_ops(a, b * 0, values, rvalues * 0, mix, op)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind=kind, fill_value=1)
        b = SparseArray(rvalues, kind=kind, fill_value=2)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_float_array_different_kind(self, mix, all_arithmetic_functions):
        op = all_arithmetic_functions

        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([2, np.nan, 2, 3, np.nan, 0, 1, 5, 2, np.nan])

        a = SparseArray(values, kind="integer")
        b = SparseArray(rvalues, kind="block")
        self._check_numeric_ops(a, b, values, rvalues, mix, op)
        self._check_numeric_ops(a, b * 0, values, rvalues * 0, mix, op)

        a = SparseArray(values, kind="integer", fill_value=0)
        b = SparseArray(rvalues, kind="block")
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind="integer", fill_value=0)
        b = SparseArray(rvalues, kind="block", fill_value=0)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind="integer", fill_value=1)
        b = SparseArray(rvalues, kind="block", fill_value=2)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_float_array_comparison(self, kind):
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([2, np.nan, 2, 3, np.nan, 0, 1, 5, 2, np.nan])

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        self._check_comparison_ops(a, b, values, rvalues)
        self._check_comparison_ops(a, b * 0, values, rvalues * 0)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, kind=kind, fill_value=1)
        b = SparseArray(rvalues, kind=kind, fill_value=2)
        self._check_comparison_ops(a, b, values, rvalues)

    def test_int_array(self, kind, mix, all_arithmetic_functions):
        op = all_arithmetic_functions

        # have to specify dtype explicitly until fixing GH 667
        dtype = np.int64

        values = np.array([0, 1, 2, 0, 0, 0, 1, 2, 1, 0], dtype=dtype)
        rvalues = np.array([2, 0, 2, 3, 0, 0, 1, 5, 2, 0], dtype=dtype)

        a = SparseArray(values, dtype=dtype, kind=kind)
        assert a.dtype == pd.SparseDtype(dtype)
        b = SparseArray(rvalues, dtype=dtype, kind=kind)
        assert b.dtype == pd.SparseDtype(dtype)

        self._check_numeric_ops(a, b, values, rvalues, mix, op)
        self._check_numeric_ops(a, b * 0, values, rvalues * 0, mix, op)

        a = SparseArray(values, fill_value=0, dtype=dtype, kind=kind)
        assert a.dtype == pd.SparseDtype(dtype)
        b = SparseArray(rvalues, dtype=dtype, kind=kind)
        assert b.dtype == pd.SparseDtype(dtype)

        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, fill_value=0, dtype=dtype, kind=kind)
        assert a.dtype == pd.SparseDtype(dtype)
        b = SparseArray(rvalues, fill_value=0, dtype=dtype, kind=kind)
        assert b.dtype == pd.SparseDtype(dtype)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, fill_value=1, dtype=dtype, kind=kind)
        assert a.dtype == pd.SparseDtype(dtype, fill_value=1)
        b = SparseArray(rvalues, fill_value=2, dtype=dtype, kind=kind)
        assert b.dtype == pd.SparseDtype(dtype, fill_value=2)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_int_array_comparison(self, kind):
        dtype = "int64"
        # int32 NI ATM

        values = np.array([0, 1, 2, 0, 0, 0, 1, 2, 1, 0], dtype=dtype)
        rvalues = np.array([2, 0, 2, 3, 0, 0, 1, 5, 2, 0], dtype=dtype)

        a = SparseArray(values, dtype=dtype, kind=kind)
        b = SparseArray(rvalues, dtype=dtype, kind=kind)
        self._check_comparison_ops(a, b, values, rvalues)
        self._check_comparison_ops(a, b * 0, values, rvalues * 0)

        a = SparseArray(values, dtype=dtype, kind=kind, fill_value=0)
        b = SparseArray(rvalues, dtype=dtype, kind=kind)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, dtype=dtype, kind=kind, fill_value=0)
        b = SparseArray(rvalues, dtype=dtype, kind=kind, fill_value=0)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, dtype=dtype, kind=kind, fill_value=1)
        b = SparseArray(rvalues, dtype=dtype, kind=kind, fill_value=2)
        self._check_comparison_ops(a, b, values, rvalues)

    @pytest.mark.parametrize("fill_value", [True, False, np.nan])
    def test_bool_same_index(self, kind, fill_value):
        # GH 14000
        # when sp_index are the same
        values = np.array([True, False, True, True], dtype=np.bool_)
        rvalues = np.array([True, False, True, True], dtype=np.bool_)

        a = SparseArray(values, kind=kind, dtype=np.bool_, fill_value=fill_value)
        b = SparseArray(rvalues, kind=kind, dtype=np.bool_, fill_value=fill_value)
        self._check_logical_ops(a, b, values, rvalues)

    @pytest.mark.parametrize("fill_value", [True, False, np.nan])
    def test_bool_array_logical(self, kind, fill_value):
        # GH 14000
        # when sp_index are the same
        values = np.array([True, False, True, False, True, True], dtype=np.bool_)
        rvalues = np.array([True, False, False, True, False, True], dtype=np.bool_)

        a = SparseArray(values, kind=kind, dtype=np.bool_, fill_value=fill_value)
        b = SparseArray(rvalues, kind=kind, dtype=np.bool_, fill_value=fill_value)
        self._check_logical_ops(a, b, values, rvalues)

    def test_mixed_array_float_int(self, kind, mix, all_arithmetic_functions, request):
        op = all_arithmetic_functions
        rdtype = "int64"
        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([2, 0, 2, 3, 0, 0, 1, 5, 2, 0], dtype=rdtype)

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        assert b.dtype == pd.SparseDtype(rdtype)

        self._check_numeric_ops(a, b, values, rvalues, mix, op)
        self._check_numeric_ops(a, b * 0, values, rvalues * 0, mix, op)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind)
        assert b.dtype == pd.SparseDtype(rdtype)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        assert b.dtype == pd.SparseDtype(rdtype)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

        a = SparseArray(values, kind=kind, fill_value=1)
        b = SparseArray(rvalues, kind=kind, fill_value=2)
        assert b.dtype == pd.SparseDtype(rdtype, fill_value=2)
        self._check_numeric_ops(a, b, values, rvalues, mix, op)

    def test_mixed_array_comparison(self, kind):
        rdtype = "int64"
        # int32 NI ATM

        values = np.array([np.nan, 1, 2, 0, np.nan, 0, 1, 2, 1, np.nan])
        rvalues = np.array([2, 0, 2, 3, 0, 0, 1, 5, 2, 0], dtype=rdtype)

        a = SparseArray(values, kind=kind)
        b = SparseArray(rvalues, kind=kind)
        assert b.dtype == pd.SparseDtype(rdtype)

        self._check_comparison_ops(a, b, values, rvalues)
        self._check_comparison_ops(a, b * 0, values, rvalues * 0)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind)
        assert b.dtype == pd.SparseDtype(rdtype)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, kind=kind, fill_value=0)
        b = SparseArray(rvalues, kind=kind, fill_value=0)
        assert b.dtype == pd.SparseDtype(rdtype)
        self._check_comparison_ops(a, b, values, rvalues)

        a = SparseArray(values, kind=kind, fill_value=1)
        b = SparseArray(rvalues, kind=kind, fill_value=2)
        assert b.dtype == pd.SparseDtype(rdtype, fill_value=2)
        self._check_comparison_ops(a, b, values, rvalues)

    def test_xor(self):
        s = SparseArray([True, True, False, False])
        t = SparseArray([True, False, True, False])
        result = s ^ t
        expected = SparseArray([False, True, True, False])
        tm.assert_sp_array_equal(result, expected)


@pytest.mark.parametrize("op", [operator.eq, operator.add])
def test_with_list(op):
    arr = SparseArray([0, 1], fill_value=0)
    result = op(arr, [0, 1])
    expected = op(arr, SparseArray([0, 1]))
    tm.assert_sp_array_equal(result, expected)


def test_with_dataframe():
    # GH#27910
    arr = SparseArray([0, 1], fill_value=0)
    df = pd.DataFrame([[1, 2], [3, 4]])
    result = arr.__add__(df)
    assert result is NotImplemented


def test_with_zerodim_ndarray():
    # GH#27910
    arr = SparseArray([0, 1], fill_value=0)

    result = arr * np.array(2)
    expected = arr * 2
    tm.assert_sp_array_equal(result, expected)


@pytest.mark.parametrize("ufunc", [np.abs, np.exp])
@pytest.mark.parametrize(
    "arr", [SparseArray([0, 0, -1, 1]), SparseArray([None, None, -1, 1])]
)
def test_ufuncs(ufunc, arr):
    result = ufunc(arr)
    fill_value = ufunc(arr.fill_value)
    expected = SparseArray(ufunc(np.asarray(arr)), fill_value=fill_value)
    tm.assert_sp_array_equal(result, expected)


@pytest.mark.parametrize(
    "a, b",
    [
        (SparseArray([0, 0, 0]), np.array([0, 1, 2])),
        (SparseArray([0, 0, 0], fill_value=1), np.array([0, 1, 2])),
    ],
)
@pytest.mark.parametrize("ufunc", [np.add, np.greater])
def test_binary_ufuncs(ufunc, a, b):
    # can't say anything about fill value here.
    result = ufunc(a, b)
    expected = ufunc(np.asarray(a), np.asarray(b))
    assert isinstance(result, SparseArray)
    tm.assert_numpy_array_equal(np.asarray(result), expected)


def test_ndarray_inplace():
    sparray = SparseArray([0, 2, 0, 0])
    ndarray = np.array([0, 1, 2, 3])
    ndarray += sparray
    expected = np.array([0, 3, 2, 3])
    tm.assert_numpy_array_equal(ndarray, expected)


def test_sparray_inplace():
    sparray = SparseArray([0, 2, 0, 0])
    ndarray = np.array([0, 1, 2, 3])
    sparray += ndarray
    expected = SparseArray([0, 3, 2, 3], fill_value=0)
    tm.assert_sp_array_equal(sparray, expected)


@pytest.mark.parametrize("cons", [list, np.array, SparseArray])
def test_mismatched_length_cmp_op(cons):
    left = SparseArray([True, True])
    right = cons([True, True, True])
    with pytest.raises(ValueError, match="operands have mismatched length"):
        left & right


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_uneven_length_series(op):
    # GH#32119 a logical op between a sparse Series and a differently-indexed
    #  Series aligns (introducing NA) and must match the non-sparse result
    #  instead of raising an uninformative error.
    sparse = pd.Series(SparseArray(np.arange(10)))
    dense = pd.Series(np.arange(11))

    result = op(sparse == 5, dense == 5)
    expected = op(pd.Series(np.arange(10)) == 5, dense == 5)

    assert isinstance(result.dtype, pd.SparseDtype)
    tm.assert_series_equal(result.astype(bool), expected)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
@pytest.mark.parametrize("subtype", [bool, object])
def test_logical_op_masked_other(op, subtype):
    # GH#68483 a masked operand keeps its own Kleene semantics; densifying it
    #  lost them -- raising on the NA for a bool subtype, silently resolving it
    #  to False for an object one
    values = np.array([True, True, False, False], dtype=subtype)
    other = pd.array([True, pd.NA, True, False], dtype="boolean")

    result = op(SparseArray(values), other)
    expected = op(values, other)
    tm.assert_extension_array_equal(result, expected)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_masked_other_without_na(op):
    # GH#68483 the operand's dtype decides, not whether it holds NA, so the
    #  result is the masked dtype either way -- as it is for the dense operand
    values = np.array([True, True, False, False])
    other = pd.array([True, False, True, False], dtype="boolean")

    result = op(SparseArray(values), other)
    expected = op(values, other)
    assert result.dtype == pd.BooleanDtype()
    tm.assert_extension_array_equal(result, expected)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_non_boolean_masked_other(op):
    # GH#68483 only a masked *boolean* operand is deferred to; the other masked
    #  dtypes reach _arith_method, which cannot consume a SparseArray
    # int64 explicitly: on 32-bit the default int would not match the int64 result
    values = np.array([1, 0, 3, 0], dtype="int64")
    other = pd.array([1, 2, 3, 4], dtype="Int64")

    result = op(SparseArray(values), other)
    expected = op(values, other.to_numpy(dtype="int64"))
    assert isinstance(result.dtype, pd.SparseDtype)
    tm.assert_numpy_array_equal(result.to_dense(), expected)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
@pytest.mark.parametrize(
    "other",
    [
        pd.Categorical([True, False, True, False]),
        pd.Categorical(["a", "b", "a", "b"]),
        pd.array([1.0, 0.0, None, 1.0], dtype="Float64"),
    ],
)
def test_logical_op_ea_other_raises_like_dense(op, other):
    # GH#68569 densifying the operand answered from the truthiness of its values;
    #  the dense path hands the EA to its own op, which rejects these
    values = np.array([True, True, False, False])

    with pytest.raises(TypeError) as dense_err:
        op(pd.Series(values), pd.Series(other))
    with pytest.raises(TypeError, match=re.escape(str(dense_err.value))):
        op(SparseArray(values), other)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_ea_result_keeps_operand_dtype(op):
    # GH#68569 the operand answers in its own dtype; wrapping that back up as sparse
    #  took an NA fill value, and the result then raised when used in another op
    pytest.importorskip("pyarrow")
    # an object subtype to reach the dense fallback; a bool subtype still takes the
    #  sparse fast path, which densifies the operand
    values = np.array([True, True, False, False], dtype=object)
    other = pd.array([True, False, True, False], dtype="bool[pyarrow]")

    result = op(SparseArray(values), other)
    expected = op(pd.Series(values), pd.Series(other))
    tm.assert_series_equal(pd.Series(result), expected)
    # the poisoned Sparse[bool, <NA>] raised here
    tm.assert_equal(op(result, result), op(expected, expected).array)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_dtypeless_sequence_raises(op):
    # GH#68569 the dense fallback hands its operand over as given, so a list now reaches
    #  logical_op's GH#52264 guard instead of np.asarray carrying it past. An object
    #  subtype to reach that fallback; a bool subtype still takes the sparse fast path,
    #  which answers for a list.
    values = np.array([True, True, False, False], dtype=object)
    other = [True, False, True, False]

    msg = "dtype-less sequences"
    with pytest.raises(TypeError, match=msg):
        op(SparseArray(values), other)
    with pytest.raises(TypeError, match=msg):
        op(pd.Series(values), other)


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
def test_logical_op_both_sparse_matches_dense(op):
    # GH#68569 logical_op rejects a float on the left and coerces it on the right, so
    #  dispatching to a sparse operand -- which re-enters with the two swapped --
    #  inverted which order is accepted
    floats = np.array([1.5, 0.0, 2.5, np.nan])
    bools = np.array([True, False, True, False])

    msg = "unsupported operand type"
    with pytest.raises(TypeError, match=msg):
        op(pd.Series(floats), pd.Series(bools))
    with pytest.raises(TypeError, match=msg):
        op(SparseArray(floats), SparseArray(bools))

    expected = op(pd.Series(bools), pd.Series(floats))
    result = op(SparseArray(bools), SparseArray(floats))
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


@pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor])
@pytest.mark.parametrize("n_sparse, n_other", [(11, 10), (10, 11)])
def test_logical_op_masked_other_uneven_length_series(op, n_sparse, n_other):
    # GH#68483 the alignment path onto the above; either operand can be the one
    #  that gains the NA, and only the shorter-sparse case upcasts it to object
    sparse = pd.Series(SparseArray(np.arange(n_sparse)))
    other = pd.Series(np.arange(n_other) == 5, dtype="boolean")

    result = op(sparse == 5, other)
    expected = op(pd.Series(np.arange(n_sparse)) == 5, other)
    tm.assert_series_equal(result, expected)


@pytest.mark.parametrize("op", [operator.eq, operator.ne, operator.lt, operator.ge])
@pytest.mark.parametrize(
    "values, masked_dtype",
    [
        (np.array([True, False, True, False]), "boolean"),
        (np.array([1, 0, 3, 0]), "Int64"),
        (np.array([1.0, 0.0, 3.0, 0.0]), "Float64"),
    ],
)
def test_cmp_op_masked_other(op, values, masked_dtype):
    # GH#68579 densifying the masked operand dropped its NA: a boolean one
    #  raised, the rest resolved it as if it were NaN where dense gives <NA>
    other = pd.array(values, dtype=masked_dtype)
    other[1] = pd.NA

    result = op(SparseArray(values), other)
    expected = op(values, other)
    tm.assert_extension_array_equal(result, expected)


def test_cmp_op_masked_other_without_na():
    # GH#68579 the operand's dtype decides the result dtype, not whether it
    #  holds NA -- as it does for the dense operand
    values = np.array([1, 0, 3, 0])
    other = pd.array(values, dtype="Int64")

    result = SparseArray(values) < other
    assert result.dtype == pd.BooleanDtype()
    tm.assert_extension_array_equal(result, values < other)


def test_cmp_op_masked_other_series():
    # GH#68579 the Series path dispatches to the same reflected op
    values = np.array([1, 0, 3, 0])
    other = pd.Series(pd.array([1, pd.NA, 3, 0], dtype="Int64"))

    result = pd.Series(SparseArray(values)) < other
    expected = pd.Series(values) < other
    tm.assert_series_equal(result, expected)


@pytest.mark.parametrize(
    "op",
    [
        operator.eq,
        operator.ne,
        operator.lt,
        operator.ge,
        operator.and_,
        operator.or_,
        operator.xor,
    ],
)
@pytest.mark.parametrize(
    "values", [np.array([True, False, True, False]), np.array([1, 0, 3, 0])]
)
def test_op_na_scalar(op, values):
    # GH#68579 NA has no truth value, so assembling the result raised; sparse
    #  is numpy-backed, so the answer is the dense Series one
    result = op(SparseArray(values), pd.NA)

    expected = op(pd.Series(values), pd.NA)
    assert isinstance(result.dtype, pd.SparseDtype)
    tm.assert_series_equal(pd.Series(result).astype(bool), expected)


@pytest.mark.parametrize("op", [operator.eq, operator.ne])
@pytest.mark.parametrize("dtype", ["m8[ns]", "M8[ns]"])
def test_eq_na_scalar_datetimelike(op, dtype):
    # GH#68579 equality against NA answers like the dense array
    values = np.array([1, 2], dtype=dtype)

    result = op(SparseArray(values), pd.NA)
    expected = op(pd.Series(values), pd.NA)
    tm.assert_series_equal(pd.Series(result).astype(bool), expected)


@pytest.mark.parametrize("op", [operator.lt, operator.ge])
@pytest.mark.parametrize("dtype", ["m8[ns]", "M8[ns]"])
def test_ordering_na_scalar_datetimelike_raises(op, dtype):
    # GH#68579 a datetimelike dtype refuses to be ordered against NA, so the
    #  NA shortcut must raise there rather than answer all-False
    values = np.array([1, 2], dtype=dtype)

    with pytest.raises(TypeError, match="Invalid comparison"):
        op(SparseArray(values), pd.NA)
    with pytest.raises(TypeError, match="Invalid comparison"):
        op(pd.Series(values), pd.NA)


@pytest.mark.parametrize(
    "a, b",
    [
        ([0, 1, 2], [0, 1, 2, 3]),
        ([0, 1, 2, 3], [0, 1, 2]),
    ],
)
def test_mismatched_length_arith_op(a, b, all_arithmetic_functions):
    op = all_arithmetic_functions
    with pytest.raises(AssertionError, match=f"length mismatch: {len(a)} vs. {len(b)}"):
        op(SparseArray(a, fill_value=0), np.array(b))


@pytest.mark.parametrize("op", ["add", "sub", "mul", "truediv", "floordiv", "pow"])
@pytest.mark.parametrize("fill_value", [np.nan, 3])
def test_binary_operators(op, fill_value):
    op = getattr(operator, op)
    data1 = np.random.default_rng(2).standard_normal(20)
    data2 = np.random.default_rng(2).standard_normal(20)

    data1[::2] = fill_value
    data2[::3] = fill_value

    first = SparseArray(data1, fill_value=fill_value)
    second = SparseArray(data2, fill_value=fill_value)

    with np.errstate(all="ignore"):
        res = op(first, second)
        exp = SparseArray(
            op(first.to_dense(), second.to_dense()), fill_value=first.fill_value
        )
        assert isinstance(res, SparseArray)
        tm.assert_almost_equal(res.to_dense(), exp.to_dense())

        res2 = op(first, second.to_dense())
        assert isinstance(res2, SparseArray)
        tm.assert_sp_array_equal(res, res2)

        res3 = op(first.to_dense(), second)
        assert isinstance(res3, SparseArray)
        tm.assert_sp_array_equal(res, res3)

        res4 = op(first, 4)
        assert isinstance(res4, SparseArray)

        # Ignore this if the actual op raises (e.g. pow).
        try:
            exp = op(first.to_dense(), 4)
            exp_fv = op(first.fill_value, 4)
        except ValueError:
            pass
        else:
            tm.assert_almost_equal(res4.fill_value, exp_fv)
            tm.assert_almost_equal(res4.to_dense(), exp)


@pytest.mark.parametrize("values", [[1, 2, 3], [True, False, True]])
@pytest.mark.parametrize("op", [operator.add, operator.gt])
@pytest.mark.parametrize("other_kind", ["datetime64", "timedelta64"])
def test_datetimelike_operand_raises_typeerror(values, op, other_kind):
    # GH#68466 the operand was coerced to self.fill_value first, so SparseDtype
    #  rejected the fill_value before the op could reject the operand
    if other_kind == "datetime64":
        other = np.asarray(pd.date_range("2016", periods=3))
    else:
        other = np.asarray(pd.timedelta_range("1 Day", periods=3))
    with pytest.raises(TypeError):
        op(SparseArray(values), other)


def test_mul_timedelta64_operand():
    # GH#68466 valid densely, but the fill_value coercion raised
    tda = np.asarray(pd.timedelta_range("1 Day", periods=3))
    result = SparseArray([1, 2, 3]) * tda
    tm.assert_numpy_array_equal(result.to_dense(), np.arange(1, 4) * tda)


_NO_COMMON_SUBTYPE_VALUES = {
    "i8": np.array([1, 2, 3]),
    "f8": np.array([1.0, np.nan, 3.0]),
    "bool": np.array([True, False, True]),
    "tda": np.asarray(pd.timedelta_range("1 Day", periods=3)),
    "dti": np.asarray(pd.date_range("2016", periods=3)),
}


@pytest.mark.parametrize(
    "lkind, rkind, op",
    [
        ("i8", "tda", operator.mul),
        ("f8", "tda", operator.mul),
        ("i8", "tda", roperator.rmul),
        ("i8", "bool", operator.mul),
        ("i8", "bool", operator.or_),
        ("i8", "bool", operator.xor),
        ("tda", "i8", operator.truediv),
        ("tda", "f8", operator.floordiv),
        ("tda", "dti", operator.add),
        ("dti", "tda", operator.sub),
    ],
)
def test_no_common_subtype_matches_dense(lkind, rkind, op):
    # GH#68562 int64 and m8[us] have no common subtype, so both operands were
    #  cast to object and the result kept that dtype
    left = _NO_COMMON_SUBTYPE_VALUES[lkind]
    right = _NO_COMMON_SUBTYPE_VALUES[rkind]

    result = op(SparseArray(left), right)
    expected = op(pd.Series(left), pd.Series(right))
    assert result.dtype == pd.SparseDtype(expected.dtype)
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


@pytest.mark.parametrize("op", [operator.eq, operator.ne])
def test_no_common_subtype_comparison_matches_dense(op):
    # GH#68562 a comparison keeps its own fill value, so only the subtype is
    #  pinned to the dense result
    tda = _NO_COMMON_SUBTYPE_VALUES["tda"]
    i8 = _NO_COMMON_SUBTYPE_VALUES["i8"]

    result = op(SparseArray(tda), i8)
    expected = op(pd.Series(tda), pd.Series(i8))
    assert result.dtype.subtype == expected.dtype
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


@pytest.mark.parametrize("op", [divmod, roperator.rdivmod])
def test_no_common_subtype_divmod(op):
    # GH#68562 divmod returns a 2-tuple, which the reflected name used to miss
    left = _NO_COMMON_SUBTYPE_VALUES["i8" if op is roperator.rdivmod else "tda"]
    right = _NO_COMMON_SUBTYPE_VALUES["tda" if op is roperator.rdivmod else "i8"]

    result = op(SparseArray(left), right)
    expected = op(pd.Series(left), pd.Series(right))
    for res, exp in zip(result, expected, strict=True):
        assert res.dtype == pd.SparseDtype(exp.dtype)
        tm.assert_numpy_array_equal(res.to_dense(), exp.to_numpy())


def test_no_common_subtype_fill_value_matches_subtype():
    # GH#68562 the float operand contributes an np.nan fill value, which is not
    #  the flavor of NA a timedelta64 result holds
    tda = _NO_COMMON_SUBTYPE_VALUES["tda"]
    result = SparseArray([1.0, np.nan, 3.0]) * tda

    assert isinstance(result.fill_value, np.timedelta64)
    assert isinstance(result[1], np.timedelta64)


def test_no_common_subtype_preserves_index_kind():
    # GH#68562 the dense path re-sparsifies from scratch, so it has to be told
    #  which kind of index to rebuild
    tda = _NO_COMMON_SUBTYPE_VALUES["tda"]
    result = SparseArray([1, 2, 3], kind="block") * tda
    assert result.kind == "block"


def test_no_common_subtype_na_fill_value_keeps_object():
    # GH#68562 a pd.NA fill escapes _get_fill's ValueError fallback, so such an
    #  operand keeps the object-cast path rather than raising
    arr = SparseArray([1, 2, 3], fill_value=pd.NA)

    result = arr * np.array([True, False, True])
    assert result.dtype.subtype == np.dtype(object)
    assert result.fill_value is pd.NA
    tm.assert_numpy_array_equal(result.to_dense(), np.array([1, 0, 3], dtype=object))


def test_no_common_subtype_both_with_gaps():
    # GH#68562 neither operand is dense, so this used to reach the splib kernels
    #  and fail on the missing sparse_mul_object
    left = SparseArray([1, 0, 3, 4])
    right = SparseArray(np.array([1, 2, "NaT", 4], dtype="m8[us]"))

    result = left * right
    expected = pd.Series(left.to_dense()) * pd.Series(right.to_dense())
    assert result.dtype == pd.SparseDtype(expected.dtype)
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


@pytest.mark.parametrize(
    "op",
    [
        operator.eq,
        operator.ne,
        operator.lt,
        operator.le,
        operator.gt,
        operator.ge,
    ],
)
@pytest.mark.parametrize("other", [1, None, np.array([1, 1, pd.NA, 2], dtype=object)])
def test_cmp_object_subtype_with_na(op, other):
    # GH#68586 comparing pd.NA gives pd.NA rather than a bool, which neither the
    #  np.bool_ result buffer nor the sparse kernels can hold
    values = np.array([1, pd.NA, 0, 1], dtype=object)
    arr = SparseArray(values, fill_value=0)

    result = op(arr, other)
    dense_other = pd.Series(other) if isinstance(other, np.ndarray) else other
    expected = op(pd.Series(values), dense_other)
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


def test_cmp_object_subtype_sparse_operands_unequal_indices():
    # GH#68586 splib has no object comparison kernel, so the unequal-index path
    #  raised AttributeError even with no missing values involved
    left = SparseArray(np.array(["a", "b", "c"], dtype=object), fill_value="a")
    right = SparseArray(np.array(["a", "b", "c"], dtype=object), fill_value="c")

    result = left == right
    tm.assert_numpy_array_equal(result.to_dense(), np.ones(3, dtype=bool))


@pytest.mark.parametrize(
    "values, left_fill, right_fill",
    [
        (np.array([1, 2, 3]), 1, 3),
        (np.array([True, False, True]), True, False),
    ],
)
def test_cmp_object_operand_against_non_object_subtype(values, left_fill, right_fill):
    # GH#68586 _sparse_array_op casts both operands to their common subtype, so
    #  the missing kernel was reached with the object subtype on either side
    left = SparseArray(values, fill_value=left_fill)
    right = SparseArray(np.asarray(values, dtype=object), fill_value=right_fill)
    # the kernel lookup is only reached when both operands have gaps and differ
    assert left.sp_index.ngaps and right.sp_index.ngaps
    assert not left.sp_index.equals(right.sp_index)

    result = left == right
    tm.assert_numpy_array_equal(result.to_dense(), np.ones(len(values), dtype=bool))


def test_cmp_object_ndarray_operand_against_non_object_subtype():
    # GH#68586 a dense operand is wrapped as Sparse[object], reaching the same
    #  missing kernel
    arr = SparseArray(np.array([1, 2, 3]), fill_value=1)
    other = np.array([3, 2, 1], dtype=object)

    result = arr == other
    expected = pd.Series([1, 2, 3]) == pd.Series(other)
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


def test_cmp_object_subtype_na_scalar_never_equal():
    # GH#68586 the raw numpy comparison matched None against None; densely a
    #  missing scalar compares unequal to everything
    values = np.array([1, None, 0, 1], dtype=object)
    arr = SparseArray(values, fill_value=0)

    result = operator.eq(arr, None)
    expected = operator.eq(pd.Series(values), None)
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


def test_cmp_object_subtype_keeps_fill_value():
    # GH#68586 the result's fill value is op(fill_value, other), not the default
    values = np.array([1, pd.NA, 0, 1], dtype=object)
    arr = SparseArray(values, fill_value=0)

    assert (arr == 0).dtype == pd.SparseDtype(bool, True)
    assert (arr == 1).dtype == pd.SparseDtype(bool, False)


@pytest.mark.parametrize("op", [operator.eq, operator.ne, operator.lt, operator.gt])
@pytest.mark.parametrize("subtype", [object, "int64"])
def test_cmp_masked_boolean_other(op, subtype):
    # GH#68586 np.asarray on the operand drops its mask, and the object route
    #  then resolved the NA to False; defer so the operand keeps it
    values = np.array([1, 2, 0], dtype=subtype)
    other = pd.array([True, False, None], dtype="boolean")

    result = op(SparseArray(values, fill_value=0), other)
    expected = op(pd.Series(values), pd.Series(other))
    tm.assert_extension_array_equal(result, expected.array)


def test_cmp_object_subtype_scalar_result_is_canonical():
    # GH#68586 the object route compares sp_values; GH#45126 it stores only the
    #  positions that differ from the result's fill value, as every route does
    arr = SparseArray(np.array([1, 2, 0, 0], dtype=object), fill_value=0)

    result = arr == 1
    tm.assert_numpy_array_equal(result.sp_index.indices, np.array([0], dtype="int32"))
    tm.assert_numpy_array_equal(
        result.to_dense(), np.array([True, False, False, False])
    )


def test_op_result_drops_stored_fill_values(kind):
    # GH#45126 the result reused the operands' shared sparse index, so a
    #  comparison that is entirely the fill value still stored every position
    arr = SparseArray([1.0, 0.0, -1.0, np.nan], fill_value=np.nan, kind=kind)

    result = arr > arr + 1
    expected = SparseArray([False, False, False, False], fill_value=False, kind=kind)
    tm.assert_sp_array_equal(result, expected)
    assert result.npoints == 0
    assert result.density == 0.0


def test_cmp_ndarray_operand_drops_stored_fill_values():
    # GH#45284 the ndarray operand is sparsified on the SparseArray's own
    #  fill_value; holding no gaps, it made the result store every position
    arr = SparseArray([1.0, 0.0, -1.0, np.nan], fill_value=np.nan)

    result = arr > np.array([0, 1, 2, 3])
    expected = SparseArray([True, False, False, False], fill_value=False)
    tm.assert_sp_array_equal(result, expected)


def test_cmp_result_as_boolean_mask():
    # GH#45284 __getitem__ takes key.sp_index.indices wholesale, so a comparison
    #  result that stored every position selected every element
    arr = SparseArray([1, 2, 3, 4, np.nan, np.nan], fill_value=np.nan)

    result = arr[arr > [3, 3, 4, 1, 0, 0]]
    expected = pd.Series([1, 2, 3, 4, np.nan, np.nan])
    expected = expected[expected > [3, 3, 4, 1, 0, 0]]
    tm.assert_numpy_array_equal(result.to_dense(), expected.to_numpy())


def test_arith_result_drops_stored_fill_values(kind):
    # GH#45126 the scalar arm applied the op to sp_values and reused sp_index
    arr = SparseArray([1, 0, 2, 0, 3], fill_value=0, kind=kind)

    result = arr * 0
    expected = SparseArray([0, 0, 0, 0, 0], fill_value=0, kind=kind)
    tm.assert_sp_array_equal(result, expected)


@pytest.mark.parametrize("subtype", [object, "int64", "float64"])
def test_cmp_sp_index_does_not_depend_on_subtype(subtype):
    # GH#45126 the object route re-sparsified its dense result while the numeric
    #  route kept every position it was handed, so sp_index differed by subtype
    left = SparseArray(np.array([1, 0, -1, 2], dtype=subtype), fill_value=0)
    right = SparseArray(np.array([5, 5, 5, 5], dtype=subtype), fill_value=0)

    result = left > right
    assert result.npoints == 0
    tm.assert_numpy_array_equal(result.to_dense(), np.zeros(4, dtype=bool))


def test_op_result_drops_stored_fill_values_splib_union(kind):
    # GH#45126 two gapped operands with differing indexes go through the splib
    #  kernels, whose result index is the union of the two
    left = SparseArray([1.0, 0.0, 0.0, 4.0], fill_value=0.0, kind=kind)
    right = SparseArray([0.0, 2.0, 0.0, 0.0], fill_value=0.0, kind=kind)

    result = left > right
    expected = SparseArray([True, False, False, True], fill_value=False, kind=kind)
    tm.assert_sp_array_equal(result, expected)


@pytest.mark.parametrize("unit", ["m8[ns]", "M8[ns]"])
def test_op_result_drops_stored_fill_values_datetimelike(unit):
    # GH#45126 the datetimelike subtypes reach the same prune as the numeric ones
    values = np.array([1, 0, 2], dtype=unit)
    arr = SparseArray(values, fill_value=values[0])

    result = arr - arr
    assert result.npoints == 0
    tm.assert_numpy_array_equal(result.to_dense(), values - values)


def test_op_result_keeps_signed_zero():
    # GH#45126 -0.0 == 0.0, so pruning on equality alone dropped a stored -0.0
    #  and densified it back as +0.0
    arr = SparseArray([1.0, 0.0, -1.0], fill_value=0.0)

    result = (arr * 0).to_dense()
    expected = (pd.Series([1.0, 0.0, -1.0]) * 0).to_numpy()
    tm.assert_numpy_array_equal(np.signbit(result), np.signbit(expected))


def test_op_result_keeps_object_na_flavor():
    # GH#45126 an object array can hold several NAs at once, so pruning every NA
    #  under an NA fill densified the stored one back as the fill's spelling
    values = np.array([1, pd.NA, 3], dtype=object)
    arr = SparseArray(values, fill_value=0)

    result = list(pd.Series(arr) + np.nan)
    expected = list(pd.Series(values) + np.nan)
    assert result[1] is expected[1] is pd.NA


@pytest.mark.parametrize("fill_value", [np.nan, pd.NA])
def test_op_result_drops_stored_fill_values_object_na_fill(fill_value):
    # GH#45126 the object mask kernel compares NA to NA, so an NA fill has to be
    #  ruled out before it runs -- as the SparseArray constructor already does
    arr = SparseArray(np.array([1.0, 2.0], dtype=object), fill_value=fill_value)

    result = arr * fill_value
    assert result.npoints == 0
    assert result.density == 0.0


@pytest.mark.parametrize("dtype", ["complex64", "complex128"])
def test_op_result_keeps_half_na_complex(dtype):
    # GH#45126 isna() is True when either part is NaN, so pruning on it dropped a
    #  stored value whose other part was real data
    values = np.array([0j, complex(np.nan, 1.0)], dtype=dtype)
    arr = SparseArray(values, fill_value=0j)

    result = (arr + np.nan).to_dense()
    expected = (pd.Series(values) + np.nan).to_numpy()
    # tm.assert_* reads any two NAs as equal, so this compares the imaginary part
    tm.assert_numpy_array_equal(result.imag, expected.imag)


def test_op_result_keeps_signed_zero_complex():
    # GH#45126 the same for a complex subtype, where the sign of each component
    #  has to survive independently
    arr = SparseArray(np.array([-1 + 1j, 2 + 0j]), fill_value=0j)

    result = (arr * 0).to_dense()
    expected = (pd.Series([-1 + 1j, 2 + 0j]) * 0).to_numpy()
    tm.assert_numpy_array_equal(np.signbit(result.real), np.signbit(expected.real))
    tm.assert_numpy_array_equal(np.signbit(result.imag), np.signbit(expected.imag))
