import warnings
from qiskit import QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer, transpile, IBMQ, assemble
from qiskit.tools.monitor import job_monitor
from qiskit.circuit.library import QFT
from qiskit.visualization import plot_bloch_multivector, plot_histogram, array_to_latex
warnings.filterwarnings("ignore", category=DeprecationWarning)
import numpy as np
import pandas as pd
import math
from math import sqrt

from qiskit.quantum_info import Statevector, random_statevector

from qiskit.circuit.random import random_circuit

from scipy.stats import chi2_contingency, ttest_ind, chisquare

import unittest
import hypothesis.strategies as st
from hypothesis import given, settings

pi = np.pi

def measure_z(circuit, qubit_indexes):
    cBitIndex = 0
    for index in qubit_indexes:
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

def measure_x(circuit, qubitIndexes):
    cBitIndex = 0
    for index in qubitIndexes:
        circuit.h(index)
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

def measure_y(circuit, qubit_indexes):
    cBitIndex = 0
    for index in qubit_indexes:
        circuit.sdg(index)
        circuit.h(index)
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

def flip_endian(dict):
    newdict = {}
    for key in list(dict):
        newdict[key[::-1]] = dict.pop(key)
    return newdict

def set_measure_x(circuit, n):
    for num in range(n):
        circuit.h(num)

def set_measure_y(circuit, n):
    for num in range(n):
        circuit.sdg(num)
        circuit.h(num)

def qft_rotations(circuit, n):
    #if qubit amount is 0, then do nothing and return
    if n == 0:
        #set it to measure the x axis
        set_measure_x(qc, 2)
        qc.measure_all()
        return circuit
    n -= 1
    circuit.h(n)
    for qubit in range(n):
        circuit.cp(pi/2**(n-qubit), qubit, n)
    return qft_rotations(circuit, n)

backend = Aer.get_backend('aer_simulator') 
qc = QuantumCircuit(2)
qc.x(0)
circ = qft_rotations(qc,2)#call the recursive qft method
print(circ)
#set it to measure the x axis
set_measure_x(qc, 2)

job = execute(qc, backend, shots=100000)#run the circuit 1000000 times
print(flip_endian(job.result().get_counts()))#return the result counts

def assertEntangled(backend,qc,qubits_to_assert,measurements_to_make,alpha = 0.05):
    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    ## classical register must be of same length as amount of qubits to assert
    ## if there is no classical register add them according to length of qubit list
    if (qc.num_clbits == 0):
        qc.add_register(ClassicalRegister(len(qubits_to_assert)))
    elif (len(qubits_to_assert) != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")
    
    zQuantumCircuit = measure_z(qc, qubits_to_assert)
    zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    zMemory = zJob.result().get_memory()
    q1=[]
    q2=[]
    qubitDict = dict.fromkeys(['qubit1','qubit2'])
    qubitDict = {'qubit1': qubits_to_assert[0],'qubit2':qubits_to_assert[1]}
    print("new dict", qubitDict)
    classicalQubitIndex = 1
    for qubit in qubitDict.keys():
        print("this qubit:",qubit)
        for measurement in zMemory:
            # print("this measurement", measurement)
            if (measurement[2-classicalQubitIndex] == '0'):
                # print("measure: ",measurement[2-classicalQubitIndex],"also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1])    
                if(qubit=='qubit1'):
                    q1.append(measurement[2-classicalQubitIndex])
                    # print("Added to q1 for measure0:", measurement[2-classicalQubitIndex])
                else:
                    q2.append(measurement[2-classicalQubitIndex])
                    # print("Added to q2 for measure0:", measurement[2-classicalQubitIndex])
            else:
                # print("measureOTHER: ",measurement[2-classicalQubitIndex], "also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1]) 
                if(qubit=='qubit1'):
                    q1.append(measurement[2-classicalQubitIndex])
                    # print("Added to q1 for measure1:", measurement[2-classicalQubitIndex])
                else:
                    q2.append(measurement[2-classicalQubitIndex])    
                    # print("Added to q2 for measure1:", measurement[2-classicalQubitIndex])               
        classicalQubitIndex+=1

    measDict = dict.fromkeys(['qubit1','qubit2'])
    measDict = {'qubit1': q1,'qubit2':q2}
    measDf1 = pd.DataFrame.from_dict(measDict,orient='index')
    measDf12=measDf1.transpose()
    print(measDf12)
    ct = pd.crosstab(measDf12.qubit1,measDf12.qubit2)
    chiVal, pVal, dOfFreedom, exp = chi2_contingency(ct)
    print("chi square value: ",chiVal,"p value: ",pVal,"expected values: ",exp)
    if(pVal>alpha):
        raise(AssertionError("states are not entangled"))
    else:
        print("states are entangled")
        
backend = Aer.get_backend('aer_simulator')
qr = QuantumRegister(2)
cr=ClassicalRegister(2)
qc3 = QuantumCircuit(qr,cr)
# qc3.x(1)
qc3.x(0)
qc3.h(0)
qc3.cnot(0,1)
# qc3.rx(np.pi/2,qr[0])
qc3.p(10*2*math.pi/100, 0)
# qc3.p(0.5*2*math.pi/100, 1)
# print(qc3)
assertEntangled(backend,qc3,[0,1],2000,0.05)

# circuit = QuantumCircuit(2)
# circuit.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], circuit.qubits)
# assertEntangled(backend,circuit,[0,1],10,0.05)

def getDf(qc,qubits_to_assert,measurements_to_make,backend):

    ## classical register must be of same length as amount of qubits to assert
    ## if there is no classical register add them according to length of qubit list
    if (qc.num_clbits == 0):
        qc.add_register(ClassicalRegister(len(qubits_to_assert)))
    elif (len(qubits_to_assert) != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")

    ## divide measurements to make by 3 as we need to run measurements twice, one for x and one for y
    measurements_to_make = measurements_to_make // 3
    yQuantumCircuit = measure_y(qc.copy(), qubits_to_assert)
    xQuantumCircuit = measure_x(qc.copy(), qubits_to_assert)
    zQuantumCircuit = measure_z(qc, qubits_to_assert)

    yJob = execute(yQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    yMemory = yJob.result().get_memory()
    yCounts = yJob.result().get_counts()

        ## get x axis results
    xJob = execute(xQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    xMemory = xJob.result().get_memory()
    xCounts = xJob.result().get_counts()
    ## get z axis results
    zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    zMemory = zJob.result().get_memory()
    zCounts = zJob.result().get_counts()

    resDf = pd.DataFrame(columns=['0','1','+','i','-','-i'])
    classical_qubit_index = 1
    for qubit in qubits_to_assert:
        zero_amount, one_amount, plus_amount, i_amount, minus_amount, minus_i_amount = 0,0,0,0,0,0
        for experiment in xCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                plus_amount += xCounts[experiment]
            else:
                minus_amount += xCounts[experiment]
        for experiment in yCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                i_amount += yCounts[experiment]
            else:
                minus_i_amount += yCounts[experiment]
        for experiment in zCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                zero_amount += zCounts[experiment]
            else:
                one_amount += zCounts[experiment]
        df = {'0':zero_amount, '1':one_amount,
              '+':plus_amount, 'i':i_amount,
              '-':minus_amount,'-i':minus_i_amount}

        resDf = resDf.append(df, ignore_index = True)
        classical_qubit_index+=1

    resDf['+'] = resDf['+'].astype(int)
    resDf['i'] = resDf['i'].astype(int)
    resDf['-'] = resDf['-'].astype(int)
    resDf['-i'] = resDf['-i'].astype(int)
    resDf['0'] = resDf['0'].astype(int)
    resDf['1'] = resDf['1'].astype(int)
    return resDf


# Completed but more testing required
## assert that qubits are equal
def assertEqual(backend, quantumCircuit, qubits_to_assert, measurements_to_make, alpha):
    ## needs to make at least 2 measurements, one for x axis, one for y axis
    ## realistically we need more for any statistical significance
    if (measurements_to_make < 2):
        raise ValueError("Must make at least 2 measurements")

    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    # Get Df is a function I made to return a dataframe containing measurements for each each qubit across all axes and I use 5 such different measurements for my dataset
    resDf1 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
    resDf2 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
    resDf3 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
    resDf4 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
    resDf5 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)

    q1Vals = []
    q2Vals = []
    q1Vals.extend([resDf1.at[0,'1'],resDf1.at[0,'-'],resDf1.at[0,'-i'], resDf2.at[0,'1'],resDf2.at[0,'-'],resDf2.at[0,'-i'], resDf3.at[0,'1'],resDf3.at[0,'-'],resDf3.at[0,'-i'],  resDf4.at[0,'1'],resDf4.at[0,'-'],resDf4.at[0,'-i'], resDf5.at[0,'1'],resDf5.at[0,'-'],resDf5.at[0,'-i']])
    print(q1Vals)
    q2Vals.extend([resDf1.at[1,'1'],resDf1.at[1,'-'],resDf1.at[1,'-i'], resDf2.at[1,'1'],resDf2.at[1,'-'],resDf2.at[1,'-i'],resDf3.at[1,'1'],resDf3.at[1,'-'],resDf3.at[1,'-i'],resDf4.at[1,'1'],resDf4.at[1,'-'],resDf4.at[1,'-i'],resDf5.at[1,'1'],resDf5.at[1,'-'],resDf5.at[1,'-i']  ])
    print(q2Vals)

    tTest, pValue = ttest_ind(q1Vals, q2Vals, alternative = 'two-sided') # Apply t test
    print("stat: ",tTest, "pValue: ", pValue)
    if pValue > alpha:
        print("The two qubits are equal (fail to reject null hypothesis) ")
    else:
        print("There is a significant difference between the two qubits (reject null hypothesis)")

qc = QuantumCircuit(2)
backend = Aer.get_backend('aer_simulator')
# qc.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], qc.qubits)
qc.h(0)
# qc.cnot(0,1)
qc.x(1)
qc.p(0.5*2*math.pi/100, 1)
# qc.h(1)
# qc.p(10*2*math.pi/100, 0)
# qc.p(20*2*math.pi/100, 1)
# assertEqual(backend, qc, [0,1], 300000, 0.05)
# assertEqual(backend, qc, [0,0], 300000, 0.05)

# Complete needs to be checked

# Assertion to check if expected probability of viewing a particular qubitstate is  to its actual probaility
# Qubitchoice is an optional argument that if passed will only compare the expected probabilty with the probability of observing
# that particular qubit (first or second) in the desired qubitState for the qc 
def assertProbability(qc, qubitState :str, expectedProbability, qubitChoice=None):
    sv = Statevector.from_label("00") # Creates a statevector with states 00
    evl = sv.evolve(qc) # Passes the qc into the statevector in order to evolve 
    # Performs a check to observe if qubitChoice has been passed or not 
    if(qubitChoice!=None):
        probs = evl.probabilities_dict([qubitChoice]) # If passed we will get the probabilities for that particular qubit
    else:
        probs = evl.probabilities_dict()
    probsRound = {key: round(values,2) for key,values in probs.items()} # rounds off the probabilities in the dictionary

    # Loops over the prob dictionary with rounded values 
    for key,value in probsRound.items():
        if(key==qubitState):
            if(value==expectedProbability):
                print("Expected Probability present")
                return True
            else:
                raise(AssertionError("Probability not present"))
    raise(AssertionError("Probability not present or Desired state has no probability"))
            

qc = QuantumCircuit(2)
qc.h(0)
assertProbability(qc,"0",1,1)
print("------")
assertProbability(qc,"00",0.5)

circuit = QuantumCircuit(2)
circuit.initialize([1, 0], 0)
circuit.initialize(random_statevector(2).data, 1)
print("another",circuit.global_phase)
# print(circuit)

qasm_circuit = circuit.decompose().decompose().decompose()
print(type(qasm_circuit))
print("here",qasm_circuit._global_phase)

qc = QuantumCircuit(2)   
initial_state = qc.initialize(random_statevector(2).data, 0)
initial_state1 = qc.initialize(random_statevector(2).data, 1)
# print(qc)

qasm_circuit1 = qc.decompose().decompose().decompose()
print(qasm_circuit1)

import random
def generateQC(theta_range,phi_range,lam_range):
    # make sure theta phi and lamba are all in lists
    # make sure they have size of 2
    lists = [theta_range,phi_range,lam_range]
    for i in lists:
        if (not isinstance(i, list)):
            i = [i]
        if len(i)!=2:
            raise ValueError("Range has to be two")
    qc = QuantumCircuit(2)
    for i in range(2):
        theta=random.randint(theta_range[0],theta_range[1])
        phi=random.randint(phi_range[0],phi_range[1])
        lam=random.randint(lam_range[0],lam_range[1])
        qc.u(theta,phi,lam,i)
    return qc

# In Qsharp qubit initialisation was perfromed as such: 
# { q : Qubit (36 ,72) (0 ,360) };
first = generateQC([36,72],[0,360],[0,360]) 
# QUBITS TO ASSERT PROBLEM HOW WILL I KNOW IF QUBIT IS 0 OR 1

print(first)

class property:
    def __init__(self,backend,theta_range,phi_range,lam_range,which_assertion,measurements_to_make,alpha,experiments,noOfTests):
        self.backend = backend
        self.theta_range = theta_range
        self.phi_range = phi_range
        self.lam_range = lam_range
        # self.predicate = predicate
        self.which_assertion = which_assertion
        self.measurements_to_make = measurements_to_make
        self.alpha = alpha
        self.experiments = experiments
        self.noOfTests = noOfTests

    def generateQC(self):
        # make sure theta phi and lamba are all in lists
        # make sure they have size of 2
        lists = [self.theta_range,self.phi_range,self.lam_range]
        for i in lists:
            if (not isinstance(i, list)):
                i = [i]
            if len(i)!=2:
                raise ValueError("Range has to be two")
        qc = QuantumCircuit(2)
        for i in range(2):
            theta=random.randint(self.theta_range[0],self.theta_range[1])
            phi=random.randint(self.phi_range[0],self.phi_range[1])
            lam=random.randint(self.lam_range[0],self.lam_range[1])
            qc.u(theta,phi,lam,i)
        return qc
    
    def run(self, qcToTest):
        # assertEntangled(backend,qc3,[1,0],2000,0.05)
        # assertEqual(backend, qc, [0,1], 300000, 3, 0.05)
        # qcToTest = generateQC(theta_range)
        # if(which_assertion=assertEntangled)
        return self.which_assertion(backend,qcToTest,[0,1], self.measurements_to_make,self.experiments,self.alpha)

    # number_of_test_cases = 7
    # number_of_measurements = 2000
    # number_of_experiments = 20
    # for tc in range(testcases):, testcases
    #   qc = new qc()
    #   for e in range(experiment):, experiments
        #   AssertEntangles(qc), measurements done here
    
    def check(self):
        for j in range(self.noOfTests):
            qcToTest = generateQC(self.theta_range,self.phi_range,self.lam_range)
            initial_state = qcToTest.copy()
            # final_state = 
            # print(qcToTest)
            # for i in range(self.experiments):
            try:
                self.run(qcToTest)
            except AssertionError:
                raise AssertionError("Property failed after run", self.noOfTests * j)


pbt = property(backend,[36,72],[0,360],[0,360],assertEntangled,2000,0.05,10,3)
pbt.check()
pbt2 = property(backend,[36,72],[0,360],[0,360],assertEqual,2000,0.05,10,3)
pbt2.check()

#pass created qubits into whichever function
#then pass that into assertions

class property:
    def __init__(self,backend,theta_range,phi_range,lam_range,which_assertion,measurements_to_make,alpha,experiments,noOfTests):
        self.backend = backend
        self.theta_range = theta_range
        self.phi_range = phi_range
        self.lam_range = lam_range
        # self.predicate = predicate
        self.which_assertion = which_assertion
        self.measurements_to_make = measurements_to_make
        self.alpha = alpha
        self.experiments = experiments
        self.noOfTests = noOfTests

    def generateQC(self):
        # make sure theta phi and lamba are all in lists
        # make sure they have size of 2
        lists = [self.theta_range,self.phi_range,self.lam_range]
        for i in lists:
            if (not isinstance(i, list)):
                i = [i]
            if len(i)!=2:
                raise ValueError("Range has to be two")
        qc = QuantumCircuit(2)
        for i in range(2):
            theta=random.randint(self.theta_range[0],self.theta_range[1])
            phi=random.randint(self.phi_range[0],self.phi_range[1])
            lam=random.randint(self.lam_range[0],self.lam_range[1])
            qc.u(theta,phi,lam,i)
        return qc
    
    def run(self, qcToTest):
        # assertEntangled(backend,qc3,[1,0],2000,0.05)
        # assertEqual(backend, qc, [0,1], 300000, 3, 0.05)
        # qcToTest = generateQC(theta_range)
        # if(which_assertion=assertEntangled)
        return self.which_assertion(backend,qcToTest,[0,0], self.measurements_to_make,self.experiments,self.alpha)

    # number_of_test_cases = 7
    # number_of_measurements = 2000
    # number_of_experiments = 20
    # for tc in range(testcases):, testcases
    #   qc = new qc()
    #   for e in range(experiment):, experiments
        #   AssertEntangles(qc), measurements done here
    
    def check(self):
        for j in range(self.noOfTests):
            qcToTest = generateQC(self.theta_range,self.phi_range,self.lam_range)
            # print(qcToTest)
            # for i in range(self.experiments):
            try:
                self.run(qcToTest)
            except AssertionError:
                raise AssertionError("Property failed after run", self.noOfTests * j)


pbt = property(backend,[36,72],[0,360],[0,360],assertEntangled,2000,0.05,10,3)
pbt.check()
pbt2 = property(backend,[36,72],[0,360],[0,360],assertEqual,2000,0.05,10,3)
pbt2.check()

#pass created qubits into whichever function
#then pass that into assertions

def assertEntangled(backend,qc,qubits_to_assert,measurements_to_make,experiments,alpha = 0.05):
    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    ## classical register must be of same length as amount of qubits to assert
    ## if there is no classical register add them according to length of qubit list
    if (qc.num_clbits == 0):
        qc.add_register(ClassicalRegister(len(qubits_to_assert)))
    elif (len(qubits_to_assert) != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")

    q1=[]
    q2=[]
    for i in range (experiments):
        zQuantumCircuit = measure_z(qc, qubits_to_assert)
        zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
        zMemory = zJob.result().get_memory()
        qubitDict = dict.fromkeys(['qubit1','qubit2'])
        qubitDict = {'qubit1': qubits_to_assert[0],'qubit2':qubits_to_assert[1]}
        # print("new dict", qubitDict)
        classicalQubitIndex = 1
        for qubit in qubitDict.keys():
            # print("this qubit:",qubit)
            for measurement in zMemory:
                # print("this measurement", measurement)
                if (measurement[2-classicalQubitIndex] == '0'):
                    # print("measure: ",measurement[2-classicalQubitIndex],"also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1])    
                    if(qubit=='qubit1'):
                        q1.append(measurement[2-classicalQubitIndex])
                        # print("Added to q1 for measure0:", measurement[2-classicalQubitIndex])
                    else:
                        q2.append(measurement[2-classicalQubitIndex])
                        # print("Added to q2 for measure0:", measurement[2-classicalQubitIndex])
                else:
                    # print("measureOTHER: ",measurement[2-classicalQubitIndex], "also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1]) 
                    if(qubit=='qubit1'):
                        q1.append(measurement[2-classicalQubitIndex])
                        # print("Added to q1 for measure1:", measurement[2-classicalQubitIndex])
                    else:
                        q2.append(measurement[2-classicalQubitIndex])    
                        # print("Added to q2 for measure1:", measurement[2-classicalQubitIndex])               
            classicalQubitIndex+=1

    measDict = dict.fromkeys(['qubit1','qubit2'])
    measDict = {'qubit1': q1,'qubit2':q2}
    measDf1 = pd.DataFrame.from_dict(measDict,orient='index')
    measDf12=measDf1.transpose()
    print(measDf12)
    # print(measDf12)
    ct = pd.crosstab(measDf12.qubit1,measDf12.qubit2)
    chiVal, pVal, dOfFreedom, exp = chi2_contingency(ct)
    print("chi square value: ",chiVal,"p value: ",pVal,"expected values: ",exp)
    if(pVal>alpha):
        raise(AssertionError("states are not entangled"))
    else:
        print("states are entangled")
        
backend = Aer.get_backend('aer_simulator')
qr = QuantumRegister(2)
cr=ClassicalRegister(2)
qc3 = QuantumCircuit(qr,cr)
# qc3.x(1)
qc3.x(0)
qc3.h(0)
qc3.cnot(0,1)
# qc3.rx(np.pi/2,qr[0])
qc3.p(10*2*math.pi/100, 0)
# qc3.p(0.5*2*math.pi/100, 1)
# print(qc3)
qc4 = QuantumCircuit(2)
qc4.u(64,343,57,0)
qc4.u(65,43,226,1)

qc1 = QuantumCircuit(2)
qc1.x(0)
qc1.x(1)
circ = qft_rotations(qc1,2)
print(circ)
assertEntangled(backend,circ,[1,1],2000,1,0.05)



# Completed but more testing required
## assert that qubits are equal
def assertEqual(backend, quantumCircuit, qubits_to_assert, measurements_to_make, experiments, alpha):
    ## needs to make at least 2 measurements, one for x axis, one for y axis
    ## realistically we need more for any statistical significance
    if (measurements_to_make < 2):
        raise ValueError("Must make at least 2 measurements")

    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]
    
    q1Vals = []
    q2Vals = []

    # Get Df is a function I made to return a dataframe containing measurements for each each qubit across all axes and I use 5 such different measurements for my dataset
    for i in range(experiments):
        resDf1 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
        q1Vals.extend([resDf1.at[0,'1'],resDf1.at[0,'-'],resDf1.at[0,'-i']])
        q2Vals.extend([resDf1.at[1,'1'],resDf1.at[1,'-'],resDf1.at[1,'-i']])

    print(q1Vals)
    print(q2Vals)
    tTest, pValue = ttest_ind(q1Vals, q2Vals, alternative = 'two-sided') # Apply t test
    print("stat: ",tTest, "pValue: ", pValue)
    if pValue > alpha:
        print("The two qubits are equal (fail to reject null hypothesis) ")
    else:
        print("There is a significant difference between the two qubits (reject null hypothesis)")

qc = QuantumCircuit(2)
backend = Aer.get_backend('aer_simulator')
# qc.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], qc.qubits)
qc.h(0)
# qc.cnot(0,1)
qc.x(1)
# qc.p(0.5*2*math.pi/100, 1)
# qc.h(1)
# qc.p(10*2*math.pi/100, 0)
# qc.p(20*2*math.pi/100, 1)
# assertEqual(backend, qc, [0,1], 300000, 3, 0.05)
# assertEqual(backend, qc, [0,0], 300000, 0.05)

qc1 = QuantumCircuit(2)
# qc1.x(0)
qc1.h(0)
qc1.cnot(0,1)
# circ = qft_rotations(qc1,2)
assertEqual(backend, qc, [0,1], 300000, 10, 0.05)
# assertEntangled(backend,circ,[1,0],3000,10,0.05)

# Completed but more testing required
## assert that qubits are equal
def assertEqual(backend, quantumCircuit, qubits_to_assert, measurements_to_make, experiments, alpha):
    ## needs to make at least 2 measurements, one for x axis, one for y axis
    ## realistically we need more for any statistical significance
    if (measurements_to_make < 2):
        raise ValueError("Must make at least 2 measurements")

    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    q1Vals = []
    q2Vals = []
    # Get Df is a function I made to return a dataframe containing measurements for each each qubit across all axes and I use 5 such different measurements for my dataset
    for i in range(experiments):
        # resDf1 = getDf(quantumCircuit,qubits_to_assert,measurements_to_make,backend)
        # q1Vals.extend([resDf1.at[0,'1'],resDf1.at[0,'-'],resDf1.at[0,'-i']])
        # q2Vals.extend([resDf1.at[1,'1'],resDf1.at[1,'-'],resDf1.at[1,'-i']])
        zQuantumCircuit = measure_z(quantumCircuit,qubits_to_assert)
        zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
        # zMemory = zJob.result().get_memory()
        zCounts = zJob.result().get_counts()
        # print(i,zCounts)
        # for k,v in zCounts.items():
        #     print(i,k,v)
        #     if(i==0):
        #         q1Vals.extend([v])
        #     else:
        #         q2Vals.extend([v])  
        resDf = pd.DataFrame(columns=['0','1'])
        classical_qubit_index = 1
        for qubit in qubits_to_assert:
            zero_amount, one_amount = 0,0
            for experiment in zCounts:
                if (experiment[2-classical_qubit_index] == '0'):
                    zero_amount += zCounts[experiment]
                else:
                    one_amount += zCounts[experiment]
            # print(i,"0:",zero_amount)
            # print(i,"1:",one_amount)
            df = {'0':zero_amount, '1':one_amount}
            resDf = resDf.append(df, ignore_index = True)
            classical_qubit_index+=1
        resDf['0'] = resDf['0'].astype(int)
        resDf['1'] = resDf['1'].astype(int)
        q1Vals.extend([resDf.at[0,'1']/measurements_to_make])
        q2Vals.extend([resDf.at[1,'1']/measurements_to_make])
        # print("here",q1Vals)
        # print("here",q2Vals)
    equalTest(q1Vals,q2Vals,alpha)  

def equalTest(q1Vals,q2Vals,alpha):
    # q1Vals.extend([resDf.at[0,'1'],resDf.at[0,'0']])
    # q2Vals.extend([resDf1.at[1,'1'],resDf1.at[1,'-'],resDf1.at[1,'-i']])
    # q2Vals.extend([resDf.at[1,'1'],resDf.at[1,'0']])
    print("q1",q1Vals)
    print("q2",q2Vals)
    tTest, pValue = ttest_ind(q1Vals, q2Vals, alternative = 'two-sided') # Apply t test
    print("stat: ",tTest, "pValue: ", pValue)
    if pValue > alpha:
        print("The two qubits are equal (fail to reject null hypothesis) ")
    else:
        print("There is a significant difference between the two qubits (reject null hypothesis)")

qc = QuantumCircuit(2,2)
backend = Aer.get_backend('aer_simulator')
# qc.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], qc.qubits)
qc.h(0)
qc.x(1)
qc.h(1)
# qc.cnot(0,1)
# qc.x(1)
qc.p(0.5*2*math.pi/100, 1)
# qc.h(1)
# qc.p(10*2*math.pi/100, 0)
# qc.p(20*2*math.pi/100, 1)
# assertEqual(backend, qc, [0,1], 300000, 3, 0.05)
# assertEqual(backend, qc, [0,0], 300000, 0.05)

qc1 = QuantumCircuit(2)
# qc1.x(0)
qc1.x(0)
qc1.cnot(0,1)
# circ = qft_rotations(qc1,2)
assertEqual(backend, qc, [0,1], 300000, 30, 0.05)
# assertEntangled(backend,circ,[1,0],3000,10,0.05) 

