"""Tests for PromptEvolver — prompt improvement from accumulated learnings.

58 tests across 6 categories:
- TestEvolution (10): evolve flow, versioning, metadata, pattern limit, length check
- TestRollback (11): dual-condition checks, None filtering, restore, cascade
- TestVersionManagement (7): version numbering, metadata roundtrip, mismatch
- TestEdgeCases (7): missing dirs/files, empty patterns, tool history, auto-init
- TestBuildEvolutionPrompt (6): helper function validation
- TestCleanAndValidatePrompt (17): BUG-016 — output validation before saving

Architecture ref: ARCHITECTURE.md Section 12, Step 8.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from netanel_core.config import NathanConfig
from netanel_core.memory_store import MemoryStore
from netanel_core.prompt_evolver import (
    MAX_PATTERNS_PER_EVOLUTION,
    PromptEvolver,
    _build_evolution_prompt,
)

# ────────────────────────────────────────────
# Mock prompt constants
# Must satisfy BUG-016 validation constraints:
#   - Evolved: len >= MIN_EVOLVED_PROMPT_LENGTH (50)
#   - Evolved: len <= current_len * MAX_PROMPT_LENGTH_RATIO (2.0)
#   - So: current_len >= evolved_len / 2.0
#
# _MOCK_CURRENT_PROMPT: 62 chars, _MOCK_EVOLVED_PROMPT: 71 chars
# 71 >= 50 ✓ and 71 <= 62 * 2.0 = 124 ✓
# ────────────────────────────────────────────

_MOCK_CURRENT_PROMPT = "You are a helpful AI assistant that completes tasks with care."
_MOCK_EVOLVED_PROMPT = "You are a precise, helpful AI assistant. Complete all tasks accurately."

# ────────────────────────────────────────────
# Test memory constants (BUG-027 tests)
# Named constants prevent magic-string drift and make intent explicit.
# ────────────────────────────────────────────

_GLOBAL_TITLE_1 = "Global 1"
_GLOBAL_TITLE_2 = "Global 2"
_GLOBAL_CONTENT_1 = "global-first"
_GLOBAL_CONTENT_2 = "global-second"

_NS_TITLE_1 = "NS 1"
_NS_TITLE_2 = "NS 2"
_NS_CONTENT_1 = "namespace-first"
_NS_CONTENT_2 = "namespace-second"

_GLOBAL_SINGLE_TITLE = "Global pattern"
_GLOBAL_SINGLE_CONTENT = "global-learning-xyz"
_NS_SINGLE_TITLE = "NS pattern"
_NS_SINGLE_CONTENT = "namespace-learning-abc"

# ────────────────────────────────────────────
# Fixtures
# ────────────────────────────────────────────


@pytest.fixture
def tmp_config(tmp_path: Path) -> NathanConfig:
    """Create a NathanConfig with tmp_path as memories directory."""
    return NathanConfig(
        namespace="test",
        memories_dir=tmp_path / "memories",
    )


@pytest.fixture
def store(tmp_config: NathanConfig) -> MemoryStore:
    """Create a MemoryStore with the tmp config."""
    return MemoryStore(tmp_config)


class MockEvolverModel:
    """Mock model that returns a predictable rewritten prompt."""

    def __init__(self, response: str = "Evolved prompt content.") -> None:
        self.response = response
        self.call_count = 0
        self.last_messages: list[Any] = []

    def invoke(self, messages: list[Any]) -> str:
        self.call_count += 1
        self.last_messages = messages
        return self.response


class ErrorEvolverModel:
    """Mock model that always raises an error."""

    def invoke(self, messages: list[Any]) -> str:
        msg = "LLM API error"
        raise RuntimeError(msg)


def _create_evolver(
    config: NathanConfig,
    store: MemoryStore,
    response: str = _MOCK_EVOLVED_PROMPT,
) -> tuple[PromptEvolver, MockEvolverModel]:
    """Helper: create a PromptEvolver with mock model."""
    model = MockEvolverModel(response)
    evolver = PromptEvolver(config, store=store, model=model)
    return evolver, model


def _setup_prompt(store: MemoryStore, content: str = _MOCK_CURRENT_PROMPT) -> None:
    """Helper: write a base prompt_current.md."""
    store.write_prompt(content)


def _setup_versioned_prompt(store: MemoryStore, version: int, content: str | None = None) -> None:
    """Helper: write a versioned prompt file."""
    store.write_prompt(
        content or f"Prompt version {version}.",
        version=f"v{version}",
    )


def _add_patterns(store: MemoryStore, count: int) -> None:
    """Helper: add N pattern memories."""
    for i in range(count):
        store.write_memory(
            title=f"Pattern {i + 1}",
            content=f"Pattern content {i + 1}",
        )


# ────────────────────────────────────────────
# TestEvolution (10 tests)
# ────────────────────────────────────────────


class TestEvolution:
    """Tests for evolve() — the core evolution flow."""

    def test_evolve_creates_new_version(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """evolve with patterns → prompt_v1.md created."""
        _setup_prompt(store)
        _add_patterns(store, 3)
        evolver, _ = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is not None
        assert result.version == 1
        assert result.trigger == "manual"
        assert result.patterns_used == 3
        assert result.previous_version == 0
        # Verify versioned file exists
        assert (tmp_config.prompts_dir / "prompt_v1.md").exists()

    def test_evolve_increments_version(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Evolve twice → v1, then v2."""
        _setup_prompt(store)
        _add_patterns(store, 2)
        evolver, _ = _create_evolver(tmp_config, store)

        result1 = evolver.evolve(trigger="call_count")
        assert result1 is not None
        assert result1.version == 1

        result2 = evolver.evolve(trigger="failure_rate")
        assert result2 is not None
        assert result2.version == 2
        assert result2.previous_version == 1

    def test_evolve_copies_to_current(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """prompt_current.md content matches new version after evolution."""
        _setup_prompt(store, _MOCK_CURRENT_PROMPT)
        _add_patterns(store, 1)
        evolver, model = _create_evolver(tmp_config, store, _MOCK_EVOLVED_PROMPT)

        evolver.evolve(trigger="manual")

        current = store.read_prompt()
        assert current == _MOCK_EVOLVED_PROMPT

    def test_evolve_with_no_patterns(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """BUG-084: Empty patterns → returns None without firing LLM call.

        Before fix: evolve() called the LLM API even with 0 patterns, wasting budget.
        After fix: returns None immediately — callers already handle None gracefully.
        """
        _setup_prompt(store)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is None
        assert model.call_count == 0  # No LLM API call fired

    def test_evolve_returns_none_when_no_memories(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """BUG-084: evolve() returns None without LLM call when namespace has no memories.

        Verifies the early-return guard in _evolve_impl() fires before the LLM
        invocation. This prevents wasted API budget on new/empty namespaces,
        which is common in serverless/startup-every-session patterns.
        """
        _setup_prompt(store)  # Prompt exists, but no memories at all
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is None, "evolve() must return None when there are no memories to learn from"
        assert model.call_count == 0, "No LLM API call should be made when memories are empty"

    def test_evolve_with_no_current_prompt(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """No prompt_current.md + patterns → auto-initializes with default template."""
        _add_patterns(store, 2)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        # Auto-init creates v1 using default template
        assert result is not None
        assert result.version == 1
        assert model.call_count == 1  # LLM called for evolution

    def test_evolve_llm_failure(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """model.invoke raises → returns None, no crash.

        BUG-084: _add_patterns() ensures the BUG-084 guard is bypassed so the
        LLM invocation is actually reached and the graceful degradation path is tested.
        Without patterns, evolve() returns None before calling the model — the
        ErrorEvolverModel would never be exercised and the test would be meaningless.
        """
        _setup_prompt(store)
        _add_patterns(store, 1)  # BUG-084: patterns required to reach LLM call
        error_model = ErrorEvolverModel()
        evolver = PromptEvolver(tmp_config, store=store, model=error_model)

        result = evolver.evolve(trigger="manual")

        assert result is None

    def test_evolve_writes_metadata(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Metadata JSON created with all required fields."""
        _setup_prompt(store)
        _add_patterns(store, 5)
        evolver, _ = _create_evolver(tmp_config, store)

        evolver.evolve(trigger="pattern_accumulation")

        meta_path = tmp_config.prompts_dir / "prompt_v1_meta.json"
        assert meta_path.exists()
        meta = json.loads(meta_path.read_text())
        assert meta["version"] == 1
        assert meta["trigger"] == "pattern_accumulation"
        assert meta["patterns_used"] == 5
        assert meta["rolled_back"] is False
        assert "created_at" in meta

    def test_evolve_pattern_count_in_metadata(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """patterns_used in metadata matches actual count fed to LLM."""
        _setup_prompt(store)
        _add_patterns(store, 7)
        evolver, _ = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is not None
        assert result.patterns_used == 7
        meta = json.loads((tmp_config.prompts_dir / "prompt_v1_meta.json").read_text())
        assert meta["patterns_used"] == 7

    def test_evolve_limits_patterns_to_20(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Only top 20 patterns included in LLM call."""
        _setup_prompt(store)
        _add_patterns(store, 25)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is not None
        assert result.patterns_used == MAX_PATTERNS_PER_EVOLUTION
        # Verify the human message doesn't contain all 25
        human_msg = str(model.last_messages[-1])
        assert "showing top 20" in human_msg

    def test_evolve_prompt_length_explosion(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Evolved prompt >threshold aborts evolution (BUG-016: not just warn; BUG-066 sliding scale).

        BUG-066 adds a MIN_ABSOLUTE_PROMPT_GROWTH floor of 1000 chars so that short
        prompts can grow meaningfully.  The threshold is now:
            max(original * 2.0, original + 1000)
        For a 62-char prompt that is max(124, 1062) = 1062.
        Use an explosion response well above 1062 to trigger the abort.
        """
        # Use a current prompt long enough that the length check fires AFTER min-length passes.
        # current = 62 chars, threshold = max(124, 62+1000) = 1062.
        # Explosion response = 1100 chars (>= 50 min ✓, > 1062 threshold ✓)
        _setup_prompt(store, _MOCK_CURRENT_PROMPT)
        _add_patterns(store, 1)
        explosion_response = "A" * 1100  # 1100 > 1062 threshold → length explosion abort
        evolver, _ = _create_evolver(tmp_config, store, explosion_response)

        result = evolver.evolve(trigger="manual")

        # BUG-016 fix: abort (not just warn) — evolution returns None, prompt preserved
        assert result is None

    def test_evolve_includes_global_memories(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Global memories appear in the evolution LLM call (BUG-027).

        Before fix: only namespace memories were read.
        After fix: global memories interleaved with namespace memories.
        """
        _setup_prompt(store)
        # Write one global memory and one namespace memory
        store.write_memory(
            title=_GLOBAL_SINGLE_TITLE, content=_GLOBAL_SINGLE_CONTENT, namespace="global"
        )
        store.write_memory(title=_NS_SINGLE_TITLE, content=_NS_SINGLE_CONTENT)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is not None
        human_msg = str(model.last_messages[-1])
        assert _GLOBAL_SINGLE_CONTENT in human_msg, (
            "Global memory content must appear in evolution prompt (BUG-027)"
        )

    def test_evolve_interleaves_global_and_namespace_memories(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Global and namespace memories are interleaved in round-robin order (BUG-027).

        Interleaving: [G1, N1, G2, N2] — not block concatenation [G1, G2, N1, N2].
        """
        _setup_prompt(store)
        store.write_memory(title=_GLOBAL_TITLE_1, content=_GLOBAL_CONTENT_1, namespace="global")
        store.write_memory(title=_GLOBAL_TITLE_2, content=_GLOBAL_CONTENT_2, namespace="global")
        store.write_memory(title=_NS_TITLE_1, content=_NS_CONTENT_1)
        store.write_memory(title=_NS_TITLE_2, content=_NS_CONTENT_2)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is not None
        human_msg = str(model.last_messages[-1])
        # Both global and namespace content must appear
        assert _GLOBAL_CONTENT_1 in human_msg
        assert _GLOBAL_CONTENT_2 in human_msg
        assert _NS_CONTENT_1 in human_msg
        assert _NS_CONTENT_2 in human_msg
        # Interleave order: global-first appears before namespace-second
        assert human_msg.index(_GLOBAL_CONTENT_1) < human_msg.index(_NS_CONTENT_2), (
            "Global memories should be interleaved with namespace memories (not appended after)"
        )
        # Verify true round-robin: N1 should appear before G2
        # Block concat [G1, G2, N1, N2] would fail this; interleave [G1, N1, G2, N2] passes
        assert human_msg.index(_NS_CONTENT_1) < human_msg.index(_GLOBAL_CONTENT_2), (
            "Round-robin interleave violated: namespace-first should appear before global-second"
        )


# ────────────────────────────────────────────
# TestRollback (11 tests)
# ────────────────────────────────────────────


class TestRollback:
    """Tests for check_rollback() and rollback()."""

    def test_check_rollback_both_conditions_true(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """post < pre AND post < threshold → True."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[0.8, 0.9, 0.85],
            post_scores=[0.4, 0.3, 0.5],  # avg 0.4 < threshold 0.6
        )

        assert should is True
        assert stats["pre_avg"] == pytest.approx(0.85, rel=1e-2)
        assert stats["post_avg"] == pytest.approx(0.4, rel=1e-2)

    def test_check_rollback_post_above_threshold(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """post < pre BUT post > threshold → False (quality is acceptable)."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[0.9, 0.95, 0.92],
            post_scores=[0.7, 0.75, 0.72],  # avg 0.723 > threshold 0.6
        )

        assert should is False

    def test_check_rollback_post_above_pre(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """post > pre → False (quality improved)."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[0.5, 0.6],
            post_scores=[0.8, 0.9],
        )

        assert should is False

    def test_check_rollback_filters_none(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """None scores excluded from averages."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[0.8, None, 0.9, None],  # avg of [0.8, 0.9] = 0.85
            post_scores=[0.3, None, 0.5],  # avg of [0.3, 0.5] = 0.4
        )

        assert should is True
        assert stats["scored_pre"] == 2.0
        assert stats["scored_post"] == 2.0
        assert stats["pre_avg"] == pytest.approx(0.85, rel=1e-2)
        assert stats["post_avg"] == pytest.approx(0.4, rel=1e-2)

    def test_check_rollback_all_none(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """All None → insufficient data → False."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[None, None],
            post_scores=[None, None],
        )

        assert should is False
        assert stats["scored_pre"] == 0.0
        assert stats["scored_post"] == 0.0

    def test_check_rollback_insufficient_scored(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """< 2 scored calls per window → False."""
        evolver, _ = _create_evolver(tmp_config, store)

        should, stats = evolver.check_rollback(
            pre_scores=[0.8],  # only 1 scored
            post_scores=[0.3, 0.4],
        )

        assert should is False
        assert stats["scored_pre"] == 1.0

    def test_check_rollback_returns_stats(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Stats dict has all 4 required fields."""
        evolver, _ = _create_evolver(tmp_config, store)

        _, stats = evolver.check_rollback(
            pre_scores=[0.8, 0.9],
            post_scores=[0.5, 0.6],
        )

        assert "pre_avg" in stats
        assert "post_avg" in stats
        assert "scored_pre" in stats
        assert "scored_post" in stats

    def test_rollback_restores_previous(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """prompt_current.md = previous version content after rollback."""
        _setup_prompt(store, "Original v0 prompt.")
        _setup_versioned_prompt(store, 1, "Version 1 prompt.")
        _setup_versioned_prompt(store, 2, "Version 2 prompt.")
        # Set current to v2
        store.write_prompt("Version 2 prompt.")

        evolver, _ = _create_evolver(tmp_config, store)

        result = evolver.rollback()

        assert result is not None
        assert result.rolled_back is True
        assert result.from_version == 2
        assert result.to_version == 1
        assert store.read_prompt() == "Version 1 prompt."

    def test_rollback_updates_metadata(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """rolled_back=True on failed version's metadata after rollback."""
        _setup_prompt(store)
        _add_patterns(store, 2)
        evolver, _ = _create_evolver(tmp_config, store)

        # Evolve to create v1 with metadata
        evolver.evolve(trigger="manual")
        # Evolve again to create v2
        evolver.evolve(trigger="call_count")

        # Rollback from v2 → v1
        result = evolver.rollback()

        assert result is not None
        meta_path = tmp_config.prompts_dir / "prompt_v2_meta.json"
        meta = json.loads(meta_path.read_text())
        assert meta["rolled_back"] is True

    def test_rollback_from_v1(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Can't rollback from v1 — returns None."""
        _setup_prompt(store)
        _setup_versioned_prompt(store, 1)

        evolver, _ = _create_evolver(tmp_config, store)

        result = evolver.rollback()

        assert result is None

    def test_rollback_cascade(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """evolve → rollback → evolve → rollback: state tracks correctly."""
        _setup_prompt(store, _MOCK_CURRENT_PROMPT)
        _add_patterns(store, 2)
        evolver, _ = _create_evolver(tmp_config, store, _MOCK_EVOLVED_PROMPT)

        # First evolution: v0 → v1
        r1 = evolver.evolve(trigger="manual")
        assert r1 is not None
        assert r1.version == 1

        # First rollback: v1 → v0 (can't — v0 doesn't exist as file)
        # But v1 exists, so rollback needs v0 which won't be found
        rb1 = evolver.rollback()
        assert rb1 is None  # v0 doesn't exist as prompt_v0.md

        # Evolve again: creates v2 (v1 already exists)
        r2 = evolver.evolve(trigger="call_count")
        assert r2 is not None
        assert r2.version == 2
        assert r2.previous_version == 1

        # Second rollback: v2 → v1
        rb2 = evolver.rollback()
        assert rb2 is not None
        assert rb2.from_version == 2
        assert rb2.to_version == 1
        assert store.read_prompt() == _MOCK_EVOLVED_PROMPT  # v1 content


# ────────────────────────────────────────────
# TestVersionManagement (7 tests)
# ────────────────────────────────────────────


class TestVersionManagement:
    """Tests for version numbering and metadata management."""

    def test_get_current_version_empty(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """No versions → 0."""
        evolver, _ = _create_evolver(tmp_config, store)
        assert evolver._get_current_version() == 0

    def test_get_current_version_sequential(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """v1, v2, v3 → 3."""
        for v in range(1, 4):
            _setup_versioned_prompt(store, v)

        evolver, _ = _create_evolver(tmp_config, store)
        assert evolver._get_current_version() == 3

    def test_get_current_version_with_gaps(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """v1, v3 → 3 (gaps are OK)."""
        _setup_versioned_prompt(store, 1)
        _setup_versioned_prompt(store, 3)

        evolver, _ = _create_evolver(tmp_config, store)
        assert evolver._get_current_version() == 3

    def test_write_metadata_all_fields(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Metadata JSON has all 5 required fields."""
        evolver, _ = _create_evolver(tmp_config, store)

        path = evolver._write_metadata(1, "call_count", 15)

        assert path.exists()
        meta = json.loads(path.read_text())
        assert set(meta.keys()) == {
            "version",
            "created_at",
            "trigger",
            "patterns_used",
            "rolled_back",
        }
        assert meta["version"] == 1
        assert meta["trigger"] == "call_count"
        assert meta["patterns_used"] == 15
        assert meta["rolled_back"] is False

    def test_read_metadata_roundtrip(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Write → read → matches."""
        evolver, _ = _create_evolver(tmp_config, store)

        evolver._write_metadata(2, "failure_rate", 8)
        meta = evolver._read_metadata(2)

        assert meta is not None
        assert meta["version"] == 2
        assert meta["trigger"] == "failure_rate"
        assert meta["patterns_used"] == 8

    def test_read_metadata_missing(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Nonexistent metadata file → None."""
        evolver, _ = _create_evolver(tmp_config, store)

        meta = evolver._read_metadata(99)

        assert meta is None

    def test_read_metadata_version_mismatch(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Metadata version != filename version → corrected on read."""
        evolver, _ = _create_evolver(tmp_config, store)

        # Write metadata with wrong version field
        prompts_dir = tmp_config.prompts_dir
        prompts_dir.mkdir(parents=True, exist_ok=True)
        meta = {
            "version": 99,  # Wrong — file is for v3
            "created_at": "2026-02-15T00:00:00Z",
            "trigger": "manual",
            "patterns_used": 5,
            "rolled_back": False,
        }
        filepath = prompts_dir / "prompt_v3_meta.json"
        filepath.write_text(json.dumps(meta))

        result = evolver._read_metadata(3)

        assert result is not None
        assert result["version"] == 3  # Corrected to match filename


# ────────────────────────────────────────────
# TestEdgeCases (7 tests)
# ────────────────────────────────────────────


class TestEdgeCases:
    """Tests for edge cases and boundary conditions."""

    def test_prompts_dir_created(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Missing prompts dir → created during evolve (requires patterns to reach dir creation)."""
        _setup_prompt(store)
        _add_patterns(store, 1)
        evolver, _ = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is not None
        assert tmp_config.prompts_dir.exists()

    def test_prompt_current_missing_auto_init(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """No prompt_current.md + patterns → auto-initializes with default template."""
        _add_patterns(store, 2)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is not None
        assert model.call_count == 1
        # Verify default template was used as base
        from netanel_core.prompt_evolver import DEFAULT_PROMPT_TEMPLATE

        human_msg = model.last_messages[-1]
        msg_content = human_msg.content if hasattr(human_msg, "content") else str(human_msg)
        assert DEFAULT_PROMPT_TEMPLATE in msg_content
        # Verify prompt_current.md was created
        assert tmp_config.prompts_dir.exists()
        current = store.read_prompt()
        assert current is not None

    def test_auto_init_creates_default_prompt(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Auto-initialization writes default template to prompt_current.md."""
        # DEFAULT_PROMPT_TEMPLATE (current) is ~300+ chars, so _MOCK_EVOLVED_PROMPT (71 chars)
        # is well within 2x ratio and passes min-length check.
        _add_patterns(store, 2)
        evolver, _ = _create_evolver(tmp_config, store, response=_MOCK_EVOLVED_PROMPT)

        # No prompt exists yet (only memories, no prompt file)
        assert store.read_prompt() is None

        result = evolver.evolve(trigger="pattern_accumulation")

        # After evolution, prompt_current.md contains evolved content
        assert result is not None
        assert result.version == 1
        current = store.read_prompt()
        assert current == _MOCK_EVOLVED_PROMPT
        # v1 also exists
        v1 = store.read_prompt(version="v1")
        assert v1 == _MOCK_EVOLVED_PROMPT

    def test_empty_patterns_evolve(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """BUG-084: evolve() returns None with 0 patterns — no LLM call made.

        Before fix: LLM was called with an empty pattern list (patterns_used=0),
        wasting API budget. After fix: early return with None.
        """
        _setup_prompt(store, _MOCK_CURRENT_PROMPT)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is None
        assert model.call_count == 0  # No API call when there is nothing to learn from

    def test_tool_history_missing(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """No tool_history.jsonl → skipped gracefully."""
        _setup_prompt(store)
        _add_patterns(store, 1)
        evolver, model = _create_evolver(tmp_config, store)

        result = evolver.evolve(trigger="manual")

        assert result is not None
        # Verify no TOOL USAGE STATS section in prompt
        human_msg = str(model.last_messages[-1])
        assert "TOOL USAGE STATS" not in human_msg

    def test_evolution_prompt_includes_patterns(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Verify patterns content appears in the LLM call."""
        _setup_prompt(store)
        store.write_memory(title="Test", content="Use bullet points for lists")
        evolver, model = _create_evolver(tmp_config, store)

        evolver.evolve(trigger="manual")

        human_msg = str(model.last_messages[-1])
        assert "Use bullet points for lists" in human_msg

    def test_constructor_auto_creates_store(
        self,
        tmp_config: NathanConfig,
    ) -> None:
        """PromptEvolver(config) creates MemoryStore automatically."""
        model = MockEvolverModel()
        evolver = PromptEvolver(tmp_config, model=model)

        # Should work — store auto-created
        assert evolver._store is not None
        assert isinstance(evolver._store, MemoryStore)


# ────────────────────────────────────────────
# Test build_evolution_prompt helper
# ────────────────────────────────────────────


class TestBuildEvolutionPrompt:
    """Tests for the _build_evolution_prompt helper function."""

    def test_includes_current_prompt(self) -> None:
        result = _build_evolution_prompt("My prompt.", ["p1"], "manual")
        assert "My prompt." in result

    def test_includes_patterns(self) -> None:
        result = _build_evolution_prompt("Prompt.", ["Pattern A", "Pattern B"], "manual")
        assert "- Pattern A" in result
        assert "- Pattern B" in result

    def test_includes_trigger(self) -> None:
        result = _build_evolution_prompt("Prompt.", [], "call_count")
        assert "call_count" in result

    def test_includes_tool_history_when_provided(self) -> None:
        result = _build_evolution_prompt("Prompt.", [], "manual", tool_history="Tool stats here")
        assert "TOOL USAGE STATS" in result
        assert "Tool stats here" in result

    def test_excludes_tool_history_when_none(self) -> None:
        result = _build_evolution_prompt("Prompt.", [], "manual", tool_history=None)
        assert "TOOL USAGE STATS" not in result

    def test_limits_patterns(self) -> None:
        patterns = [f"Pattern {i}" for i in range(30)]
        result = _build_evolution_prompt("Prompt.", patterns, "manual")
        assert f"showing top {MAX_PATTERNS_PER_EVOLUTION}" in result
        assert "Pattern 0" in result  # First included
        assert "Pattern 19" in result  # Last included
        assert "- Pattern 20" not in result  # 21st excluded


# ────────────────────────────────────────────
# TestModelInit (2 tests — BUG-007)
# ────────────────────────────────────────────


class TestModelInit:
    """Verify PromptEvolver uses config.evolution.max_evolution_tokens (BUG-007)."""

    def test_uses_max_evolution_tokens_from_config(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """PromptEvolver passes config.evolution.max_evolution_tokens to ChatOpenAI.

        Not config.safety.max_tokens_per_task (which is 100,000 and causes API rejection).
        """
        from unittest.mock import patch

        from netanel_core.config import EvolutionConfig, NathanConfig
        from netanel_core.memory_store import MemoryStore
        from netanel_core.prompt_evolver import PromptEvolver

        config = NathanConfig(
            namespace="test",
            memories_dir=tmp_path / "memories",
            evolution=EvolutionConfig(max_evolution_tokens=2048),
        )
        store = MemoryStore(config)

        captured_kwargs: dict = {}

        class MockChatOpenAI:
            def __init__(self, **kwargs: object) -> None:
                captured_kwargs.update(kwargs)

        # ChatOpenAI is lazily imported inside __init__ via `from langchain_openai import ...`
        # Patch at the source module to intercept the lazy import.
        with patch("langchain_openai.ChatOpenAI", MockChatOpenAI):
            PromptEvolver(config, store=store)

        assert captured_kwargs.get("max_completion_tokens") == 2048, (
            f"Expected max_completion_tokens=2048, got {captured_kwargs.get('max_completion_tokens')}. "
            "PromptEvolver must use config.evolution.max_evolution_tokens via max_completion_tokens "
            "(renamed from deprecated max_tokens in Sep 2024 — BUG-041)."
        )

    def test_does_not_use_max_tokens_per_task(self, tmp_path: Path) -> None:
        """Ensure task budget cap (100,000) is NOT passed as completion token limit."""
        from unittest.mock import patch

        from netanel_core.config import NathanConfig
        from netanel_core.memory_store import MemoryStore
        from netanel_core.prompt_evolver import PromptEvolver

        config = NathanConfig(
            namespace="test",
            memories_dir=tmp_path / "memories",
        )
        store = MemoryStore(config)

        captured_kwargs: dict = {}

        class MockChatOpenAI:
            def __init__(self, **kwargs: object) -> None:
                captured_kwargs.update(kwargs)

        # ChatOpenAI is lazily imported inside __init__ via `from langchain_openai import ...`
        with patch("langchain_openai.ChatOpenAI", MockChatOpenAI):
            PromptEvolver(config, store=store)

        # max_tokens_per_task is 100,000 — must never be used as completion limit
        assert captured_kwargs.get("max_completion_tokens") != config.safety.max_tokens_per_task, (
            "PromptEvolver must not pass max_tokens_per_task as completion token limit. "
            "It exceeds gpt-4o-mini's 16,384 limit and causes API rejection (BUG-007)."
        )


# ────────────────────────────────────────────
# TestCleanAndValidatePrompt (BUG-016)
# ────────────────────────────────────────────


class TestCleanAndValidatePrompt:
    """BUG-016 regression: validate and clean LLM output before saving as evolved prompt.

    _clean_and_validate_prompt() must:
    - Strip markdown code fences (plain and language-tagged)
    - Strip common preamble patterns (first line)
    - Reject prompts shorter than MIN_EVOLVED_PROMPT_LENGTH
    - Reject whitespace-only prompts (even if len >= 50)
    - Reject empty current_prompt (defensive guard)
    - Reject length explosions (abort, not just warn)
    - Pass valid prompts through unchanged
    """

    VALID_PROMPT = "You are a helpful assistant that writes production-grade Python code."
    # 69 chars — above MIN_EVOLVED_PROMPT_LENGTH=50

    def test_valid_prompt_passes_through(self) -> None:
        """A clean, valid prompt is returned unchanged."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        result = _clean_and_validate_prompt(self.VALID_PROMPT, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_code_fence_stripped(self) -> None:
        """Plain code fence is stripped, leaving clean prompt text."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        fenced = f"```\n{self.VALID_PROMPT}\n```"
        result = _clean_and_validate_prompt(fenced, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_language_tagged_code_fence_stripped(self) -> None:
        """Language-tagged code fence (```text, ```markdown) is stripped."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        fenced = f"```markdown\n{self.VALID_PROMPT}\n```"
        result = _clean_and_validate_prompt(fenced, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_code_fence_no_trailing_newline_stripped(self) -> None:
        """Code fence with no trailing newline before closing ``` is stripped (Gate 9 fix)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        # No \n before closing fence: ```\ncontent```
        fenced = f"```\n{self.VALID_PROMPT}```"
        result = _clean_and_validate_prompt(fenced, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_leading_whitespace_before_code_fence_stripped(self) -> None:
        """Code fence preceded by leading whitespace/newline is still stripped (Gate 9 fix)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        # LLM output starts with \n before the fence: \n```\nYour prompt\n```
        fenced = f"\n```\n{self.VALID_PROMPT}\n```"
        result = _clean_and_validate_prompt(fenced, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_preamble_pattern_stripped(self) -> None:
        """LLM preamble on its own line is stripped, content on next line preserved."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        with_preamble = f"Here's the improved prompt:\n{self.VALID_PROMPT}"
        result = _clean_and_validate_prompt(with_preamble, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT

    def test_inline_preamble_content_preserved(self) -> None:
        """Inline preamble prefix stripped, same-line content kept (Gate 9 fix)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        # Preamble and content on same line: "Here's the improved prompt: <actual content>"
        inline = f"Here's the improved prompt: {self.VALID_PROMPT}"
        result = _clean_and_validate_prompt(inline, self.VALID_PROMPT)
        assert result == self.VALID_PROMPT  # Content preserved, prefix stripped

    def test_empty_prompt_returns_none(self) -> None:
        """Empty LLM output is rejected (returns None, aborts evolution)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        result = _clean_and_validate_prompt("", self.VALID_PROMPT)
        assert result is None

    def test_too_short_prompt_returns_none(self) -> None:
        """Prompt shorter than MIN_EVOLVED_PROMPT_LENGTH is rejected."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        short = "Too short."  # 10 chars < 50
        result = _clean_and_validate_prompt(short, self.VALID_PROMPT)
        assert result is None

    def test_whitespace_only_prompt_returns_none(self) -> None:
        """Whitespace-only prompt is rejected even if raw len >= 50 (BUG-016 edge case)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        whitespace = "   \n   \n   \n   \n   \n   \n   \n   \n   "  # 36+ chars but all whitespace
        result = _clean_and_validate_prompt(whitespace, "short")
        assert result is None

    def test_empty_current_prompt_returns_none(self) -> None:
        """Empty current_prompt is rejected defensively (can't validate ratio)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        result = _clean_and_validate_prompt(self.VALID_PROMPT, "")
        assert result is None

    def test_length_explosion_returns_none(self) -> None:
        """Prompt exceeding sliding threshold is rejected (BUG-016 + BUG-066 fix).

        Threshold = max(original * 2.0, original + MIN_ABSOLUTE_PROMPT_GROWTH).
        For a 21-char prompt: max(42, 1021) = 1021.
        """
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        short_current = "Short current prompt."  # 21 chars
        # Threshold = max(21*2, 21+1000) = 1021 — exploded must exceed this
        exploded = "A" * 1100  # 1100 > 1021 threshold → rejected
        result = _clean_and_validate_prompt(exploded, short_current)
        assert result is None

    def test_length_within_ratio_accepted(self) -> None:
        """Prompt within 2x ratio passes the length explosion check."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        current = (
            "You are a helpful assistant that writes clean code every time always."  # 70 chars
        )
        # 1.54x growth: 108 chars < 70 * 2.0 = 140 threshold ✓
        within = current + " Always follow instructions carefully."  # 108 chars
        result = _clean_and_validate_prompt(within, current)
        assert result is not None
        assert "Always follow instructions carefully" in result

    def test_evolve_impl_aborts_on_invalid_output(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Integration: _evolve_impl() returns None when validation fails (BUG-016)."""
        from unittest.mock import MagicMock

        from netanel_core.prompt_evolver import PromptEvolver

        store.write_prompt(self.VALID_PROMPT)  # Set current prompt
        _add_patterns(store, 1)  # BUG-084: memories required to reach LLM call

        # MagicMock used (not MockEvolverModel) to explicitly exercise the
        # `hasattr(result, "content")` branch in _evolve_impl(). MockEvolverModel
        # returns a plain string, which takes the else-branch. Both are valid.
        mock_result = MagicMock()
        mock_result.content = ""
        mock_model = MagicMock()
        mock_model.invoke.return_value = mock_result

        evolver = PromptEvolver(tmp_config, store=store, model=mock_model)
        result = evolver._evolve_impl(trigger="test")

        assert result is None  # Evolution aborted — current prompt preserved

    def test_evolve_impl_saves_cleaned_prompt(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Integration: code fence stripped BEFORE saving — not raw output."""
        from unittest.mock import MagicMock

        from netanel_core.prompt_evolver import PromptEvolver

        store.write_prompt(self.VALID_PROMPT)
        _add_patterns(store, 1)  # BUG-084: memories required to reach LLM call

        # MagicMock used (not MockEvolverModel) to explicitly exercise the
        # `hasattr(result, "content")` branch. See test_evolve_impl_aborts_on_invalid_output.
        fenced = f"```\n{self.VALID_PROMPT}\n```"
        mock_result = MagicMock()
        mock_result.content = fenced
        mock_model = MagicMock()
        mock_model.invoke.return_value = mock_result

        evolver = PromptEvolver(tmp_config, store=store, model=mock_model)
        result = evolver._evolve_impl(trigger="test")

        assert result is not None  # Evolution succeeded
        saved = store.read_prompt()
        assert saved is not None
        assert "```" not in saved  # Code fence was stripped before saving
        assert self.VALID_PROMPT in saved


# ────────────────────────────────────────────
# TestBug028PatternCategoryFilter (BUG-028)
# ────────────────────────────────────────────


class TestBug028PatternCategoryFilter:
    """BUG-028: Evolution uses only category='pattern' memories when available.

    Trigger counter increments only for 'pattern' category learnings, but
    evolve() was passing ALL memory categories (patterns + decisions + improvements)
    to the LLM as if they were patterns. Fix: filter by category='pattern' before
    building the evolution prompt. Fall back to all memories if none are tagged.
    """

    def test_evolve_uses_only_pattern_category(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Only pattern-category memories are fed to the evolution LLM call.

        Decision and improvement memories must NOT appear in the evolution input
        when pattern-category memories exist.
        """
        _setup_prompt(store)

        # Write a pattern memory (should appear in evolution prompt)
        store.write_memory(
            title="Pattern: use bullet points",
            content="Always use bullet points for lists",
            metadata={"category": "pattern"},
        )
        # Write a decision memory (must NOT appear when patterns exist)
        store.write_memory(
            title="Decision: chose Redis",
            content="We chose Redis over Memcached for persistence",
            metadata={"category": "decision"},
        )
        # Write an improvement memory (must NOT appear when patterns exist)
        store.write_memory(
            title="Improvement: retry logic",
            content="Added exponential backoff to retry logic",
            metadata={"category": "improvement"},
        )

        evolver, model = _create_evolver(tmp_config, store)
        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is not None
        human_msg = str(model.last_messages[-1])

        # Pattern content must be present
        assert "Always use bullet points for lists" in human_msg
        # Non-pattern categories must be absent
        assert "We chose Redis over Memcached for persistence" not in human_msg
        assert "Added exponential backoff to retry logic" not in human_msg

    def test_evolve_fallback_when_no_pattern_category(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """When no pattern-category memories exist, fall back to all memories.

        Backward compatibility: namespaces that stored memories without a
        'category' tag must still receive evolution input (not an empty prompt).
        """
        _setup_prompt(store)

        # Write memories without any category tag (legacy style)
        store.write_memory(title="Old learning 1", content="Legacy learning alpha")
        store.write_memory(title="Old learning 2", content="Legacy learning beta")

        evolver, model = _create_evolver(tmp_config, store)
        result = evolver.evolve(trigger="pattern_accumulation")

        assert result is not None
        human_msg = str(model.last_messages[-1])

        # Both legacy memories must appear in the evolution prompt
        assert "Legacy learning alpha" in human_msg
        assert "Legacy learning beta" in human_msg


# ────────────────────────────────────────────
# TestBug029ToolHistorySummary (BUG-029)
# ────────────────────────────────────────────


class TestBug029ToolHistorySummary:
    """BUG-029: _read_tool_history() summarizes stats instead of returning raw JSONL.

    Raw JSONL injected directly into the LLM prompt wastes tokens (~50k for 1000
    entries). Fix: aggregate per-tool stats and return one summary line per tool.
    """

    def _write_tool_history(self, config: NathanConfig, records: list[dict]) -> None:
        """Helper: write JSONL tool history records to the namespace dir."""
        import json

        namespace_dir = config.namespace_dir
        namespace_dir.mkdir(parents=True, exist_ok=True)
        path = namespace_dir / "tool_history.jsonl"
        path.write_text(
            "\n".join(json.dumps(r) for r in records) + "\n",
            encoding="utf-8",
        )

    def test_read_tool_history_summarizes_not_raw(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Output is a summary format — no JSON braces, no timestamps in the result.

        5 records for one tool → one summary line with call count, success rate,
        and average duration. No raw JSONL in the output.
        """

        records = [
            {"tool": "search", "success": True, "duration_ms": 100},
            {"tool": "search", "success": True, "duration_ms": 200},
            {"tool": "search", "success": False, "duration_ms": 150},
            {"tool": "search", "success": True, "duration_ms": 300},
            {"tool": "search", "success": True, "duration_ms": 250},
        ]
        self._write_tool_history(tmp_config, records)

        evolver, _ = _create_evolver(tmp_config, store)
        result = evolver._read_tool_history()

        assert result is not None
        # Must be one summary line for "search"
        assert "search" in result
        assert "5 calls" in result
        assert "80% success" in result
        # No raw JSON — no braces, no "duration_ms" key names
        assert "{" not in result
        assert "duration_ms" not in result

    def test_read_tool_history_multiple_tools(
        self, tmp_config: NathanConfig, store: MemoryStore
    ) -> None:
        """Each tool gets its own summary line, sorted alphabetically."""
        records = [
            {"tool": "bash", "success": True, "duration_ms": 50},
            {"tool": "bash", "success": False, "duration_ms": 75},
            {"tool": "search", "success": True, "duration_ms": 200},
            {"tool": "write_file", "success": True, "duration_ms": 30},
            {"tool": "write_file", "success": True, "duration_ms": 40},
        ]
        self._write_tool_history(tmp_config, records)

        evolver, _ = _create_evolver(tmp_config, store)
        result = evolver._read_tool_history()

        assert result is not None
        lines = result.strip().splitlines()

        # Three tools → three lines
        assert len(lines) == 3

        # Alphabetical order: bash, search, write_file
        assert "bash" in lines[0]
        assert "search" in lines[1]
        assert "write_file" in lines[2]

        # bash: 2 calls, 50% success
        assert "2 calls" in lines[0]
        assert "50% success" in lines[0]

        # search: 1 call, 100% success
        assert "1 calls" in lines[1]
        assert "100% success" in lines[1]

        # write_file: 2 calls, 100% success, avg 35ms
        assert "2 calls" in lines[2]
        assert "100% success" in lines[2]
        assert "35ms" in lines[2]


# ────────────────────────────────────────────
# TestVersionCache (BUG-047)
# ────────────────────────────────────────────


class TestVersionCache:
    """BUG-047: _get_current_version() caches result instead of scanning directory every call."""

    def test_version_cache_initialized_to_none(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Cache starts as None before any call."""
        evolver, _ = _create_evolver(tmp_config, store)
        assert evolver._version_cache is None

    def test_first_call_populates_cache(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """After first _get_current_version() call, cache is set."""
        evolver, _ = _create_evolver(tmp_config, store)
        _setup_prompt(store)
        evolver._get_current_version()
        assert evolver._version_cache is not None

    def test_cache_returns_correct_version(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """Cached value matches actual directory scan result."""
        evolver, _ = _create_evolver(tmp_config, store)
        _setup_prompt(store)
        direct = evolver._get_current_version()  # populates cache
        cached = evolver._get_current_version()  # returns cache
        assert direct == cached

    def test_evolve_updates_cache(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """After successful evolve(), cache reflects the new version."""
        evolver, _ = _create_evolver(tmp_config, store)
        _setup_prompt(store)
        _add_patterns(store, 2)
        result = evolver.evolve(trigger="manual")
        assert result is not None
        assert evolver._version_cache == result.version

    def test_rollback_invalidates_cache(self, tmp_config: NathanConfig, store: MemoryStore) -> None:
        """After rollback(), cache is None so next call re-scans."""
        evolver, _ = _create_evolver(tmp_config, store)
        _setup_prompt(store)
        _add_patterns(store, 2)
        evolver.evolve(trigger="manual")  # v0 → v1
        evolver.evolve(trigger="manual")  # v1 → v2
        evolver.rollback()
        # Cache invalidated — must re-scan
        assert evolver._version_cache is None


# ────────────────────────────────────────────
# TestAbsoluteGrowthFloor (BUG-066)
# ────────────────────────────────────────────


class TestAbsoluteGrowthFloor:
    """BUG-066: short prompts can grow by at least MIN_ABSOLUTE_PROMPT_GROWTH chars."""

    def test_short_prompt_allowed_to_grow_beyond_2x(self) -> None:
        """A 100-char prompt growing to 900 chars is accepted (< floor of 100+1000=1100)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        short = "A" * 100  # 100 chars; old 2x limit = 200
        # 900 chars — would be rejected at 2x (>200) but floor allows up to 1100
        grown = "B" * 900
        result = _clean_and_validate_prompt(grown, short)
        assert result is not None, "Growth within absolute floor should be accepted"

    def test_short_prompt_explosion_still_blocked(self) -> None:
        """A 100-char prompt growing to 1200 chars is rejected (> floor of 100+1000=1100)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        short = "A" * 100
        exploded = "B" * 1200  # 1200 > max(200, 1100)=1100 → blocked
        result = _clean_and_validate_prompt(exploded, short)
        assert result is None

    def test_long_prompt_ratio_still_applies(self) -> None:
        """A 2000-char prompt: 2x=4000 wins over floor 3000 — 4500 chars rejected."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        long_prompt = "A" * 2000
        # max(2000*2, 2000+1000) = max(4000, 3000) = 4000
        too_long = "B" * 4100  # 4100 > 4000 → blocked
        result = _clean_and_validate_prompt(too_long, long_prompt)
        assert result is None

    def test_long_prompt_within_ratio_accepted(self) -> None:
        """A 2000-char prompt growing to 3500 chars is accepted (< 4000 threshold)."""
        from netanel_core.prompt_evolver import _clean_and_validate_prompt

        long_prompt = "A" * 2000
        grown = "B" * 3500  # 3500 < max(4000, 3000)=4000 → accepted
        result = _clean_and_validate_prompt(grown, long_prompt)
        assert result is not None
