# Copyright (C) 2024 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.

"""
Unit tests for qbraid.programs.qasm.OpenQasm3Program

"""
import numpy as np
import pytest
from qiskit.qasm3 import dumps, loads

from qbraid.interface.random.qasm3_random import qasm3_random
from qbraid.interface.random.qiskit_random import qiskit_random
from qbraid.programs.exceptions import ProgramTypeError
from qbraid.programs.gate_model.qasm3 import OpenQasm3Program
from qbraid.programs.registry import unregister_program_type

from ..fixtures.qasm3.circuits import qasm3_bell, qasm3_shared15


def test_qasm_qubits():
    """Test getting QASM qubits"""
    program1 = OpenQasm3Program(qasm3_bell())
    program1.validate()
    assert program1.qubits == {"q": 2}

    program2 = OpenQasm3Program(qasm3_shared15())
    program2.validate()
    assert program2.qubits == {"q": 4}


def test_qasm_clbits():
    """Test getting QASM clbits"""
    program1 = OpenQasm3Program("OPENQASM 3.0; bit[2] c;")
    program1.validate()
    assert program1.clbits == {"c": 2}


def test_qasm3_num_qubits():
    """Test calculating number of qubits in qasm3 circuit"""
    num_qubits = np.random.randint(2, 10)
    depth = np.random.randint(1, 4)
    qiskit_circuit = qiskit_random(num_qubits=num_qubits, depth=depth)
    qasm3_str = dumps(qiskit_circuit)
    assert OpenQasm3Program(qasm3_str).num_qubits == num_qubits


def test_qasm3_depth_alternate_qubit_syntax():
    """Test calculating qasm depth of qasm3 circuit"""
    qasm3_str = """OPENQASM 3.0;
bit[1] __bits__;
qubit[1] __qubits__;
h __qubits__[0];
__bits__[0] = measure __qubits__[0];"""
    assert OpenQasm3Program(qasm3_str).depth == 2


def _check_output(output, expected):
    actual_circuit = loads(output)
    expected_circuit = loads(expected)
    assert actual_circuit == expected_circuit


@pytest.mark.parametrize(
    "num_qubits, depth, max_operands, seed, measure",
    [
        (
            None,
            None,
            None,
            None,
            False,
        ),  # Test case 1: Generate random circuit with default parameters
        (3, 3, 3, 42, False),  # Test case 2: Generate random circuit with specified parameters
        (None, None, None, None, True),  # Test case 3: Generate random circuit with measurement
    ],
)
def test_qasm3_random(num_qubits, depth, max_operands, seed, measure):
    """Test random circuit generation using _qasm_random"""

    circuit = qasm3_random(
        num_qubits=num_qubits, depth=depth, max_operands=max_operands, seed=seed, measure=measure
    )
    assert OpenQasm3Program(circuit).num_qubits >= 1
    if num_qubits is not None:
        assert OpenQasm3Program(circuit).num_qubits == num_qubits


def test_qasm3_random_with_known_seed():
    """Test generating random OpenQASM 3 circuit from known seed"""
    circuit = qasm3_random(num_qubits=3, depth=3, max_operands=3, seed=42, measure=True)
    assert OpenQasm3Program(circuit).num_qubits == 3

    out__expected = """
// Random Circuit generated by qBraid
OPENQASM 3.0;
include "stdgates.inc";
/*
    seed = 42
    num_qubits = 3
    depth = 3
    max_operands = 3
*/
qubit[3] q;
bit[3] c;
y q[0];
crx(5.3947298351621535) q[1],q[2];
cz q[0],q[2];
t q[1];
cp(0.8049616944763924) q[1],q[2];
u1(2.829858307545725) q[0];
c[0] = measure q[0];
c[1] = measure q[1];
c[2] = measure q[2];

"""
    _check_output(circuit, out__expected)


def test_qasm3_num_qubits_alternate_synatx():
    """Test calculating num qubits for qasm3 syntax edge-case"""
    qasm3_str = """
OPENQASM 3;
include "stdgates.inc";
qubit _qubit0;
qubit _qubit1;
h _qubit0;
cx _qubit0, _qubit1;
"""
    qprogram = OpenQasm3Program(qasm3_str)
    assert qprogram.num_qubits == 2


def test_qasm3_num_clbits():
    """Test calculating num classical bits for qasm3"""
    qasm3_str = """
OPENQASM 3;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
"""
    qprogram = OpenQasm3Program(qasm3_str)
    assert qprogram.num_clbits == 2


def test_populate_idle_qubits_qasm3():
    """Test conversion of qasm3 to contiguous qasm3"""
    qasm_test = """OPENQASM 3.0;
    gate custom q1, q2, q3{
        x q1;
        y q2;
        z q3;
    }
    qubit[2] q1;
    qubit[2] q2;
    qubit[3] q3;
    qubit[1] q4;
    x q1[0];
    y q2[0];
    z q3;
    """

    qprogram = OpenQasm3Program(qasm_test)
    qprogram.populate_idle_qubits()
    actual_qasm = qprogram.program

    id_ops = ["id q1[1];", "id q2[1];", "id q4[0];"]
    for id_op in id_ops:
        assert id_op in actual_qasm


def test_remove_idle_qubits_qasm3():
    """Test that remove_idle_qubits for qasm3 string"""
    qasm3_str = """
    OPENQASM 3;
    include "stdgates.inc";
    qubit[4] q;
    h q[1];
    cx q[1], q[3];
    """
    qprogram = OpenQasm3Program(qasm3_str)
    qprogram.remove_idle_qubits()
    assert qprogram.num_qubits == 2


def test_reverse_qubit_order():
    """Test the reverse qubit ordering function"""
    qasm_str = """
    OPENQASM 3.0;
    include "stdgates.inc";
    qubit[2] q;
    qubit[4] q2;
    qubit[1] q3;
    cx q[0], q[1];
    cx q2[0], q2[1];
    x q2[3];
    cx q2[0], q2[2];
    x q3[0];
    """

    program = OpenQasm3Program(qasm_str)
    program.reverse_qubit_order()
    reverse_qasm = program.program

    expected_qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
qubit[4] q2;
qubit[1] q3;
cx q[1], q[0];
cx q2[3], q2[2];
x q2[0];
cx q2[3], q2[1];
x q3[0];
"""
    assert reverse_qasm == expected_qasm


def test_raise_program_type_error():
    """Test that initializing OpenQasm3Program with an invalid type raises ProgramTypeError."""
    try:
        with pytest.raises(ProgramTypeError):
            OpenQasm3Program(42)
    finally:
        unregister_program_type("int")
