"""
Tests for Phase 23.7 Full Version — Self-Improving Loop (Auto mode)

Covers:
  - SafetyConfig.full_mode() / lite_mode() presets
  - SafetyGuard auto-unlock: check_unlock_readiness, check_update with auto_unlock
  - ReadinessResult structure and blocking conditions
  - ImprovementWorker.run_once() (bulk scan)
  - ImprovementWorker.check_all_readiness()
  - ImprovementWorkerThread start/stop/status
  - SDK: check_improvement_readiness, run_all_improvements, check_all_readiness
  - SDK: start/stop/status improvement worker
  - SDK: run_improvement_cycle with auto_unlock=True
  - REST: /experience/improve/readiness
  - REST: /experience/improve/readiness/all
  - REST: /experience/improve/all
  - REST: /experience/worker/start, stop, status
"""

import time
import pytest

from openmemo.storage.sqlite_store import SQLiteStore
from openmemo.improvement.safety import (
    SafetyConfig, SafetyGuard, SafetyCheckResult, ReadinessResult
)
from openmemo.improvement.evaluator import EvaluationEngine
from openmemo.improvement.outcome import Outcome
from openmemo.experience.models import Experience
from openmemo.workers.improvement_worker import ImprovementWorker, ImprovementWorkerThread
from openmemo.api.sdk import Memory


# ── Shared fixtures ──────────────────────────────────────────────────

@pytest.fixture
def store(tmp_path):
    db = str(tmp_path / "test_full.db")
    s = SQLiteStore(db_path=db)
    yield s
    s.conn.close()


@pytest.fixture
def memory_obj(store):
    m = Memory.__new__(Memory)
    m.store = store
    m._exp_registry = None
    m._llm_fn = None
    return m


@pytest.fixture
def rest(tmp_path):
    """Flask test client + shared store for REST tests."""
    db = str(tmp_path / "rest_full.db")
    from openmemo.api.rest_server import create_app
    app = create_app(db_path=db)
    app.config["TESTING"] = True
    with app.test_client() as client:
        store = SQLiteStore(db_path=db)
        yield client, store
        store.conn.close()


def _make_experience(store, exp_id="exp-full-001", title="Full Test Experience"):
    """Insert a minimal experience via put_experience."""
    exp = Experience(
        title=title,
        type="workflow",
        steps=["step1"],
        outcome="success",
        confidence=0.65,
    )
    exp.id = exp_id
    store.put_experience(exp.to_dict())
    return exp_id


def _add_outcomes(store, exp_id, n_success=5, n_failure=0):
    """Add N outcomes for the experience and bump usage_count."""
    for _ in range(n_success):
        store.put_outcome(Outcome(
            experience_id=exp_id,
            result="success",
            agent_id="agent-test",
            metrics={"latency_ms": 100},
        ).to_dict())
    for _ in range(n_failure):
        store.put_outcome(Outcome(
            experience_id=exp_id,
            result="failure",
            agent_id="agent-test",
        ).to_dict())
    store.conn.execute(
        "UPDATE experiences SET usage_count = usage_count + ? WHERE id = ?",
        (n_success + n_failure, exp_id),
    )
    store.conn.commit()


def _set_usage(store, exp_id, count):
    store.conn.execute(
        "UPDATE experiences SET usage_count = ? WHERE id = ?", (count, exp_id)
    )
    store.conn.commit()


# ══════════════════════════════════════════════════════════════════════
# SafetyConfig presets
# ══════════════════════════════════════════════════════════════════════

class TestSafetyConfigPresets:
    def test_full_mode_preset(self):
        cfg = SafetyConfig.full_mode()
        assert cfg.auto_improve is True
        assert cfg.auto_unlock is True
        assert cfg.unlock_min_outcomes == 10
        assert cfg.unlock_min_usage == 5

    def test_lite_mode_preset(self):
        cfg = SafetyConfig.lite_mode()
        assert cfg.auto_improve is False
        assert cfg.auto_unlock is False

    def test_full_mode_still_has_safety_rules(self):
        cfg = SafetyConfig.full_mode()
        assert cfg.min_outcomes >= 1
        assert cfg.max_versions >= 1
        assert cfg.cooldown_seconds >= 0

    def test_default_is_lite(self):
        cfg = SafetyConfig()
        assert cfg.auto_improve is False
        assert cfg.auto_unlock is False


# ══════════════════════════════════════════════════════════════════════
# SafetyGuard.check_unlock_readiness
# ══════════════════════════════════════════════════════════════════════

class TestCheckUnlockReadiness:
    def test_not_ready_no_data(self, store):
        exp_id = _make_experience(store)
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        r = guard.check_unlock_readiness(exp_id)
        assert isinstance(r, ReadinessResult)
        assert r.ready is False
        assert r.outcomes_count == 0
        assert r.usage_count == 0
        assert len(r.blocking) > 0

    def test_not_ready_partial_conditions(self, store):
        exp_id = _make_experience(store)
        _add_outcomes(store, exp_id, n_success=3)
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        r = guard.check_unlock_readiness(exp_id)
        assert r.ready is False
        assert r.outcomes_count == 3

    def test_ready_when_conditions_met(self, store):
        exp_id = _make_experience(store)
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        r = guard.check_unlock_readiness(exp_id)
        assert r.ready is True
        assert r.blocking == []

    def test_to_dict_structure(self, store):
        exp_id = _make_experience(store)
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        d = guard.check_unlock_readiness(exp_id).to_dict()
        assert "experience_id" in d
        assert "ready" in d
        assert "conditions" in d
        assert "outcomes_count" in d
        assert "usage_count" in d
        assert "blocking" in d

    def test_check_readiness_batch(self, store):
        ids = [_make_experience(store, f"exp-batch-{i}", f"Batch {i}") for i in range(3)]
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        results = guard.check_readiness_batch(ids)
        assert len(results) == 3
        for eid in ids:
            assert eid in results
            assert isinstance(results[eid], ReadinessResult)

    def test_conditions_dict_has_expected_keys(self, store):
        exp_id = _make_experience(store)
        guard = SafetyGuard(config=SafetyConfig.full_mode(), store=store)
        r = guard.check_unlock_readiness(exp_id)
        assert len(r.conditions) == 2  # outcomes + usage


# ══════════════════════════════════════════════════════════════════════
# SafetyGuard.check_update with auto_unlock
# ══════════════════════════════════════════════════════════════════════

class TestCheckUpdateAutoUnlock:
    def test_auto_unlock_blocks_when_conditions_not_met(self, store):
        exp_id = _make_experience(store)
        cfg = SafetyConfig(auto_improve=False, auto_unlock=True,
                           unlock_min_outcomes=10, unlock_min_usage=5)
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        _add_outcomes(store, exp_id, n_success=2)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is False
        assert result.reason == "conditions_not_met"
        assert "blocking" in result.details

    def test_auto_unlock_grants_when_conditions_met(self, store):
        exp_id = _make_experience(store)
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=10, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5,
            cooldown_seconds=0,
        )
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is True

    def test_lite_mode_always_blocks(self, store):
        exp_id = _make_experience(store)
        _add_outcomes(store, exp_id, n_success=20)
        _set_usage(store, exp_id, 20)
        cfg = SafetyConfig.lite_mode()
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is False
        assert result.reason == "auto_improve_disabled"

    def test_hint_in_conditions_not_met(self, store):
        exp_id = _make_experience(store)
        cfg = SafetyConfig(auto_improve=False, auto_unlock=True,
                           unlock_min_outcomes=10, unlock_min_usage=5)
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert "hint" in result.details

    def test_auto_unlock_false_and_auto_improve_false_blocks(self, store):
        exp_id = _make_experience(store)
        cfg = SafetyConfig(auto_improve=False, auto_unlock=False)
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is False
        assert result.reason == "auto_improve_disabled"

    def test_auto_improve_true_skips_auto_unlock_check(self, store):
        exp_id = _make_experience(store)
        _add_outcomes(store, exp_id, n_success=10)
        cfg = SafetyConfig(
            auto_improve=True, auto_unlock=False,
            min_outcomes=5, min_success_rate=0.5, cooldown_seconds=0,
        )
        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is True


# ══════════════════════════════════════════════════════════════════════
# ImprovementWorker (synchronous)
# ══════════════════════════════════════════════════════════════════════

class TestImprovementWorker:
    def test_run_once_empty_store(self, store):
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.run_once()
        assert result["total_scanned"] == 0
        assert result["improved"] == 0
        assert result["errors"] == 0

    def test_run_once_skips_not_ready(self, store):
        _make_experience(store, "exp-w-001")
        _add_outcomes(store, "exp-w-001", n_success=2)
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.run_once()
        assert result["total_scanned"] == 1
        assert result["skipped"] == 1
        assert result["improved"] == 0

    def test_run_once_improves_ready_experience(self, store):
        exp_id = _make_experience(store, "exp-w-002")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=5, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5,
            cooldown_seconds=0,
        )
        worker = ImprovementWorker(store, config=cfg)
        result = worker.run_once()
        assert result["total_scanned"] == 1
        assert result["ready_for_improvement"] == 1
        assert result["improved"] == 1

    def test_run_once_result_structure(self, store):
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.run_once()
        for key in ("total_scanned", "ready_for_improvement", "improved",
                    "blocked", "skipped", "errors", "details"):
            assert key in result
        assert isinstance(result["details"], list)

    def test_check_all_readiness_empty(self, store):
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.check_all_readiness()
        assert result["total"] == 0
        assert result["ready"] == 0
        assert result["not_ready"] == 0
        assert result["results"] == []

    def test_check_all_readiness_mixed(self, store):
        exp1 = _make_experience(store, "exp-r-001", "Exp 1")
        exp2 = _make_experience(store, "exp-r-002", "Exp 2")
        _add_outcomes(store, exp1, n_success=2)
        _add_outcomes(store, exp2, n_success=10)
        _set_usage(store, exp2, 10)
        cfg = SafetyConfig.full_mode()
        worker = ImprovementWorker(store, config=cfg)
        result = worker.check_all_readiness()
        assert result["total"] == 2
        assert result["ready"] == 1
        assert result["not_ready"] == 1

    def test_check_all_readiness_structure(self, store):
        _make_experience(store, "exp-r-struct")
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.check_all_readiness()
        assert "total" in result
        assert "ready" in result
        assert "not_ready" in result
        assert "results" in result

    def test_multiple_experiences_partial_improvement(self, store):
        for i in range(3):
            eid = _make_experience(store, f"exp-multi-{i}", f"Multi {i}")
            _add_outcomes(store, eid, n_success=2)
        worker = ImprovementWorker(store, config=SafetyConfig.full_mode())
        result = worker.run_once()
        assert result["total_scanned"] == 3
        assert result["skipped"] == 3
        assert result["improved"] == 0


# ══════════════════════════════════════════════════════════════════════
# ImprovementWorkerThread
# ══════════════════════════════════════════════════════════════════════

class TestImprovementWorkerThread:
    def test_start_and_is_alive(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=9999
        )
        thread.start()
        try:
            assert thread.is_alive()
        finally:
            thread.stop()

    def test_stop(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=9999
        )
        thread.start()
        thread.stop()
        assert not thread.is_alive()

    def test_status_not_started(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=60
        )
        status = thread.status()
        assert status["running"] is False
        assert status["run_count"] == 0

    def test_status_after_start(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=9999
        )
        thread.start()
        time.sleep(0.2)
        try:
            status = thread.status()
            assert status["running"] is True
            assert status["interval_seconds"] == 9999
        finally:
            thread.stop()

    def test_double_start_idempotent(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=9999
        )
        thread.start()
        thread.start()
        try:
            assert thread.is_alive()
        finally:
            thread.stop()

    def test_run_count_increments(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=0.01
        )
        thread.start()
        time.sleep(0.2)
        try:
            assert thread._run_count >= 1
        finally:
            thread.stop()

    def test_last_run_stats_populated(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=0.01
        )
        thread.start()
        time.sleep(0.2)
        try:
            status = thread.status()
            assert status["last_run_stats"] is not None
        finally:
            thread.stop()


# ══════════════════════════════════════════════════════════════════════
# SDK Full mode methods
# ══════════════════════════════════════════════════════════════════════

class TestSDKFullMode:
    def test_check_improvement_readiness_not_ready(self, memory_obj, store):
        exp_id = _make_experience(store)
        result = memory_obj.check_improvement_readiness(exp_id)
        assert result["ready"] is False
        assert "blocking" in result

    def test_check_improvement_readiness_ready(self, memory_obj, store):
        exp_id = _make_experience(store, "exp-sdk-ready")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        result = memory_obj.check_improvement_readiness(exp_id)
        assert result["ready"] is True

    def test_check_improvement_readiness_structure(self, memory_obj, store):
        exp_id = _make_experience(store, "exp-sdk-struct")
        result = memory_obj.check_improvement_readiness(exp_id)
        for key in ("experience_id", "ready", "conditions",
                    "outcomes_count", "usage_count", "blocking"):
            assert key in result

    def test_run_all_improvements_empty(self, memory_obj):
        result = memory_obj.run_all_improvements()
        assert result["total_scanned"] == 0

    def test_run_all_improvements_skips_not_ready(self, memory_obj, store):
        _make_experience(store, "exp-sdk-bulk")
        _add_outcomes(store, "exp-sdk-bulk", n_success=2)
        result = memory_obj.run_all_improvements()
        assert result["total_scanned"] == 1
        assert result["skipped"] == 1

    def test_check_all_readiness_structure(self, memory_obj, store):
        _make_experience(store, "exp-sdk-r1")
        result = memory_obj.check_all_readiness()
        assert "total" in result
        assert "ready" in result
        assert "not_ready" in result
        assert "results" in result

    def test_run_improvement_cycle_auto_unlock_blocks_when_not_ready(
            self, memory_obj, store):
        exp_id = _make_experience(store, "exp-sdk-cycle-nr")
        result = memory_obj.run_improvement_cycle(
            exp_id, auto_improve=False, auto_unlock=True
        )
        assert result["allowed"] is False

    def test_run_improvement_cycle_auto_unlock_allows_when_ready(
            self, memory_obj, store):
        exp_id = _make_experience(store, "exp-sdk-cycle-r")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        result = memory_obj.run_improvement_cycle(
            exp_id, auto_improve=False, auto_unlock=True
        )
        assert "evaluation" in result

    def test_worker_start_stop_status(self, memory_obj):
        start_result = memory_obj.start_improvement_worker(interval_seconds=9999)
        assert start_result["started"] is True
        status = memory_obj.improvement_worker_status()
        assert status["running"] is True
        stop_result = memory_obj.stop_improvement_worker()
        assert stop_result["stopped"] is True
        status_after = memory_obj.improvement_worker_status()
        assert status_after["running"] is False

    def test_worker_double_start(self, memory_obj):
        memory_obj.start_improvement_worker(interval_seconds=9999)
        result2 = memory_obj.start_improvement_worker(interval_seconds=9999)
        assert result2["started"] is False
        assert "already running" in result2["message"]
        memory_obj.stop_improvement_worker()

    def test_worker_stop_when_not_running(self, memory_obj):
        result = memory_obj.stop_improvement_worker()
        assert result["stopped"] is False

    def test_worker_status_not_initialized(self, memory_obj):
        result = memory_obj.improvement_worker_status()
        assert result["running"] is False


# ══════════════════════════════════════════════════════════════════════
# REST Full mode endpoints
# ══════════════════════════════════════════════════════════════════════

class TestRESTFullMode:
    def test_readiness_missing_param(self, rest):
        client, store = rest
        r = client.get("/experience/improve/readiness")
        assert r.status_code == 400

    def test_readiness_not_ready(self, rest):
        client, store = rest
        exp_id = _make_experience(store, "exp-rest-r1")
        r = client.get(f"/experience/improve/readiness?experience_id={exp_id}")
        assert r.status_code == 200
        data = r.get_json()
        assert data["ready"] is False
        assert "blocking" in data

    def test_readiness_ready(self, rest):
        client, store = rest
        exp_id = _make_experience(store, "exp-rest-r2")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        r = client.get(f"/experience/improve/readiness?experience_id={exp_id}")
        assert r.status_code == 200
        data = r.get_json()
        assert data["ready"] is True

    def test_readiness_all_empty(self, rest):
        client, store = rest
        r = client.get("/experience/improve/readiness/all")
        assert r.status_code == 200
        data = r.get_json()
        assert data["total"] == 0
        assert data["ready"] == 0
        assert data["results"] == []

    def test_readiness_all_with_data(self, rest):
        client, store = rest
        _make_experience(store, "exp-rest-all-1")
        _add_outcomes(store, "exp-rest-all-1", n_success=2)
        r = client.get("/experience/improve/readiness/all")
        assert r.status_code == 200
        data = r.get_json()
        assert data["total"] == 1
        assert data["not_ready"] == 1

    def test_readiness_all_structure(self, rest):
        client, _ = rest
        r = client.get("/experience/improve/readiness/all")
        data = r.get_json()
        assert "total" in data
        assert "ready" in data
        assert "not_ready" in data
        assert "results" in data

    def test_improve_all_empty(self, rest):
        client, store = rest
        r = client.post("/experience/improve/all", json={})
        assert r.status_code == 200
        data = r.get_json()
        assert data["total_scanned"] == 0

    def test_improve_all_skips_not_ready(self, rest):
        client, store = rest
        _make_experience(store, "exp-rest-bulk")
        _add_outcomes(store, "exp-rest-bulk", n_success=2)
        r = client.post("/experience/improve/all", json={})
        assert r.status_code == 200
        data = r.get_json()
        assert data["total_scanned"] == 1
        assert data["skipped"] == 1

    def test_improve_all_result_keys(self, rest):
        client, _ = rest
        r = client.post("/experience/improve/all", json={})
        data = r.get_json()
        for key in ("total_scanned", "improved", "blocked", "skipped", "errors"):
            assert key in data

    def test_worker_start(self, rest):
        client, store = rest
        r = client.post("/experience/worker/start",
                        json={"interval_seconds": 9999})
        assert r.status_code == 200
        data = r.get_json()
        assert data["started"] is True
        client.post("/experience/worker/stop", json={})

    def test_worker_stop_when_not_running(self, rest):
        client, store = rest
        r = client.post("/experience/worker/stop", json={})
        assert r.status_code == 200
        data = r.get_json()
        assert data["stopped"] is False

    def test_worker_status_endpoint(self, rest):
        client, store = rest
        r = client.get("/experience/worker/status")
        assert r.status_code == 200
        data = r.get_json()
        assert "running" in data

    def test_worker_start_stop_roundtrip(self, rest):
        client, store = rest
        start_r = client.post("/experience/worker/start",
                              json={"interval_seconds": 9999})
        assert start_r.get_json()["started"] is True
        stop_r = client.post("/experience/worker/stop", json={})
        assert stop_r.get_json()["stopped"] is True
        status_r = client.get("/experience/worker/status")
        assert status_r.get_json()["running"] is False

    def test_improve_all_with_limit_param(self, rest):
        client, store = rest
        r = client.post("/experience/improve/all", json={"limit": 10})
        assert r.status_code == 200

    def test_readiness_all_with_filters(self, rest):
        client, store = rest
        r = client.get(
            "/experience/improve/readiness/all?agent_id=agent-x&scene=s1"
        )
        assert r.status_code == 200
        data = r.get_json()
        assert "total" in data


# ══════════════════════════════════════════════════════════════════════
# Integration: full auto-improve cycle
# ══════════════════════════════════════════════════════════════════════

class TestFullAutoImproveCycle:
    def test_end_to_end_auto_improve(self, store):
        exp_id = _make_experience(store, "exp-e2e-full")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=5, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5,
            cooldown_seconds=0,
        )
        worker = ImprovementWorker(store, config=cfg)
        result = worker.run_once()
        assert result["total_scanned"] == 1
        assert result["ready_for_improvement"] == 1
        assert result["improved"] == 1
        assert result["errors"] == 0

    def test_auto_improve_creates_version_snapshot(self, store):
        exp_id = _make_experience(store, "exp-snap-full")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=5, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5, cooldown_seconds=0,
        )
        worker = ImprovementWorker(store, config=cfg)
        worker.run_once()
        versions = store.list_experience_versions(exp_id)
        assert len(versions) >= 1

    def test_cooldown_prevents_double_improvement(self, store):
        exp_id = _make_experience(store, "exp-cooldown-full")
        _add_outcomes(store, exp_id, n_success=10)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=5, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5,
            cooldown_seconds=9999,
        )
        worker = ImprovementWorker(store, config=cfg)
        r1 = worker.run_once()
        r2 = worker.run_once()
        assert r1["improved"] == 1
        assert r2["improved"] == 0
        assert r2["blocked"] == 1

    def test_version_cap_prevents_infinite_versioning(self, store):
        exp_id = _make_experience(store, "exp-cap-full")
        cfg = SafetyConfig(
            auto_improve=True,
            max_versions=2,
            min_outcomes=1,
            min_success_rate=0.5,
            cooldown_seconds=0,
        )
        from openmemo.experience.versioning import ExperienceVersioning
        versioning = ExperienceVersioning(store)
        exp_obj = Experience(title="Cap Test", type="workflow",
                             steps=[], outcome="ok", confidence=0.5)
        exp_obj.id = exp_id
        versioning.snapshot(exp_obj, reason="v1")
        exp_obj.confidence = 0.6
        versioning.snapshot(exp_obj, reason="v2")

        _add_outcomes(store, exp_id, n_success=5)
        _set_usage(store, exp_id, 5)

        guard = SafetyGuard(config=cfg, store=store)
        evaluator = EvaluationEngine(store)
        evaluation = evaluator.evaluate(exp_id)
        result = guard.check_update(exp_id, evaluation, "reinforce")
        assert result.allowed is False
        assert result.reason == "version_cap_reached"

    def test_failure_heavy_experience_gets_downweighted(self, store):
        exp_id = _make_experience(store, "exp-fail-heavy")
        _add_outcomes(store, exp_id, n_success=2, n_failure=8)
        _set_usage(store, exp_id, 10)
        cfg = SafetyConfig(
            auto_improve=False, auto_unlock=True,
            unlock_min_outcomes=5, unlock_min_usage=5,
            min_outcomes=5, min_success_rate=0.5,
            cooldown_seconds=0,
        )
        worker = ImprovementWorker(store, config=cfg)
        result = worker.run_once()
        assert result["total_scanned"] == 1
        assert result["ready_for_improvement"] == 1
        # either improved (downweighted) or blocked by success_rate check
        assert result["improved"] + result["blocked"] == 1

    def test_worker_thread_completes_one_cycle(self, store):
        thread = ImprovementWorkerThread(
            store, config=SafetyConfig.full_mode(), interval_seconds=0.05
        )
        thread.start()
        time.sleep(0.3)
        try:
            assert thread._run_count >= 1
            assert thread._last_run_stats is not None
        finally:
            thread.stop()
