"""Tests for the crystallization probe."""

import numpy as np
import pytest

from abiogenesis.probe import CrystallizationProbe, ProbeReading
from abiogenesis.metrics import shannon_entropy


class TestShannonEntropy:
    def test_uniform_max_entropy(self):
        """All-unique tapes should have maximum entropy (log2(n))."""
        rng = np.random.default_rng(42)
        soup = rng.integers(0, 256, size=(64, 32), dtype=np.uint8)
        h = shannon_entropy(soup)
        # 64 unique tapes -> H ~ log2(64) = 6.0
        assert 5.5 < h < 6.1

    def test_monoculture_zero_entropy(self):
        """All-identical tapes should have entropy 0."""
        tape = np.array([42] * 8, dtype=np.uint8)
        soup = np.tile(tape, (64, 1))
        h = shannon_entropy(soup)
        assert h == 0.0

    def test_two_species(self):
        """Two equally common tapes -> H = 1.0."""
        tape_a = np.zeros(8, dtype=np.uint8)
        tape_b = np.ones(8, dtype=np.uint8)
        soup = np.vstack([np.tile(tape_a, (32, 1)), np.tile(tape_b, (32, 1))])
        h = shannon_entropy(soup)
        assert abs(h - 1.0) < 0.01


class TestCrystallizationProbe:
    def _make_diverse_soup(self, n=64, tape_len=8, seed=42):
        """Create a soup with high diversity."""
        rng = np.random.default_rng(seed)
        return rng.integers(0, 256, size=(n, tape_len), dtype=np.uint8)

    def _make_crystallized_soup(self, n=64, tape_len=8):
        """Create a monoculture soup."""
        tape = np.array([0x42] * tape_len, dtype=np.uint8)
        return np.tile(tape, (n, 1))

    def test_reading_fields(self):
        """ProbeReading should have all required fields."""
        probe = CrystallizationProbe()
        soup = self._make_diverse_soup()
        reading = probe.measure(soup, interaction=1000)

        assert isinstance(reading, ProbeReading)
        assert reading.interaction == 1000
        assert reading.shannon_entropy > 0
        assert reading.compression_ratio > 0
        assert reading.alert_level == "none"
        assert reading.unique_tapes > 0

    def test_first_reading_zero_delta(self):
        """First reading should have delta and accel of 0."""
        probe = CrystallizationProbe()
        soup = self._make_diverse_soup()
        reading = probe.measure(soup, interaction=1000)
        assert reading.entropy_delta == 0.0
        assert reading.entropy_accel == 0.0

    def test_stable_entropy_no_alert(self):
        """Constant entropy should produce no alerts."""
        probe = CrystallizationProbe(window_size=3)
        soup = self._make_diverse_soup()

        # Take identical readings (same soup -> same entropy)
        for i in range(20):
            reading = probe.measure(soup, interaction=i * 1000)
        assert reading.alert_level == "none"

    def test_warning_on_diversity_collapse(self):
        """Warning should fire when diversity drops below 50% of peak."""
        probe = CrystallizationProbe(window_size=3)
        n = 64
        tape_len = 8

        # Start with diverse soup to establish peak
        rng = np.random.default_rng(42)
        soup = rng.integers(0, 256, size=(n, tape_len), dtype=np.uint8)
        probe.measure(soup, interaction=0)  # Establish peak unique count

        # Gradually crystallize with decreasing entropy
        dominant = np.array([0x42] * tape_len, dtype=np.uint8)
        alerts = []
        for step in range(1, 15):
            test_soup = soup.copy()
            # Replace increasing fraction with dominant tape
            n_replace = min(step * 5, n - 1)
            test_soup[:n_replace] = dominant
            reading = probe.measure(test_soup, interaction=step * 1000)
            alerts.append(reading.alert_level)

        assert "warning" in alerts or "critical" in alerts

    def test_critical_on_accelerating_loss(self):
        """Critical should fire when entropy loss is accelerating."""
        probe = CrystallizationProbe(window_size=3, critical_threshold=-0.1)
        n = 64
        tape_len = 8

        # Start with diverse soup
        rng = np.random.default_rng(42)
        base_soup = rng.integers(0, 256, size=(n, tape_len), dtype=np.uint8)
        probe.measure(base_soup, interaction=0)

        dominant = np.array([0x42] * tape_len, dtype=np.uint8)

        # Create increasingly crystallized soups with accelerating pace
        for step in range(1, 20):
            soup = base_soup.copy()
            # Accelerating replacement: step^2 growth
            n_replace = min(step * step, n - 1)
            soup[:n_replace] = dominant
            reading = probe.measure(soup, interaction=step * 1000)

        history = probe.get_history()
        alerts = [r.alert_level for r in history]
        assert "critical" in alerts

    def test_no_alert_without_diversity_loss(self):
        """No alert if entropy decreases but diversity stays high."""
        probe = CrystallizationProbe(window_size=3)

        # Create soups with decreasing entropy but still high unique count
        for step in range(10):
            rng = np.random.default_rng(step)
            soup = rng.integers(0, 256, size=(64, 8), dtype=np.uint8)
            reading = probe.measure(soup, interaction=step * 1000)

        # All readings should have high diversity, no alert
        assert reading.alert_level == "none"

    def test_history_accumulates(self):
        """get_history() should return all readings."""
        probe = CrystallizationProbe()
        soup = self._make_diverse_soup()
        for i in range(5):
            probe.measure(soup, interaction=i * 1000)
        assert len(probe.get_history()) == 5

    def test_reading_to_dict(self):
        """ProbeReading.to_dict() should return a plain dict."""
        probe = CrystallizationProbe()
        soup = self._make_diverse_soup()
        reading = probe.measure(soup, interaction=1000)
        d = reading.to_dict()
        assert isinstance(d, dict)
        assert "shannon_entropy" in d
        assert "alert_level" in d
        assert "unique_tapes" in d
