# Copyright (c) [2024] []
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
import pytest
from hypothesis import given
from hypothesis import strategies as st
from numpy.testing import assert_allclose, assert_array_almost_equal

from grogupy.utilities import (
    RotM,
    RotMa2b,
    crossM,
    hsk,
    int_de_ke,
    make_contour,
    make_kset,
    read_siesta_emin,
    tau_0,
    tau_u,
    tau_x,
    tau_y,
    tau_z,
)


# Test basic Pauli matrix properties
def test_pauli_matrices_properties():
    """Test fundamental properties of Pauli matrices"""
    # Test anticommutation relations
    assert_array_almost_equal(tau_x @ tau_y + tau_y @ tau_x, np.zeros((2, 2)))
    assert_array_almost_equal(tau_y @ tau_z + tau_z @ tau_y, np.zeros((2, 2)))
    assert_array_almost_equal(tau_z @ tau_x + tau_x @ tau_z, np.zeros((2, 2)))

    # Test square of Pauli matrices equals identity
    assert_array_almost_equal(tau_x @ tau_x, tau_0)
    assert_array_almost_equal(tau_y @ tau_y, tau_0)
    assert_array_almost_equal(tau_z @ tau_z, tau_0)

    # Test Hermiticity
    assert_array_almost_equal(tau_x, tau_x.conj().T)
    assert_array_almost_equal(tau_y, tau_y.conj().T)
    assert_array_almost_equal(tau_z, tau_z.conj().T)


# Test hsk function
def test_hsk_basics():
    """Test basic properties of the hsk function"""
    size = 4
    H = np.random.random((3, size, size)) + 1j * np.random.random((3, size, size))
    ss = np.random.random((3, size, size)) + 1j * np.random.random((3, size, size))
    sc_off = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
    k = np.array([0.1, 0.2, 0.3])

    HK, SK = hsk(H, ss, sc_off, k)

    # Check shapes
    assert HK.shape == (size, size)
    assert SK.shape == (size, size)

    # Test k=0 case gives the central cell contribution
    HK0, SK0 = hsk(H, ss, sc_off, np.zeros(3))
    assert_array_almost_equal(HK0, H[0])
    assert_array_almost_equal(SK0, ss[0])


@given(st.integers(min_value=1, max_value=20))
def test_make_kset_dimensions(num_k):
    """Test k-point set generation with different dimensions"""
    # Test 1D
    kset_x = make_kset("x", num_k)
    assert kset_x.shape[1] == 3
    assert len(np.unique(kset_x[:, 1])) == 1  # y coordinates should be same
    assert len(np.unique(kset_x[:, 2])) == 1  # z coordinates should be same

    # Test 2D
    kset_xy = make_kset("xy", num_k)
    assert kset_xy.shape[1] == 3
    assert len(np.unique(kset_xy[:, 2])) == 1  # z coordinates should be same

    # Test 3D
    kset_xyz = make_kset("xyz", num_k)
    assert kset_xyz.shape[1] == 3
    assert len(kset_xyz) == num_k**3


def test_make_contour():
    """Test contour generation for energy integration"""
    emin = -20
    emax = 0.0
    enum = 42
    p = 150

    cont = make_contour(emin, emax, enum, p)

    # Check attributes
    assert hasattr(cont, "R")
    assert hasattr(cont, "z0")
    assert hasattr(cont, "ze")
    assert hasattr(cont, "we")
    assert hasattr(cont, "enum")

    # Check dimensions
    assert len(cont.ze) == enum
    assert len(cont.we) == enum

    # Check basic properties
    assert cont.z0 == (emax + emin) / 2
    assert cont.R == (emax - emin) / 2
    assert cont.enum == enum


@given(st.lists(st.floats(min_value=-1, max_value=1), min_size=3, max_size=3))
def test_tau_u(u):
    """Test generation of Pauli matrix in arbitrary direction"""
    u = np.array(u)
    if np.all(u == 0):
        u = np.array([1, 0, 0])  # Handle zero vector case
    u = u / np.linalg.norm(u)

    result = tau_u(u)

    # Check result is 2x2
    assert result.shape == (2, 2)

    # Check Hermiticity
    assert_array_almost_equal(result, result.conj().T)

    # Check trace is zero
    assert abs(np.trace(result)) < 1e-10

    # Check square equals identity
    assert_array_almost_equal(result @ result, np.eye(2))


def test_crossM():
    """Test cross product matrix generation"""
    u = np.array([1.0, 2.0, 3.0])
    v = np.array([4.0, 5.0, 6.0])

    # Test that crossM(u) @ v equals np.cross(u, v)
    assert_array_almost_equal(crossM(u) @ v, np.cross(u, v))

    # Test antisymmetry
    assert_array_almost_equal(crossM(u), -crossM(u).T)


@pytest.mark.parametrize("theta", [0, np.pi / 4, np.pi / 2, np.pi])
def test_RotM(theta):
    """Test rotation matrix generation"""
    # Test rotation around z-axis
    u = np.array([0, 0, 1])
    R = RotM(theta, u)

    # Check orthogonality
    assert_array_almost_equal(R @ R.T, np.eye(3))

    # Check determinant is 1
    assert_allclose(np.linalg.det(R), 1.0)

    # Test rotation of vector
    v = np.array([1, 0, 0])
    rotated_v = R @ v
    expected_v = np.array([np.cos(theta), np.sin(theta), 0])
    assert_array_almost_equal(rotated_v, expected_v)


def test_RotMa2b():
    """Test rotation matrix between two vectors"""
    # Test simple cases
    a = np.array([1, 0, 0])
    b = np.array([0, 1, 0])
    R = RotMa2b(a, b)

    # Check that R rotates a to b
    assert_array_almost_equal(R @ a, b)

    # Check orthogonality
    assert_array_almost_equal(R @ R.T, np.eye(3))

    # Check determinant is 1
    assert_allclose(np.linalg.det(R), 1.0)


def test_read_siesta_emin(tmpdir):
    """Test reading minimum energy from SIESTA .EIG file"""
    # Create mock .EIG file
    eig_file = tmpdir / "test.EIG"
    with open(eig_file, "w") as f:
        # Write mock eigenvalues
        f.write("1.0 2.0 3.0\n-1.0 0.0 1.0\n")

    # Test reading minimum energy
    emin = read_siesta_emin(str(eig_file))
    assert emin == -1.0


def test_int_de_ke():
    """Test energy integral calculation"""
    # Create test data
    energy = np.linspace(-10, 10, 1000)
    traced = np.exp(-(energy**2))  # Gaussian function
    we = np.ones_like(energy)  # Unit weights

    result = int_de_ke(traced, we)

    # Result should be real for real input
    assert np.imag(result) < 1e-10

    # Test with complex input
    traced_complex = traced + 1j * traced
    result_complex = int_de_ke(traced_complex, we)
    assert isinstance(result_complex, complex)


# Property-based tests
@given(st.integers(min_value=2, max_value=10))
def test_make_kset_dimensions_property(num_k):
    """Property-based test for k-point set generation"""
    kset = make_kset("xyz", num_k)

    # All k-points should be in [0,1)
    assert np.all(kset >= 0)
    assert np.all(kset < 1)

    # Shape should be (num_k^3, 3)
    assert kset.shape == (num_k**3, 3)


@given(
    st.floats(min_value=-20, max_value=-1),
    st.floats(min_value=0, max_value=1),
    st.integers(min_value=10, max_value=100),
)
def test_make_contour_properties(emin, emax, enum):
    """Property-based test for contour generation"""
    cont = make_contour(emin, emax, enum, 150)

    # Check basic properties
    assert len(cont.ze) == enum
    assert len(cont.we) == enum
    assert cont.R > 0
    assert cont.z0 == (emax + emin) / 2


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