import math
import random

import pytest
from qibo import Circuit, construct_backend, gates, hamiltonians
from qibo.symbols import X, Z


def build_observable(nqubits):
    """Helper function to construct a target observable."""
    hamiltonian_form = 0
    for i in range(nqubits):
        hamiltonian_form += 0.5 * X(i % nqubits) * Z((i + 1) % nqubits)

    hamiltonian = hamiltonians.SymbolicHamiltonian(form=hamiltonian_form)
    return hamiltonian, hamiltonian_form


def build_circuit(nqubits, nlayers, seed=42):
    """Helper function to construct a layered quantum circuit."""
    random.seed(seed)

    circ = Circuit(nqubits)
    for _ in range(nlayers):
        for q in range(nqubits):
            circ.add(gates.RY(q=q, theta=random.uniform(-math.pi, math.pi)))
            circ.add(gates.RZ(q=q, theta=random.uniform(-math.pi, math.pi)))
        [circ.add(gates.CNOT(q % nqubits, (q + 1) % nqubits) for q in range(nqubits))]
    circ.add(gates.M(*range(nqubits)))
    return circ


@pytest.mark.parametrize("nqubits", [2, 5, 10])
def test_observable_expval(backend, nqubits):
    numpy_backend = construct_backend("numpy")
    ham, ham_form = build_observable(nqubits)
    circ = build_circuit(nqubits=nqubits, nlayers=1)
    
    exact_expval = numpy_backend.calculate_expectation_state(
        hamiltonian=ham,
        state=circ().state(),
        normalize=False,
    )

    tn_expval = backend.expectation(circuit=circ, observable=ham_form)

    assert math.isclose(exact_expval, tn_expval, abs_tol=1e-7)
