import math

import numpy as np
from qibo import Circuit, gates, hamiltonians
from qibo.symbols import X, Z

from qibotn.backends.cpu import CpuTensorNet
from qibotn.benchmark_cases import (
    build_circuit as build_benchmark_circuit,
    exact_pauli_sum,
)


def build_circuit(nqubits=6):
    circuit = Circuit(nqubits)
    for qubit in range(nqubits):
        circuit.add(gates.RY(qubit, theta=0.1 * (qubit + 1)))
        circuit.add(gates.RZ(qubit, theta=-0.05 * (qubit + 1)))
    for qubit in range(nqubits - 1):
        circuit.add(gates.CNOT(qubit, qubit + 1))
    return circuit


def build_observable(nqubits):
    form = 0
    for qubit in range(nqubits):
        form += 0.5 * X(qubit) * Z((qubit + 1) % nqubits)
    return hamiltonians.SymbolicHamiltonian(form=form)


def test_cpu_generic_tn_expectation_matches_statevector():
    circuit = build_circuit()
    observable = build_observable(circuit.nqubits)
    exact = observable.expectation_from_state(circuit().state(numpy=True))

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": False,
            "NCCL_enabled": False,
            "expectation_enabled": observable,
        }
    )
    value = backend.execute_circuit(circuit)[0]

    assert math.isclose(value, exact, abs_tol=1e-12)


def test_cpu_mps_expectation_matches_statevector():
    circuit = build_circuit()
    observable = build_observable(circuit.nqubits)
    exact = observable.expectation_from_state(circuit().state(numpy=True))

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": True,
            "NCCL_enabled": False,
            "expectation_enabled": observable,
            "max_bond_dimension": 64,
            "tensor_module": "torch",
            "torch_threads": 1,
        }
    )
    value = backend.execute_circuit(circuit)[0]

    assert math.isclose(value, exact, abs_tol=1e-12)


def test_cpu_runcard_pauli_pattern_matches_statevector():
    circuit = build_circuit()
    observable = {"pauli_string_pattern": "IXZ"}
    exact_hamiltonian = hamiltonians.SymbolicHamiltonian(
        form=X(1) * Z(2) * X(4) * Z(5)
    )
    exact = exact_hamiltonian.expectation_from_state(circuit().state(numpy=True))

    for mps_enabled in (False, True):
        backend = CpuTensorNet(
            {
                "MPI_enabled": False,
                "MPS_enabled": mps_enabled,
                "NCCL_enabled": False,
                "expectation_enabled": observable,
                "max_bond_dimension": 64,
                "tensor_module": "torch",
                "torch_threads": 1,
            }
        )
        value = backend.execute_circuit(circuit)[0]

        assert math.isclose(value, exact, abs_tol=1e-12)


def test_cpu_mps_sampling_uses_nshots():
    circuit = Circuit(4)
    circuit.add(gates.H(0))
    for qubit in range(3):
        circuit.add(gates.CNOT(qubit, qubit + 1))

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": True,
            "NCCL_enabled": False,
            "expectation_enabled": False,
        }
    )
    result = backend.execute_circuit(circuit, nshots=100)

    assert sum(result.frequencies().values()) == 100
    assert set(result.frequencies()) <= {"0000", "1111"}


def test_cpu_mps_mpo_expectation_matches_statevector():
    circuit = build_circuit(nqubits=4)
    x = np.array([[0, 1], [1, 0]], dtype=complex)
    z = np.array([[1, 0], [0, -1]], dtype=complex)
    i2 = np.eye(2, dtype=complex)
    mpo = [
        x.reshape(1, 2, 2, 1),
        z.reshape(1, 2, 2, 1),
        i2.reshape(1, 2, 2, 1),
        i2.reshape(1, 2, 2, 1),
    ]
    exact = exact_pauli_sum(circuit, [(1.0, (("X", 0), ("Z", 1)))], 4)

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": True,
            "NCCL_enabled": False,
            "expectation_enabled": {"mpo_tensors": mpo},
            "max_bond_dimension": 64,
            "tensor_module": "torch",
            "torch_threads": 1,
        }
    )
    value = backend.execute_circuit(circuit)[0]

    assert math.isclose(value, exact, abs_tol=1e-12)


def test_cpu_mps_dense_observable_dict_matches_known_value():
    circuit = Circuit(2)
    circuit.add(gates.H(0))
    circuit.add(gates.CNOT(0, 1))

    bell = np.zeros((4, 4), dtype=complex)
    bell[0, 0] = bell[0, 3] = bell[3, 0] = bell[3, 3] = 0.5

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": True,
            "NCCL_enabled": False,
            "expectation_enabled": {"matrix": bell, "qubits": [0, 1]},
            "max_bond_dimension": 16,
            "tensor_module": "torch",
            "torch_threads": 1,
        }
    )
    value = backend.execute_circuit(circuit)[0]

    assert math.isclose(value, 1.0, abs_tol=1e-12)


def test_cpu_generic_tn_long_pauli_string_matches_statevector():
    circuit = build_benchmark_circuit("rxx_rzz", 10, 2, 42)
    observable = {"pauli_string_pattern": "XZ"}
    exact_hamiltonian = hamiltonians.SymbolicHamiltonian(
        form=X(0) * Z(1) * X(2) * Z(3) * X(4) * Z(5) * X(6) * Z(7) * X(8) * Z(9)
    )
    exact = exact_hamiltonian.expectation_from_state(circuit().state(numpy=True))

    backend = CpuTensorNet(
        {
            "MPI_enabled": False,
            "MPS_enabled": False,
            "NCCL_enabled": False,
            "expectation_enabled": observable,
        }
    )
    value = backend.execute_circuit(circuit)[0]

    assert math.isclose(value, exact, abs_tol=1e-12)
