import pytest
import numpy as np
import itertools
from scipy.special import laguerre
from numpy.random import rand
from numpy.testing import assert_, run_module_suite, assert_equal, \
    assert_almost_equal, assert_allclose

import qutip
from qutip.states import coherent, fock, ket, bell_state
from qutip.wigner import wigner, wigner_transform, _parity
from qutip.random_objects import rand_dm, rand_ket


class TestHusimiQ:
    @pytest.mark.parametrize('xs', ["", 1, None], ids=['str', 'int', 'none'])
    def test_failure_if_non_arraylike_coordinates(self, xs):
        state = qutip.rand_ket(4)
        valid = np.linspace(-1, 1, 5)
        with pytest.raises(TypeError) as e:
            qutip.qfunc(state, xs, valid)
        assert "must be array-like" in e.value.args[0]
        with pytest.raises(TypeError) as e:
            qutip.qfunc(state, valid, xs)
        assert "must be array-like" in e.value.args[0]
        with pytest.raises(TypeError) as e:
            qutip.QFunc(xs, valid)
        assert "must be array-like" in e.value.args[0]
        with pytest.raises(TypeError) as e:
            qutip.QFunc(valid, xs)
        assert "must be array-like" in e.value.args[0]

    @pytest.mark.parametrize('ndim', [2, 3])
    def test_failure_if_coordinates_not_1d(self, ndim):
        state = qutip.rand_ket(4)
        valid = np.linspace(-1, 1, 5)
        bad = valid.reshape((-1,) + (1,)*(ndim - 1))
        with pytest.raises(ValueError) as e:
            qutip.qfunc(state, bad, valid)
        assert "must be 1D" in e.value.args[0]
        with pytest.raises(ValueError) as e:
            qutip.qfunc(state, valid, bad)
        assert "must be 1D" in e.value.args[0]
        with pytest.raises(ValueError) as e:
            qutip.QFunc(bad, valid)
        assert "must be 1D" in e.value.args[0]
        with pytest.raises(ValueError) as e:
            qutip.QFunc(valid, bad)
        assert "must be 1D" in e.value.args[0]

    @pytest.mark.parametrize('dm', [True, False], ids=['dm', 'ket'])
    def test_failure_if_tensor_hilbert_space(self, dm):
        if dm:
            state = qutip.rand_dm(4, dims=[[2, 2], [2, 2]])
        else:
            state = qutip.rand_ket(4, dims=[[2, 2], [1, 1]])
        xs = np.linspace(-1, 1, 5)
        with pytest.raises(ValueError) as e:
            qutip.qfunc(state, xs, xs)
        assert "must not have tensor structure" in e.value.args[0]
        with pytest.raises(ValueError) as e:
            qutip.QFunc(xs, xs)(state)
        assert "must not have tensor structure" in e.value.args[0]

    def test_QFunc_raises_if_insufficient_memory(self):
        xs = np.linspace(-1, 1, 11)
        state = qutip.rand_ket(4)
        qfunc = qutip.QFunc(xs, xs, memory=0)
        with pytest.raises(MemoryError) as e:
            qfunc(state)
        assert e.value.args[0].startswith("Refusing to precompute")

    def test_qfunc_warns_if_insufficient_memory(self):
        xs = np.linspace(-1, 1, 11)
        state = qutip.rand_dm(4)
        with pytest.warns(UserWarning) as e:
            qutip.qfunc(state, xs, xs, precompute_memory=0)
        assert (
            e[0].message.args[0]
            .startswith("Falling back to iterative algorithm")
        )

    @pytest.mark.parametrize('obj', [
        pytest.param(np.eye(2, dtype=np.complex128), id='ndarray'),
        pytest.param([[1, 0], [0, 1]], id='list'),
        pytest.param(1, id='int'),
    ])
    def test_failure_if_not_a_Qobj(self, obj):
        xs = np.linspace(-1, 1, 11)
        with pytest.raises(TypeError) as e:
            qutip.qfunc(obj, xs, xs)
        assert e.value.args[0].startswith("state must be Qobj")
        qfunc = qutip.QFunc(xs, xs)
        with pytest.raises(TypeError) as e:
            qfunc(obj)
        assert e.value.args[0].startswith("state must be Qobj")

    # Use indirection so that the tests can still be collected if there's a bug
    # in the generating QuTiP functions.
    @pytest.mark.parametrize('state', [
        pytest.param(lambda: qutip.rand_super(2), id='super'),
        pytest.param(lambda: qutip.rand_ket(2).dag(), id='bra'),
        pytest.param(lambda: 1j*qutip.rand_dm(2), id='non-dm operator'),
        pytest.param(lambda: qutip.Qobj([[1, 0], [0, 0]], dims=[[2], [2, 1]]),
                     id='nonsquare dm'),
        pytest.param(lambda: qutip.operator_to_vector(qutip.qeye(2)),
                     id='operator-ket'),
        pytest.param(lambda: qutip.operator_to_vector(qutip.qeye(2)).dag(),
                     id='operator-bra'),
    ])
    def test_failure_if_not_a_state(self, state):
        xs = np.linspace(-1, 1, 11)
        state = state()
        with pytest.raises(ValueError) as e:
            qutip.qfunc(state, xs, xs)
        assert (
            e.value.args[0].startswith("state must be a ket or density matrix")
        )
        qfunc = qutip.QFunc(xs, xs)
        with pytest.raises(ValueError) as e:
            qfunc(state)
        assert (
            e.value.args[0].startswith("state must be a ket or density matrix")
        )

    @pytest.mark.parametrize('g', [
        pytest.param(np.sqrt(2), id='natural units'),
        pytest.param(1, id='arb units'),
    ])
    @pytest.mark.parametrize('n_ys', [5, 101])
    @pytest.mark.parametrize('n_xs', [5, 101])
    @pytest.mark.parametrize('dm', [True, False], ids=['dm', 'ket'])
    @pytest.mark.parametrize('size', [5, 32])
    def test_function_and_class_are_equivalent(self, size, dm, n_xs, n_ys, g):
        xs = np.linspace(-1, 1, n_xs)
        ys = np.linspace(0, 2, n_ys)
        state = qutip.rand_dm(size) if dm else qutip.rand_ket(size)
        function = qutip.qfunc(state, xs, ys, g)
        class_ = qutip.QFunc(xs, ys, g)(state)
        np.testing.assert_allclose(function, class_)

    @pytest.mark.parametrize('g', [
        pytest.param(np.sqrt(2), id='natural units'),
        pytest.param(1, id='arb units'),
    ])
    @pytest.mark.parametrize('n_ys', [5, 101])
    @pytest.mark.parametrize('n_xs', [5, 101])
    @pytest.mark.parametrize('size', [5, 32])
    def test_iterate_and_precompute_are_equivalent(self, size, n_xs, n_ys, g):
        xs = np.linspace(-1, 1, n_xs)
        ys = np.linspace(0, 2, n_ys)
        state = qutip.rand_dm(size)
        iterate = qutip.qfunc(state, xs, ys, g, precompute_memory=None)
        precompute = qutip.qfunc(state, xs, ys, g, precompute_memory=np.inf)
        np.testing.assert_allclose(iterate, precompute)

    @pytest.mark.parametrize('initial_size', [5, 8])
    @pytest.mark.parametrize('dm', [True, False], ids=['dm', 'ket'])
    def test_same_class_can_take_many_sizes(self, dm, initial_size):
        xs = np.linspace(-1, 1, 11)
        ys = np.linspace(0, 2, 11)
        shape = np.meshgrid(xs, ys)[0].shape
        sizes = initial_size + np.array([0, 1, -1, 4])
        qfunc = qutip.QFunc(xs, ys)
        for size in sizes:
            state = qutip.rand_dm(size) if dm else qutip.rand_ket(size)
            out = qfunc(state)
            assert isinstance(out, np.ndarray)
            assert out.shape == shape

    @pytest.mark.parametrize('dm_first', [True, False])
    def test_same_class_can_mix_ket_and_dm(self, dm_first):
        dms = [True, False, True, False]
        if not dm_first:
            dms = dms[::-1]
        xs = np.linspace(-1, 1, 11)
        ys = np.linspace(0, 2, 11)
        shape = np.meshgrid(xs, ys)[0].shape
        qfunc = qutip.QFunc(xs, ys)
        for dm in dms:
            state = qutip.rand_dm(4) if dm else qutip.rand_ket(4)
            out = qfunc(state)
            assert isinstance(out, np.ndarray)
            assert out.shape == shape

    @pytest.mark.parametrize('n_ys', [5, 101])
    @pytest.mark.parametrize('n_xs', [5, 101])
    @pytest.mark.parametrize('mix', [0.1, 0.5])
    def test_qfunc_is_linear(self, n_xs, n_ys, mix):
        xs = np.linspace(-1, 1, n_xs)
        ys = np.linspace(-1, 1, n_ys)
        qfunc = qutip.QFunc(xs, ys)
        left, right = qutip.rand_dm(5), qutip.rand_dm(5)
        qleft, qright = qfunc(left), qfunc(right)
        qboth = qfunc(mix*left + (1-mix)*right)
        np.testing.assert_allclose(mix*qleft + (1-mix)*qright, qboth)

    @pytest.mark.parametrize('n_ys', [5, 101])
    @pytest.mark.parametrize('n_xs', [5, 101])
    @pytest.mark.parametrize('size', [5, 32])
    def test_ket_and_dm_give_same_result(self, n_xs, n_ys, size):
        xs = np.linspace(-1, 1, n_xs)
        ys = np.linspace(-1, 1, n_ys)
        state = qutip.rand_ket(size)
        qfunc = qutip.QFunc(xs, ys)
        np.testing.assert_allclose(qfunc(state), qfunc(state.proj()))

    @pytest.mark.parametrize('g', [
        pytest.param(np.sqrt(2), id='natural units'),
        pytest.param(1, id='arb units'),
    ])
    @pytest.mark.parametrize('ys', [
        pytest.param(np.linspace(-1, 1, 5), id='(-1,1,5)'),
        pytest.param(np.linspace(0, 2, 3), id='(0,2,3)'),
    ])
    @pytest.mark.parametrize('xs', [
        pytest.param(np.linspace(-1, 1, 5), id='(-1,1,5)'),
        pytest.param(np.linspace(0, 2, 3), id='(0,2,3)'),
    ])
    @pytest.mark.parametrize('size', [3, 5])
    def test_against_naive_implementation(self, xs, ys, g, size):
        state = qutip.rand_dm(size)
        state_np = state.full()
        x, y = np.meshgrid(xs, ys)
        alphas = 0.5*g * (x + 1j*y)
        naive = np.empty(alphas.shape, dtype=np.float64)
        for i, alpha in enumerate(alphas.flat):
            coh = qutip.coherent(size, alpha, method='analytic').full()
            naive.flat[i] = (coh.conj().T @ state_np @ coh).real
        naive *= (0.5*g)**2 / np.pi
        np.testing.assert_allclose(naive, qutip.qfunc(state, xs, ys, g))
        np.testing.assert_allclose(naive, qutip.QFunc(xs, ys, g)(state))


def test_wigner_bell1_su2parity():
    """wigner: testing the SU2 parity of the first Bell state.
    """
    psi = bell_state('00')

    steps = 25
    theta = np.tile(np.linspace(0, np.pi, steps), 2).reshape(2, steps)
    phi = np.tile(np.linspace(0, 2 * np.pi, steps), 2).reshape(2, steps)
    slicearray = ['l', 'l']

    wigner_analyt = np.zeros((steps, steps))
    for t in range(steps):
        for p in range(steps):
            wigner_analyt[t, p] = np.real(((1 + np.sqrt(3)
                                            * np.cos(theta[0, t]))
                                           * (1 + np.sqrt(3)
                                           * np.cos(theta[1, t]))
                                           + 3 * (np.sin(theta[0, t])
                                           * np.exp(-1j * phi[0, p])
                                           * np.sin(theta[1, t])
                                           * np.exp(-1j * phi[1, p])
                                           + np.sin(theta[0, t])
                                           * np.exp(1j * phi[0, p])
                                           * np.sin(theta[1, t])
                                           * np.exp(1j * phi[1, p]))
                                           + (1 - np.sqrt(3)
                                           * np.cos(theta[0, t]))
                                           * (1 - np.sqrt(3)
                                           * np.cos(theta[1, t]))) / 8.)

    wigner_theo = wigner_transform(psi, 0.5, False, steps, slicearray)

    assert_(np.sum(np.abs(wigner_analyt - wigner_theo)) < 1e-11)


@pytest.mark.slow
def test_wigner_bell4_su2parity():
    """wigner: testing the SU2 parity of the fourth Bell state.
    """
    psi = bell_state('11')

    steps = 25
    slicearray = ['l', 'l']

    wigner_analyt = np.zeros((steps, steps))
    for t in range(steps):
        for p in range(steps):
            wigner_analyt[t, p] = -0.5

    wigner_theo = wigner_transform(psi, 0.5, False, steps, slicearray)

    assert_(np.sum(np.abs(wigner_analyt - wigner_theo)) < 1e-11)


@pytest.mark.slow
def test_wigner_bell4_fullparity():
    """wigner: testing the parity of the fourth Bell state using the parity of
    the full space.
    """
    psi = bell_state('11')

    steps = 25
    slicearray = ['l', 'l']

    wigner_analyt = np.zeros((steps, steps))
    for t in range(steps):
        for p in range(steps):
            wigner_analyt[t, p] = -0.30901699

    print("wigner anal: ", wigner_analyt)
    wigner_theo = wigner_transform(psi, 0.5, True, steps, slicearray)

    print("wigner theo: ", wigner_theo)
    assert_(np.sum(np.abs(wigner_analyt - wigner_theo)) < 1e-4)


def test_parity():
    """wigner: testing the parity function.
    """
    j = 0.5
    assert_(_parity(2, j)[0, 0] - (1 - np.sqrt(3)) / 2. < 1e-11)
    assert_(_parity(2, j)[0, 1] < 1e-11)
    assert_(_parity(2, j)[1, 1] - (1 + np.sqrt(3)) / 2. < 1e-11)
    assert_(_parity(2, j)[1, 0] < 1e-11)


@pytest.mark.slow
def test_wigner_pure_su2():
    """wigner: testing the SU2 wigner transformation of a pure state.
    """
    psi = (ket([1]))
    steps = 25
    theta = np.linspace(0, np.pi, steps)
    phi = np.linspace(0, 2 * np.pi, steps)
    theta = theta[None, :]
    phi = phi[None, :]
    slicearray = ['l']

    wigner_analyt = np.zeros((steps, steps))
    for t in range(steps):
        for p in range(steps):
            wigner_analyt[t, p] = (1 + np.sqrt(3) * np.cos(theta[0, t])) / 2.

    wigner_theo = wigner_transform(psi, 0.5, False, steps, slicearray)

    assert_(np.sum(np.abs(wigner_analyt - wigner_theo)) < 1e-11)


@pytest.mark.slow
def test_wigner_ghz_su2parity():
    """wigner: testing the SU2 wigner transformation of the GHZ state.
    """
    psi = (ket([0, 0, 0]) + ket([1, 1, 1])) / np.sqrt(2)

    steps = 25
    N = 3
    theta = np.tile(np.linspace(0, np.pi, steps), N).reshape(N, steps)
    phi = np.tile(np.linspace(0, 2 * np.pi, steps), N).reshape(N, steps)
    slicearray = ['l', 'l', 'l']

    wigner_analyt = np.zeros((steps, steps))
    for t in range(steps):
        for p in range(steps):
            wigner_analyt[t, p] = np.real(((1 + np.sqrt(3)*np.cos(theta[0, t]))
                                           * (1 + np.sqrt(3)
                                           * np.cos(theta[1, t]))
                                           * (1 + np.sqrt(3)
                                           * np.cos(theta[2, t]))
                                           + 3**(3 / 2) * (np.sin(theta[0, t])
                                           * np.exp(-1j * phi[0, p])
                                           * np.sin(theta[1, t])
                                           * np.exp(-1j * phi[1, p])
                                           * np.sin(theta[2, t])
                                           * np.exp(-1j * phi[2, p])
                                           + np.sin(theta[0, t])
                                           * np.exp(1j * phi[0, p])
                                           * np.sin(theta[1, t])
                                           * np.exp(1j * phi[1, p])
                                           * np.sin(theta[2, t])
                                           * np.exp(1j * phi[2, p]))
                                           + (1 - np.sqrt(3)
                                           * np.cos(theta[0, t]))
                                           * (1 - np.sqrt(3)
                                           * np.cos(theta[1, t]))
                                           * (1 - np.sqrt(3)
                                           * np.cos(theta[2, t]))) / 16.)

    wigner_theo = wigner_transform(psi, 0.5, False, steps, slicearray)

    assert_(np.sum(np.abs(wigner_analyt - wigner_theo)) < 1e-11)


@pytest.mark.slow
def test_angle_slicing():
    """wigner: tests angle slicing.
    """
    psi1 = bell_state('00')
    psi2 = bell_state('01')
    psi3 = bell_state('10')
    psi4 = bell_state('11')

    steps = 25
    j = 0.5

    wigner1 = wigner_transform(psi1, j, False, steps, ['l', 'l'])
    wigner2 = wigner_transform(psi2, j, False, steps, ['l', 'z'])
    wigner3 = wigner_transform(psi3, j, False, steps, ['l', 'x'])
    wigner4 = wigner_transform(psi4, j, False, steps, ['l', 'y'])

    assert_(np.sum(np.abs(wigner2 - wigner1)) < 1e-11)
    assert_(np.sum(np.abs(wigner3 - wigner2)) < 1e-11)
    assert_(np.sum(np.abs(wigner4 - wigner3)) < 1e-11)
    assert_(np.sum(np.abs(wigner4 - wigner1)) < 1e-11)


def test_wigner_coherent():
    "wigner: test wigner function calculation for coherent states"
    xvec = np.linspace(-5.0, 5.0, 100)
    yvec = xvec

    X, Y = np.meshgrid(xvec, yvec)

    a = X + 1j * Y  # consistent with g=2 option to wigner function

    dx = xvec[1] - xvec[0]
    dy = yvec[1] - yvec[0]

    N = 20
    beta = rand() + rand() * 1.0j
    psi = coherent(N, beta)

    # calculate the wigner function using qutip and analytic formula
    W_qutip = wigner(psi, xvec, yvec, g=2)
    W_qutip_cl = wigner(psi, xvec, yvec, g=2, method='clenshaw')
    W_analytic = 2 / np.pi * np.exp(-2 * abs(a - beta) ** 2)

    # check difference
    assert_(np.sum(abs(W_qutip - W_analytic) ** 2) < 1e-4)
    assert_(np.sum(abs(W_qutip_cl - W_analytic) ** 2) < 1e-4)

    # check normalization
    assert_(np.sum(W_qutip) * dx * dy - 1.0 < 1e-8)
    assert_(np.sum(W_qutip_cl) * dx * dy - 1.0 < 1e-8)
    assert_(np.sum(W_analytic) * dx * dy - 1.0 < 1e-8)


def test_wigner_fock():
    "wigner: test wigner function calculation for Fock states"

    xvec = np.linspace(-5.0, 5.0, 100)
    yvec = xvec

    X, Y = np.meshgrid(xvec, yvec)

    a = X + 1j * Y  # consistent with g=2 option to wigner function

    dx = xvec[1] - xvec[0]
    dy = yvec[1] - yvec[0]

    N = 15

    for n in [2, 3, 4, 5, 6]:

        psi = fock(N, n)

        # calculate the wigner function using qutip and analytic formula
        W_qutip = wigner(psi, xvec, yvec, g=2)
        W_qutip_cl = wigner(psi, xvec, yvec, g=2, method='clenshaw')
        W_qutip_sparse = wigner(psi, xvec, yvec, g=2, sparse=True, method='clenshaw')
        W_analytic = 2 / np.pi * (-1) ** n * \
            np.exp(-2 * abs(a) ** 2) * np.polyval(laguerre(n), 4 * abs(a) ** 2)

        # check difference
        assert_(np.sum(abs(W_qutip - W_analytic)) < 1e-4)
        assert_(np.sum(abs(W_qutip_cl - W_analytic)) < 1e-4)
        assert_(np.sum(abs(W_qutip_sparse - W_analytic)) < 1e-4)

        # check normalization
        assert_(np.sum(W_qutip) * dx * dy - 1.0 < 1e-8)
        assert_(np.sum(W_qutip_cl) * dx * dy - 1.0 < 1e-8)
        assert_(np.sum(W_qutip_sparse) * dx * dy - 1.0 < 1e-8)
        assert_(np.sum(W_analytic) * dx * dy - 1.0 < 1e-8)


def test_wigner_compare_methods_dm():
    "wigner: compare wigner methods for random density matrices"

    xvec = np.linspace(-5.0, 5.0, 100)
    yvec = xvec

    X, Y = np.meshgrid(xvec, yvec)

    # a = X + 1j * Y  # consistent with g=2 option to wigner function

    dx = xvec[1] - xvec[0]
    dy = yvec[1] - yvec[0]

    N = 15

    for n in range(10):
        # try ten different random density matrices

        rho = rand_dm(N, 0.5 + rand() / 2)

        # calculate the wigner function using qutip and analytic formula
        W_qutip1 = wigner(rho, xvec, yvec, g=2)
        W_qutip2 = wigner(rho, xvec, yvec, g=2, method='laguerre')

        # check difference
        assert_(np.sum(abs(W_qutip1 - W_qutip1)) < 1e-4)

        # check normalization
        assert_(np.sum(W_qutip1) * dx * dy - 1.0 < 1e-8)
        assert_(np.sum(W_qutip2) * dx * dy - 1.0 < 1e-8)


def test_wigner_compare_methods_ket():
    "wigner: compare wigner methods for random state vectors"

    xvec = np.linspace(-5.0, 5.0, 100)
    yvec = xvec

    X, Y = np.meshgrid(xvec, yvec)

    # a = X + 1j * Y  # consistent with g=2 option to wigner function

    dx = xvec[1] - xvec[0]
    dy = yvec[1] - yvec[0]

    N = 15

    for n in range(10):
        # try ten different random density matrices

        psi = rand_ket(N, 0.5 + rand() / 2)

        # calculate the wigner function using qutip and analytic formula
        W_qutip1 = wigner(psi, xvec, yvec, g=2)
        W_qutip2 = wigner(psi, xvec, yvec, g=2, sparse=True)

        # check difference
        assert_(np.sum(abs(W_qutip1 - W_qutip2)) < 1e-4)

        # check normalization
        assert_(np.sum(W_qutip1) * dx * dy - 1.0 < 1e-8)
        assert_(np.sum(W_qutip2) * dx * dy - 1.0 < 1e-8)


def test_wigner_fft_comparse_ket():
    "Wigner: Compare Wigner fft and iterative for rand. ket"
    N = 20
    xvec = np.linspace(-10, 10, 128)
    for i in range(3):
        rho = rand_ket(N)

        Wfft, yvec = wigner(rho, xvec, xvec, method='fft')
        W = wigner(rho, xvec, yvec, method='iterative')

        Wdiff = abs(W - Wfft)
        assert_equal(np.sum(abs(Wdiff)) < 1e-7, True)


def test_wigner_fft_comparse_dm():
    "Wigner: Compare Wigner fft and iterative for rand. dm"
    N = 20
    xvec = np.linspace(-10, 10, 128)
    for i in range(3):
        rho = rand_dm(N)

        Wfft, yvec = wigner(rho, xvec, xvec, method='fft')
        W = wigner(rho, xvec, yvec, method='iterative')

        Wdiff = abs(W - Wfft)
        assert_equal(np.sum(abs(Wdiff)) < 1e-7, True)


def test_wigner_clenshaw_iter_dm():
    "Wigner: Compare Wigner clenshaw and iterative for rand. dm"
    N = 20
    xvec = np.linspace(-10, 10, 128)
    for i in range(3):
        rho = rand_dm(N)

        Wclen = wigner(rho, xvec, xvec, method='clenshaw')
        W = wigner(rho, xvec, xvec, method='iterative')

        Wdiff = abs(W - Wclen)
        assert_equal(np.sum(abs(Wdiff)) < 1e-7, True)


def test_wigner_clenshaw_sp_iter_dm():
    "Wigner: Compare Wigner sparse clenshaw and iterative for rand. dm"
    N = 20
    xvec = np.linspace(-10, 10, 128)
    for i in range(3):
        rho = rand_dm(N)

        Wclen = wigner(rho, xvec, xvec, method='clenshaw', sparse=True)
        W = wigner(rho, xvec, xvec, method='iterative')

        Wdiff = abs(W - Wclen)
        assert_equal(np.sum(abs(Wdiff)) < 1e-7, True)

@pytest.mark.parametrize(['spin'], [
    pytest.param(1/2, id="spin-one-half"),
    pytest.param(3, id="spin-three"),
    pytest.param(13/2, id="spin-thirteen-half"),
    pytest.param(7, id="spin-seven")
])
@pytest.mark.parametrize("pure", [
    pytest.param(True, id="pure"),
    pytest.param(False, id="mixed")
])
def test_spin_q_function(spin, pure):
    d = int(2*spin + 1)
    rho = rand_dm(d, pure=pure)

    # Points at which to evaluate the spin Q function
    theta = np.linspace(0, np.pi, 16, endpoint=True)
    phi = np.linspace(-np.pi, np.pi, 32, endpoint=True)
    Q, _, _ = qutip.spin_q_function(rho, theta, phi)

    for k, (phi_prime, theta_prime) in enumerate(itertools.product(phi, theta)):
        state = qutip.spin_coherent(spin, theta_prime, phi_prime)
        direct_Q = (state.dag() * rho * state).norm()
        assert_almost_equal(Q.flat[k], direct_Q, decimal=9)

@pytest.mark.parametrize(['spin'], [
    pytest.param(1/2, id="spin-one-half"),
    pytest.param(3, id="spin-three"),
    pytest.param(13/2, id="spin-thirteen-half"),
    pytest.param(7, id="spin-seven")
])
@pytest.mark.parametrize("pure", [
    pytest.param(True, id="pure"),
    pytest.param(False, id="mixed")
])
def test_spin_q_function_normalized(spin, pure):
    d = int(2 * spin + 1)
    rho = rand_dm(d, pure=pure)

    # Points at which to evaluate the spin Q function
    theta = np.linspace(0, np.pi, 128, endpoint=True)
    phi = np.linspace(-np.pi, np.pi, 256, endpoint=True)
    Q, THETA, _ = qutip.spin_q_function(rho, theta, phi)

    norm = d / (4 * np.pi) * np.trapz(np.trapz(Q * np.sin(THETA), theta), phi)
    assert_almost_equal(norm, 1, decimal=4)


@pytest.mark.parametrize(["spin"], [
    pytest.param(1/2, id="spin-one-half"),
    pytest.param(1, id="spin-one"),
    pytest.param(3/2, id="spin-three-half"),
    pytest.param(2, id="spin-two")
])
@pytest.mark.parametrize("pure", [
    pytest.param(True, id="pure"),
    pytest.param(False, id="mixed")
])
def test_spin_wigner_normalized(spin, pure):
    d = int(2*spin + 1)
    rho = rand_dm(d, pure=pure)

    # Points at which to evaluate the spin Wigner function
    theta = np.linspace(0, np.pi, 256, endpoint=True)
    phi = np.linspace(-np.pi, np.pi, 512, endpoint=True)
    W, THETA, PHI = qutip.spin_wigner(rho, theta, phi)

    norm = np.trapz(np.trapz(W * np.sin(THETA) * np.sqrt(d / (4*np.pi)), theta), phi)
    assert_almost_equal(norm, 1, decimal=4)

@pytest.mark.parametrize(['spin'], [
    pytest.param(1 / 2, id="spin-one-half"),
    pytest.param(1, id="spin-one"),
    pytest.param(3 / 2, id="spin-three-half"),
    pytest.param(2, id="spin-two")
])
@pytest.mark.parametrize("pure", [
    pytest.param(True, id="pure"),
    pytest.param(False, id="mixed")
])
def test_spin_wigner_overlap(spin, pure, n=5):
    d = int(2*spin + 1)
    rho = rand_dm(d, pure=pure)

    # Points at which to evaluate the spin Wigner function
    theta = np.linspace(0, np.pi, 256, endpoint=True)
    phi = np.linspace(-np.pi, np.pi, 512, endpoint=True)
    W, THETA, _ = qutip.spin_wigner(rho, theta, phi)

    for k in range(n):
        test_state = rand_dm(d)
        state_overlap = (test_state*rho).tr().real

        W_state, _, _ = qutip.spin_wigner(test_state, theta, phi)
        W_overlap = np.trapz(
            np.trapz(W_state * W * np.sin(THETA), theta), phi).real
        assert_almost_equal(W_overlap, state_overlap, decimal=4)

if __name__ == "__main__":
    run_module_suite()
