# -*- coding: utf-8 -*-
#   Copyright 2017 ProjectQ-Framework (www.projectq.ch)
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
"""
Tests for projectq.backends._sim._simulator.py, using both the Python
and the C++ simulator as backends.
"""

import copy
import math
import numpy
import pytest
import random
import scipy
import scipy.sparse
import scipy.sparse.linalg

from projectq import MainEngine
from projectq.cengines import (
    BasicEngine,
    BasicMapperEngine,
    DummyEngine,
    LocalOptimizer,
    NotYetMeasuredError,
)
from projectq.ops import (
    All,
    Allocate,
    BasicGate,
    BasicMathGate,
    CNOT,
    Command,
    H,
    MatrixGate,
    Measure,
    QubitOperator,
    Rx,
    Ry,
    Rz,
    S,
    TimeEvolution,
    Toffoli,
    X,
    Y,
    Z,
)
from projectq.meta import Control, Dagger, LogicalQubitIDTag
from projectq.types import WeakQubitRef

from projectq.backends import Simulator


def test_is_cpp_simulator_present():
    import projectq.backends._sim._cppsim

    assert projectq.backends._sim._cppsim


def get_available_simulators():
    result = ["py_simulator"]
    try:
        import projectq.backends._sim._cppsim  # noqa: F401

        result.append("cpp_simulator")
    except ImportError:
        # The C++ simulator was either not installed or is misconfigured. Skip.
        pass
    return result


@pytest.fixture(params=get_available_simulators())
def sim(request):
    if request.param == "cpp_simulator":
        from projectq.backends._sim._cppsim import Simulator as CppSim

        sim = Simulator(gate_fusion=True)
        sim._simulator = CppSim(1)
        return sim
    if request.param == "py_simulator":
        from projectq.backends._sim._pysim import Simulator as PySim

        sim = Simulator()
        sim._simulator = PySim(1)
        return sim


@pytest.fixture(params=["mapper", "no_mapper"])
def mapper(request):
    """
    Adds a mapper which changes qubit ids by adding 1
    """
    if request.param == "mapper":

        class TrivialMapper(BasicMapperEngine):
            def __init__(self):
                BasicEngine.__init__(self)
                self.current_mapping = dict()

            def receive(self, command_list):
                for cmd in command_list:
                    for qureg in cmd.all_qubits:
                        for qubit in qureg:
                            if qubit.id == -1:
                                continue
                            elif qubit.id not in self.current_mapping:
                                previous_map = self.current_mapping
                                previous_map[qubit.id] = qubit.id + 1
                                self.current_mapping = previous_map
                    self._send_cmd_with_mapped_ids(cmd)

        return TrivialMapper()
    if request.param == "no_mapper":
        return None


class Mock1QubitGate(MatrixGate):
    def __init__(self):
        MatrixGate.__init__(self)
        self.cnt = 0

    @property
    def matrix(self):
        self.cnt += 1
        return [[0, 1], [1, 0]]


class Mock6QubitGate(MatrixGate):
    def __init__(self):
        MatrixGate.__init__(self)
        self.cnt = 0

    @property
    def matrix(self):
        self.cnt += 1
        return numpy.eye(2 ** 6)


class MockNoMatrixGate(BasicGate):
    def __init__(self):
        BasicGate.__init__(self)
        self.cnt = 0


def test_simulator_is_available(sim):
    backend = DummyEngine(save_commands=True)
    eng = MainEngine(backend, [])
    qubit = eng.allocate_qubit()
    Measure | qubit
    BasicMathGate(lambda x: x) | qubit
    qubit[0].__del__()
    assert len(backend.received_commands) == 4

    # Test that allocate, measure, basic math, and deallocate are available.
    for cmd in backend.received_commands:
        assert sim.is_available(cmd)

    new_cmd = backend.received_commands[-1]

    new_cmd.gate = Mock1QubitGate()
    assert sim.is_available(new_cmd)
    assert new_cmd.gate.cnt == 1

    new_cmd.gate = Mock6QubitGate()
    assert not sim.is_available(new_cmd)
    assert new_cmd.gate.cnt == 1

    new_cmd.gate = MockNoMatrixGate()
    assert not sim.is_available(new_cmd)
    assert new_cmd.gate.cnt == 0


def test_simulator_is_available_negative_control(sim):
    qb0 = WeakQubitRef(engine=None, idx=0)
    qb1 = WeakQubitRef(engine=None, idx=1)
    qb2 = WeakQubitRef(engine=None, idx=2)

    assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1]))
    assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1'))
    assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0'))

    assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2]))
    assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11'))
    assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01'))


def test_simulator_cheat(sim):
    # cheat function should return a tuple
    assert isinstance(sim.cheat(), tuple)
    # first entry is the qubit mapping.
    # should be empty:
    assert len(sim.cheat()[0]) == 0
    # state vector should only have 1 entry:
    assert len(sim.cheat()[1]) == 1

    eng = MainEngine(sim, [])
    qubit = eng.allocate_qubit()

    # one qubit has been allocated
    assert len(sim.cheat()[0]) == 1
    assert sim.cheat()[0][0] == 0
    assert len(sim.cheat()[1]) == 2
    assert 1.0 == pytest.approx(abs(sim.cheat()[1][0]))

    qubit[0].__del__()
    # should be empty:
    assert len(sim.cheat()[0]) == 0
    # state vector should only have 1 entry:
    assert len(sim.cheat()[1]) == 1


def test_simulator_functional_measurement(sim):
    eng = MainEngine(sim, [])
    qubits = eng.allocate_qureg(5)
    # entangle all qubits:
    H | qubits[0]
    for qb in qubits[1:]:
        CNOT | (qubits[0], qb)

    All(Measure) | qubits

    bit_value_sum = sum([int(qubit) for qubit in qubits])
    assert bit_value_sum == 0 or bit_value_sum == 5

    qb1 = WeakQubitRef(engine=eng, idx=qubits[0].id)
    qb2 = WeakQubitRef(engine=eng, idx=qubits[1].id)
    with pytest.raises(ValueError):
        eng.backend._handle(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2]))


def test_simulator_measure_mapped_qubit(sim):
    eng = MainEngine(sim, [])
    qb1 = WeakQubitRef(engine=eng, idx=1)
    qb2 = WeakQubitRef(engine=eng, idx=2)
    cmd0 = Command(engine=eng, gate=Allocate, qubits=([qb1],))
    cmd1 = Command(engine=eng, gate=X, qubits=([qb1],))
    cmd2 = Command(
        engine=eng,
        gate=Measure,
        qubits=([qb1],),
        controls=[],
        tags=[LogicalQubitIDTag(2)],
    )
    with pytest.raises(NotYetMeasuredError):
        int(qb1)
    with pytest.raises(NotYetMeasuredError):
        int(qb2)
    eng.send([cmd0, cmd1, cmd2])
    eng.flush()
    with pytest.raises(NotYetMeasuredError):
        int(qb1)
    assert int(qb2) == 1


class Plus2Gate(BasicMathGate):
    def __init__(self):
        BasicMathGate.__init__(self, lambda x: (x + 2,))


def test_simulator_emulation(sim):
    eng = MainEngine(sim, [])
    qubit1 = eng.allocate_qubit()
    qubit2 = eng.allocate_qubit()
    qubit3 = eng.allocate_qubit()

    with Control(eng, qubit3):
        Plus2Gate() | (qubit1 + qubit2)

    assert 1.0 == pytest.approx(sim.cheat()[1][0])

    X | qubit3
    with Control(eng, qubit3):
        Plus2Gate() | (qubit1 + qubit2)
    assert 1.0 == pytest.approx(sim.cheat()[1][6])

    All(Measure) | (qubit1 + qubit2 + qubit3)


def test_simulator_kqubit_gate(sim):
    m1 = Rx(0.3).matrix
    m2 = Rx(0.8).matrix
    m3 = Ry(0.1).matrix
    m4 = Rz(0.9).matrix.dot(Ry(-0.1).matrix)
    m = numpy.kron(m4, numpy.kron(m3, numpy.kron(m2, m1)))

    class KQubitGate(BasicGate):
        @property
        def matrix(self):
            return m

    eng = MainEngine(sim, [])
    qureg = eng.allocate_qureg(4)
    qubit = eng.allocate_qubit()
    Rx(-0.3) | qureg[0]
    Rx(-0.8) | qureg[1]
    Ry(-0.1) | qureg[2]
    Rz(-0.9) | qureg[3]
    Ry(0.1) | qureg[3]
    X | qubit
    with Control(eng, qubit):
        KQubitGate() | qureg
    X | qubit
    with Control(eng, qubit):
        with Dagger(eng):
            KQubitGate() | qureg
    assert sim.get_amplitude('0' * 5, qubit + qureg) == pytest.approx(1.0)

    class LargerGate(BasicGate):
        @property
        def matrix(self):
            return numpy.eye(2 ** 6)

    with pytest.raises(Exception):
        LargerGate() | (qureg + qubit)


def test_simulator_kqubit_exception(sim):
    m1 = Rx(0.3).matrix
    m2 = Rx(0.8).matrix
    m3 = Ry(0.1).matrix
    m4 = Rz(0.9).matrix.dot(Ry(-0.1).matrix)
    m = numpy.kron(m4, numpy.kron(m3, numpy.kron(m2, m1)))

    class KQubitGate(BasicGate):
        @property
        def matrix(self):
            return m

    eng = MainEngine(sim, [])
    qureg = eng.allocate_qureg(3)
    with pytest.raises(Exception):
        KQubitGate() | qureg
    with pytest.raises(Exception):
        H | qureg


def test_simulator_probability(sim, mapper):
    engine_list = [LocalOptimizer()]
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list)
    qubits = eng.allocate_qureg(6)
    All(H) | qubits
    eng.flush()
    bits = [0, 0, 1, 0, 1, 0]
    for i in range(6):
        assert eng.backend.get_probability(bits[:i], qubits[:i]) == pytest.approx(0.5 ** i)
    extra_qubit = eng.allocate_qubit()
    with pytest.raises(RuntimeError):
        eng.backend.get_probability([0], extra_qubit)
    del extra_qubit
    All(H) | qubits
    Ry(2 * math.acos(math.sqrt(0.3))) | qubits[0]
    eng.flush()
    assert eng.backend.get_probability([0], [qubits[0]]) == pytest.approx(0.3)
    Ry(2 * math.acos(math.sqrt(0.4))) | qubits[2]
    eng.flush()
    assert eng.backend.get_probability([0], [qubits[2]]) == pytest.approx(0.4)
    assert eng.backend.get_probability([0, 0], qubits[:3:2]) == pytest.approx(0.12)
    assert eng.backend.get_probability([0, 1], qubits[:3:2]) == pytest.approx(0.18)
    assert eng.backend.get_probability([1, 0], qubits[:3:2]) == pytest.approx(0.28)
    All(Measure) | qubits


def test_simulator_amplitude(sim, mapper):
    engine_list = [LocalOptimizer()]
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list)
    qubits = eng.allocate_qureg(6)
    All(X) | qubits
    All(H) | qubits
    eng.flush()
    bits = [0, 0, 1, 0, 1, 0]
    assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(1.0 / 8.0)
    bits = [0, 0, 0, 0, 1, 0]
    assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1.0 / 8.0)
    bits = [0, 1, 1, 0, 1, 0]
    assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1.0 / 8.0)
    All(H) | qubits
    All(X) | qubits
    Ry(2 * math.acos(0.3)) | qubits[0]
    eng.flush()
    bits = [0] * 6
    assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(0.3)
    bits[0] = 1
    assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(math.sqrt(0.91))
    All(Measure) | qubits
    # raises if not all qubits are in the list:
    with pytest.raises(RuntimeError):
        eng.backend.get_amplitude(bits, qubits[:-1])
    # doesn't just check for length:
    with pytest.raises(RuntimeError):
        eng.backend.get_amplitude(bits, qubits[:-1] + [qubits[0]])
    extra_qubit = eng.allocate_qubit()  # noqa: F841
    eng.flush()
    # there is a new qubit now!
    with pytest.raises(RuntimeError):
        eng.backend.get_amplitude(bits, qubits)


def test_simulator_expectation(sim, mapper):
    engine_list = []
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list)
    qureg = eng.allocate_qureg(3)
    op0 = QubitOperator('Z0')
    expectation = sim.get_expectation_value(op0, qureg)
    assert 1.0 == pytest.approx(expectation)
    X | qureg[0]
    expectation = sim.get_expectation_value(op0, qureg)
    assert -1.0 == pytest.approx(expectation)
    H | qureg[0]
    op1 = QubitOperator('X0')
    expectation = sim.get_expectation_value(op1, qureg)
    assert -1.0 == pytest.approx(expectation)
    Z | qureg[0]
    expectation = sim.get_expectation_value(op1, qureg)
    assert 1.0 == pytest.approx(expectation)
    X | qureg[0]
    S | qureg[0]
    Z | qureg[0]
    X | qureg[0]
    op2 = QubitOperator('Y0')
    expectation = sim.get_expectation_value(op2, qureg)
    assert 1.0 == pytest.approx(expectation)
    Z | qureg[0]
    expectation = sim.get_expectation_value(op2, qureg)
    assert -1.0 == pytest.approx(expectation)

    op_sum = QubitOperator('Y0 X1 Z2') + QubitOperator('X1')
    H | qureg[1]
    X | qureg[2]
    expectation = sim.get_expectation_value(op_sum, qureg)
    assert 2.0 == pytest.approx(expectation)

    op_sum = QubitOperator('Y0 X1 Z2') + QubitOperator('X1')
    X | qureg[2]
    expectation = sim.get_expectation_value(op_sum, qureg)
    assert 0.0 == pytest.approx(expectation)

    op_id = 0.4 * QubitOperator(())
    expectation = sim.get_expectation_value(op_id, qureg)
    assert 0.4 == pytest.approx(expectation)

    All(Measure) | qureg


def test_simulator_expectation_exception(sim):
    eng = MainEngine(sim, [])
    qureg = eng.allocate_qureg(3)
    op = QubitOperator('Z2')
    sim.get_expectation_value(op, qureg)
    op2 = QubitOperator('Z3')
    with pytest.raises(Exception):
        sim.get_expectation_value(op2, qureg)
    op3 = QubitOperator('Z1') + QubitOperator('X1 Y3')
    with pytest.raises(Exception):
        sim.get_expectation_value(op3, qureg)


def test_simulator_applyqubitoperator_exception(sim):
    eng = MainEngine(sim, [])
    qureg = eng.allocate_qureg(3)
    op = QubitOperator('Z2')
    sim.apply_qubit_operator(op, qureg)
    op2 = QubitOperator('Z3')
    with pytest.raises(Exception):
        sim.apply_qubit_operator(op2, qureg)
    op3 = QubitOperator('Z1') + QubitOperator('X1 Y3')
    with pytest.raises(Exception):
        sim.apply_qubit_operator(op3, qureg)


def test_simulator_applyqubitoperator(sim, mapper):
    engine_list = []
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list, verbose=True)
    qureg = eng.allocate_qureg(3)
    op = QubitOperator('X0 Y1 Z2')
    sim.apply_qubit_operator(op, qureg)
    X | qureg[0]
    Y | qureg[1]
    Z | qureg[2]
    assert sim.get_amplitude('000', qureg) == pytest.approx(1.0)

    H | qureg[0]
    op_H = 1.0 / math.sqrt(2.0) * (QubitOperator('X0') + QubitOperator('Z0'))
    sim.apply_qubit_operator(op_H, [qureg[0]])
    assert sim.get_amplitude('000', qureg) == pytest.approx(1.0)

    op_Proj0 = 0.5 * (QubitOperator('') + QubitOperator('Z0'))
    op_Proj1 = 0.5 * (QubitOperator('') - QubitOperator('Z0'))
    H | qureg[0]
    sim.apply_qubit_operator(op_Proj0, [qureg[0]])
    assert sim.get_amplitude('000', qureg) == pytest.approx(1.0 / math.sqrt(2.0))
    sim.apply_qubit_operator(op_Proj1, [qureg[0]])
    assert sim.get_amplitude('000', qureg) == pytest.approx(0.0)

    # TODO: this is suspicious...
    eng.backend.set_wavefunction([1, 0, 0, 0, 0, 0, 0, 0], qureg)


def test_simulator_time_evolution(sim):
    N = 8  # number of qubits
    time_to_evolve = 1.1  # time to evolve for
    eng = MainEngine(sim, [])
    qureg = eng.allocate_qureg(N)
    # initialize in random wavefunction by applying some gates:
    for qb in qureg:
        Rx(random.random()) | qb
        Ry(random.random()) | qb
    eng.flush()
    # Use cheat to get initial start wavefunction:
    qubit_to_bit_map, init_wavefunction = copy.deepcopy(eng.backend.cheat())
    Qop = QubitOperator
    op = 0.3 * Qop("X0 Y1 Z2 Y3 X4")
    op += 1.1 * Qop(())
    op += -1.4 * Qop("Y0 Z1 X3 Y5")
    op += -1.1 * Qop("Y1 X2 X3 Y4")
    ctrl_qubit = eng.allocate_qubit()
    H | ctrl_qubit
    with Control(eng, ctrl_qubit):
        TimeEvolution(time_to_evolve, op) | qureg
    eng.flush()
    qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat())
    All(Measure) | qureg + ctrl_qubit

    # Check manually:

    def build_matrix(list_single_matrices):
        res = list_single_matrices[0]
        for i in range(1, len(list_single_matrices)):
            res = scipy.sparse.kron(res, list_single_matrices[i])
        return res

    id_sp = scipy.sparse.identity(2, format="csr", dtype=complex)
    x_sp = scipy.sparse.csr_matrix([[0.0, 1.0], [1.0, 0.0]], dtype=complex)
    y_sp = scipy.sparse.csr_matrix([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)
    z_sp = scipy.sparse.csr_matrix([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
    gates = [x_sp, y_sp, z_sp]

    res_matrix = 0
    for t, c in op.terms.items():
        matrix = [id_sp] * N
        for idx, gate in t:
            matrix[qbit_to_bit_map[qureg[idx].id]] = gates[ord(gate) - ord('X')]
        matrix.reverse()
        res_matrix += build_matrix(matrix) * c
    res_matrix *= -1j * time_to_evolve

    init_wavefunction = numpy.array(init_wavefunction, copy=False)
    final_wavefunction = numpy.array(final_wavefunction, copy=False)
    res = scipy.sparse.linalg.expm_multiply(res_matrix, init_wavefunction)

    half = int(len(final_wavefunction) / 2)
    hadamard_f = 1.0 / math.sqrt(2.0)
    # check evolution and control
    assert numpy.allclose(hadamard_f * res, final_wavefunction[half:])
    assert numpy.allclose(final_wavefunction[:half], hadamard_f * init_wavefunction)


def test_simulator_set_wavefunction(sim, mapper):
    engine_list = [LocalOptimizer()]
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list)
    qubits = eng.allocate_qureg(2)
    wf = [0.0, 0.0, math.sqrt(0.2), math.sqrt(0.8)]
    with pytest.raises(RuntimeError):
        eng.backend.set_wavefunction(wf, qubits)
    eng.flush()
    eng.backend.set_wavefunction(wf, qubits)
    assert pytest.approx(eng.backend.get_probability('1', [qubits[0]])) == 0.8
    assert pytest.approx(eng.backend.get_probability('01', qubits)) == 0.2
    assert pytest.approx(eng.backend.get_probability('1', [qubits[1]])) == 1.0
    All(Measure) | qubits


def test_simulator_set_wavefunction_always_complex(sim):
    """Checks that wavefunction is always complex"""
    eng = MainEngine(sim)
    qubit = eng.allocate_qubit()
    eng.flush()
    wf = [1.0, 0]
    eng.backend.set_wavefunction(wf, qubit)
    Y | qubit
    eng.flush()
    assert eng.backend.get_amplitude('1', qubit) == pytest.approx(1j)


def test_simulator_collapse_wavefunction(sim, mapper):
    engine_list = [LocalOptimizer()]
    if mapper is not None:
        engine_list.append(mapper)
    eng = MainEngine(sim, engine_list=engine_list)
    qubits = eng.allocate_qureg(4)
    # unknown qubits: raises
    with pytest.raises(RuntimeError):
        eng.backend.collapse_wavefunction(qubits, [0] * 4)
    eng.flush()

    # mismatch in length: raises
    with pytest.raises(ValueError):
        eng.backend.collapse_wavefunction(qubits, [0] * 5)
    eng.backend.collapse_wavefunction(qubits, [0] * 4)
    assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == 1.0
    All(H) | qubits[1:]
    eng.flush()
    assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == 0.125
    # impossible outcome: raises
    with pytest.raises(RuntimeError):
        eng.backend.collapse_wavefunction(qubits, [1] + [0] * 3)
    eng.backend.collapse_wavefunction(qubits[:-1], [0, 1, 0])
    probability = eng.backend.get_probability([0, 1, 0, 1], qubits)
    assert probability == pytest.approx(0.5)
    eng.backend.set_wavefunction([1.0] + [0.0] * 15, qubits)
    H | qubits[0]
    CNOT | (qubits[0], qubits[1])
    eng.flush()
    eng.backend.collapse_wavefunction([qubits[0]], [1])
    probability = eng.backend.get_probability([1, 1], qubits[0:2])
    assert probability == pytest.approx(1.0)


def test_simulator_no_uncompute_exception(sim):
    eng = MainEngine(sim, [])
    qubit = eng.allocate_qubit()
    H | qubit
    with pytest.raises(RuntimeError):
        qubit[0].__del__()
    # If you wanted to keep using the qubit, you shouldn't have deleted it.
    assert qubit[0].id == -1


class MockSimulatorBackend(object):
    def __init__(self):
        self.run_cnt = 0

    def run(self):
        self.run_cnt += 1


def test_simulator_flush():
    sim = Simulator()
    sim._simulator = MockSimulatorBackend()

    eng = MainEngine(sim)
    eng.flush()

    assert sim._simulator.run_cnt == 1


def test_simulator_send():
    sim = Simulator()
    backend = DummyEngine(save_commands=True)

    eng = MainEngine(backend, [sim])

    qubit = eng.allocate_qubit()
    H | qubit
    Measure | qubit
    del qubit
    eng.flush()

    assert len(backend.received_commands) == 5


def test_simulator_functional_entangle(sim):
    eng = MainEngine(sim, [])
    qubits = eng.allocate_qureg(5)
    # entangle all qubits:
    H | qubits[0]
    for qb in qubits[1:]:
        CNOT | (qubits[0], qb)

    # check the state vector:
    assert 0.5 == pytest.approx(abs(sim.cheat()[1][0]) ** 2)
    assert 0.5 == pytest.approx(abs(sim.cheat()[1][31]) ** 2)
    for i in range(1, 31):
        assert 0.0 == pytest.approx(abs(sim.cheat()[1][i]))

    # unentangle all except the first 2
    for qb in qubits[2:]:
        CNOT | (qubits[0], qb)

    # entangle using Toffolis
    for qb in qubits[2:]:
        Toffoli | (qubits[0], qubits[1], qb)

    # check the state vector:
    assert 0.5 == pytest.approx(abs(sim.cheat()[1][0]) ** 2)
    assert 0.5 == pytest.approx(abs(sim.cheat()[1][31]) ** 2)
    for i in range(1, 31):
        assert 0.0 == pytest.approx(abs(sim.cheat()[1][i]))

    # uncompute using multi-controlled NOTs
    with Control(eng, qubits[0:-1]):
        X | qubits[-1]
    with Control(eng, qubits[0:-2]):
        X | qubits[-2]
    with Control(eng, qubits[0:-3]):
        X | qubits[-3]
    CNOT | (qubits[0], qubits[1])
    H | qubits[0]

    # check the state vector:
    assert 1.0 == pytest.approx(abs(sim.cheat()[1][0]) ** 2)
    for i in range(1, 32):
        assert 0.0 == pytest.approx(abs(sim.cheat()[1][i]))

    All(Measure) | qubits


def test_simulator_convert_logical_to_mapped_qubits(sim):
    mapper = BasicMapperEngine()

    def receive(command_list):
        pass

    mapper.receive = receive
    eng = MainEngine(sim, [mapper])
    qubit0 = eng.allocate_qubit()
    qubit1 = eng.allocate_qubit()
    mapper.current_mapping = {qubit0[0].id: qubit1[0].id, qubit1[0].id: qubit0[0].id}
    assert sim._convert_logical_to_mapped_qureg(qubit0 + qubit1) == qubit1 + qubit0


def test_simulator_constant_math_emulation():
    if "cpp_simulator" not in get_available_simulators():
        pytest.skip("No C++ simulator")
        return

    results = [[[1, 1, 0, 0, 0]], [[0, 1, 0, 0, 0]], [[0, 1, 1, 1, 0]]]

    import projectq.backends._sim._simulator as _sim
    from projectq.backends._sim._pysim import Simulator as PySim
    from projectq.backends._sim._cppsim import Simulator as CppSim
    from projectq.libs.math import AddConstant, AddConstantModN, MultiplyByConstantModN

    def gate_filter(eng, cmd):
        g = cmd.gate
        if isinstance(g, BasicMathGate):
            return False
        return eng.next_engine.is_available(cmd)

    def run_simulation(sim):
        eng = MainEngine(sim, [])
        quint = eng.allocate_qureg(5)
        AddConstant(3) | quint
        All(Measure) | quint
        eng.flush()
        results[0].append([int(qb) for qb in quint])

        AddConstantModN(4, 5) | quint
        All(Measure) | quint
        eng.flush()
        results[1].append([int(qb) for qb in quint])

        MultiplyByConstantModN(15, 16) | quint
        All(Measure) | quint
        eng.flush()
        results[2].append([int(qb) for qb in quint])

    cppsim = Simulator(gate_fusion=False)
    cppsim._simulator = CppSim(1)
    run_simulation(cppsim)

    _sim.FALLBACK_TO_PYSIM = True
    pysim = Simulator()
    pysim._simulator = PySim(1)
    # run_simulation(pysim)

    for result in results:
        ref = result[0]
        for res in result[1:]:
            assert ref == res
