"""Numerical and error-path tests for Fisher/LEACE direction extraction."""

from __future__ import annotations

import pytest
import torch

from obliteratus.analysis.leace import LEACEExtractor


def _activations():
    harmful = [
        torch.tensor([2.0, 0.0, 1.0]),
        torch.tensor([3.0, 1.0, 0.0]),
        torch.tensor([4.0, -1.0, 1.0]),
    ]
    harmless = [
        torch.tensor([-2.0, 0.0, 1.0]),
        torch.tensor([-3.0, 1.0, 0.0]),
        torch.tensor([-4.0, -1.0, 1.0]),
    ]
    return harmful, harmless


def test_extract_returns_normalized_reproducible_diagnostics():
    harmful, harmless = _activations()
    result = LEACEExtractor().extract(harmful, harmless, layer_idx=7)
    assert result.layer_idx == 7
    assert result.direction.norm().item() == pytest.approx(1.0)
    assert result.direction[0].abs().item() > 0.99
    assert result.generalized_eigenvalue > 0
    assert result.mean_diff_norm == pytest.approx(6.0)
    assert result.erasure_loss > 0
    assert result.within_class_condition >= 1


def test_extract_accepts_singleton_sequence_axis_and_shrinkage():
    harmful, harmless = _activations()
    harmful_3d = [value.unsqueeze(0) for value in harmful]
    harmless_3d = [value.unsqueeze(0) for value in harmless]
    result = LEACEExtractor(shrinkage=0.5).extract(harmful_3d, harmless_3d)
    assert result.direction.shape == (3,)
    assert torch.isfinite(result.direction).all()


def test_degenerate_classes_return_zero_direction():
    samples = [torch.ones(3), torch.ones(3)]
    result = LEACEExtractor().extract(samples, samples)
    assert torch.equal(result.direction, torch.zeros(3))
    assert result.generalized_eigenvalue == 0


def test_solver_failure_uses_least_squares(monkeypatch):
    harmful, harmless = _activations()

    def fail_solve(*_args, **_kwargs):
        raise torch.linalg.LinAlgError("fixture")

    monkeypatch.setattr(torch.linalg, "solve", fail_solve)
    result = LEACEExtractor().extract(harmful, harmless)
    assert result.direction.norm().item() == pytest.approx(1.0)


def test_condition_failure_is_reported_as_infinite(monkeypatch):
    harmful, harmless = _activations()
    original_solve = torch.linalg.solve
    monkeypatch.setattr(
        torch.linalg,
        "eigvalsh",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("fixture")),
    )
    monkeypatch.setattr(torch.linalg, "solve", original_solve)
    result = LEACEExtractor().extract(harmful, harmless)
    assert result.within_class_condition == float("inf")


def test_extract_all_layers_skips_unpaired_layers_and_sorts():
    harmful, harmless = _activations()
    results = LEACEExtractor().extract_all_layers(
        {2: harmful, 1: harmful, 3: harmful},
        {1: harmless, 2: harmless},
    )
    assert list(results) == [1, 2]
    assert results[1].layer_idx == 1


def test_compare_with_diff_of_means_handles_regular_and_degenerate_difference():
    harmful, harmless = _activations()
    result = LEACEExtractor().extract(harmful, harmless)
    comparison = LEACEExtractor.compare_with_diff_of_means(
        result,
        torch.stack(harmful).mean(0),
        torch.stack(harmless).mean(0),
    )
    assert comparison["cosine_similarity"] == pytest.approx(1.0)
    degenerate = LEACEExtractor.compare_with_diff_of_means(
        result, torch.zeros(3), torch.zeros(3),
    )
    assert degenerate["cosine_similarity"] == 0
