"""Reference-oracle tests for whitened SVD extraction.

## Test Context

- Code to test: `obliteratus/analysis/whitened_svd.py`
- Testing framework: pytest with torch CPU tensors
- Coverage target: repository default, minimum 80% Gate 3 target
- Test types needed: deterministic unit and metamorphic tests
- External dependencies to mock: none; these tests intentionally exercise the
  real CPU tensor math and production `WhitenedSVDExtractor`
- Edge cases identified: paired sample permutation, feature-coordinate
  permutation, common translation, SVD sign ambiguity, dtype normalization,
  singular harmless covariance, zero signal, non-finite input, deterministic
  replay, over-requested directions, and `extract_all_layers` intersection order

The static fixtures below are tiny deterministic activation matrices. Dynamic
fixture factories clone rows into the public list-of-tensors input shape so
tests cannot pass by mutating shared tensors across cases.
"""

from __future__ import annotations

from dataclasses import dataclass

import pytest
import torch

from obliteratus.analysis.whitened_svd import WhitenedSVDExtractor


REGULARIZATION_EPS = 1e-4


@dataclass(frozen=True)
class ReferenceWhitenedSVD:
    directions: torch.Tensor
    whitened_directions: torch.Tensor
    singular_values: torch.Tensor
    variance_explained: float


def _activation_pair_fixture(
    dtype: torch.dtype = torch.float32,
) -> tuple[torch.Tensor, torch.Tensor]:
    harmless = torch.tensor(
        [
            [-2.0, -1.0, 0.5, 1.0],
            [-1.0, 0.0, -0.5, -1.0],
            [0.0, 1.0, 1.5, 0.0],
            [1.0, -2.0, -1.5, 2.0],
            [2.0, 2.0, 0.0, -2.0],
        ],
        dtype=dtype,
    )
    paired_delta = torch.tensor(
        [
            [1.25, -0.50, 0.75, 0.10],
            [0.80, 0.25, -0.10, 0.40],
            [1.60, -0.75, 0.35, -0.20],
            [0.45, 0.90, -0.60, 0.55],
            [1.10, -0.20, 0.95, -0.35],
        ],
        dtype=dtype,
    )
    return harmless + paired_delta, harmless


def _singular_covariance_fixture() -> tuple[torch.Tensor, torch.Tensor]:
    harmless = torch.tensor(
        [
            [-2.0, 0.0, 0.0, 1.0],
            [-1.0, 0.0, 0.0, 1.0],
            [0.0, 0.0, 0.0, 1.0],
            [1.0, 0.0, 0.0, 1.0],
            [2.0, 0.0, 0.0, 1.0],
        ],
    )
    harmful = harmless + torch.tensor([1.5, 0.0, 0.0, 0.0])
    return harmful, harmless


def _as_public_samples(matrix: torch.Tensor) -> list[torch.Tensor]:
    return [row.clone() for row in matrix]


def _reference_whitened_svd(
    harmful: torch.Tensor,
    harmless: torch.Tensor,
    *,
    n_directions: int,
    min_variance_ratio: float = 0.0,
) -> ReferenceWhitenedSVD:
    """Independent tiny-tensor reference for the published mathematical contract."""
    harmful = harmful.to(torch.float32).to(torch.float64)
    harmless = harmless.to(torch.float32).to(torch.float64)
    baseline_mean = harmless.mean(dim=0, keepdim=True)
    centered_baseline = harmless - baseline_mean
    covariance = centered_baseline.T.mm(centered_baseline) / max(harmless.shape[0] - 1, 1)
    eigenvalues, eigenvectors = torch.linalg.eigh(covariance)
    eigenvalues = torch.clamp(eigenvalues, min=0.0)
    threshold = eigenvalues.max() * min_variance_ratio
    kept = eigenvalues >= threshold
    kept_values = eigenvalues[kept]
    kept_vectors = eigenvectors[:, kept]

    whitening = kept_vectors.mm(torch.diag(torch.rsqrt(kept_values + REGULARIZATION_EPS)))
    whitened_delta = (harmful - baseline_mean).mm(whitening) - centered_baseline.mm(whitening)
    _, singular_values, right_vectors_t = torch.linalg.svd(whitened_delta, full_matrices=False)
    count = min(n_directions, whitened_delta.shape[0], whitened_delta.shape[1])
    whitened_directions = right_vectors_t[:count]

    inverse_whitening = kept_vectors.mm(torch.diag(torch.sqrt(kept_values + REGULARIZATION_EPS)))
    original_directions = whitened_directions.mm(inverse_whitening.T)
    original_directions = torch.nn.functional.normalize(original_directions, dim=1)
    whitened_directions = torch.nn.functional.normalize(whitened_directions, dim=1)
    selected_singular_values = singular_values[:count]
    variance_explained = (
        selected_singular_values.square().sum() / singular_values.square().sum().clamp(min=1e-12)
    ).item()
    return ReferenceWhitenedSVD(
        directions=original_directions,
        whitened_directions=whitened_directions,
        singular_values=selected_singular_values,
        variance_explained=variance_explained,
    )


def _row_space_projector(rows: torch.Tensor, *, tolerance: float = 1e-9) -> torch.Tensor:
    rows = rows.to(torch.float64)
    _, singular_values, right_vectors_t = torch.linalg.svd(rows, full_matrices=False)
    basis = right_vectors_t[singular_values > tolerance]
    return basis.T.mm(basis)


def _assert_same_subspace(left: torch.Tensor, right: torch.Tensor, *, atol: float = 3e-5) -> None:
    assert torch.allclose(
        _row_space_projector(left),
        _row_space_projector(right),
        atol=atol,
        rtol=0,
    )


def test_matches_independent_reference_not_raw_svd_or_identity_whitening() -> None:
    harmful, harmless = _activation_pair_fixture()
    result = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    ).extract(_as_public_samples(harmful), _as_public_samples(harmless), n_directions=3)
    reference = _reference_whitened_svd(harmful, harmless, n_directions=3)

    _assert_same_subspace(result.directions, reference.directions)
    _assert_same_subspace(result.whitened_directions, reference.whitened_directions)
    assert result.singular_values.double() == pytest.approx(
        reference.singular_values,
        rel=3e-5,
        abs=3e-5,
    )
    assert result.variance_explained == pytest.approx(reference.variance_explained, abs=3e-6)

    raw_delta = harmful - harmless
    _, _, raw_right_vectors_t = torch.linalg.svd(raw_delta, full_matrices=False)
    raw_primary_alignment = torch.dot(result.directions[0], raw_right_vectors_t[0]).abs()
    assert raw_primary_alignment < 0.95


def test_joint_sample_permutation_preserves_sign_invariant_refusal_subspace() -> None:
    harmful, harmless = _activation_pair_fixture()
    permutation = torch.tensor([3, 0, 4, 1, 2])
    extractor = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    )

    original = extractor.extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)
    permuted = extractor.extract(
        _as_public_samples(harmful[permutation]),
        _as_public_samples(harmless[permutation]),
        3,
    )

    _assert_same_subspace(original.directions, permuted.directions)
    assert permuted.singular_values == pytest.approx(original.singular_values, rel=3e-5, abs=3e-5)
    assert permuted.variance_explained == pytest.approx(original.variance_explained, abs=3e-6)


def test_feature_coordinate_permutation_round_trips_through_inverse_mapping() -> None:
    harmful, harmless = _activation_pair_fixture()
    feature_permutation = torch.tensor([2, 0, 3, 1])
    extractor = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        # The fixture has one mathematically null covariance dimension. Apply
        # the production-default truncation policy so this metamorphic oracle
        # compares the well-defined retained subspace across LAPACK backends.
        min_variance_ratio=0.01,
    )

    original = extractor.extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)
    permuted = extractor.extract(
        _as_public_samples(harmful[:, feature_permutation]),
        _as_public_samples(harmless[:, feature_permutation]),
        3,
    )
    mapped_back = torch.empty_like(permuted.directions)
    mapped_back[:, feature_permutation] = permuted.directions

    _assert_same_subspace(original.directions, mapped_back)
    assert permuted.singular_values == pytest.approx(original.singular_values, rel=5e-5, abs=5e-5)


def test_common_translation_cannot_change_whitened_svd_oracle_values() -> None:
    harmful, harmless = _activation_pair_fixture()
    offset = torch.tensor([8.0, -3.0, 0.25, 11.0])
    extractor = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    )

    original = extractor.extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)
    translated = extractor.extract(
        _as_public_samples(harmful + offset),
        _as_public_samples(harmless + offset),
        3,
    )

    _assert_same_subspace(original.directions, translated.directions)
    _assert_same_subspace(original.whitened_directions, translated.whitened_directions)
    assert translated.singular_values == pytest.approx(original.singular_values, rel=3e-5, abs=3e-5)
    assert translated.variance_explained == pytest.approx(original.variance_explained, abs=3e-6)


@pytest.mark.parametrize("dtype", [torch.float32, torch.float64])
def test_float_inputs_follow_float32_output_policy_with_dtype_tolerances(
    dtype: torch.dtype,
) -> None:
    harmful, harmless = _activation_pair_fixture(dtype)
    result = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    ).extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)
    reference = _reference_whitened_svd(harmful, harmless, n_directions=3)

    assert result.directions.dtype is torch.float32
    assert result.whitened_directions.dtype is torch.float32
    assert result.singular_values.dtype is torch.float32
    _assert_same_subspace(result.directions, reference.directions, atol=4e-5)
    assert result.singular_values.double() == pytest.approx(
        reference.singular_values,
        rel=4e-5,
        abs=4e-5,
    )


def test_singular_covariance_limits_over_requested_directions_to_effective_rank() -> None:
    harmful, harmless = _singular_covariance_fixture()

    result = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.01,
    ).extract(_as_public_samples(harmful), _as_public_samples(harmless), n_directions=5)

    assert result.directions.shape == (1, 4)
    assert result.whitened_directions.shape == (1, 1)
    assert result.singular_values.shape == (1,)
    assert torch.count_nonzero(result.directions[0, 1:].abs() > 1e-6) == 0
    assert result.directions.norm() == pytest.approx(1.0)
    assert result.variance_explained == pytest.approx(1.0)
    assert result.effective_rank == pytest.approx(1.0, abs=1e-6)


def test_identical_harmful_and_harmless_inputs_are_rejected_as_no_refusal_signal() -> None:
    _, harmless = _activation_pair_fixture()
    extractor = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    )

    with pytest.raises(ValueError, match="without activation difference"):
        extractor.extract(_as_public_samples(harmless), _as_public_samples(harmless), 2)


@pytest.mark.parametrize(
    ("poisoned_side", "poisoned_value", "message"),
    [
        ("harmful", float("nan"), "finite"),
        ("harmless", float("inf"), "finite"),
    ],
)
def test_non_finite_activation_values_are_rejected_before_linear_algebra(
    poisoned_side: str,
    poisoned_value: float,
    message: str,
) -> None:
    harmful, harmless = _activation_pair_fixture()
    target = harmful if poisoned_side == "harmful" else harmless
    target[1, 2] = poisoned_value

    with pytest.raises(ValueError, match=message):
        WhitenedSVDExtractor().extract(_as_public_samples(harmful), _as_public_samples(harmless), 1)


def test_deterministic_replay_returns_byte_stable_cpu_outputs() -> None:
    harmful, harmless = _activation_pair_fixture()
    extractor = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    )

    first = extractor.extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)
    second = extractor.extract(_as_public_samples(harmful), _as_public_samples(harmless), 3)

    assert torch.equal(first.directions, second.directions)
    assert torch.equal(first.whitened_directions, second.whitened_directions)
    assert torch.equal(first.singular_values, second.singular_values)
    assert first.variance_explained == second.variance_explained
    assert first.condition_number == second.condition_number
    assert first.effective_rank == second.effective_rank


def test_extract_all_layers_returns_sorted_harmful_harmless_intersection_only() -> None:
    base_harmful, base_harmless = _activation_pair_fixture()
    harmful_by_layer = {
        8: _as_public_samples(base_harmful + 0.25),
        2: _as_public_samples(base_harmful),
        5: _as_public_samples(base_harmful * 1.5),
    }
    harmless_by_layer = {
        9: _as_public_samples(base_harmless),
        5: _as_public_samples(base_harmless * 1.5),
        2: _as_public_samples(base_harmless),
    }

    results = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    ).extract_all_layers(harmful_by_layer, harmless_by_layer, n_directions=2)

    assert list(results) == [2, 5]
    assert [result.layer_idx for result in results.values()] == [2, 5]
    assert all(result.directions.shape == (2, 4) for result in results.values())


def test_extract_all_layers_skips_missing_layers_without_stopping_later_matches() -> None:
    base_harmful, base_harmless = _activation_pair_fixture()
    harmful_by_layer = {
        2: _as_public_samples(base_harmful),
        5: _as_public_samples(base_harmful * 1.5),
        8: _as_public_samples(base_harmful + 0.25),
    }
    harmless_by_layer = {
        5: _as_public_samples(base_harmless * 1.5),
        8: _as_public_samples(base_harmless),
    }

    results = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    ).extract_all_layers(harmful_by_layer, harmless_by_layer, n_directions=2)

    assert list(results) == [5, 8]
    assert [result.layer_idx for result in results.values()] == [5, 8]


def test_extract_all_layers_uses_documented_default_direction_count() -> None:
    base_harmful, base_harmless = _activation_pair_fixture()
    harmful_by_layer = {5: _as_public_samples(base_harmful)}
    harmless_by_layer = {5: _as_public_samples(base_harmless)}

    results = WhitenedSVDExtractor(
        regularization_eps=REGULARIZATION_EPS,
        min_variance_ratio=0.0,
    ).extract_all_layers(harmful_by_layer, harmless_by_layer)

    assert list(results) == [5]
    assert results[5].directions.shape == (4, 4)
