#!/usr/bin/env python3
"""Unit tests for Maestro nanobind Python bindings using pytest"""

import pytest
import maestro


class TestEnums:
    """Test that enums are properly exposed"""

    def test_simulator_type_enum(self):
        """Test SimulatorType enum accessibility"""
        assert hasattr(maestro, 'SimulatorType')
        assert hasattr(maestro.SimulatorType, 'QCSim')
        assert hasattr(maestro.SimulatorType, 'CompositeQCSim')
        assert hasattr(maestro.SimulatorType, 'QuestSim')

    def test_simulation_type_enum(self):
        """Test SimulationType enum accessibility"""
        assert hasattr(maestro, 'SimulationType')
        assert hasattr(maestro.SimulationType, 'Statevector')
        assert hasattr(maestro.SimulationType, 'MatrixProductState')
        assert hasattr(maestro.SimulationType, 'Stabilizer')
        assert hasattr(maestro.SimulationType, 'TensorNetwork')
        assert hasattr(maestro.SimulationType, 'PauliPropagator')
        assert hasattr(maestro.SimulationType, 'ExtendedStabilizer')


class TestMaestroClass:
    """Test Maestro class instantiation and basic methods"""

    def test_maestro_creation(self):
        """Test Maestro instance creation"""
        m = maestro.Maestro()
        assert m is not None

    def test_simulator_creation(self):
        """Test simulator creation and destruction"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        assert sim_handle > 0

        m.destroy_simulator(sim_handle)

    def test_multiple_simulators(self):
        """Test creating multiple simulators"""
        m = maestro.Maestro()

        sim1 = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim2 = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.MatrixProductState
        )

        assert sim1 > 0
        assert sim2 > 0
        assert sim1 != sim2

        m.destroy_simulator(sim1)
        m.destroy_simulator(sim2)


class TestQasmParser:
    """Test QASM parsing functionality"""

    def test_qasm_parser_creation(self):
        """Test QasmToCirc instance creation"""
        parser = maestro.QasmToCirc()
        assert parser is not None

    def test_simple_qasm_parsing(self):
        """Test parsing a simple QASM circuit"""
        qasm = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        creg c[2];
        h q[0];
        cx q[0], q[1];
        measure q -> c;
        """

        parser = maestro.QasmToCirc()
        circuit = parser.parse_and_translate(qasm)
        assert circuit is not None

    def test_qubit_count_detection(self):
        """Test circuit qubit count detection"""
        qasm = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[3];
        creg c[3];
        h q[2];
        """

        parser = maestro.QasmToCirc()
        circuit = parser.parse_and_translate(qasm)
        num_qubits = circuit.num_qubits
        assert num_qubits == 3


@pytest.mark.skip(reason="ISimulator is not yet exposed as a nanobind type")
class TestSimulatorOperations:
    """Test simulator gate operations (requires ISimulator binding)"""

    def test_qubit_allocation(self):
        """Test qubit allocation and initialization"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim = m.get_simulator(sim_handle)

        sim.AllocateQubits(2)
        sim.Initialize()
        assert sim.GetNumberOfQubits() == 2

        m.destroy_simulator(sim_handle)

    def test_single_qubit_gates(self):
        """Test single-qubit gate operations"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim = m.get_simulator(sim_handle)

        sim.AllocateQubits(1)
        sim.Initialize()

        # These should not raise exceptions
        sim.ApplyH(0)
        sim.ApplyX(0)
        sim.ApplyY(0)
        sim.ApplyZ(0)

        m.destroy_simulator(sim_handle)

    def test_two_qubit_gates(self):
        """Test two-qubit gate operations"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim = m.get_simulator(sim_handle)

        sim.AllocateQubits(2)
        sim.Initialize()

        # These should not raise exceptions
        sim.ApplyCX(0, 1)
        sim.ApplyCZ(0, 1)
        sim.ApplySwap(0, 1)

        m.destroy_simulator(sim_handle)

    def test_measurement(self):
        """Test measurement operations"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim = m.get_simulator(sim_handle)

        sim.AllocateQubits(2)
        sim.Initialize()
        sim.ApplyH(0)
        sim.ApplyCX(0, 1)

        results = sim.SampleCounts([0, 1], 1000)
        assert isinstance(results, dict)
        assert len(results) > 0
        total_shots = sum(results.values())
        assert total_shots == 1000

        m.destroy_simulator(sim_handle)

    def test_bell_state_distribution(self):
        """Test Bell state produces correct distribution"""
        m = maestro.Maestro()
        sim_handle = m.create_simulator(
            maestro.SimulatorType.QCSim,
            maestro.SimulationType.Statevector
        )
        sim = m.get_simulator(sim_handle)

        sim.AllocateQubits(2)
        sim.Initialize()
        sim.ApplyH(0)
        sim.ApplyCX(0, 1)

        results = sim.SampleCounts([0, 1], 10000)

        # Bell state should produce |00> and |11> with ~50% each
        # Allow for statistical variation
        assert 0 in results or 3 in results  # |00> or |11>
        total = sum(results.values())
        assert total == 10000

        m.destroy_simulator(sim_handle)


class TestSimpleExecute:
    """Test the simple_execute convenience function"""

    def test_simple_execute_basic(self):
        """Test simple_execute with default parameters"""
        qasm_bell = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        creg c[2];
        h q[0];
        cx q[0], q[1];
        measure q[0] -> c[0];
        measure q[1] -> c[1];
        """

        result = maestro.simple_execute(qasm_bell)
        assert result is not None
        assert 'counts' in result
        assert 'simulator' in result
        assert 'method' in result
        assert 'time_taken' in result

    def test_simple_execute_custom_shots(self):
        """Test simple_execute with custom shot count"""
        qasm_bell = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        creg c[2];
        h q[0];
        cx q[0], q[1];
        measure q -> c;
        """

        result = maestro.simple_execute(qasm_bell, shots=500)
        total = sum(result['counts'].values())
        assert total == 500

    def test_simple_execute_mps(self):
        """Test simple_execute with Matrix Product State"""
        qasm_bell = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        creg c[2];
        h q[0];
        cx q[0], q[1];
        measure q -> c;
        """

        result = maestro.simple_execute(
            qasm_bell,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4,
            singular_value_threshold=1e-10
        )
        assert result is not None
        assert result['method'] == maestro.SimulationType.MatrixProductState.value

    def test_simple_execute_ghz_state(self):
        """Test simple_execute with GHZ state"""
        qasm_ghz = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[3];
        creg c[3];
        h q[0];
        cx q[0], q[1];
        cx q[1], q[2];
        measure q -> c;
        """

        result = maestro.simple_execute(qasm_ghz, shots=1000)
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 1000

    def test_simple_execute_four_qubit(self):
        """Test simple_execute with 4-qubit circuit"""
        qasm_4q = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[4];
        creg c[4];
        h q[0];
        cx q[0], q[1];
        cx q[1], q[2];
        cx q[2], q[3];
        measure q -> c;
        """

        result = maestro.simple_execute(qasm_4q, shots=1000)
        assert result is not None

        # Should produce mostly |0000> and |1111>
        counts = result['counts']
        dominant_states = sorted(counts.items(), key=lambda x: x[1], reverse=True)[:2]
        assert len(dominant_states) >= 1


class TestSimpleEstimate:
    """Test the simple_estimate convenience function"""

    def test_simple_estimate_basic(self):
        """Test simple_estimate with default parameters"""
        qasm_bell = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        h q[0];
        cx q[0], q[1];
        """

        # Estimate expectation values for ZZ, XX, and YY
        observables = "ZZ;XX;YY"
        result = maestro.simple_estimate(qasm_bell, observables)

        assert result is not None
        assert 'expectation_values' in result
        assert 'simulator' in result
        assert 'method' in result
        assert 'time_taken' in result

        exp_vals = result['expectation_values']
        assert len(exp_vals) == 3

        # Bell state (|00> + |11>) / sqrt(2)
        # <ZZ> = 1.0
        # <XX> = 1.0 (for this specific Bell state)
        # <YY> = -1.0
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[2] == pytest.approx(-1.0, abs=1e-5)

    def test_simple_estimate_single_qubit(self):
        """Test simple_estimate for single qubit observables"""
        qasm_h = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[1];
        h q[0];
        """

        # <X> for H|0> is 1.0
        # <Z> for H|0> is 0.0
        result = maestro.simple_estimate(qasm_h, "X;Z")
        assert result is not None
        exp_vals = result['expectation_values']
        assert len(exp_vals) == 2
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(0.0, abs=1e-5)

    def test_simple_estimate_mps(self):
        """Test simple_estimate using MPS method"""
        qasm_bell = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[2];
        h q[0];
        cx q[0], q[1];
        """

        result = maestro.simple_estimate(
            qasm_bell,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=2
        )
        assert result is not None
        assert result['method'] == maestro.SimulationType.MatrixProductState.value
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)


class TestComplexCircuits:
    """Test with more complex quantum circuits"""

    def test_superposition(self):
        """Test simple superposition circuit"""
        qasm = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[1];
        creg c[1];
        h q[0];
        measure q -> c;
        """

        result = maestro.simple_execute(qasm, shots=10000)
        counts = result['counts']

        # Should be roughly 50/50 between |0> and |1>
        # Allow for statistical variation (40-60%)
        total = sum(counts.values())
        assert total == 10000

    def test_parametric_gates(self):
        """Test circuit with parametric rotation gates"""
        qasm = """
        OPENQASM 2.0;
        include "qelib1.inc";
        qreg q[1];
        creg c[1];
        rx(1.5708) q[0];
        measure q -> c;
        """

        result = maestro.simple_execute(qasm, shots=1000)
        assert result is not None
        assert 'counts' in result


if __name__ == "__main__":
    pytest.main([__file__, "-v"])


# --- Clifford-only QASM for Stabilizer tests ---
# Stabilizer only supports Clifford gates (H, S, CX, X, Y, Z)
CLIFFORD_BELL_QASM = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""

CLIFFORD_BELL_NO_MEASURE_QASM = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0], q[1];
"""

# General QASM (with non-Clifford gates) for PauliProp / ExtStab
GENERAL_QASM = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
t q[0];
cx q[0], q[1];
measure q -> c;
"""

GENERAL_NO_MEASURE_QASM = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
t q[0];
cx q[0], q[1];
"""


class TestStabilizerSimulation:
    """Test the Stabilizer simulation backend (Clifford-only circuits)"""

    def test_stabilizer_execute(self):
        """Test execute with Stabilizer backend on a Clifford circuit"""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Stabilizer,
            shots=1000
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 1000

    def test_stabilizer_estimate(self):
        """Test estimate with Stabilizer backend on a Bell state"""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Stabilizer
        )
        assert result is not None
        assert 'expectation_values' in result
        # Bell state: <ZZ> = 1.0
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)

    def test_stabilizer_bell_distribution(self):
        """Test Stabilizer produces correct Bell state distribution"""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Stabilizer,
            shots=10000
        )
        counts = result['counts']
        total = sum(counts.values())
        assert total == 10000


class TestPauliPropagatorSimulation:
    """Test the Pauli Propagator simulation backend"""

    def test_pauli_propagator_execute(self):
        """Test execute with Pauli Propagator backend"""
        result = maestro.simple_execute(
            GENERAL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.PauliPropagator,
            shots=1000
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 1000

    def test_pauli_propagator_estimate(self):
        """Test estimate with Pauli Propagator backend"""
        result = maestro.simple_estimate(
            GENERAL_NO_MEASURE_QASM,
            "ZZ;XX",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.PauliPropagator
        )
        assert result is not None
        assert 'expectation_values' in result
        exp_vals = result['expectation_values']
        assert len(exp_vals) == 2
        # State is (|00> + e^{i*pi/4}|11>) / sqrt(2)
        # <ZZ> = 1.0, <XX> = cos(pi/4) = 1/sqrt(2)
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(0.7071, abs=1e-3)


class TestExtendedStabilizerSimulation:
    """Test the Extended Stabilizer simulation backend"""

    def test_extended_stabilizer_execute(self):
        """Test execute with Extended Stabilizer backend"""
        result = maestro.simple_execute(
            GENERAL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.ExtendedStabilizer,
            shots=1000
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 1000

    def test_extended_stabilizer_estimate(self):
        """Test estimate with Extended Stabilizer backend"""
        result = maestro.simple_estimate(
            GENERAL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.ExtendedStabilizer
        )
        assert result is not None
        assert 'expectation_values' in result
        # (|00> + e^{i*pi/4}|11>) / sqrt(2): <ZZ> = 1.0
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)


class TestSimulatorTypeIsHonored:
    """Regression tests: verify the requested simulator/method is actually used.

    These tests ensure we don't silently fall back to defaults (e.g., kQCSim)
    when a specific simulator_type or simulation_type is requested.
    GPU tests are skipped here (requires Linux + CUDA) but the pattern applies.
    """

    def test_execute_statevector_type_honored(self):
        """Execute with Statevector reports correct simulator and method."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=10
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.Statevector.value

    def test_execute_mps_type_honored(self):
        """Execute with MPS reports correct simulator and method."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            shots=10
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.MatrixProductState.value

    def test_execute_pauli_propagator_type_honored(self):
        """Execute with PauliPropagator reports correct simulator type.

        Note: the internal optimizer may legitimately switch the simulation
        method, so we only assert the simulator type here.
        """
        result = maestro.simple_execute(
            GENERAL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.PauliPropagator,
            shots=10
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value

    def test_estimate_statevector_type_honored(self):
        """Estimate with Statevector reports correct simulator and method."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.Statevector.value

    def test_estimate_mps_type_honored(self):
        """Estimate with MPS reports correct simulator and method."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.MatrixProductState.value

    def test_estimate_pauli_propagator_type_honored(self):
        """Estimate with PauliPropagator reports correct simulator type.

        Note: the internal optimizer may legitimately switch the simulation
        method, so we only assert the simulator type here.
        """
        result = maestro.simple_estimate(
            GENERAL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.PauliPropagator,
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value

    def test_circuit_execute_type_honored(self):
        """Circuit.execute() reports correct simulator and method."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)
        qc.measure_all()

        result = qc.execute(
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            shots=10,
            max_bond_dimension=4,
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.MatrixProductState.value

    def test_circuit_estimate_type_honored(self):
        """Circuit.estimate() reports correct simulator and method."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)

        result = qc.estimate(
            observables=["ZZ"],
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4,
        )
        assert result['simulator'] == maestro.SimulatorType.QCSim.value
        assert result['method'] == maestro.SimulationType.MatrixProductState.value


class TestDoublePrecision:
    """Test the use_double_precision parameter.

    On CPU (QCSim), this flag has no effect (CPU already uses float64).
    These tests verify the parameter is accepted without errors and results
    remain correct. GPU-specific precision tests require Linux + CUDA.
    """

    def test_simple_execute_accepts_double_precision(self):
        """simple_execute accepts use_double_precision without error."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=100,
            use_double_precision=True
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 100

    def test_simple_execute_double_precision_false(self):
        """simple_execute with use_double_precision=False (default) works."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=100,
            use_double_precision=False
        )
        assert result is not None
        assert 'counts' in result

    def test_simple_estimate_accepts_double_precision(self):
        """simple_estimate accepts use_double_precision without error."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            use_double_precision=True
        )
        assert result is not None
        assert 'expectation_values' in result
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)

    def test_simple_estimate_mps_double_precision(self):
        """simple_estimate with MPS + use_double_precision produces correct results."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ;XX;YY",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4,
            use_double_precision=True
        )
        assert result is not None
        exp_vals = result['expectation_values']
        assert len(exp_vals) == 3
        # Bell state: <ZZ> = 1.0, <XX> = 1.0, <YY> = -1.0
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[2] == pytest.approx(-1.0, abs=1e-5)

    def test_circuit_execute_accepts_double_precision(self):
        """Circuit.execute() accepts use_double_precision parameter."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)
        qc.measure_all()

        result = qc.execute(
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            shots=100,
            max_bond_dimension=4,
            use_double_precision=True
        )
        assert result is not None
        assert 'counts' in result

    def test_circuit_estimate_accepts_double_precision(self):
        """Circuit.estimate() accepts use_double_precision parameter."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)

        result = qc.estimate(
            observables=["ZZ"],
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4,
            use_double_precision=True
        )
        assert result is not None
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)

    def test_qasm_execute_accepts_double_precision(self):
        """QASM-based simple_execute accepts use_double_precision."""
        result = maestro.simple_execute(
            GENERAL_QASM,
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=100,
            use_double_precision=True
        )
        assert result is not None
        assert 'counts' in result

    def test_qasm_estimate_accepts_double_precision(self):
        """QASM-based simple_estimate accepts use_double_precision."""
        result = maestro.simple_estimate(
            GENERAL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.QCSim,
            simulation_type=maestro.SimulationType.Statevector,
            use_double_precision=True
        )
        assert result is not None
        assert 'expectation_values' in result
        # (|00> + e^{i*pi/4}|11>) / sqrt(2): <ZZ> = 1.0
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)


class TestGpuSimulator:
    """Test GPU simulator bindings.

    GPU simulation registers GPU via RemoveAllOptimizationSimulatorsAndAdd,
    but CreateSimulator creates a cheap QCSim MPS placeholder (not a GPU
    simulator) to avoid wasting GPU memory. The network creates the actual
    GPU simulator on-demand during execution.

    On macOS (no GPU library), these tests verify that the binding path
    still works using the QCSim MPS fallback.
    """

    def test_gpu_enum_exists(self):
        """SimulatorType.Gpu enum value is exposed."""
        assert hasattr(maestro.SimulatorType, 'Gpu')

    def test_gpu_execute_returns_result(self):
        """simple_execute with GPU type returns a valid result dict."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.Gpu,
            simulation_type=maestro.SimulationType.MatrixProductState,
            shots=100
        )
        assert result is not None
        assert 'counts' in result
        assert 'time_taken' in result
        total = sum(result['counts'].values())
        assert total == 100

    def test_gpu_estimate_returns_result(self):
        """simple_estimate with GPU type returns valid expectation values."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ",
            simulator_type=maestro.SimulatorType.Gpu,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4
        )
        assert result is not None
        assert 'expectation_values' in result
        # Bell state <ZZ> = 1.0
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)

    def test_circuit_gpu_execute(self):
        """Circuit.execute() with GPU type works through the network."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)
        qc.measure_all()

        result = qc.execute(
            simulator_type=maestro.SimulatorType.Gpu,
            simulation_type=maestro.SimulationType.MatrixProductState,
            shots=100,
            max_bond_dimension=4
        )
        assert result is not None
        assert 'counts' in result

    def test_circuit_gpu_estimate(self):
        """Circuit.estimate() with GPU type works through the network."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)

        result = qc.estimate(
            observables=["ZZ"],
            simulator_type=maestro.SimulatorType.Gpu,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4
        )
        assert result is not None
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)

    def test_gpu_with_double_precision(self):
        """GPU + use_double_precision flag is accepted."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ;XX",
            simulator_type=maestro.SimulatorType.Gpu,
            simulation_type=maestro.SimulationType.MatrixProductState,
            max_bond_dimension=4,
            use_double_precision=True
        )
        assert result is not None
        exp_vals = result['expectation_values']
        assert len(exp_vals) == 2
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(1.0, abs=1e-5)


class TestQuestSimulator:
    """Test QuEST simulator bindings.

    QuEST is loaded as an external shared library (libmaestroquest).
    These tests verify:
    - The QuestSim enum value is exposed
    - init_quest() / is_quest_available() helpers work
    - Non-statevector simulation types are rejected with a clear error
    - When the library is available, execute and estimate produce correct results
    """

    def test_quest_enum_exists(self):
        """SimulatorType.QuestSim enum value is exposed."""
        assert hasattr(maestro.SimulatorType, 'QuestSim')

    def test_init_quest_function_exists(self):
        """init_quest() module function is exposed."""
        assert hasattr(maestro, 'init_quest')
        assert callable(maestro.init_quest)

    def test_is_quest_available_function_exists(self):
        """is_quest_available() module function is exposed."""
        assert hasattr(maestro, 'is_quest_available')
        assert callable(maestro.is_quest_available)

    def test_is_quest_available_returns_bool(self):
        """is_quest_available() returns a boolean."""
        result = maestro.is_quest_available()
        assert isinstance(result, bool)

    def test_quest_rejects_mps(self):
        """QuestSim with MatrixProductState raises an error."""
        with pytest.raises(Exception, match="QuestSim only supports Statevector"):
            maestro.simple_execute(
                CLIFFORD_BELL_QASM,
                simulator_type=maestro.SimulatorType.QuestSim,
                simulation_type=maestro.SimulationType.MatrixProductState,
                shots=10
            )

    def test_quest_rejects_stabilizer(self):
        """QuestSim with Stabilizer raises an error."""
        with pytest.raises(Exception, match="QuestSim only supports Statevector"):
            maestro.simple_execute(
                CLIFFORD_BELL_QASM,
                simulator_type=maestro.SimulatorType.QuestSim,
                simulation_type=maestro.SimulationType.Stabilizer,
                shots=10
            )

    def test_quest_rejects_tensor_network(self):
        """QuestSim with TensorNetwork raises an error."""
        with pytest.raises(Exception, match="QuestSim only supports Statevector"):
            maestro.simple_execute(
                CLIFFORD_BELL_QASM,
                simulator_type=maestro.SimulatorType.QuestSim,
                simulation_type=maestro.SimulationType.TensorNetwork,
                shots=10
            )

    def test_quest_rejects_pauli_propagator(self):
        """QuestSim with PauliPropagator raises an error."""
        with pytest.raises(Exception, match="QuestSim only supports Statevector"):
            maestro.simple_execute(
                GENERAL_QASM,
                simulator_type=maestro.SimulatorType.QuestSim,
                simulation_type=maestro.SimulationType.PauliPropagator,
                shots=10
            )

    def test_quest_rejects_mps_estimate(self):
        """QuestSim with MPS on simple_estimate raises an error."""
        with pytest.raises(Exception, match="QuestSim only supports Statevector"):
            maestro.simple_estimate(
                CLIFFORD_BELL_NO_MEASURE_QASM,
                "ZZ",
                simulator_type=maestro.SimulatorType.QuestSim,
                simulation_type=maestro.SimulationType.MatrixProductState,
            )

    @pytest.mark.skipif(
        not maestro.is_quest_available(),
        reason="QuEST library (libmaestroquest) not available"
    )
    def test_quest_execute_bell_state(self):
        """QuestSim executes a Bell state circuit correctly."""
        result = maestro.simple_execute(
            CLIFFORD_BELL_QASM,
            simulator_type=maestro.SimulatorType.QuestSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=1000
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 1000

    @pytest.mark.skipif(
        not maestro.is_quest_available(),
        reason="QuEST library (libmaestroquest) not available"
    )
    def test_quest_estimate_bell_state(self):
        """QuestSim estimates expectation values correctly."""
        result = maestro.simple_estimate(
            CLIFFORD_BELL_NO_MEASURE_QASM,
            "ZZ;XX;YY",
            simulator_type=maestro.SimulatorType.QuestSim,
            simulation_type=maestro.SimulationType.Statevector,
        )
        assert result is not None
        assert 'expectation_values' in result
        exp_vals = result['expectation_values']
        assert len(exp_vals) == 3
        # Bell state: <ZZ> = 1.0, <XX> = 1.0, <YY> = -1.0
        assert exp_vals[0] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[1] == pytest.approx(1.0, abs=1e-5)
        assert exp_vals[2] == pytest.approx(-1.0, abs=1e-5)

    @pytest.mark.skipif(
        not maestro.is_quest_available(),
        reason="QuEST library (libmaestroquest) not available"
    )
    def test_quest_circuit_execute(self):
        """Circuit.execute() with QuestSim works through the network."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)
        qc.measure_all()

        result = qc.execute(
            simulator_type=maestro.SimulatorType.QuestSim,
            simulation_type=maestro.SimulationType.Statevector,
            shots=100
        )
        assert result is not None
        assert 'counts' in result
        total = sum(result['counts'].values())
        assert total == 100

    @pytest.mark.skipif(
        not maestro.is_quest_available(),
        reason="QuEST library (libmaestroquest) not available"
    )
    def test_quest_circuit_estimate(self):
        """Circuit.estimate() with QuestSim produces correct results."""
        from maestro.circuits import QuantumCircuit
        qc = QuantumCircuit()
        qc.h(0)
        qc.cx(0, 1)

        result = qc.estimate(
            observables=["ZZ"],
            simulator_type=maestro.SimulatorType.QuestSim,
            simulation_type=maestro.SimulationType.Statevector,
        )
        assert result is not None
        assert result['expectation_values'][0] == pytest.approx(1.0, abs=1e-5)
