# This code is part of Qiskit.
#
# (C) Copyright IBM 2023
#
# 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.

# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring

import io
import math
import os
import pathlib
import pickle
import shutil
import tempfile
import unittest

import ddt

import qiskit.qasm2
from qiskit import qpy
from qiskit.circuit import (
    ClassicalRegister,
    Gate,
    Parameter,
    QuantumCircuit,
    QuantumRegister,
    Qubit,
    library as lib,
)
from qiskit.test import QiskitTestCase

from . import gate_builder


class TestEmpty(QiskitTestCase):
    def test_allows_empty(self):
        self.assertEqual(qiskit.qasm2.loads(""), QuantumCircuit())


class TestVersion(QiskitTestCase):
    def test_complete_version(self):
        program = "OPENQASM 2.0;"
        parsed = qiskit.qasm2.loads(program)
        self.assertEqual(parsed, QuantumCircuit())

    def test_incomplete_version(self):
        program = "OPENQASM 2;"
        parsed = qiskit.qasm2.loads(program)
        self.assertEqual(parsed, QuantumCircuit())

    def test_after_comment(self):
        program = """
            // hello, world
            OPENQASM 2.0;
            qreg q[2];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        self.assertEqual(parsed, qc)


class TestRegisters(QiskitTestCase):
    def test_qreg(self):
        program = "qreg q1[2]; qreg q2[1]; qreg q3[4];"
        parsed = qiskit.qasm2.loads(program)
        regs = [QuantumRegister(2, "q1"), QuantumRegister(1, "q2"), QuantumRegister(4, "q3")]
        self.assertEqual(list(parsed.qregs), regs)
        self.assertEqual(list(parsed.cregs), [])

    def test_creg(self):
        program = "creg c1[2]; creg c2[1]; creg c3[4];"
        parsed = qiskit.qasm2.loads(program)
        regs = [ClassicalRegister(2, "c1"), ClassicalRegister(1, "c2"), ClassicalRegister(4, "c3")]
        self.assertEqual(list(parsed.cregs), regs)
        self.assertEqual(list(parsed.qregs), [])

    def test_interleaved_registers(self):
        program = "qreg q1[3]; creg c1[2]; qreg q2[1]; creg c2[1];"
        parsed = qiskit.qasm2.loads(program)
        qregs = [QuantumRegister(3, "q1"), QuantumRegister(1, "q2")]
        cregs = [ClassicalRegister(2, "c1"), ClassicalRegister(1, "c2")]
        self.assertEqual(list(parsed.qregs), qregs)
        self.assertEqual(list(parsed.cregs), cregs)

    def test_registers_after_gate(self):
        program = "qreg before[2]; CX before[0], before[1]; qreg after[2]; CX after[0], after[1];"
        parsed = qiskit.qasm2.loads(program)
        before = QuantumRegister(2, "before")
        after = QuantumRegister(2, "after")
        qc = QuantumCircuit(before, after)
        qc.cx(before[0], before[1])
        qc.cx(after[0], after[1])
        self.assertEqual(parsed, qc)

    def test_empty_registers(self):
        program = "qreg q[0]; creg c[0];"
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(0, "q"), ClassicalRegister(0, "c"))
        self.assertEqual(parsed, qc)


@ddt.ddt
class TestGateApplication(QiskitTestCase):
    def test_builtin_single(self):
        program = """
            qreg q[2];
            U(0, 0, 0) q[0];
            CX q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.u(0, 0, 0, 0)
        qc.cx(0, 1)
        self.assertEqual(parsed, qc)

    def test_builtin_1q_broadcast(self):
        program = "qreg q[2]; U(0, 0, 0) q;"
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.u(0, 0, 0, 0)
        qc.u(0, 0, 0, 1)
        self.assertEqual(parsed, qc)

    def test_builtin_2q_broadcast(self):
        program = """
            qreg q1[2];
            qreg q2[2];
            CX q1[0], q2;
            barrier;
            CX q1, q2[1];
            barrier;
            CX q1, q2;
        """
        parsed = qiskit.qasm2.loads(program)
        q1 = QuantumRegister(2, "q1")
        q2 = QuantumRegister(2, "q2")
        qc = QuantumCircuit(q1, q2)
        qc.cx(q1[0], q2[0])
        qc.cx(q1[0], q2[1])
        qc.barrier()
        qc.cx(q1[0], q2[1])
        qc.cx(q1[1], q2[1])
        qc.barrier()
        qc.cx(q1[0], q2[0])
        qc.cx(q1[1], q2[1])
        self.assertEqual(parsed, qc)

    def test_3q_broadcast(self):
        program = """
            include "qelib1.inc";
            qreg q1[2];
            qreg q2[2];
            qreg q3[2];

            ccx q1, q2[0], q3[1];
            ccx q1[1], q2, q3[0];
            ccx q1[0], q2[1], q3;
            barrier;

            ccx q1, q2, q3[1];
            ccx q1[1], q2, q3;
            ccx q1, q2[1], q3;
            barrier;

            ccx q1, q2, q3;
        """
        parsed = qiskit.qasm2.loads(program)
        q1 = QuantumRegister(2, "q1")
        q2 = QuantumRegister(2, "q2")
        q3 = QuantumRegister(2, "q3")
        qc = QuantumCircuit(q1, q2, q3)
        qc.ccx(q1[0], q2[0], q3[1])
        qc.ccx(q1[1], q2[0], q3[1])
        qc.ccx(q1[1], q2[0], q3[0])
        qc.ccx(q1[1], q2[1], q3[0])
        qc.ccx(q1[0], q2[1], q3[0])
        qc.ccx(q1[0], q2[1], q3[1])
        qc.barrier()
        qc.ccx(q1[0], q2[0], q3[1])
        qc.ccx(q1[1], q2[1], q3[1])
        qc.ccx(q1[1], q2[0], q3[0])
        qc.ccx(q1[1], q2[1], q3[1])
        qc.ccx(q1[0], q2[1], q3[0])
        qc.ccx(q1[1], q2[1], q3[1])
        qc.barrier()
        qc.ccx(q1[0], q2[0], q3[0])
        qc.ccx(q1[1], q2[1], q3[1])
        self.assertEqual(parsed, qc)

    @ddt.data(True, False)
    def test_broadcast_against_empty_register(self, conditioned):
        cond = "if (cond == 0) " if conditioned else ""
        program = f"""
            OPENQASM 2;
            include "qelib1.inc";
            qreg q1[1];
            qreg q2[1];
            qreg empty1[0];
            qreg empty2[0];
            qreg empty3[0];
            creg cond[1];

            // None of the following statements should produce any gate applications.
            {cond}h empty1;

            {cond}cx q1[0], empty1;
            {cond}cx empty1, q2[0];
            {cond}cx empty1, empty2;

            {cond}ccx empty1, q1[0], q2[0];
            {cond}ccx q1[0], empty2, q2[0];
            {cond}ccx q1[0], q2[0], empty3;

            {cond}ccx empty1, empty2, q1[0];
            {cond}ccx empty1, q1[0], empty2;
            {cond}ccx q1[0], empty1, empty2;

            {cond}ccx empty1, empty2, empty3;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(
            QuantumRegister(1, "q1"),
            QuantumRegister(1, "q2"),
            QuantumRegister(0, "empty1"),
            QuantumRegister(0, "empty2"),
            QuantumRegister(0, "empty3"),
            ClassicalRegister(1, "cond"),
        )
        self.assertEqual(parsed, qc)

    def test_conditioned(self):
        program = """
            qreg q[2];
            creg cond[1];
            if (cond == 0) U(0, 0, 0) q[0];
            if (cond == 1) CX q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        cond = ClassicalRegister(1, "cond")
        qc = QuantumCircuit(QuantumRegister(2, "q"), cond)
        qc.u(0, 0, 0, 0).c_if(cond, 0)
        qc.cx(1, 0).c_if(cond, 1)
        self.assertEqual(parsed, qc)

    def test_conditioned_broadcast(self):
        program = """
            qreg q1[2];
            qreg q2[2];
            creg cond[1];
            if (cond == 0) U(0, 0, 0) q1;
            if (cond == 1) CX q1[0], q2;
        """
        parsed = qiskit.qasm2.loads(program)
        cond = ClassicalRegister(1, "cond")
        q1 = QuantumRegister(2, "q1")
        q2 = QuantumRegister(2, "q2")
        qc = QuantumCircuit(q1, q2, cond)
        qc.u(0, 0, 0, q1[0]).c_if(cond, 0)
        qc.u(0, 0, 0, q1[1]).c_if(cond, 0)
        qc.cx(q1[0], q2[0]).c_if(cond, 1)
        qc.cx(q1[0], q2[1]).c_if(cond, 1)
        self.assertEqual(parsed, qc)

    def test_constant_folding(self):
        # Most expression-related things are tested in `test_expression.py` instead.
        program = """
            qreg q[1];
            U(4 + 3 * 2 ^ 2, cos(pi) * (1 - ln(1)), 2 ^ 3 ^ 2) q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.u(16.0, -1.0, 512.0, 0)
        self.assertEqual(parsed, qc)

    def test_call_defined_gate(self):
        program = """
            gate my_gate a {
                U(0, 0, 0) a;
            }
            qreg q[2];
            my_gate q[0];
            my_gate q;
        """
        parsed = qiskit.qasm2.loads(program)
        my_gate_def = QuantumCircuit([Qubit()])
        my_gate_def.u(0, 0, 0, 0)
        my_gate = gate_builder("my_gate", [], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate(), [0])
        qc.append(my_gate(), [0])
        qc.append(my_gate(), [1])
        self.assertEqual(parsed, qc)

    def test_parameterless_gates_accept_parentheses(self):
        program = """
            qreg q[2];
            CX q[0], q[1];
            CX() q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.cx(0, 1)
        qc.cx(1, 0)
        self.assertEqual(parsed, qc)


class TestGateDefinition(QiskitTestCase):
    def test_simple_definition(self):
        program = """
            gate not_bell a, b {
                U(0, 0, 0) a;
                CX a, b;
            }
            qreg q[2];
            not_bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        not_bell_def = QuantumCircuit([Qubit(), Qubit()])
        not_bell_def.u(0, 0, 0, 0)
        not_bell_def.cx(0, 1)
        not_bell = gate_builder("not_bell", [], not_bell_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(not_bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_conditioned(self):
        program = """
            gate not_bell a, b {
                U(0, 0, 0) a;
                CX a, b;
            }
            qreg q[2];
            creg cond[1];
            if (cond == 0) not_bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        not_bell_def = QuantumCircuit([Qubit(), Qubit()])
        not_bell_def.u(0, 0, 0, 0)
        not_bell_def.cx(0, 1)
        not_bell = gate_builder("not_bell", [], not_bell_def)
        cond = ClassicalRegister(1, "cond")
        qc = QuantumCircuit(QuantumRegister(2, "q"), cond)
        qc.append(not_bell().c_if(cond, 0), [0, 1])
        self.assertEqual(parsed, qc)

    def test_constant_folding_in_definition(self):
        program = """
            gate bell a, b {
                U(pi/2, 0, pi) a;
                CX a, b;
            }
            qreg q[2];
            bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.u(math.pi / 2, 0, math.pi, 0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_parameterised_gate(self):
        # Most of the tests of deep parameter expressions are in `test_expression.py`.
        program = """
            gate my_gate(a, b) c {
                U(a, b, a + 2 * b) c;
            }
            qreg q[1];
            my_gate(0.25, 0.5) q[0];
            my_gate(0.5, 0.25) q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        a, b = Parameter("a"), Parameter("b")
        my_gate_def = QuantumCircuit([Qubit()])
        my_gate_def.u(a, b, a + 2 * b, 0)
        my_gate = gate_builder("my_gate", [a, b], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(my_gate(0.25, 0.5), [0])
        qc.append(my_gate(0.5, 0.25), [0])
        self.assertEqual(parsed, qc)

        # Also check the decomposition has come out exactly as expected.  The floating-point
        # assertions are safe as exact equality checks because there are no lossy operations with
        # these parameters, and the answer should be exact.
        decomposed = qc.decompose()
        self.assertEqual(decomposed.data[0].operation.name, "u")
        self.assertEqual(list(decomposed.data[0].operation.params), [0.25, 0.5, 1.25])
        self.assertEqual(decomposed.data[1].operation.name, "u")
        self.assertEqual(list(decomposed.data[1].operation.params), [0.5, 0.25, 1.0])

    def test_parameterless_gate_with_parentheses(self):
        program = """
            gate my_gate() a {
                U(0, 0, 0) a;
            }
            qreg q[1];
            my_gate q[0];
            my_gate() q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        my_gate_def = QuantumCircuit([Qubit()])
        my_gate_def.u(0, 0, 0, 0)
        my_gate = gate_builder("my_gate", [], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(my_gate(), [0])
        qc.append(my_gate(), [0])
        self.assertEqual(parsed, qc)

    def test_access_includes_in_definition(self):
        program = """
            include "qelib1.inc";
            gate bell a, b {
                h a;
                cx a, b;
            }
            qreg q[2];
            bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.h(0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_access_previous_defined_gate(self):
        program = """
            include "qelib1.inc";
            gate bell a, b {
                h a;
                cx a, b;
            }
            gate second_bell a, b {
                bell b, a;
            }
            qreg q[2];
            second_bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.h(0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)

        second_bell_def = QuantumCircuit([Qubit(), Qubit()])
        second_bell_def.append(bell(), [1, 0])
        second_bell = gate_builder("second_bell", [], second_bell_def)

        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(second_bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_qubits_lookup_differently_to_gates(self):
        # The spec is somewhat unclear on this, and this leads to super weird text, but it's
        # technically unambiguously resolvable and this is more permissive.
        program = """
            include "qelib1.inc";
            gate bell h, cx {
                h h;
                cx h, cx;
            }
            qreg q[2];
            bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.h(0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_parameters_lookup_differently_to_gates(self):
        # The spec is somewhat unclear on this, and this leads to super weird text, but it's
        # technically unambiguously resolvable and this is more permissive.
        program = """
            include "qelib1.inc";
            gate shadow(rx, rz) a {
                rz(rz) a;
                rx(rx) a;
            }
            qreg q[1];
            shadow(0.5, 2.0) q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        rx, rz = Parameter("rx"), Parameter("rz")
        shadow_def = QuantumCircuit([Qubit()])
        shadow_def.rz(rz, 0)
        shadow_def.rx(rx, 0)
        shadow = gate_builder("shadow", [rx, rz], shadow_def)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(shadow(0.5, 2.0), [0])
        self.assertEqual(parsed, qc)

    def test_unused_parameters_convert_correctly(self):
        # The main risk here is that there might be lazy application in the gate definition
        # bindings, and we might accidentally try and bind parameters that aren't actually in the
        # definition.
        program = """
            gate my_gate(p) q {
                U(0, 0, 0) q;
            }
            qreg q[1];
            my_gate(0.5) q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        # No top-level circuit equality test here, because all the internals of gate application are
        # an implementation detail, and we don't want to tie the tests and implementation together
        # too closely.
        self.assertEqual(list(parsed.qregs), [QuantumRegister(1, "q")])
        self.assertEqual(list(parsed.cregs), [])
        self.assertEqual(len(parsed.data), 1)
        self.assertEqual(parsed.data[0].qubits, (parsed.qubits[0],))
        self.assertEqual(parsed.data[0].clbits, ())
        self.assertEqual(parsed.data[0].operation.name, "my_gate")
        self.assertEqual(list(parsed.data[0].operation.params), [0.5])

        decomposed = QuantumCircuit(QuantumRegister(1, "q"))
        decomposed.u(0, 0, 0, 0)
        self.assertEqual(parsed.decompose(), decomposed)

    def test_qubit_barrier_in_definition(self):
        program = """
            gate my_gate a, b {
                barrier a;
                barrier b;
                barrier a, b;
            }
            qreg q[2];
            my_gate q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        my_gate_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate_def.barrier(0)
        my_gate_def.barrier(1)
        my_gate_def.barrier([0, 1])
        my_gate = gate_builder("my_gate", [], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_bare_barrier_in_definition(self):
        program = """
            gate my_gate a, b {
                barrier;
            }
            qreg q[2];
            my_gate q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        my_gate_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate_def.barrier(my_gate_def.qubits)
        my_gate = gate_builder("my_gate", [], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_duplicate_barrier_in_definition(self):
        program = """
            gate my_gate a, b {
                barrier a, a;
                barrier b, a, b;
            }
            qreg q[2];
            my_gate q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        my_gate_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate_def.barrier(0)
        my_gate_def.barrier([1, 0])
        my_gate = gate_builder("my_gate", [], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_pickleable(self):
        program = """
            include "qelib1.inc";
            gate my_gate(a) b, c {
                rz(2 * a) b;
                h b;
                cx b, c;
            }
            qreg q[2];
            my_gate(0.5) q[0], q[1];
            my_gate(0.25) q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        a = Parameter("a")
        my_gate_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate_def.rz(2 * a, 0)
        my_gate_def.h(0)
        my_gate_def.cx(0, 1)
        my_gate = gate_builder("my_gate", [a], my_gate_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate(0.5), [0, 1])
        qc.append(my_gate(0.25), [1, 0])
        self.assertEqual(parsed, qc)
        with io.BytesIO() as fptr:
            pickle.dump(parsed, fptr)
            fptr.seek(0)
            loaded = pickle.load(fptr)
        self.assertEqual(parsed, loaded)

    def test_qpy_single_call_roundtrip(self):
        program = """
            include "qelib1.inc";
            gate my_gate(a) b, c {
                rz(2 * a) b;
                h b;
                cx b, c;
            }
            qreg q[2];
            my_gate(0.5) q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)

        # QPY won't persist custom gates by design choice, so instead let us check against the
        # explicit form it uses.
        my_gate_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate_def.rz(1.0, 0)
        my_gate_def.h(0)
        my_gate_def.cx(0, 1)
        my_gate = Gate("my_gate", 2, [0.5])
        my_gate.definition = my_gate_def
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate, [0, 1])

        with io.BytesIO() as fptr:
            qpy.dump(parsed, fptr)
            fptr.seek(0)
            loaded = qpy.load(fptr)[0]
        self.assertEqual(loaded, qc)

    # See https://github.com/Qiskit/qiskit-terra/issues/8941
    @unittest.expectedFailure
    def test_qpy_double_call_roundtrip(self):
        program = """
            include "qelib1.inc";
            gate my_gate(a) b, c {
                rz(2 * a) b;
                h b;
                cx b, c;
            }
            qreg q[2];
            my_gate(0.5) q[0], q[1];
            my_gate(0.25) q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)

        my_gate1_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate1_def.rz(1.0, 0)
        my_gate1_def.h(0)
        my_gate1_def.cx(0, 1)
        my_gate1 = Gate("my_gate", 2, [0.5])
        my_gate1.definition = my_gate1_def

        my_gate2_def = QuantumCircuit([Qubit(), Qubit()])
        my_gate2_def.rz(0.5, 0)
        my_gate2_def.h(0)
        my_gate2_def.cx(0, 1)
        my_gate2 = Gate("my_gate", 2, [0.25])
        my_gate2.definition = my_gate2_def

        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(my_gate1, [0, 1])
        qc.append(my_gate2, [1, 0])

        with io.BytesIO() as fptr:
            qpy.dump(parsed, fptr)
            fptr.seek(0)
            loaded = qpy.load(fptr)[0]
        self.assertEqual(loaded, qc)


class TestOpaque(QiskitTestCase):
    def test_simple(self):
        program = """
            opaque my_gate a;
            opaque my_gate2() a;
            qreg q[2];
            my_gate q[0];
            my_gate() q[1];
            my_gate2 q[0];
            my_gate2() q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(Gate("my_gate", 1, []), [0])
        qc.append(Gate("my_gate", 1, []), [1])
        qc.append(Gate("my_gate2", 1, []), [0])
        qc.append(Gate("my_gate2", 1, []), [1])
        self.assertEqual(parsed, qc)

    def test_parameterised(self):
        program = """
            opaque my_gate(a, b) c, d;
            qreg q[2];
            my_gate(0.5, 0.25) q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(Gate("my_gate", 2, [0.5, 0.25]), [1, 0])
        self.assertEqual(parsed, qc)


class TestBarrier(QiskitTestCase):
    def test_single_register_argument(self):
        program = """
            qreg first[3];
            qreg second[3];
            barrier first;
            barrier second;
        """
        parsed = qiskit.qasm2.loads(program)
        first = QuantumRegister(3, "first")
        second = QuantumRegister(3, "second")
        qc = QuantumCircuit(first, second)
        qc.barrier(first)
        qc.barrier(second)
        self.assertEqual(parsed, qc)

    def test_single_qubit_argument(self):
        program = """
            qreg first[3];
            qreg second[3];
            barrier first[1];
            barrier second[0];
        """
        parsed = qiskit.qasm2.loads(program)
        first = QuantumRegister(3, "first")
        second = QuantumRegister(3, "second")
        qc = QuantumCircuit(first, second)
        qc.barrier(first[1])
        qc.barrier(second[0])
        self.assertEqual(parsed, qc)

    def test_empty_circuit_empty_arguments(self):
        program = "barrier;"
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit()
        self.assertEqual(parsed, qc)

    def test_one_register_circuit_empty_arguments(self):
        program = "qreg q1[2]; barrier;"
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q1"))
        qc.barrier(qc.qubits)
        self.assertEqual(parsed, qc)

    def test_multi_register_circuit_empty_arguments(self):
        program = "qreg q1[2]; qreg q2[3]; qreg q3[1]; barrier;"
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(
            QuantumRegister(2, "q1"), QuantumRegister(3, "q2"), QuantumRegister(1, "q3")
        )
        qc.barrier(qc.qubits)
        self.assertEqual(parsed, qc)

    def test_include_empty_register(self):
        program = """
            qreg q[2];
            qreg empty[0];
            barrier empty;
            barrier q, empty;
            barrier;
        """
        parsed = qiskit.qasm2.loads(program)
        q = QuantumRegister(2, "q")
        qc = QuantumCircuit(q, QuantumRegister(0, "empty"))
        qc.barrier(q)
        qc.barrier(qc.qubits)
        self.assertEqual(parsed, qc)

    def test_allows_duplicate_arguments(self):
        # There's nothing in the paper that implies this should be forbidden.
        program = """
            qreg q1[3];
            qreg q2[2];
            barrier q1, q1;
            barrier q1[0], q1;
            barrier q1, q1[0];
            barrier q1, q2, q1;
        """
        parsed = qiskit.qasm2.loads(program)
        q1 = QuantumRegister(3, "q1")
        q2 = QuantumRegister(2, "q2")
        qc = QuantumCircuit(q1, q2)
        qc.barrier(q1)
        qc.barrier(q1)
        qc.barrier(q1)
        qc.barrier(q1, q2)
        self.assertEqual(parsed, qc)


class TestMeasure(QiskitTestCase):
    def test_single(self):
        program = """
            qreg q[1];
            creg c[1];
            measure q[0] -> c[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(1, "q"), ClassicalRegister(1, "c"))
        qc.measure(0, 0)
        self.assertEqual(parsed, qc)

    def test_broadcast(self):
        program = """
            qreg q[2];
            creg c[2];
            measure q -> c;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c"))
        qc.measure(0, 0)
        qc.measure(1, 1)
        self.assertEqual(parsed, qc)

    def test_conditioned(self):
        program = """
            qreg q[2];
            creg c[2];
            creg cond[1];
            if (cond == 0) measure q[0] -> c[0];
            if (cond == 1) measure q -> c;
        """
        parsed = qiskit.qasm2.loads(program)
        cond = ClassicalRegister(1, "cond")
        qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c"), cond)
        qc.measure(0, 0).c_if(cond, 0)
        qc.measure(0, 0).c_if(cond, 1)
        qc.measure(1, 1).c_if(cond, 1)
        self.assertEqual(parsed, qc)

    def test_broadcast_against_empty_register(self):
        program = """
            qreg q_empty[0];
            creg c_empty[0];
            measure q_empty -> c_empty;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(0, "q_empty"), ClassicalRegister(0, "c_empty"))
        self.assertEqual(parsed, qc)

    def test_conditioned_broadcast_against_empty_register(self):
        program = """
            qreg q_empty[0];
            creg c_empty[0];
            creg cond[1];
            if (cond == 0) measure q_empty -> c_empty;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(
            QuantumRegister(0, "q_empty"),
            ClassicalRegister(0, "c_empty"),
            ClassicalRegister(1, "cond"),
        )
        self.assertEqual(parsed, qc)


class TestReset(QiskitTestCase):
    def test_single(self):
        program = """
            qreg q[1];
            reset q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.reset(0)
        self.assertEqual(parsed, qc)

    def test_broadcast(self):
        program = """
            qreg q[2];
            reset q;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.reset(0)
        qc.reset(1)
        self.assertEqual(parsed, qc)

    def test_conditioned(self):
        program = """
            qreg q[2];
            creg cond[1];
            if (cond == 0) reset q[0];
            if (cond == 1) reset q;
        """
        parsed = qiskit.qasm2.loads(program)
        cond = ClassicalRegister(1, "cond")
        qc = QuantumCircuit(QuantumRegister(2, "q"), cond)
        qc.reset(0).c_if(cond, 0)
        qc.reset(0).c_if(cond, 1)
        qc.reset(1).c_if(cond, 1)
        self.assertEqual(parsed, qc)

    def test_broadcast_against_empty_register(self):
        program = """
            qreg empty[0];
            reset empty;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(0, "empty"))
        self.assertEqual(parsed, qc)

    def test_conditioned_broadcast_against_empty_register(self):
        program = """
            qreg empty[0];
            creg cond[1];
            if (cond == 0) reset empty;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(0, "empty"), ClassicalRegister(1, "cond"))
        self.assertEqual(parsed, qc)


class TestInclude(QiskitTestCase):
    def setUp(self):
        super().setUp()
        self.tmp_dir = pathlib.Path(tempfile.mkdtemp())

    def tearDown(self):
        # Doesn't really matter if the removal fails, since this was a tempdir anyway; it'll get
        # cleaned up by the OS at some point.
        shutil.rmtree(self.tmp_dir, ignore_errors=True)
        super().tearDown()

    def test_qelib1_include(self):
        program = """
            include "qelib1.inc";
            qreg q[3];
            u3(0.5, 0.25, 0.125) q[0];
            u2(0.5, 0.25) q[0];
            u1(0.5) q[0];
            cx q[0], q[1];
            id q[0];
            x q[0];
            y q[0];
            z q[0];
            h q[0];
            s q[0];
            sdg q[0];
            t q[0];
            tdg q[0];
            rx(0.5) q[0];
            ry(0.5) q[0];
            rz(0.5) q[0];
            cz q[0], q[1];
            cy q[0], q[1];
            ch q[0], q[1];
            ccx q[0], q[1], q[2];
            crz(0.5) q[0], q[1];
            cu1(0.5) q[0], q[1];
            cu3(0.5, 0.25, 0.125) q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(3, "q"))
        qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0])
        qc.append(lib.U2Gate(0.5, 0.25), [0])
        qc.append(lib.U1Gate(0.5), [0])
        qc.append(lib.CXGate(), [0, 1])
        qc.append(lib.UGate(0, 0, 0), [0])  # Stand-in for id.
        qc.append(lib.XGate(), [0])
        qc.append(lib.YGate(), [0])
        qc.append(lib.ZGate(), [0])
        qc.append(lib.HGate(), [0])
        qc.append(lib.SGate(), [0])
        qc.append(lib.SdgGate(), [0])
        qc.append(lib.TGate(), [0])
        qc.append(lib.TdgGate(), [0])
        qc.append(lib.RXGate(0.5), [0])
        qc.append(lib.RYGate(0.5), [0])
        qc.append(lib.RZGate(0.5), [0])
        qc.append(lib.CZGate(), [0, 1])
        qc.append(lib.CYGate(), [0, 1])
        qc.append(lib.CHGate(), [0, 1])
        qc.append(lib.CCXGate(), [0, 1, 2])
        qc.append(lib.CRZGate(0.5), [0, 1])
        qc.append(lib.CU1Gate(0.5), [0, 1])
        qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1])
        self.assertEqual(parsed, qc)

    def test_qelib1_after_gate_definition(self):
        program = """
            gate bell a, b {
                U(pi/2, 0, pi) a;
                CX a, b;
            }
            include "qelib1.inc";
            qreg q[2];
            bell q[0], q[1];
            rx(0.5) q[0];
            bell q[1], q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.u(math.pi / 2, 0, math.pi, 0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)

        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        qc.rx(0.5, 0)
        qc.append(bell(), [1, 0])
        self.assertEqual(parsed, qc)

    def test_include_can_define_version(self):
        include = """
            OPENQASM 2.0;
            qreg inner_q[2];
        """
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write(include)
        program = """
            OPENQASM 2.0;
            include "include.qasm";
        """
        parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,))
        qc = QuantumCircuit(QuantumRegister(2, "inner_q"))
        self.assertEqual(parsed, qc)

    def test_can_define_gates(self):
        include = """
            gate bell a, b {
                h a;
                cx a, b;
            }
        """
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write(include)
        program = """
            OPENQASM 2.0;
            include "qelib1.inc";
            include "include.qasm";
            qreg q[2];
            bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,))
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.h(0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)

        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_nested_include(self):
        inner = "creg c[2];"
        with open(self.tmp_dir / "inner.qasm", "w") as fp:
            fp.write(inner)
        outer = """
            qreg q[2];
            include "inner.qasm";
        """
        with open(self.tmp_dir / "outer.qasm", "w") as fp:
            fp.write(outer)
        program = """
            OPENQASM 2.0;
            include "outer.qasm";
        """
        parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,))
        qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c"))
        self.assertEqual(parsed, qc)

    def test_first_hit_is_used(self):
        empty = self.tmp_dir / "empty"
        empty.mkdir()
        first = self.tmp_dir / "first"
        first.mkdir()
        with open(first / "include.qasm", "w") as fp:
            fp.write("qreg q[1];")
        second = self.tmp_dir / "second"
        second.mkdir()
        with open(second / "include.qasm", "w") as fp:
            fp.write("qreg q[2];")
        program = 'include "include.qasm";'
        parsed = qiskit.qasm2.loads(program, include_path=(empty, first, second))
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        self.assertEqual(parsed, qc)

    def test_qelib1_ignores_search_path(self):
        with open(self.tmp_dir / "qelib1.inc", "w") as fp:
            fp.write("qreg not_used[2];")
        program = 'include "qelib1.inc";'
        parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,))
        qc = QuantumCircuit()
        self.assertEqual(parsed, qc)

    def test_include_from_current_directory(self):
        include = """
            qreg q[2];
        """
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write(include)
        program = """
            OPENQASM 2.0;
            include "include.qasm";
        """
        prevdir = os.getcwd()
        os.chdir(self.tmp_dir)
        try:
            parsed = qiskit.qasm2.loads(program)
            qc = QuantumCircuit(QuantumRegister(2, "q"))
            self.assertEqual(parsed, qc)
        finally:
            os.chdir(prevdir)

    def test_load_searches_source_directory(self):
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write("qreg q[2];")
        program = 'include "include.qasm";'
        with open(self.tmp_dir / "program.qasm", "w") as fp:
            fp.write(program)
        parsed = qiskit.qasm2.load(self.tmp_dir / "program.qasm")
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        self.assertEqual(parsed, qc)

    def test_load_searches_source_directory_last(self):
        first = self.tmp_dir / "first"
        first.mkdir()
        with open(first / "include.qasm", "w") as fp:
            fp.write("qreg q[2];")
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write("qreg not_used[2];")
        program = 'include "include.qasm";'
        with open(self.tmp_dir / "program.qasm", "w") as fp:
            fp.write(program)
        parsed = qiskit.qasm2.load(self.tmp_dir / "program.qasm", include_path=(first,))
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        self.assertEqual(parsed, qc)

    def test_load_searches_source_directory_prepend(self):
        first = self.tmp_dir / "first"
        first.mkdir()
        with open(first / "include.qasm", "w") as fp:
            fp.write("qreg not_used[2];")
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write("qreg q[2];")
        program = 'include "include.qasm";'
        with open(self.tmp_dir / "program.qasm", "w") as fp:
            fp.write(program)
        parsed = qiskit.qasm2.load(
            self.tmp_dir / "program.qasm", include_path=(first,), include_input_directory="prepend"
        )
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        self.assertEqual(parsed, qc)

    def test_load_can_ignore_source_directory(self):
        with open(self.tmp_dir / "include.qasm", "w") as fp:
            fp.write("qreg q[2];")
        program = 'include "include.qasm";'
        with open(self.tmp_dir / "program.qasm", "w") as fp:
            fp.write(program)
        with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "unable to find 'include.qasm'"):
            qiskit.qasm2.load(self.tmp_dir / "program.qasm", include_input_directory=None)


@ddt.ddt
class TestCustomInstructions(QiskitTestCase):
    def test_qelib1_include_overridden(self):
        program = """
            OPENQASM 2.0;
            include "qelib1.inc";
            qreg q[3];
            u3(0.5, 0.25, 0.125) q[0];
            u2(0.5, 0.25) q[0];
            u1(0.5) q[0];
            cx q[0], q[1];
            id q[0];
            x q[0];
            y q[0];
            z q[0];
            h q[0];
            s q[0];
            sdg q[0];
            t q[0];
            tdg q[0];
            rx(0.5) q[0];
            ry(0.5) q[0];
            rz(0.5) q[0];
            cz q[0], q[1];
            cy q[0], q[1];
            ch q[0], q[1];
            ccx q[0], q[1], q[2];
            crz(0.5) q[0], q[1];
            cu1(0.5) q[0], q[1];
            cu3(0.5, 0.25, 0.125) q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        qc = QuantumCircuit(QuantumRegister(3, "q"))
        qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0])
        qc.append(lib.U2Gate(0.5, 0.25), [0])
        qc.append(lib.U1Gate(0.5), [0])
        qc.append(lib.CXGate(), [0, 1])
        qc.append(lib.IGate(), [0])
        qc.append(lib.XGate(), [0])
        qc.append(lib.YGate(), [0])
        qc.append(lib.ZGate(), [0])
        qc.append(lib.HGate(), [0])
        qc.append(lib.SGate(), [0])
        qc.append(lib.SdgGate(), [0])
        qc.append(lib.TGate(), [0])
        qc.append(lib.TdgGate(), [0])
        qc.append(lib.RXGate(0.5), [0])
        qc.append(lib.RYGate(0.5), [0])
        qc.append(lib.RZGate(0.5), [0])
        qc.append(lib.CZGate(), [0, 1])
        qc.append(lib.CYGate(), [0, 1])
        qc.append(lib.CHGate(), [0, 1])
        qc.append(lib.CCXGate(), [0, 1, 2])
        qc.append(lib.CRZGate(0.5), [0, 1])
        qc.append(lib.CU1Gate(0.5), [0, 1])
        qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1])
        self.assertEqual(parsed, qc)

        # Also test that the output matches what Qiskit puts out.
        from_qiskit = QuantumCircuit.from_qasm_str(program)
        self.assertEqual(parsed, from_qiskit)

    def test_qelib1_sparse_overrides(self):
        """Test that the qelib1 special import still works as expected when a couple of gates in the
        middle of it are custom.  As long as qelib1 is handled specially, there is a risk that this
        handling will break in weird ways when custom instructions overlap it."""
        program = """
            include "qelib1.inc";
            qreg q[3];
            u3(0.5, 0.25, 0.125) q[0];
            u2(0.5, 0.25) q[0];
            u1(0.5) q[0];
            cx q[0], q[1];
            id q[0];
            x q[0];
            y q[0];
            z q[0];
            h q[0];
            s q[0];
            sdg q[0];
            t q[0];
            tdg q[0];
            rx(0.5) q[0];
            ry(0.5) q[0];
            rz(0.5) q[0];
            cz q[0], q[1];
            cy q[0], q[1];
            ch q[0], q[1];
            ccx q[0], q[1], q[2];
            crz(0.5) q[0], q[1];
            cu1(0.5) q[0], q[1];
            cu3(0.5, 0.25, 0.125) q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[
                qiskit.qasm2.CustomInstruction("id", 0, 1, lib.IGate),
                qiskit.qasm2.CustomInstruction("h", 0, 1, lib.HGate),
                qiskit.qasm2.CustomInstruction("crz", 1, 2, lib.CRZGate),
            ],
        )
        qc = QuantumCircuit(QuantumRegister(3, "q"))
        qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0])
        qc.append(lib.U2Gate(0.5, 0.25), [0])
        qc.append(lib.U1Gate(0.5), [0])
        qc.append(lib.CXGate(), [0, 1])
        qc.append(lib.IGate(), [0])
        qc.append(lib.XGate(), [0])
        qc.append(lib.YGate(), [0])
        qc.append(lib.ZGate(), [0])
        qc.append(lib.HGate(), [0])
        qc.append(lib.SGate(), [0])
        qc.append(lib.SdgGate(), [0])
        qc.append(lib.TGate(), [0])
        qc.append(lib.TdgGate(), [0])
        qc.append(lib.RXGate(0.5), [0])
        qc.append(lib.RYGate(0.5), [0])
        qc.append(lib.RZGate(0.5), [0])
        qc.append(lib.CZGate(), [0, 1])
        qc.append(lib.CYGate(), [0, 1])
        qc.append(lib.CHGate(), [0, 1])
        qc.append(lib.CCXGate(), [0, 1, 2])
        qc.append(lib.CRZGate(0.5), [0, 1])
        qc.append(lib.CU1Gate(0.5), [0, 1])
        qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1])
        self.assertEqual(parsed, qc)

    def test_user_gate_after_overidden_qelib1(self):
        program = """
            include "qelib1.inc";
            qreg q[1];
            opaque my_gate q;
            my_gate q[0];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(Gate("my_gate", 1, []), [0])
        self.assertEqual(parsed, qc)

    def test_qiskit_extra_builtins(self):
        program = """
            qreg q[5];
            u(0.5 ,0.25, 0.125) q[0];
            p(0.5) q[0];
            sx q[0];
            sxdg q[0];
            swap q[0], q[1];
            cswap q[0], q[1], q[2];
            crx(0.5) q[0], q[1];
            cry(0.5) q[0], q[1];
            cp(0.5) q[0], q[1];
            csx q[0], q[1];
            cu(0.5, 0.25, 0.125, 0.0625) q[0], q[1];
            rxx(0.5) q[0], q[1];
            rzz(0.5) q[0], q[1];
            rccx q[0], q[1], q[2];
            rc3x q[0], q[1], q[2], q[3];
            c3x q[0], q[1], q[2], q[3];
            c3sqrtx q[0], q[1], q[2], q[3];
            c4x q[0], q[1], q[2], q[3], q[4];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        qc = QuantumCircuit(QuantumRegister(5, "q"))
        qc.append(lib.UGate(0.5, 0.25, 0.125), [0])
        qc.append(lib.PhaseGate(0.5), [0])
        qc.append(lib.SXGate(), [0])
        qc.append(lib.SXdgGate(), [0])
        qc.append(lib.SwapGate(), [0, 1])
        qc.append(lib.CSwapGate(), [0, 1, 2])
        qc.append(lib.CRXGate(0.5), [0, 1])
        qc.append(lib.CRYGate(0.5), [0, 1])
        qc.append(lib.CPhaseGate(0.5), [0, 1])
        qc.append(lib.CSXGate(), [0, 1])
        qc.append(lib.CUGate(0.5, 0.25, 0.125, 0.0625), [0, 1])
        qc.append(lib.RXXGate(0.5), [0, 1])
        qc.append(lib.RZZGate(0.5), [0, 1])
        qc.append(lib.RCCXGate(), [0, 1, 2])
        qc.append(lib.RC3XGate(), [0, 1, 2, 3])
        qc.append(lib.C3XGate(), [0, 1, 2, 3])
        qc.append(lib.C3SXGate(), [0, 1, 2, 3])
        qc.append(lib.C4XGate(), [0, 1, 2, 3, 4])
        self.assertEqual(parsed, qc)

        # There's also the 'u0' gate, but this is weird so we don't wildly care what its definition
        # is and it has no Qiskit equivalent, so we'll just test that it using it doesn't produce an
        # error.
        parsed = qiskit.qasm2.loads(
            "qreg q[1]; u0(1) q[0];", custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        self.assertEqual(parsed.data[0].operation.name, "u0")

    def test_qiskit_override_delay_opaque(self):
        program = """
            opaque delay(t) q;
            qreg q[1];
            delay(1) q[0];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.delay(1, 0, unit="dt")
        self.assertEqual(parsed, qc)

    def test_qiskit_override_u0_opaque(self):
        program = """
            opaque u0(n) q;
            qreg q[1];
            u0(2) q[0];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.id(0)
        qc.id(0)
        self.assertEqual(parsed.decompose(), qc)

    def test_can_override_u(self):
        program = """
            qreg q[1];
            U(0.5, 0.25, 0.125) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a, b, c):
                super().__init__("u", 1, [a, b, c])

        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[qiskit.qasm2.CustomInstruction("U", 3, 1, MyGate, builtin=True)],
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5, 0.25, 0.125), [0])
        self.assertEqual(parsed, qc)

    def test_can_override_cx(self):
        program = """
            qreg q[2];
            CX q[0], q[1];
        """

        class MyGate(Gate):
            def __init__(self):
                super().__init__("cx", 2, [])

        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[qiskit.qasm2.CustomInstruction("CX", 0, 2, MyGate, builtin=True)],
        )
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(MyGate(), [0, 1])
        self.assertEqual(parsed, qc)

    @ddt.data(lambda x: x, reversed)
    def test_can_override_both_builtins_with_other_gates(self, order):
        program = """
            gate unimportant q {}
            qreg q[2];
            U(0.5, 0.25, 0.125) q[0];
            CX q[0], q[1];
        """

        class MyUGate(Gate):
            def __init__(self, a, b, c):
                super().__init__("u", 1, [a, b, c])

        class MyCXGate(Gate):
            def __init__(self):
                super().__init__("cx", 2, [])

        custom = [
            qiskit.qasm2.CustomInstruction("unused", 0, 1, lambda: Gate("unused", 1, [])),
            qiskit.qasm2.CustomInstruction("U", 3, 1, MyUGate, builtin=True),
            qiskit.qasm2.CustomInstruction("CX", 0, 2, MyCXGate, builtin=True),
        ]
        custom = order(custom)
        parsed = qiskit.qasm2.loads(program, custom_instructions=custom)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(MyUGate(0.5, 0.25, 0.125), [0])
        qc.append(MyCXGate(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_custom_builtin_gate(self):
        program = """
            qreg q[1];
            builtin(0.5) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a):
                super().__init__("builtin", 1, [a])

        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[
                qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True)
            ],
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5), [0])
        self.assertEqual(parsed, qc)

    def test_can_define_builtin_as_gate(self):
        program = """
            qreg q[1];
            gate builtin(t) q {}
            builtin(0.5) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a):
                super().__init__("builtin", 1, [a])

        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[
                qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True)
            ],
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5), [0])
        self.assertEqual(parsed, qc)

    def test_can_define_builtin_as_opaque(self):
        program = """
            qreg q[1];
            opaque builtin(t) q;
            builtin(0.5) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a):
                super().__init__("builtin", 1, [a])

        parsed = qiskit.qasm2.loads(
            program,
            custom_instructions=[
                qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True)
            ],
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5), [0])
        self.assertEqual(parsed, qc)

    def test_can_define_custom_as_gate(self):
        program = """
            qreg q[1];
            gate my_gate(t) q {}
            my_gate(0.5) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a):
                super().__init__("my_gate", 1, [a])

        parsed = qiskit.qasm2.loads(
            program, custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 1, 1, MyGate)]
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5), [0])
        self.assertEqual(parsed, qc)

    def test_can_define_custom_as_opaque(self):
        program = """
            qreg q[1];
            opaque my_gate(t) q;
            my_gate(0.5) q[0];
        """

        class MyGate(Gate):
            def __init__(self, a):
                super().__init__("my_gate", 1, [a])

        parsed = qiskit.qasm2.loads(
            program, custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 1, 1, MyGate)]
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.append(MyGate(0.5), [0])
        self.assertEqual(parsed, qc)


class TestCustomClassical(QiskitTestCase):
    def test_qiskit_extensions(self):
        program = """
            include "qelib1.inc";
            qreg q[1];
            rx(asin(0.3)) q[0];
            ry(acos(0.3)) q[0];
            rz(atan(0.3)) q[0];
        """
        parsed = qiskit.qasm2.loads(program, custom_classical=qiskit.qasm2.LEGACY_CUSTOM_CLASSICAL)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.rx(math.asin(0.3), 0)
        qc.ry(math.acos(0.3), 0)
        qc.rz(math.atan(0.3), 0)
        self.assertEqual(parsed, qc)

    def test_zero_parameter_custom(self):
        program = """
            qreg q[1];
            U(f(), 0, 0) q[0];
        """
        parsed = qiskit.qasm2.loads(
            program, custom_classical=[qiskit.qasm2.CustomClassical("f", 0, lambda: 0.2)]
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.u(0.2, 0, 0, 0)
        self.assertEqual(parsed, qc)

    def test_multi_parameter_custom(self):
        program = """
            qreg q[1];
            U(f(0.2), g(0.4, 0.1), h(1, 2, 3)) q[0];
        """
        parsed = qiskit.qasm2.loads(
            program,
            custom_classical=[
                qiskit.qasm2.CustomClassical("f", 1, lambda x: 1 + x),
                qiskit.qasm2.CustomClassical("g", 2, math.atan2),
                qiskit.qasm2.CustomClassical("h", 3, lambda x, y, z: z - y + x),
            ],
        )
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.u(1.2, math.atan2(0.4, 0.1), 2, 0)
        self.assertEqual(parsed, qc)

    def test_use_in_gate_definition(self):
        # pylint: disable=invalid-name
        program = """
            gate my_gate(a, b) q {
                U(f(a, b), g(f(b, f(b, a))), b) q;
            }
            qreg q[1];
            my_gate(0.5, 0.25) q[0];
            my_gate(0.25, 0.5) q[0];
        """
        f = lambda x, y: x - y
        g = lambda x: 2 * x
        parsed = qiskit.qasm2.loads(
            program,
            custom_classical=[
                qiskit.qasm2.CustomClassical("f", 2, f),
                qiskit.qasm2.CustomClassical("g", 1, g),
            ],
        )
        first_gate = parsed.data[0].operation
        second_gate = parsed.data[1].operation
        self.assertEqual(list(first_gate.params), [0.5, 0.25])
        self.assertEqual(list(second_gate.params), [0.25, 0.5])

        self.assertEqual(
            list(first_gate.definition.data[0].operation.params),
            [
                f(0.5, 0.25),
                g(f(0.25, f(0.25, 0.5))),
                0.25,
            ],
        )
        self.assertEqual(
            list(second_gate.definition.data[0].operation.params),
            [
                f(0.25, 0.5),
                g(f(0.5, f(0.5, 0.25))),
                0.5,
            ],
        )


@ddt.ddt
class TestStrict(QiskitTestCase):
    @ddt.data(
        "gate my_gate(p0, p1$) q0, q1 {}",
        "gate my_gate(p0, p1) q0, q1$ {}",
        "opaque my_gate(p0, p1$) q0, q1;",
        "opaque my_gate(p0, p1) q0, q1$;",
        'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125$) q[0], q[1];',
        'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125) q[0], q[1]$;',
        "qreg q[2]; barrier q[0], q[1]$;",
        'include "qelib1.inc"; qreg q[1]; rx(sin(pi$)) q[0];',
    )
    def test_trailing_comma(self, program):
        without = qiskit.qasm2.loads("OPENQASM 2.0;\n" + program.replace("$", ""), strict=True)
        with_ = qiskit.qasm2.loads(program.replace("$", ","), strict=False)
        self.assertEqual(with_, without)

    def test_trailing_semicolon_after_gate(self):
        program = """
            include "qelib1.inc";
            gate bell a, b {
                h a;
                cx a, b;
            };  // <- the important bit of the test
            qreg q[2];
            bell q[0], q[1];
        """
        parsed = qiskit.qasm2.loads(program)
        bell_def = QuantumCircuit([Qubit(), Qubit()])
        bell_def.h(0)
        bell_def.cx(0, 1)
        bell = gate_builder("bell", [], bell_def)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.append(bell(), [0, 1])
        self.assertEqual(parsed, qc)

    def test_empty_statement(self):
        # This is allowed more as a side-effect of allowing the trailing semicolon after gate
        # definitions.
        program = """
            OPENQASM 2.0;
            include "qelib1.inc";
            qreg q[2];
            h q[0];
            ;
            cx q[0], q[1];
            ;;;;
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(2, "q"))
        qc.h(0)
        qc.cx(0, 1)
        self.assertEqual(parsed, qc)

    def test_single_quoted_path(self):
        program = """
            include 'qelib1.inc';
            qreg q[1];
            h q[0];
        """
        parsed = qiskit.qasm2.loads(program)
        qc = QuantumCircuit(QuantumRegister(1, "q"))
        qc.h(0)
        self.assertEqual(parsed, qc)
