#!/usr/bin/env python3
"""Tests for SC-LDPC construction."""

import numpy as np
import pytest
import sys
import os

sys.path.insert(0, os.path.dirname(__file__))


class TestSCLDPCConstruction:
    """Tests for SC-LDPC chain construction."""

    def test_split_preserves_edges(self):
        """Split B into components, verify sum(B_i >= 0) == (B >= 0) for each position."""
        from sc_ldpc import split_protograph
        from ldpc_sim import H_BASE
        components = split_protograph(H_BASE, w=2, seed=42)
        assert len(components) == 2
        # Each edge in B should appear in exactly one component
        for r in range(H_BASE.shape[0]):
            for c in range(H_BASE.shape[1]):
                if H_BASE[r, c] >= 0:
                    count = sum(1 for comp in components if comp[r, c] >= 0)
                    assert count == 1, f"Edge ({r},{c}) in {count} components, expected 1"
                else:
                    for comp in components:
                        assert comp[r, c] < 0, f"Edge ({r},{c}) should be absent"

    def test_chain_dimensions(self):
        """Build chain with L=5, w=2, Z=32. Verify H dimensions."""
        from sc_ldpc import build_sc_chain
        from ldpc_sim import H_BASE
        m_base, n_base = H_BASE.shape
        L, w, z = 5, 2, 32
        H_full, components, meta = build_sc_chain(H_BASE, L=L, w=w, z=z, seed=42)
        expected_rows = L * m_base * z  # 5*7*32 = 1120
        expected_cols = (L + w - 1) * n_base * z  # 6*8*32 = 1536
        assert H_full.shape == (expected_rows, expected_cols), (
            f"Expected ({expected_rows}, {expected_cols}), got {H_full.shape}"
        )

    def test_chain_has_coupled_structure(self):
        """Verify non-zero blocks only appear at positions t and t+1 for w=2."""
        from sc_ldpc import build_sc_chain
        from ldpc_sim import H_BASE
        m_base, n_base = H_BASE.shape
        L, w, z = 5, 2, 32
        H_full, components, meta = build_sc_chain(H_BASE, L=L, w=w, z=z, seed=42)
        # For each CN position t, check which VN positions have connections
        for t in range(L):
            cn_rows = slice(t * m_base * z, (t + 1) * m_base * z)
            for v in range(L + w - 1):
                vn_cols = slice(v * n_base * z, (v + 1) * n_base * z)
                block = H_full[cn_rows, vn_cols]
                has_connections = np.any(block != 0)
                if t <= v <= t + w - 1:
                    # Should have connections (t connects to t..t+w-1)
                    assert has_connections, f"CN pos {t} should connect to VN pos {v}"
                else:
                    # Should NOT have connections
                    assert not has_connections, f"CN pos {t} should NOT connect to VN pos {v}"


class TestWindowedDecode:
    """Tests for windowed SC-LDPC decoder."""

    def test_windowed_decode_trivial(self):
        """Build chain L=5, encode all-zeros, decode at lam_s=10. Verify correct decode."""
        from sc_ldpc import build_sc_chain, windowed_decode
        from ldpc_sim import H_BASE, poisson_channel, quantize_llr
        np.random.seed(42)
        L, w, z = 5, 2, 32
        m_base, n_base = H_BASE.shape
        H_full, components, meta = build_sc_chain(H_BASE, L=L, w=w, z=z, seed=42)
        n_total = H_full.shape[1]
        # All-zeros codeword (always valid)
        codeword = np.zeros(n_total, dtype=np.int8)
        llr_float, _ = poisson_channel(codeword, lam_s=10.0, lam_b=0.1)
        llr_q = quantize_llr(llr_float)
        decoded, converged, iters = windowed_decode(
            llr_q, H_full, L=L, w=w, z=z, n_base=n_base, m_base=m_base,
            W=5, max_iter=20, cn_mode='normalized', alpha=0.75
        )
        assert len(decoded) == n_total
        # At high SNR, should decode mostly correctly
        error_rate = np.mean(decoded != 0)
        assert error_rate < 0.05, f"Error rate {error_rate} too high at lam_s=10"

    def test_windowed_decode_with_noise(self):
        """Encode random info at lam_s=5, decode. Verify low BER."""
        from sc_ldpc import build_sc_chain, sc_encode, windowed_decode
        from ldpc_sim import H_BASE, poisson_channel, quantize_llr
        np.random.seed(42)
        L, w, z = 3, 2, 32
        m_base, n_base = H_BASE.shape
        H_full, components, meta = build_sc_chain(H_BASE, L=L, w=w, z=z, seed=42)
        n_total = H_full.shape[1]
        m_total = H_full.shape[0]
        k_total = n_total - m_total  # approximate info bits
        if k_total <= 0:
            k_total = n_total // 4  # fallback
        # Use all-zeros codeword for simplicity (always valid)
        codeword = np.zeros(n_total, dtype=np.int8)
        llr_float, _ = poisson_channel(codeword, lam_s=5.0, lam_b=0.1)
        llr_q = quantize_llr(llr_float)
        decoded, converged, iters = windowed_decode(
            llr_q, H_full, L=L, w=w, z=z, n_base=n_base, m_base=m_base,
            W=3, max_iter=20, cn_mode='normalized', alpha=0.75
        )
        error_rate = np.mean(decoded != 0)
        assert error_rate < 0.15, f"Error rate {error_rate} too high at lam_s=5"

    def test_window_size_effect(self):
        """Larger window should decode at least as well as smaller window."""
        from sc_ldpc import build_sc_chain, windowed_decode
        from ldpc_sim import H_BASE, poisson_channel, quantize_llr
        np.random.seed(42)
        L, w, z = 5, 2, 32
        m_base, n_base = H_BASE.shape
        H_full, components, meta = build_sc_chain(H_BASE, L=L, w=w, z=z, seed=42)
        n_total = H_full.shape[1]
        codeword = np.zeros(n_total, dtype=np.int8)
        llr_float, _ = poisson_channel(codeword, lam_s=3.0, lam_b=0.1)
        llr_q = quantize_llr(llr_float)
        # Small window
        dec_small, _, _ = windowed_decode(
            llr_q.copy(), H_full, L=L, w=w, z=z, n_base=n_base, m_base=m_base,
            W=2, max_iter=15, cn_mode='normalized', alpha=0.75
        )
        err_small = np.mean(dec_small != 0)
        # Large window
        dec_large, _, _ = windowed_decode(
            llr_q.copy(), H_full, L=L, w=w, z=z, n_base=n_base, m_base=m_base,
            W=5, max_iter=15, cn_mode='normalized', alpha=0.75
        )
        err_large = np.mean(dec_large != 0)
        # Larger window should be at least as good (with some tolerance for randomness)
        assert err_large <= err_small + 0.05, (
            f"Large window error {err_large} should be <= small window {err_small} + tolerance"
        )


class TestSCDensityEvolution:
    """Tests for SC-LDPC density evolution."""

    def test_sc_de_converges_at_high_snr(self):
        """SC-DE should converge at lam_s=10 (well above threshold)."""
        from sc_ldpc import sc_density_evolution
        from ldpc_sim import H_BASE
        np.random.seed(42)
        converged, pos_errors, iters = sc_density_evolution(
            H_BASE, L=10, w=2, lam_s=10.0, lam_b=0.1,
            z_pop=5000, max_iter=50, cn_mode='normalized', alpha=0.75
        )
        assert converged, f"SC-DE should converge at lam_s=10, max error={max(pos_errors):.4f}"

    def test_sc_threshold_lower_than_uncoupled(self):
        """SC threshold should be lower than uncoupled threshold (~3.05)."""
        from sc_ldpc import compute_sc_threshold
        from ldpc_sim import H_BASE
        np.random.seed(42)
        sc_thresh = compute_sc_threshold(
            H_BASE, L=20, w=2, lam_b=0.1,
            z_pop=5000, tol=0.5, cn_mode='normalized', alpha=0.75
        )
        # Uncoupled threshold is ~3.05 for offset, ~2.9 for normalized
        # SC should be lower (threshold saturation)
        assert sc_thresh < 3.5, f"SC threshold {sc_thresh} should be < 3.5"

    def test_sc_de_wave_effect(self):
        """SC-LDPC should show position-dependent error profile.

        In SC-LDPC with flooding DE, boundary positions have fewer CN
        connections (lower effective VN degree), so they typically show
        different error rates than interior positions. The error profile
        should NOT be uniform -- the spatial coupling creates a non-trivial
        position-dependent structure.
        """
        from sc_ldpc import sc_density_evolution
        from ldpc_sim import H_BASE
        np.random.seed(42)
        # Run at a moderate SNR where errors are visible but not saturated
        converged, pos_errors, iters = sc_density_evolution(
            H_BASE, L=20, w=2, lam_s=2.5, lam_b=0.1,
            z_pop=5000, max_iter=100, cn_mode='normalized', alpha=0.75
        )
        # The error profile should be non-uniform across positions
        # (spatial coupling creates position-dependent behavior)
        err_std = np.std(pos_errors)
        assert err_std > 0.001, (
            f"Error profile std {err_std:.6f} too uniform; "
            f"SC-LDPC should show position-dependent errors"
        )
        # Interior positions (well-connected) should have lower error
        # than the max error across all positions
        interior_err = np.mean(pos_errors[len(pos_errors)//3 : 2*len(pos_errors)//3])
        max_err = max(pos_errors)
        assert interior_err < max_err, (
            f"Interior error {interior_err:.4f} should be less than max {max_err:.4f}"
        )
