# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Test the BarrierBeforeFinalMeasurements pass"""

import random
import unittest

from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements
from qiskit.converters import circuit_to_dag
from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister, Clbit
from test import QiskitTestCase  # pylint: disable=wrong-import-order


class TestBarrierBeforeFinalMeasurements(QiskitTestCase):
    """Tests the BarrierBeforeFinalMeasurements pass."""

    def test_single_measure(self):
        """A single measurement at the end
                          |
        q:--[m]--     q:--|-[m]---
             |    ->      |  |
        c:---.---     c:-----.---
        """
        qr = QuantumRegister(1, "q")
        cr = ClassicalRegister(1, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.measure(qr, cr)

        expected = QuantumCircuit(qr, cr)
        expected.barrier(qr)
        expected.measure(qr, cr)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_ignore_single_measure(self):
        """Ignore single measurement because it is not at the end
        q:--[m]-[H]-      q:--[m]-[H]-
             |        ->       |
        c:---.------      c:---.------
        """
        qr = QuantumRegister(1, "q")
        cr = ClassicalRegister(1, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.measure(qr, cr)
        circuit.h(qr[0])

        expected = QuantumCircuit(qr, cr)
        expected.measure(qr, cr)
        expected.h(qr[0])

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_single_measure_mix(self):
        """Two measurements, but only one is at the end
                                                |
        q0:--[m]--[H]--[m]--     q0:--[m]--[H]--|-[m]---
              |         |    ->        |        |  |
         c:---.---------.---      c:---.-----------.---
        """
        qr = QuantumRegister(1, "q")
        cr = ClassicalRegister(1, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.measure(qr, cr)
        circuit.h(qr)
        circuit.measure(qr, cr)

        expected = QuantumCircuit(qr, cr)
        expected.measure(qr, cr)
        expected.h(qr)
        expected.barrier(qr)
        expected.measure(qr, cr)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_two_qregs(self):
        """Two measurements in different qregs to different cregs
                                          |
        q0:--[H]--[m]------     q0:--[H]--|--[m]------
                   |                      |   |
        q1:--------|--[m]--  -> q1:-------|---|--[m]--
                   |   |                  |   |   |
        c0:--------.---|---      c0:----------.---|---
                       |                          |
        c1:------------.---      c0:--------------.---
        """
        qr0 = QuantumRegister(1, "q0")
        qr1 = QuantumRegister(1, "q1")
        cr0 = ClassicalRegister(1, "c0")
        cr1 = ClassicalRegister(1, "c1")

        circuit = QuantumCircuit(qr0, qr1, cr0, cr1)
        circuit.h(qr0)
        circuit.measure(qr0, cr0)
        circuit.measure(qr1, cr1)

        expected = QuantumCircuit(qr0, qr1, cr0, cr1)
        expected.h(qr0)
        expected.barrier(qr0, qr1)
        expected.measure(qr0, cr0)
        expected.measure(qr1, cr1)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_two_qregs_to_a_single_creg(self):
        """Two measurements in different qregs to the same creg
                                          |
        q0:--[H]--[m]------     q0:--[H]--|--[m]------
                   |                      |   |
        q1:--------|--[m]--  -> q1:-------|---|--[m]--
                   |   |                  |   |   |
        c0:--------.---|---     c0:-----------.---|---
           ------------.---        ---------------.---
        """
        qr0 = QuantumRegister(1, "q0")
        qr1 = QuantumRegister(1, "q1")
        cr0 = ClassicalRegister(2, "c0")

        circuit = QuantumCircuit(qr0, qr1, cr0)
        circuit.h(qr0)
        circuit.measure(qr0, cr0[0])
        circuit.measure(qr1, cr0[1])

        expected = QuantumCircuit(qr0, qr1, cr0)
        expected.h(qr0)
        expected.barrier(qr0, qr1)
        expected.measure(qr0, cr0[0])
        expected.measure(qr1, cr0[1])

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_preserve_measure_for_conditional(self):
        """Test barrier is inserted after any measurements used for conditionals

        q0:--[H]--[m]------------     q0:--[H]--[m]-------|-------
                   |                             |        |
        q1:--------|--[ z]--[m]--  -> q1:--------|--[ z]--|--[m]--
                   |    |    |                   |    |       |
        c0:--------.--[=1]---|---     c0:--------.--[=1]------|---
                             |                                |
        c1:------------------.---     c1:---------------------.---
        """
        qr0 = QuantumRegister(1, "q0")
        qr1 = QuantumRegister(1, "q1")
        cr0 = ClassicalRegister(1, "c0")
        cr1 = ClassicalRegister(1, "c1")
        circuit = QuantumCircuit(qr0, qr1, cr0, cr1)

        circuit.h(qr0)
        circuit.measure(qr0, cr0)
        with self.assertWarns(DeprecationWarning):
            circuit.z(qr1).c_if(cr0, 1)
        circuit.measure(qr1, cr1)

        expected = QuantumCircuit(qr0, qr1, cr0, cr1)
        expected.h(qr0)
        expected.measure(qr0, cr0)
        with self.assertWarns(DeprecationWarning):
            expected.z(qr1).c_if(cr0, 1)
        expected.barrier(qr0, qr1)
        expected.measure(qr1, cr1)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))


class TestBarrierBeforeMeasurementsWhenABarrierIsAlreadyThere(QiskitTestCase):
    """Tests the BarrierBeforeFinalMeasurements pass when there is a barrier already"""

    def test_handle_redundancy(self):
        """The pass is idempotent
            |                |
        q:--|-[m]--      q:--|-[m]---
            |  |     ->      |  |
        c:-----.---      c:-----.---
        """
        qr = QuantumRegister(1, "q")
        cr = ClassicalRegister(1, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.barrier(qr)
        circuit.measure(qr, cr)

        expected = QuantumCircuit(qr, cr)
        expected.barrier(qr)
        expected.measure(qr, cr)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_preserve_barriers_for_measurement_ordering(self):
        """If the circuit has a barrier to enforce a measurement order,
        preserve it in the output.

         q:---[m]--|-------     q:---|--[m]--|-------
           ----|---|--[m]--  ->   ---|---|---|--[m]--
               |       |                 |       |
         c:----.-------|---     c:-------.-------|---
           ------------.---       ---------------.---
        """
        qr = QuantumRegister(2, "q")
        cr = ClassicalRegister(2, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.measure(qr[0], cr[0])
        circuit.barrier(qr)
        circuit.measure(qr[1], cr[1])

        expected = QuantumCircuit(qr, cr)
        expected.barrier(qr)
        expected.measure(qr[0], cr[0])
        expected.barrier(qr)
        expected.measure(qr[1], cr[1])

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_measures_followed_by_barriers_should_be_final(self):
        """If a measurement is followed only by a barrier,
        insert the barrier before it.

         q:---[H]--|--[m]--|-------     q:---[H]--|--[m]-|-------
           ---[H]--|---|---|--[m]--  ->   ---[H]--|---|--|--[m]--
                       |       |                      |      |
         c:------------.-------|---     c:------------.------|---
           --------------------.---       -------------------.---
        """
        qr = QuantumRegister(2, "q")
        cr = ClassicalRegister(2, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.h(qr)
        circuit.barrier(qr)
        circuit.measure(qr[0], cr[0])
        circuit.barrier(qr)
        circuit.measure(qr[1], cr[1])

        expected = QuantumCircuit(qr, cr)
        expected.h(qr)
        expected.barrier(qr)
        expected.measure(qr[0], cr[0])
        expected.barrier(qr)
        expected.measure(qr[1], cr[1])

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_should_merge_with_smaller_duplicate_barrier(self):
        """If an equivalent barrier exists covering a subset of the qubits
        covered by the new barrier, it should be replaced.

         q:---|--[m]-------------     q:---|--[m]-------------
           ---|---|---[m]--------  ->   ---|---|---[m]--------
           -------|----|---[m]---       ---|---|----|---[m]---
                  |    |    |                  |    |    |
         c:-------.----|----|----     c:-------.----|----|----
           ------------.----|----       ------------.----|----
           -----------------.----       -----------------.----
        """
        qr = QuantumRegister(3, "q")
        cr = ClassicalRegister(3, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.barrier(qr[0], qr[1])
        circuit.measure(qr, cr)

        expected = QuantumCircuit(qr, cr)
        expected.barrier(qr)
        expected.measure(qr, cr)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_should_merge_with_larger_duplicate_barrier(self):
        """If a barrier exists and is stronger than the barrier to be inserted,
        preserve the existing barrier and do not insert a new barrier.

         q:---|--[m]--|-------     q:---|--[m]-|-------
           ---|---|---|--[m]--  ->   ---|---|--|--[m]--
           ---|---|---|---|---       ---|---|--|---|---
                  |       |                 |      |
         c:-------.-------|---     c:-------.------|---
           ---------------.---       --------------.---
           -------------------       ------------------
        """
        qr = QuantumRegister(3, "q")
        cr = ClassicalRegister(3, "c")

        circuit = QuantumCircuit(qr, cr)
        circuit.barrier(qr)
        circuit.measure(qr[0], cr[0])
        circuit.barrier(qr)
        circuit.measure(qr[1], cr[1])

        expected = circuit

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_barrier_doesnt_reorder_gates(self):
        """A barrier should not allow the reordering of gates, as pointed out in #2102

        q:--[p(0)]----------[m]---------      q:--[p(0)]-----------|--[m]---------
          --[p(1)]-----------|-[m]------  ->    --[p(1)]-----------|---|-[m]------
          --[p(2)]-|---------|--|-[m]----       --[p(2)]-|---------|---|--|-[m]----
          ---------|-[p(03)]-|--|--|-[m]-       ---------|-[p(03)]-|---|--|--|-[m]-
                             |  |  |  |                                |  |  |  |
        c:-------------------.--|--|--|-     c:------------------------.--|--|--|-
          ----------------------.--|--|-       ---------------------------.--|--|-
          -------------------------.--|-       ------------------------------.--|-
          ----------------------------.-       ---------------------------------.-

        """

        qr = QuantumRegister(4)
        cr = ClassicalRegister(4)
        circuit = QuantumCircuit(qr, cr)

        circuit.p(0, qr[0])
        circuit.p(1, qr[1])
        circuit.p(2, qr[2])
        circuit.barrier(qr[2], qr[3])
        circuit.p(3, qr[3])

        test_circuit = circuit.copy()
        test_circuit.measure(qr, cr)

        # expected circuit is the same, just with a barrier before the measurements
        expected = circuit.copy()
        expected.barrier(qr)
        expected.measure(qr, cr)

        pass_ = BarrierBeforeFinalMeasurements()
        result = pass_.run(circuit_to_dag(test_circuit))

        self.assertEqual(result, circuit_to_dag(expected))

    def test_conditioned_on_single_bit(self):
        """Test that the pass can handle cases where there is a loose-bit condition."""
        circuit = QuantumCircuit(QuantumRegister(3), ClassicalRegister(2), [Clbit()])
        circuit.h(range(3))
        circuit.measure(range(3), range(3))
        with self.assertWarns(DeprecationWarning):
            circuit.h(0).c_if(circuit.cregs[0], 3)
        with self.assertWarns(DeprecationWarning):
            circuit.h(1).c_if(circuit.clbits[-1], True)
        with self.assertWarns(DeprecationWarning):
            circuit.h(2).c_if(circuit.clbits[-1], False)
        circuit.measure(range(3), range(3))

        expected = circuit.copy_empty_like()
        expected.h(range(3))
        expected.measure(range(3), range(3))
        with self.assertWarns(DeprecationWarning):
            expected.h(0).c_if(expected.cregs[0], 3)
        with self.assertWarns(DeprecationWarning):
            expected.h(1).c_if(expected.clbits[-1], True)
        with self.assertWarns(DeprecationWarning):
            expected.h(2).c_if(expected.clbits[-1], False)
        expected.barrier(range(3))
        expected.measure(range(3), range(3))

        pass_ = BarrierBeforeFinalMeasurements()
        self.assertEqual(expected, pass_(circuit))

    def test_output_deterministic(self):
        """Test that the output barriers have a deterministic ordering (independent of
        PYTHONHASHSEED).  This is important to guarantee that any subsequent topological iterations
        through the circuit are also deterministic; it's in general not possible for all transpiler
        passes to produce identical outputs across all valid topological orderings, especially if
        those passes have some stochastic element."""
        measure_order = list(range(20))
        random.Random(2023_02_10).shuffle(measure_order)
        circuit = QuantumCircuit(20, 20)
        circuit.barrier([5, 2, 3])
        circuit.barrier([7, 11, 14, 2, 4])
        circuit.measure(measure_order, measure_order)

        # All the barriers should get merged together.
        expected = QuantumCircuit(20, 20)
        expected.barrier(range(20))
        expected.measure(measure_order, measure_order)

        output = BarrierBeforeFinalMeasurements()(circuit)
        self.assertEqual(expected, output)
        # This assertion is that the ordering of the arguments in the barrier is fixed.
        self.assertEqual(list(output.data[0].qubits), list(output.qubits))


if __name__ == "__main__":
    unittest.main()
