"""
Asymmetric Causality Test Module

This module implements the asymmetric causality test developed by Hatemi-J (2012).
The test allows for testing causal relationships between positive and negative 
components of variables separately, which is important when economic agents react 
differently to positive vs. negative shocks.

Reference:
    Hatemi-J, A. (2012). Asymmetric Causality Tests with an Application.
    Empirical Economics, 43(1), 447-456.

Original GAUSS code by Abdulnasser Hatemi-J and Scott Hacker.
Python implementation by Dr. Merwan Roudane.

License: MIT
"""

import numpy as np
from numpy.linalg import inv, det
from scipy.stats import chi2
from typing import Tuple, Dict, Optional, Union, List
from dataclasses import dataclass
import warnings


@dataclass
class AsymmetricCausalityResult:
    """
    Data class to store asymmetric causality test results.
    
    Attributes
    ----------
    wald_stat : float
        The Wald test statistic.
    bootstrap_cv : np.ndarray
        Bootstrap critical values at 1%, 5%, and 10% significance levels.
    optimal_lag : int
        Optimal lag order selected by information criterion.
    p_value_chi2 : float
        P-value based on chi-squared distribution.
    reject_null : Dict[str, bool]
        Dictionary indicating whether null hypothesis is rejected at each level.
    component_type : str
        Type of component tested ('positive', 'negative', 'positive_to_negative', 
        'negative_to_positive', or 'original').
    variable_names : Tuple[str, str]
        Names of the variables (cause, effect).
    info_criterion : str
        Information criterion used for lag selection.
    """
    wald_stat: float
    bootstrap_cv: np.ndarray
    optimal_lag: int
    p_value_chi2: float
    reject_null: Dict[str, bool]
    component_type: str
    variable_names: Tuple[str, str]
    info_criterion: str
    
    def __repr__(self) -> str:
        return (
            f"AsymmetricCausalityResult(\n"
            f"  Component: {self.component_type}\n"
            f"  H0: {self.variable_names[0]} does not cause {self.variable_names[1]}\n"
            f"  Wald Statistic: {self.wald_stat:.4f}\n"
            f"  Chi2 p-value: {self.p_value_chi2:.4f}\n"
            f"  Bootstrap CVs (1%, 5%, 10%): {self.bootstrap_cv}\n"
            f"  Optimal Lag: {self.optimal_lag}\n"
            f"  Reject H0 at 1%: {self.reject_null['1%']}\n"
            f"  Reject H0 at 5%: {self.reject_null['5%']}\n"
            f"  Reject H0 at 10%: {self.reject_null['10%']}\n"
            f")"
        )
    
    def summary(self) -> str:
        """Generate a formatted summary of the test results."""
        separator = "=" * 70
        header = f"""
{separator}
                    ASYMMETRIC CAUSALITY TEST RESULTS
{separator}
Test Type: Hatemi-J (2012) Asymmetric Causality Test
Component: {self.component_type.upper()}
Null Hypothesis: {self.variable_names[0]} does not Granger-cause {self.variable_names[1]}
Information Criterion: {self.info_criterion}
Optimal Lag Order: {self.optimal_lag}
{separator}
"""
        
        results = f"""
                         Test Statistics
{'-' * 70}
Wald Statistic:                    {self.wald_stat:>15.4f}
Chi-squared p-value:               {self.p_value_chi2:>15.4f}

                    Bootstrap Critical Values
{'-' * 70}
{'Significance Level':<25}{'Critical Value':>15}{'Reject H0':>15}
{'-' * 70}
{'1%':<25}{self.bootstrap_cv[0]:>15.4f}{'Yes' if self.reject_null['1%'] else 'No':>15}
{'5%':<25}{self.bootstrap_cv[1]:>15.4f}{'Yes' if self.reject_null['5%'] else 'No':>15}
{'10%':<25}{self.bootstrap_cv[2]:>15.4f}{'Yes' if self.reject_null['10%'] else 'No':>15}
{separator}
"""
        
        conclusion = "\nConclusion: "
        if self.reject_null['1%']:
            conclusion += f"Strong evidence of causality from {self.variable_names[0]} to {self.variable_names[1]} (significant at 1% level)."
        elif self.reject_null['5%']:
            conclusion += f"Evidence of causality from {self.variable_names[0]} to {self.variable_names[1]} (significant at 5% level)."
        elif self.reject_null['10%']:
            conclusion += f"Weak evidence of causality from {self.variable_names[0]} to {self.variable_names[1]} (significant at 10% level)."
        else:
            conclusion += f"No significant causal relationship found from {self.variable_names[0]} to {self.variable_names[1]}."
        
        return header + results + conclusion + "\n"


def cumulative_components(y: np.ndarray, 
                          log_transform: bool = False) -> Tuple[np.ndarray, np.ndarray]:
    """
    Calculate cumulative positive and negative components of a time series.
    
    This function decomposes a time series into its cumulative positive and 
    negative changes, following the methodology of Granger and Yoon (2002) 
    and Hatemi-J (2012).
    
    Parameters
    ----------
    y : np.ndarray
        Input time series data. Can be 1D or 2D array.
    log_transform : bool, optional
        If True, apply log transformation before calculating changes.
        Default is False.
    
    Returns
    -------
    Tuple[np.ndarray, np.ndarray]
        Tuple containing:
        - cumulative_positive: Cumulative sum of positive changes
        - cumulative_negative: Cumulative sum of negative changes
    
    Notes
    -----
    For a time series y_t, the positive and negative shocks are defined as:
    - ε⁺ = max(Δy_t, 0)
    - ε⁻ = min(Δy_t, 0)
    
    The cumulative sums are then:
    - y⁺_t = Σ(i=1 to t) ε⁺_i
    - y⁻_t = Σ(i=1 to t) ε⁻_i
    """
    y = np.asarray(y, dtype=np.float64)
    
    # Handle 1D arrays
    if y.ndim == 1:
        y = y.reshape(-1, 1)
    
    # Apply log transformation if requested
    if log_transform:
        y = np.log(y)
    
    # Calculate first differences
    dy = np.diff(y, axis=0)
    
    # Separate positive and negative changes
    positive_changes = np.maximum(dy, 0)
    negative_changes = np.minimum(dy, 0)
    
    # Calculate cumulative sums
    cumulative_positive = np.cumsum(positive_changes, axis=0)
    cumulative_negative = np.cumsum(negative_changes, axis=0)
    
    return cumulative_positive, cumulative_negative


def var_lags(data: np.ndarray, lags: int) -> Tuple[np.ndarray, np.ndarray]:
    """
    Create lagged variables for VAR estimation.
    
    Parameters
    ----------
    data : np.ndarray
        T x K matrix of time series data.
    lags : int
        Number of lags to create.
    
    Returns
    -------
    Tuple[np.ndarray, np.ndarray]
        Tuple containing:
        - y: (T - lags) x K matrix of dependent variables
        - y_lags: (T - lags) x (K * lags) matrix of lagged variables
    """
    T, K = data.shape
    
    if lags <= 0:
        return data, np.ones((T, 1))
    
    # Create lagged matrix
    y_lags = np.zeros((T - lags, K * lags))
    
    for lag in range(1, lags + 1):
        start_col = (lag - 1) * K
        end_col = lag * K
        y_lags[:, start_col:end_col] = data[lags - lag:T - lag, :]
    
    # Dependent variable (after removing initial observations)
    y = data[lags:, :]
    
    return y, y_lags


def information_criterion(varcov: np.ndarray, 
                          T: int, 
                          n_vars: int, 
                          lag: int, 
                          criterion: str = 'hjc') -> float:
    """
    Calculate information criterion for VAR lag selection.
    
    Parameters
    ----------
    varcov : np.ndarray
        Variance-covariance matrix of residuals.
    T : int
        Number of observations.
    n_vars : int
        Number of variables in the VAR system.
    lag : int
        Lag order being evaluated.
    criterion : str, optional
        Information criterion to use. Options:
        - 'aic': Akaike Information Criterion
        - 'aicc': Corrected AIC
        - 'bic' or 'sbc': Schwarz Bayesian Criterion
        - 'hqc': Hannan-Quinn Criterion
        - 'hjc': Hatemi-J Criterion (average of SBC and HQC)
        Default is 'hjc'.
    
    Returns
    -------
    float
        Value of the information criterion.
    """
    det_varcov = det(varcov)
    
    # Handle numerical issues
    if det_varcov <= 0:
        return np.inf
    
    log_det = np.log(det_varcov)
    n_params = n_vars * n_vars * lag + n_vars
    constant_term = n_vars * (1 + np.log(2 * np.pi))
    
    criterion = criterion.lower()
    
    if criterion == 'aic':
        return log_det + (2 / T) * n_params + constant_term
    
    elif criterion == 'aicc':
        # Corrected AIC for small samples
        k = 1 + lag * n_vars
        denom = T - k - n_vars - 1
        if denom <= 0:
            return np.inf
        return log_det + ((T + k) * n_vars) / denom
    
    elif criterion in ['bic', 'sbc']:
        return log_det + (1 / T) * n_params * np.log(T) + constant_term
    
    elif criterion == 'hqc':
        return log_det + (2 / T) * n_params * np.log(np.log(T)) + constant_term
    
    elif criterion == 'hjc':
        # Hatemi-J Criterion: average of SBC and HQC
        sbc = log_det + (1 / T) * n_params * np.log(T) + constant_term
        hqc = log_det + (2 / T) * n_params * np.log(np.log(T)) + constant_term
        return (sbc + hqc) / 2
    
    else:
        raise ValueError(f"Unknown information criterion: {criterion}. "
                        f"Use 'aic', 'aicc', 'bic', 'sbc', 'hqc', or 'hjc'.")


def select_lag_order(data: np.ndarray, 
                     max_lag: int = 8, 
                     min_lag: int = 1,
                     criterion: str = 'hjc') -> Tuple[int, np.ndarray]:
    """
    Select optimal lag order for VAR model using information criteria.
    
    Parameters
    ----------
    data : np.ndarray
        T x K matrix of time series data.
    max_lag : int, optional
        Maximum lag order to consider. Default is 8.
    min_lag : int, optional
        Minimum lag order to consider. Default is 1.
    criterion : str, optional
        Information criterion to use. Default is 'hjc'.
    
    Returns
    -------
    Tuple[int, np.ndarray]
        Tuple containing:
        - optimal_lag: Selected lag order
        - ic_values: Array of IC values for each lag
    """
    T, n_vars = data.shape
    ic_values = np.full(max_lag - min_lag + 1, np.inf)
    
    for lag in range(min_lag, max_lag + 1):
        if lag >= T - n_vars - 1:
            continue
            
        y, y_lags = var_lags(data, lag)
        X = np.column_stack([np.ones(len(y)), y_lags])
        
        # OLS estimation
        try:
            beta = np.linalg.lstsq(X, y, rcond=None)[0]
            residuals = y - X @ beta
            T_eff = len(y)
            varcov = (residuals.T @ residuals) / T_eff
            
            ic_values[lag - min_lag] = information_criterion(
                varcov, T_eff, n_vars, lag, criterion
            )
        except np.linalg.LinAlgError:
            continue
    
    optimal_lag = np.argmin(ic_values) + min_lag
    
    return optimal_lag, ic_values


def create_restriction_matrix(n_vars: int, 
                              var_order: int, 
                              add_lags: int = 0) -> Tuple[np.ndarray, np.ndarray]:
    """
    Create restriction matrices for Granger causality testing.
    
    This function creates the restriction matrices for testing whether the 
    second variable Granger-causes the first variable.
    
    Parameters
    ----------
    n_vars : int
        Number of variables in VAR system (typically 2).
    var_order : int
        Order of the VAR system.
    add_lags : int, optional
        Number of additional lags for Toda-Yamamoto procedure. Default is 0.
    
    Returns
    -------
    Tuple[np.ndarray, np.ndarray]
        Tuple containing:
        - rvector: Row vector indicating restricted coefficients
        - rmatrix: Full restriction matrix for Wald test
    """
    total_lags = var_order + add_lags
    n_params_per_eq = 1 + n_vars * total_lags
    
    # Vector indicating which coefficients are restricted in first equation
    rvector = np.zeros(n_params_per_eq)
    
    # Matrix for Wald test restrictions
    rmatrix = np.zeros((var_order, n_params_per_eq * n_vars))
    
    for r in range(var_order):
        # Position of second variable's coefficient at lag r+1 in first equation
        pos_in_eq = 1 + r * n_vars + 1  # +1 for constant, +1 for second variable
        rvector[pos_in_eq] = 1
        
        # Position in vectorized coefficient matrix
        pos_in_vec = pos_in_eq * n_vars
        rmatrix[r, pos_in_vec] = 1
    
    return rvector, rmatrix


def estimate_var(y: np.ndarray, 
                 X: np.ndarray, 
                 restrict: bool = False,
                 rvector: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray]:
    """
    Estimate VAR model parameters with optional restrictions.
    
    Parameters
    ----------
    y : np.ndarray
        T x K matrix of dependent variables.
    X : np.ndarray
        T x M matrix of regressors (including constant and lags).
    restrict : bool, optional
        If True, impose zero restrictions for causality testing. Default is False.
    rvector : np.ndarray, optional
        Vector indicating which coefficients to restrict. Required if restrict=True.
    
    Returns
    -------
    Tuple[np.ndarray, np.ndarray]
        Tuple containing:
        - Ahat: K x M matrix of estimated coefficients
        - leverage: Leverage values for bootstrap adjustment
    """
    T = y.shape[0]
    n_vars = y.shape[1]
    
    if not restrict:
        # Unrestricted OLS
        XtX_inv = inv(X.T @ X)
        Ahat = (XtX_inv @ X.T @ y).T
        leverage = np.ones((T, n_vars))  # Placeholder
        return Ahat, leverage
    
    # Restricted estimation
    XtX_inv = inv(X.T @ X)
    
    # Estimate second equation (unrestricted)
    Ahat2 = (XtX_inv @ X.T @ y[:, 1:]).T
    
    # Calculate leverage for second equation
    leverage2 = np.zeros(T)
    for i in range(T):
        leverage2[i] = X[i, :] @ XtX_inv @ X[i, :]
    
    # Estimate first equation with restrictions
    # Remove columns corresponding to restricted parameters
    keep_cols = np.where(rvector == 0)[0]
    X_restricted = X[:, keep_cols]
    
    XrXr_inv = inv(X_restricted.T @ X_restricted)
    Ahat1_restricted = XrXr_inv @ X_restricted.T @ y[:, 0]
    
    # Calculate leverage for first equation
    leverage1 = np.zeros(T)
    for i in range(T):
        leverage1[i] = X_restricted[i, :] @ XrXr_inv @ X_restricted[i, :]
    
    # Insert zeros for restricted parameters
    Ahat1 = np.zeros(X.shape[1])
    Ahat1[keep_cols] = Ahat1_restricted
    
    # Combine coefficient matrices
    Ahat = np.vstack([Ahat1, Ahat2])
    leverage = np.column_stack([leverage1, leverage2])
    
    return Ahat, leverage


def wald_test(y: np.ndarray, 
              X: np.ndarray, 
              Ahat: np.ndarray, 
              rmatrix: np.ndarray) -> float:
    """
    Calculate the Wald test statistic for Granger non-causality.
    
    Parameters
    ----------
    y : np.ndarray
        T x K matrix of dependent variables.
    X : np.ndarray
        T x M matrix of regressors.
    Ahat : np.ndarray
        K x M matrix of unrestricted coefficient estimates.
    rmatrix : np.ndarray
        Restriction matrix for Wald test.
    
    Returns
    -------
    float
        Wald test statistic.
    """
    T = y.shape[0]
    n_params = Ahat.shape[1]
    
    # Calculate residuals
    residuals = y - X @ Ahat.T
    
    # Estimate variance-covariance matrix
    varcov = (residuals.T @ residuals) / (T - n_params)
    
    # Vectorize coefficients (column-stacking)
    vec_Ahat = Ahat.T.flatten(order='F')
    
    # Calculate Wald statistic
    XtX_inv = inv(X.T @ X)
    f = rmatrix @ vec_Ahat
    
    # Kronecker product of XtX_inv and varcov
    kron_prod = np.kron(XtX_inv, varcov)
    
    middle_term = rmatrix @ kron_prod @ rmatrix.T
    
    try:
        wald_stat = f.T @ inv(middle_term) @ f
    except np.linalg.LinAlgError:
        # Use pseudo-inverse if singular
        wald_stat = f.T @ np.linalg.pinv(middle_term) @ f
    
    return float(wald_stat)


def bootstrap_critical_values(y: np.ndarray, 
                              X: np.ndarray,
                              z_initial: np.ndarray,
                              Ahat: np.ndarray,
                              leverage: np.ndarray,
                              order: int,
                              add_lags: int,
                              n_bootstrap: int,
                              rmatrix: np.ndarray,
                              random_state: Optional[int] = None) -> np.ndarray:
    """
    Generate bootstrap critical values for the Wald test using leverage adjustment.
    
    This function implements the bootstrap simulation approach with leverage 
    adjustment as described in Hacker and Hatemi-J (2006).
    
    Parameters
    ----------
    y : np.ndarray
        T x K matrix of dependent variables.
    X : np.ndarray
        T x M matrix of regressors.
    z_initial : np.ndarray
        Initial values for VAR system.
    Ahat : np.ndarray
        Restricted coefficient estimates.
    leverage : np.ndarray
        Leverage values for adjustment.
    order : int
        VAR order.
    add_lags : int
        Additional lags for Toda-Yamamoto.
    n_bootstrap : int
        Number of bootstrap replications.
    rmatrix : np.ndarray
        Restriction matrix.
    random_state : int, optional
        Random seed for reproducibility.
    
    Returns
    -------
    np.ndarray
        Bootstrap critical values at 1%, 5%, and 10% significance levels.
    """
    if random_state is not None:
        np.random.seed(random_state)
    
    T = y.shape[0]
    n_vars = y.shape[1]
    max_lag = order + add_lags
    
    # Calculate residuals from restricted model
    residuals = y - X @ Ahat.T
    
    # Leverage adjustment
    adjuster = np.zeros((T, n_vars))
    for v in range(n_vars):
        lev_col = min(v, 1)  # Use same leverage for vars > 1
        adjuster[:, v] = 1 / np.sqrt(np.maximum(1 - leverage[:, lev_col], 0.01))
    
    adjusted_residuals = residuals * adjuster
    
    # Bootstrap simulations
    wald_stats = np.zeros(n_bootstrap)
    
    for b in range(n_bootstrap):
        # Draw bootstrap residuals
        indices = np.random.randint(0, T, size=T)
        boot_residuals = adjusted_residuals[indices, :]
        
        # Mean-adjust bootstrap residuals
        boot_residuals = boot_residuals - boot_residuals.mean(axis=0)
        
        # Generate bootstrap data using restricted coefficients
        y_boot = np.zeros((T + max_lag, n_vars))
        y_boot[:max_lag, :] = z_initial[:max_lag, :]
        
        for t in range(T):
            # Construct regressor row
            x_row = np.ones(1)
            for lag in range(1, max_lag + 1):
                x_row = np.concatenate([x_row, y_boot[t + max_lag - lag, :]])
            
            y_boot[t + max_lag, :] = x_row @ Ahat.T + boot_residuals[t, :]
        
        # Extract bootstrap sample (excluding initial values)
        y_b = y_boot[max_lag:, :]
        
        # Create lagged variables
        y_dep, y_lags = var_lags(y_boot, max_lag)
        X_b = np.column_stack([np.ones(len(y_dep)), y_lags])
        
        # Estimate unrestricted VAR
        Ahat_b, _ = estimate_var(y_dep, X_b, restrict=False)
        
        # Calculate Wald statistic
        wald_stats[b] = wald_test(y_dep, X_b, Ahat_b, rmatrix)
    
    # Sort and find critical values
    wald_stats = np.sort(wald_stats)
    
    # Critical value indices
    idx_1pct = int(n_bootstrap * 0.99)
    idx_5pct = int(n_bootstrap * 0.95)
    idx_10pct = int(n_bootstrap * 0.90)
    
    # Average adjacent values for smoothing
    cv_1pct = (wald_stats[idx_1pct] + wald_stats[min(idx_1pct + 1, n_bootstrap - 1)]) / 2
    cv_5pct = (wald_stats[idx_5pct] + wald_stats[min(idx_5pct + 1, n_bootstrap - 1)]) / 2
    cv_10pct = (wald_stats[idx_10pct] + wald_stats[min(idx_10pct + 1, n_bootstrap - 1)]) / 2
    
    return np.array([cv_1pct, cv_5pct, cv_10pct])


def asymmetric_causality_test(y: np.ndarray,
                              z: np.ndarray,
                              component: str = 'positive',
                              criterion: str = 'hjc',
                              max_lag: int = 8,
                              integration_order: int = 0,
                              n_bootstrap: int = 1000,
                              log_transform: bool = False,
                              var_names: Optional[Tuple[str, str]] = None,
                              random_state: Optional[int] = None) -> AsymmetricCausalityResult:
    """
    Perform asymmetric causality test (Hatemi-J, 2012).
    
    This function tests whether variable z Granger-causes variable y, allowing
    for asymmetric effects by separately analyzing positive and negative 
    components.
    
    Parameters
    ----------
    y : np.ndarray
        Dependent variable (effect). 1D array of length T.
    z : np.ndarray
        Independent variable (cause). 1D array of length T.
    component : str, optional
        Type of component to test:
        - 'positive': Test causality between positive components (z+ -> y+)
        - 'negative': Test causality between negative components (z- -> y-)
        - 'positive_to_negative': Test z+ -> y-
        - 'negative_to_positive': Test z- -> y+
        - 'original': Test original variables without decomposition
        Default is 'positive'.
    criterion : str, optional
        Information criterion for lag selection:
        'aic', 'aicc', 'bic', 'sbc', 'hqc', 'hjc'. Default is 'hjc'.
    max_lag : int, optional
        Maximum lag order to consider. Default is 8.
    integration_order : int, optional
        Order of integration for Toda-Yamamoto procedure. Default is 0.
    n_bootstrap : int, optional
        Number of bootstrap replications. Default is 1000.
    log_transform : bool, optional
        Apply log transformation to data. Default is False.
    var_names : Tuple[str, str], optional
        Names for (cause, effect) variables. Default is ('Z', 'Y').
    random_state : int, optional
        Random seed for reproducibility.
    
    Returns
    -------
    AsymmetricCausalityResult
        Object containing test results, critical values, and diagnostics.
    
    Examples
    --------
    >>> import numpy as np
    >>> from asymcaus import asymmetric_causality_test
    >>> 
    >>> # Generate sample data
    >>> np.random.seed(42)
    >>> T = 500
    >>> z = np.cumsum(np.random.randn(T))
    >>> y = np.cumsum(np.random.randn(T))
    >>> 
    >>> # Test positive component causality
    >>> result = asymmetric_causality_test(y, z, component='positive')
    >>> print(result.summary())
    
    Notes
    -----
    The test is based on the following VAR model:
    
    y⁺_t = ν + A₁y⁺_{t-1} + ... + Aₚy⁺_{t-p} + u_t
    
    where y⁺ represents the cumulative positive (or negative) shocks.
    
    The null hypothesis H₀: z does not Granger-cause y is tested using a 
    Wald statistic. Bootstrap critical values are generated using leverage-
    adjusted residuals to account for non-normality and heteroskedasticity.
    
    References
    ----------
    Hatemi-J, A. (2012). Asymmetric Causality Tests with an Application.
    Empirical Economics, 43(1), 447-456.
    
    Granger, C.W.J. & Yoon, G. (2002). Hidden Cointegration.
    Department of Economics Working Paper, UC San Diego.
    
    Toda, H.Y. & Yamamoto, T. (1995). Statistical Inference in Vector 
    Autoregressions with Possibly Integrated Processes. Journal of 
    Econometrics, 66, 225-250.
    """
    # Set default variable names
    if var_names is None:
        var_names = ('Z', 'Y')
    
    # Ensure arrays are properly formatted
    y = np.asarray(y, dtype=np.float64).flatten()
    z = np.asarray(z, dtype=np.float64).flatten()
    
    if len(y) != len(z):
        raise ValueError("y and z must have the same length.")
    
    # Combine data
    data = np.column_stack([y, z])
    
    # Calculate cumulative components
    cum_pos, cum_neg = cumulative_components(data, log_transform=log_transform)
    
    # Select appropriate components based on test type
    component = component.lower()
    
    if component == 'positive':
        test_data = cum_pos
        comp_label = 'Positive components (Z+ → Y+)'
    elif component == 'negative':
        test_data = cum_neg
        comp_label = 'Negative components (Z- → Y-)'
    elif component == 'positive_to_negative':
        test_data = np.column_stack([cum_neg[:, 0], cum_pos[:, 1]])
        comp_label = 'Cross components (Z+ → Y-)'
    elif component == 'negative_to_positive':
        test_data = np.column_stack([cum_pos[:, 0], cum_neg[:, 1]])
        comp_label = 'Cross components (Z- → Y+)'
    elif component == 'original':
        test_data = data[1:, :]  # Remove first observation to match cumulative length
        comp_label = 'Original variables'
    else:
        raise ValueError(f"Unknown component type: {component}. "
                        f"Use 'positive', 'negative', 'positive_to_negative', "
                        f"'negative_to_positive', or 'original'.")
    
    # Number of variables
    n_vars = 2
    
    # Select optimal lag order
    optimal_lag, _ = select_lag_order(test_data, max_lag=max_lag, criterion=criterion)
    
    # Total lags including Toda-Yamamoto adjustment
    total_lag = optimal_lag + integration_order
    
    # Create lagged variables
    y_dep, y_lags = var_lags(test_data, total_lag)
    X = np.column_stack([np.ones(len(y_dep)), y_lags])
    
    # Create restriction matrices
    rvector, rmatrix = create_restriction_matrix(n_vars, optimal_lag, integration_order)
    
    # Estimate unrestricted VAR
    Ahat_unrestricted, _ = estimate_var(y_dep, X, restrict=False)
    
    # Estimate restricted VAR (for bootstrap)
    Ahat_restricted, leverage = estimate_var(y_dep, X, restrict=True, rvector=rvector)
    
    # Calculate Wald statistic
    wald_stat = wald_test(y_dep, X, Ahat_unrestricted, rmatrix)
    
    # Calculate chi-squared p-value
    p_value = 1 - chi2.cdf(wald_stat, df=optimal_lag)
    
    # Generate bootstrap critical values
    bootstrap_cv = bootstrap_critical_values(
        y_dep, X, test_data[:total_lag, :],
        Ahat_restricted, leverage,
        optimal_lag, integration_order,
        n_bootstrap, rmatrix,
        random_state=random_state
    )
    
    # Determine rejection at each significance level
    reject_null = {
        '1%': wald_stat > bootstrap_cv[0],
        '5%': wald_stat > bootstrap_cv[1],
        '10%': wald_stat > bootstrap_cv[2]
    }
    
    # Map criterion name for display
    criterion_names = {
        'aic': 'AIC',
        'aicc': 'AICC',
        'bic': 'SBC',
        'sbc': 'SBC',
        'hqc': 'HQC',
        'hjc': 'Hatemi-J Criterion (HJC)'
    }
    
    return AsymmetricCausalityResult(
        wald_stat=wald_stat,
        bootstrap_cv=bootstrap_cv,
        optimal_lag=optimal_lag,
        p_value_chi2=p_value,
        reject_null=reject_null,
        component_type=comp_label,
        variable_names=(var_names[1], var_names[0]),  # (cause, effect) -> (z, y)
        info_criterion=criterion_names.get(criterion.lower(), criterion)
    )


def full_asymmetric_causality_analysis(y: np.ndarray,
                                       z: np.ndarray,
                                       criterion: str = 'hjc',
                                       max_lag: int = 8,
                                       integration_order: int = 0,
                                       n_bootstrap: int = 1000,
                                       log_transform: bool = False,
                                       var_names: Optional[Tuple[str, str]] = None,
                                       random_state: Optional[int] = None) -> Dict[str, AsymmetricCausalityResult]:
    """
    Perform comprehensive asymmetric causality analysis.
    
    This function tests all combinations of positive and negative components
    for causal relationships between two variables.
    
    Parameters
    ----------
    y : np.ndarray
        Dependent variable (effect).
    z : np.ndarray
        Independent variable (cause).
    criterion : str, optional
        Information criterion for lag selection. Default is 'hjc'.
    max_lag : int, optional
        Maximum lag order. Default is 8.
    integration_order : int, optional
        Order of integration for Toda-Yamamoto. Default is 0.
    n_bootstrap : int, optional
        Number of bootstrap replications. Default is 1000.
    log_transform : bool, optional
        Apply log transformation. Default is False.
    var_names : Tuple[str, str], optional
        Names for (cause, effect) variables.
    random_state : int, optional
        Random seed for reproducibility.
    
    Returns
    -------
    Dict[str, AsymmetricCausalityResult]
        Dictionary containing results for all component combinations:
        - 'original': Original variables
        - 'positive': Positive components (Z+ → Y+)
        - 'negative': Negative components (Z- → Y-)
        - 'positive_to_negative': Z+ → Y-
        - 'negative_to_positive': Z- → Y+
    
    Examples
    --------
    >>> results = full_asymmetric_causality_analysis(y, z, var_names=('Oil', 'Stock'))
    >>> for name, result in results.items():
    ...     print(f"{name}: Wald = {result.wald_stat:.4f}")
    """
    components = ['original', 'positive', 'negative', 
                  'positive_to_negative', 'negative_to_positive']
    
    results = {}
    
    for comp in components:
        results[comp] = asymmetric_causality_test(
            y=y, z=z,
            component=comp,
            criterion=criterion,
            max_lag=max_lag,
            integration_order=integration_order,
            n_bootstrap=n_bootstrap,
            log_transform=log_transform,
            var_names=var_names,
            random_state=random_state
        )
    
    return results


def print_full_analysis(results: Dict[str, AsymmetricCausalityResult]) -> str:
    """
    Generate a comprehensive summary of full asymmetric causality analysis.
    
    Parameters
    ----------
    results : Dict[str, AsymmetricCausalityResult]
        Results from full_asymmetric_causality_analysis.
    
    Returns
    -------
    str
        Formatted summary string.
    """
    separator = "=" * 80
    
    output = f"""
{separator}
              COMPREHENSIVE ASYMMETRIC CAUSALITY ANALYSIS
                   Hatemi-J (2012) Test Results
{separator}

"""
    
    header = f"{'Component':<30}{'Wald Stat':>12}{'CV (5%)':>12}{'p-value':>12}{'Reject H0':>12}"
    output += header + "\n"
    output += "-" * 78 + "\n"
    
    for name, result in results.items():
        reject_str = "Yes**" if result.reject_null['1%'] else \
                     "Yes*" if result.reject_null['5%'] else \
                     "Yes" if result.reject_null['10%'] else "No"
        
        output += f"{name:<30}{result.wald_stat:>12.4f}{result.bootstrap_cv[1]:>12.4f}"
        output += f"{result.p_value_chi2:>12.4f}{reject_str:>12}\n"
    
    output += "-" * 78 + "\n"
    output += "Note: ** significant at 1%, * significant at 5%\n"
    output += separator + "\n"
    
    return output
