"""
Integration tests for GCONCORD package.

Tests the complete Python interface and ensures backward compatibility.
"""

import pytest
import numpy as np
import scipy.sparse
import gconcord as gc


class TestBasicFunctionality:
    """Test basic functionality and interface."""
    
    def setup_method(self):
        """Set up test data."""
        np.random.seed(42)
        self.p = 5
        # Create a simple test covariance matrix
        A = np.random.randn(self.p, self.p)
        self.S = A @ A.T + 0.1 * np.eye(self.p)
        self.lambda_scalar = 0.1
        
    def test_package_import(self):
        """Test that package imports correctly."""
        assert hasattr(gc, 'concord')
        assert hasattr(gc, 'symlasso')
        assert hasattr(gc, 'ccista')
        assert hasattr(gc, '_cpp_available')
        
    def test_concord_basic(self):
        """Test basic concord functionality."""
        result = gc.concord(self.S, self.lambda_scalar)
        
        # Check return type and structure
        assert isinstance(result, dict)
        assert 'omega' in result
        assert 'converged' in result
        assert 'iterations' in result
        assert 'algorithm_used' in result
        
        # Check omega properties
        omega = result['omega']
        assert scipy.sparse.issparse(omega)
        assert omega.shape == (self.p, self.p)
        
        # Check convergence
        assert isinstance(result['converged'], bool)
        assert isinstance(result['iterations'], int)
        
    def test_symlasso_basic(self):
        """Test basic symlasso functionality."""
        result = gc.symlasso(self.S, self.lambda_scalar)
        
        # Check return structure 
        assert isinstance(result, dict)
        assert 'omega' in result
        assert scipy.sparse.issparse(result['omega'])
        assert result['omega'].shape == (self.p, self.p)
        
    def test_ccista_backward_compatibility(self):
        """Test ccista backward compatibility."""
        omega, hist = gc.ccista(self.S, self.lambda_scalar)
        
        # Check return types
        assert scipy.sparse.issparse(omega)
        assert isinstance(hist, np.ndarray)
        assert omega.shape == (self.p, self.p)
        assert hist.ndim == 2
        assert hist.shape[1] == 3  # [inner_iter, delta_subg, objective]


class TestAlgorithmSelection:
    """Test algorithm selection functionality."""
    
    def setup_method(self):
        """Set up test data."""
        np.random.seed(42)
        self.p = 10
        A = np.random.randn(self.p, self.p)
        self.S = A @ A.T + 0.1 * np.eye(self.p)
        self.lambda_val = 0.05
        
    def test_algorithm_strings(self):
        """Test different algorithm string options."""
        algorithms = ['auto', 'coordinate', 'ista']
        
        for alg in algorithms:
            result = gc.concord(self.S, self.lambda_val, algorithm=alg)
            assert result['converged'] in [True, False]
            assert isinstance(result['iterations'], int)
            
    def test_symlasso_algorithms(self):
        """Test symmetric lasso algorithm variants."""
        algorithms = ['auto', 'symlasso', 'symlasso_iterates']
        
        for alg in algorithms:
            result = gc.symlasso(self.S, self.lambda_val, algorithm=alg)
            assert result['converged'] in [True, False]
            assert isinstance(result['iterations'], int)


class TestInputValidation:
    """Test input validation and error handling."""
    
    def setup_method(self):
        """Set up test data."""
        self.valid_S = np.eye(3)
        self.lambda_val = 0.1
        
    def test_invalid_S_shape(self):
        """Test error on invalid S matrix shape."""
        with pytest.raises(ValueError, match="square matrix"):
            gc.concord(np.random.randn(3, 4), self.lambda_val)
            
    def test_invalid_S_symmetry(self):
        """Test error on non-symmetric S matrix."""
        S_nonsym = np.random.randn(3, 3)
        with pytest.raises(ValueError, match="symmetric"):
            gc.concord(S_nonsym, self.lambda_val)
            
    def test_invalid_algorithm(self):
        """Test error on invalid algorithm string."""
        with pytest.raises(ValueError, match="Unknown algorithm"):
            gc.concord(self.valid_S, self.lambda_val, algorithm='invalid')
            
    def test_invalid_lambda_type(self):
        """Test error on invalid lambda type."""
        with pytest.raises(TypeError, match="must be a scalar or array"):
            gc.concord(self.valid_S, "invalid")


class TestAdvancedFeatures:
    """Test advanced features and options."""
    
    def setup_method(self):
        """Set up test data."""
        np.random.seed(42)
        self.p = 8
        A = np.random.randn(self.p, self.p)
        self.S = A @ A.T + 0.1 * np.eye(self.p)
        self.lambda_val = 0.1
        
    def test_save_iterates(self):
        """Test save_iterates functionality."""
        result = gc.concord(self.S, self.lambda_val, save_iterates=True)
        
        assert 'objective_values' in result
        assert 'convergence_metrics' in result
        
        if result['objective_values'] is not None:
            assert isinstance(result['objective_values'], list)
            assert len(result['objective_values']) > 0
            
    def test_matrix_lambda(self):
        """Test matrix-valued lambda parameter."""
        Lambda = 0.1 * np.ones((self.p, self.p))
        np.fill_diagonal(Lambda, 0)  # Don't penalize diagonal
        
        result = gc.concord(self.S, Lambda)
        assert result['omega'].shape == (self.p, self.p)
        
    def test_tolerance_settings(self):
        """Test different tolerance settings."""
        # Loose tolerance should converge faster
        result_loose = gc.concord(self.S, self.lambda_val, tolerance=1e-2)
        result_tight = gc.concord(self.S, self.lambda_val, tolerance=1e-8)
        
        # Loose tolerance typically needs fewer iterations
        if result_loose['converged'] and result_tight['converged']:
            assert result_loose['iterations'] <= result_tight['iterations']


class TestMathematicalProperties:
    """Test mathematical properties of solutions."""
    
    def setup_method(self):
        """Set up test data."""
        np.random.seed(42)
        self.p = 6
        A = np.random.randn(self.p, self.p)
        self.S = A @ A.T + 0.1 * np.eye(self.p)
        self.lambda_val = 0.05
        
    def test_solution_symmetry(self):
        """Test that solutions are symmetric."""
        result = gc.concord(self.S, self.lambda_val)
        omega_dense = result['omega'].toarray()
        
        np.testing.assert_allclose(omega_dense, omega_dense.T, rtol=1e-10)
        
    def test_sparsity_pattern(self):
        """Test that larger lambda produces sparser solutions."""
        result_small = gc.concord(self.S, 0.01)
        result_large = gc.concord(self.S, 0.5)
        
        nnz_small = result_small['omega'].nnz
        nnz_large = result_large['omega'].nnz
        
        # Larger lambda should produce sparser solutions
        assert nnz_large <= nnz_small
        
    def test_diagonal_positivity(self):
        """Test that diagonal elements are positive."""
        result = gc.concord(self.S, self.lambda_val)
        omega_dense = result['omega'].toarray()
        
        assert np.all(np.diag(omega_dense) > 0)


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