# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2024.
#
# 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=invalid-name

"""Randomized tests of transpiler circuit equivalence.

This test can be optionally configured (e.g. by CI) via the
following env vars:

QISKIT_RANDOMIZED_TEST_LAYOUT_METHODS

    A space-delimited list of layout method names from which the
    randomizer should pick the layout method. Defaults to all
    available built-in methods if unspecified.

QISKIT_RANDOMIZED_TEST_ROUTING_METHODS

    A space-delimited list of routing method names from which the
    randomizer should pick the routing method. Defaults to all
    available built-in methods if unspecified.

QISKIT_RANDOMIZED_TEST_SCHEDULING_METHODS

    A space-delimited list of scheduling method names from which the
    randomizer should pick the scheduling method. Defaults to all
    available built-in methods if unspecified.

QISKIT_RANDOMIZED_TEST_BACKEND_NEEDS_DURATIONS

    A boolean value (e.g. "true", "Y", etc.) which, when true, forces
    the randomizer to pick a backend which fully supports scheduling
    (i.e. has fully specified duration info). Defaults to False.

QISKIT_RANDOMIZED_TEST_ALLOW_BARRIERS

    A boolean value (e.g. "true", "Y", etc.) which, when false,
    prevents the randomizer from emitting barrier instructions.
    Defaults to True.
"""

import os
import warnings

from test.utils.base import dicts_almost_equal

from math import pi

from hypothesis import assume, settings, HealthCheck
from hypothesis.stateful import multiple, rule, precondition, invariant
from hypothesis.stateful import Bundle, RuleBasedStateMachine
import hypothesis.strategies as st

from qiskit import transpile, qasm2
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit import Measure, Reset, Gate, Barrier
from qiskit.providers.fake_provider import (
    Fake5QV1,
    Fake20QV1,
    Fake7QPulseV1,
    Fake27QPulseV1,
    Fake127QPulseV1,
)

# pylint: disable=wildcard-import,unused-wildcard-import
from qiskit.circuit.library.standard_gates import *

from qiskit_aer import Aer  # pylint: disable=wrong-import-order

default_profile = "transpiler_equivalence"
settings.register_profile(
    default_profile,
    report_multiple_bugs=False,
    max_examples=200,
    deadline=None,
    suppress_health_check=[HealthCheck.filter_too_much],
)

settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", default_profile))

BASE_INSTRUCTIONS = {
    # Key is (n_qubits, n_clbits, n_params).  All gates here should be directly known by Aer so they
    # can be simulated without an initial transpile (whether that's via `execute` or not).
    (1, 0, 0): [HGate, IGate, SGate, SdgGate, TGate, TdgGate, XGate, YGate, ZGate, Reset],
    (2, 0, 0): [CXGate, CYGate, CZGate, SwapGate],
    (3, 0, 0): [CCXGate, CSwapGate],
    (1, 0, 1): [PhaseGate, RXGate, RYGate, RZGate],
    (1, 0, 3): [UGate],
    (2, 0, 1): [RZZGate, CPhaseGate],
    (2, 0, 4): [CUGate],
    (1, 1, 0): [Measure],
}
variadic_gates = [Barrier]


def _strtobool(s):
    return s.lower() in ("y", "yes", "t", "true", "on", "1")


if not _strtobool(os.getenv("QISKIT_RANDOMIZED_TEST_ALLOW_BARRIERS", "True")):
    variadic_gates.remove(Barrier)


def _getenv_list(var_name):
    value = os.getenv(var_name)
    return None if value is None else value.split()


# Note: a value of `None` for any of the following methods means that
# the selected pass manager gets to choose. However, to avoid complexity,
# its not possible to specify `None` when overriding these with environment
# variables. Really, `None` is useful only for testing Terra's pass managers,
# and if you're overriding these, your goal is probably to test a specific
# pass or set of passes instead.
layout_methods = _getenv_list("QISKIT_RANDOMIZED_TEST_LAYOUT_METHODS") or [
    None,
    "trivial",
    "dense",
    "sabre",
]
routing_methods = _getenv_list("QISKIT_RANDOMIZED_TEST_ROUTING_METHODS") or [
    None,
    "basic",
    "stochastic",
    "lookahead",
    "sabre",
]
scheduling_methods = _getenv_list("QISKIT_RANDOMIZED_TEST_SCHEDULING_METHODS") or [
    None,
    "alap",
    "asap",
]

backend_needs_durations = _strtobool(
    os.getenv("QISKIT_RANDOMIZED_TEST_BACKEND_NEEDS_DURATIONS", "False")
)


def _fully_supports_scheduling(backend):
    """Checks if backend is not in the set of backends known not to have specified gate durations."""
    return not isinstance(
        backend,
        (Fake20QV1, Fake5QV1),
    )


mock_backends = [Fake5QV1(), Fake20QV1(), Fake7QPulseV1(), Fake27QPulseV1(), Fake127QPulseV1()]

mock_backends_with_scheduling = [b for b in mock_backends if _fully_supports_scheduling(b)]


@st.composite
def transpiler_conf(draw):
    """Composite search strategy to pick a valid transpiler config."""
    all_backends = st.one_of(st.none(), st.sampled_from(mock_backends))
    scheduling_backends = st.sampled_from(mock_backends_with_scheduling)
    scheduling_method = draw(st.sampled_from(scheduling_methods))
    backend = (
        draw(scheduling_backends)
        if scheduling_method or backend_needs_durations
        else draw(all_backends)
    )
    return {
        "backend": backend,
        "optimization_level": draw(st.integers(min_value=0, max_value=3)),
        "layout_method": draw(st.sampled_from(layout_methods)),
        "routing_method": draw(st.sampled_from(routing_methods)),
        "scheduling_method": scheduling_method,
        "seed_transpiler": draw(st.integers(min_value=0, max_value=1_000_000)),
    }


class QCircuitMachine(RuleBasedStateMachine):
    """Build a Hypothesis rule based state machine for constructing, transpiling
    and simulating a series of random QuantumCircuits.

    Build circuits with up to QISKIT_RANDOM_QUBITS qubits, apply a random
    selection of gates from qiskit.circuit.library with randomly selected
    qargs, cargs, and parameters. At random intervals, transpile the circuit for
    a random backend with a random optimization level and simulate both the
    initial and the transpiled circuits to verify that their counts are the
    same.
    """

    qubits = Bundle("qubits")
    clbits = Bundle("clbits")

    backend = Aer.get_backend("aer_simulator")
    max_qubits = int(backend.configuration().n_qubits / 2)

    # Limit reg generation for more interesting circuits
    max_qregs = 3
    max_cregs = 3

    def __init__(self):
        super().__init__()
        self.qc = QuantumCircuit()
        self.enable_variadic = bool(variadic_gates)

    @precondition(lambda self: len(self.qc.qubits) < self.max_qubits)
    @precondition(lambda self: len(self.qc.qregs) < self.max_qregs)
    @rule(target=qubits, n=st.integers(min_value=1, max_value=max_qubits))
    def add_qreg(self, n):
        """Adds a new variable sized qreg to the circuit, up to max_qubits."""
        n = min(n, self.max_qubits - len(self.qc.qubits))
        qreg = QuantumRegister(n)
        self.qc.add_register(qreg)
        return multiple(*list(qreg))

    @precondition(lambda self: len(self.qc.cregs) < self.max_cregs)
    @rule(target=clbits, n=st.integers(1, 5))
    def add_creg(self, n):
        """Add a new variable sized creg to the circuit."""
        creg = ClassicalRegister(n)
        self.qc.add_register(creg)
        return multiple(*list(creg))

    # Gates of various shapes

    @precondition(lambda self: self.qc.num_qubits > 0 and self.qc.num_clbits > 0)
    @rule(n_arguments=st.sampled_from(sorted(BASE_INSTRUCTIONS.keys())), data=st.data())
    def add_gate(self, n_arguments, data):
        """Append a random fixed gate to the circuit."""
        n_qubits, n_clbits, n_params = n_arguments
        gate_class = data.draw(st.sampled_from(BASE_INSTRUCTIONS[n_qubits, n_clbits, n_params]))
        qubits = data.draw(st.lists(self.qubits, min_size=n_qubits, max_size=n_qubits, unique=True))
        clbits = data.draw(st.lists(self.clbits, min_size=n_clbits, max_size=n_clbits, unique=True))
        params = data.draw(
            st.lists(
                st.floats(
                    allow_nan=False, allow_infinity=False, min_value=-10 * pi, max_value=10 * pi
                ),
                min_size=n_params,
                max_size=n_params,
            )
        )
        self.qc.append(gate_class(*params), qubits, clbits)

    @precondition(lambda self: self.enable_variadic)
    @rule(gate=st.sampled_from(variadic_gates), qargs=st.lists(qubits, min_size=1, unique=True))
    def add_variQ_gate(self, gate, qargs):
        """Append a gate with a variable number of qargs."""
        self.qc.append(gate(len(qargs)), qargs)

    @precondition(lambda self: len(self.qc.data) > 0)
    @rule(carg=clbits, data=st.data())
    def add_c_if_last_gate(self, carg, data):
        """Modify the last gate to be conditional on a classical register."""
        creg = self.qc.find_bit(carg).registers[0][0]
        val = data.draw(st.integers(min_value=0, max_value=2 ** len(creg) - 1))

        last_gate = self.qc.data[-1]

        # Conditional instructions are not supported
        assume(isinstance(last_gate.operation, Gate))

        last_gate.operation.c_if(creg, val)

    # Properties to check

    @invariant()
    def qasm(self):
        """After each circuit operation, it should be possible to build QASM."""
        qasm2.dumps(self.qc)

    @precondition(lambda self: any(isinstance(d.operation, Measure) for d in self.qc.data))
    @rule(kwargs=transpiler_conf())
    def equivalent_transpile(self, kwargs):
        """Simulate, transpile and simulate the present circuit. Verify that the
        counts are not significantly different before and after transpilation.

        """
        assume(
            kwargs["backend"] is None
            or kwargs["backend"].configuration().n_qubits >= len(self.qc.qubits)
        )

        call = (
            "transpile(qc, "
            + ", ".join(f"{key:s}={value!r}" for key, value in kwargs.items() if value is not None)
            + ")"
        )
        print(f"Evaluating {call} for:\n{qasm2.dumps(self.qc)}")

        shots = 4096

        # Note that there's no transpilation here, which is why the gates are limited to only ones
        # that Aer supports natively.
        with warnings.catch_warnings():
            # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version
            # of Aer.
            warnings.filterwarnings(
                "default",
                category=DeprecationWarning,
                module="qiskit_aer",
                message="Treating CircuitInstruction as an iterable",
            )
            aer_counts = self.backend.run(self.qc, shots=shots).result().get_counts()

        try:
            xpiled_qc = transpile(self.qc, **kwargs)
        except Exception as e:
            failed_qasm = (
                f"Exception caught during transpilation of circuit: \n{qasm2.dumps(self.qc)}"
            )
            raise RuntimeError(failed_qasm) from e

        xpiled_aer_counts = self.backend.run(xpiled_qc, shots=shots).result().get_counts()

        count_differences = dicts_almost_equal(aer_counts, xpiled_aer_counts, 0.05 * shots)

        assert count_differences == "", (
            f"Counts not equivalent: {count_differences}\nFailing QASM Input:\n"
            f"{qasm2.dumps(self.qc)}\n\nFailing QASM Output:\n{qasm2.dumps(xpiled_qc)}"
        )


TestQuantumCircuit = QCircuitMachine.TestCase
