# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import functools

import numpy as np
import pytest

from braket.circuits.quantum_operator_helpers import (
    get_pauli_eigenvalues,
    is_cptp,
    is_hermitian,
    is_square_matrix,
    is_unitary,
    verify_quantum_operator_matrix_dimensions,
)

valid_unitary_hermitian_matrix = np.array([[0, 1], [1, 0]])

valid_CPTP_matrices = [
    np.array([[0, 1], [1, 0]]) / np.sqrt(2),
    np.array([[0, 1], [1, 0]]) / np.sqrt(2),
]

invalid_dimension_matrices = [
    (np.array([[1]])),
    (np.array([1])),
    (np.array([0, 1, 2])),
    (np.array([[0, 1], [1, 2], [3, 4]])),
    (np.array([[0, 1, 2], [2, 3]], dtype=object)),
    (np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])),
]

invalid_unitary_matrices_false = [(np.array([[0, 1], [1, 1]])), (np.array([[1, 2], [3, 4]]))]

invalid_hermitian_matrices_false = [(np.array([[1, 0], [0, 1j]])), (np.array([[1, 2], [3, 4]]))]

invalid_CPTP_matrices_false = [np.array([[1, 0], [0, 1]]), np.array([[0, 1], [1, 0]])]

invalid_matrix_type_error = np.array([[0, 1], ["a", 0]])

z_matrix = np.array([[1, 0], [0, -1]])


def test_verify_quantum_operator_matrix_dimensions():
    assert verify_quantum_operator_matrix_dimensions(valid_unitary_hermitian_matrix) is None


def test_is_unitary_true():
    assert is_unitary(valid_unitary_hermitian_matrix)


def test_is_hermitian_true():
    assert is_hermitian(valid_unitary_hermitian_matrix)


def test_is_cptp_true():
    assert is_cptp(valid_CPTP_matrices)


def test_is_square_matrix():
    assert is_square_matrix(valid_unitary_hermitian_matrix)


@pytest.mark.parametrize("matrix", invalid_dimension_matrices)
def test_verify_quantum_operator_matrix_dimensions_value_error(matrix):
    with pytest.raises(ValueError):
        verify_quantum_operator_matrix_dimensions(matrix)


@pytest.mark.parametrize("matrix", invalid_unitary_matrices_false)
def test_is_unitary_false(matrix):
    assert not is_unitary(matrix)


@pytest.mark.parametrize("matrix", invalid_hermitian_matrices_false)
def test_is_hermitian_false(matrix):
    assert not is_hermitian(matrix)


def test_is_cptp_false():
    assert not is_cptp(invalid_CPTP_matrices_false)


def test_is_hermitian_exception():
    with pytest.raises(Exception):
        is_hermitian(invalid_matrix_type_error)


def test_is_unitary_exception():
    with pytest.raises(Exception):
        is_unitary(invalid_matrix_type_error)


def test_is_cptp_exception():
    with pytest.raises(Exception):
        is_cptp([invalid_matrix_type_error])


def test_get_pauli_eigenvalues_correct_eigenvalues_one_qubit():
    """Test the get_pauli_eigenvalues function for one qubit"""
    assert np.array_equal(get_pauli_eigenvalues(1), np.diag(z_matrix))


def test_get_pauli_eigenvalues_correct_eigenvalues_two_qubits():
    """Test the get_pauli_eigenvalues function for two qubits"""
    assert np.array_equal(get_pauli_eigenvalues(2), np.diag(np.kron(z_matrix, z_matrix)))


def test_get_pauli_eigenvalues_correct_eigenvalues_three_qubits():
    """Test the get_pauli_eigenvalues function for three qubits"""
    assert np.array_equal(
        get_pauli_eigenvalues(3),
        np.diag(np.kron(z_matrix, np.kron(z_matrix, z_matrix))),
    )


@pytest.mark.parametrize("depth", list(range(1, 6)))
def test_get_pauli_eigenvalues_cache_usage(depth):
    """Test that the right number of cachings have been executed after clearing the cache"""
    get_pauli_eigenvalues.cache_clear()
    get_pauli_eigenvalues(depth)
    assert functools._CacheInfo(depth - 1, depth, 128, depth) == get_pauli_eigenvalues.cache_info()


@pytest.mark.parametrize("num_qubits", [1, 2])
def test_get_pauli_eigenvalues_immutable(num_qubits):
    with pytest.raises(ValueError):
        get_pauli_eigenvalues(num_qubits)[0] = 100
