"""
Phase 23.7 — Self-Improving Loop Tests

Tests:
  - Outcome data model
  - EvaluationEngine (score formula, status mapping, recommendations)
  - SafetyGuard (min samples, cooldown, auto_improve gate, rollback always OK)
  - ExperienceVersioning (snapshot, rollback, list)
  - ExperienceUpdater (full improvement cycle, rollback)
  - SQLiteStore integration (outcomes + experience_versions tables)
  - SDK methods (capture_outcome, evaluate_experience, run_improvement_cycle, etc.)
  - REST API endpoints
"""

import json
import sys
import time
import os
import pytest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from openmemo.improvement.outcome import Outcome
from openmemo.improvement.evaluator import EvaluationEngine, EvaluationResult
from openmemo.improvement.safety import SafetyGuard, SafetyConfig
from openmemo.improvement.updater import ExperienceUpdater
from openmemo.experience.models import Experience
from openmemo.experience.versioning import ExperienceVersioning, ExperienceVersion
from openmemo.storage.sqlite_store import SQLiteStore
from openmemo.api.sdk import Memory


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

@pytest.fixture
def store(tmp_path):
    db_path = str(tmp_path / "test_phase237.db")
    s = SQLiteStore(db_path=db_path)
    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 sample_experience(store):
    exp = Experience(
        title="Deploy Backend",
        type="workflow",
        steps=["build image", "push", "deploy"],
        outcome="success",
        confidence=0.75,
        scene="deployment",
        agent_id="agent-1",
    )
    store.put_experience(exp.to_dict())
    return exp


def make_outcome(exp_id, result="success", latency=1.0, error=False, score=-1.0):
    return Outcome(
        experience_id=exp_id,
        result=result,
        metrics={"latency": latency, "error": error},
        score=score,
    )


# ─── 1. Outcome model ─────────────────────────────────────────────

class TestOutcomeModel:
    def test_defaults(self):
        o = Outcome()
        assert o.outcome_id.startswith("out_")
        assert o.result == "unknown"
        assert o.score == -1.0

    def test_invalid_result_normalized(self):
        o = Outcome(result="invalid")
        assert o.result == "unknown"

    def test_valid_results(self):
        for r in ("success", "failure", "partial", "unknown"):
            o = Outcome(result=r)
            assert o.result == r

    def test_score_clamped(self):
        o = Outcome(score=5.0)
        assert o.score == 1.0
        o2 = Outcome(score=-0.5)
        assert o2.score == 0.0

    def test_score_minus_one_means_not_set(self):
        o = Outcome(score=-1.0)
        assert o.explicit_score is None
        o2 = Outcome(score=0.8)
        assert o2.explicit_score == pytest.approx(0.8)

    def test_is_success_is_failure(self):
        s = Outcome(result="success")
        assert s.is_success is True
        assert s.is_failure is False
        f = Outcome(result="failure")
        assert f.is_failure is True
        assert f.is_success is False

    def test_has_error(self):
        o = Outcome(metrics={"error": True})
        assert o.has_error is True
        o2 = Outcome(metrics={"error": False})
        assert o2.has_error is False

    def test_latency(self):
        o = Outcome(metrics={"latency": 2.5})
        assert o.latency == 2.5
        o2 = Outcome(metrics={})
        assert o2.latency is None

    def test_to_dict_from_dict_roundtrip(self):
        o = Outcome(
            experience_id="exp_123",
            result="success",
            metrics={"latency": 1.2, "error": False},
            agent_feedback="deployed ok",
            score=0.9,
        )
        d = o.to_dict()
        o2 = Outcome.from_dict(d)
        assert o2.outcome_id == o.outcome_id
        assert o2.result == "success"
        assert o2.score == pytest.approx(0.9)
        assert o2.metrics["latency"] == 1.2

    def test_from_dict_json_strings(self):
        d = {
            "outcome_id": "out_abc",
            "experience_id": "exp_x",
            "result": "failure",
            "metrics": '{"latency": 3.0, "error": true}',
            "score": "0.3",
            "timestamp": str(time.time()),
            "metadata": "{}",
        }
        o = Outcome.from_dict(d)
        assert o.result == "failure"
        assert o.metrics["latency"] == 3.0
        assert o.score == pytest.approx(0.3)


# ─── 2. EvaluationEngine ──────────────────────────────────────────

class TestEvaluationEngine:
    def test_no_outcomes(self, store):
        engine = EvaluationEngine(store)
        result = engine.evaluate("exp_nonexistent")
        assert result.status == "no_data"
        assert result.total_outcomes == 0
        assert result.recommendation == "need_more_data"

    def test_all_success(self, store, sample_experience):
        outcomes = [make_outcome(sample_experience.id, result="success") for _ in range(5)]
        for o in outcomes:
            store.put_outcome(o.to_dict())

        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        assert result.success_rate == 1.0
        assert result.score > 0.7
        assert result.status in ("excellent", "good")
        assert result.recommendation in ("reinforce", "keep")

    def test_all_failure(self, store, sample_experience):
        outcomes = [make_outcome(sample_experience.id, result="failure") for _ in range(5)]
        for o in outcomes:
            store.put_outcome(o.to_dict())

        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        assert result.success_rate == 0.0
        assert result.score < 0.4
        assert result.status in ("poor", "fair")
        assert result.recommendation in ("deprecate", "downweight", "monitor")

    def test_score_formula(self, store, sample_experience):
        # 6 success, 4 failure → success_rate=0.6
        for i in range(6):
            store.put_outcome(make_outcome(sample_experience.id, result="success").to_dict())
        for i in range(4):
            store.put_outcome(make_outcome(sample_experience.id, result="failure").to_dict())

        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        # score ≈ 0.6*0.6 + 1.0*0.2 + 0.6*0.2 = 0.36 + 0.20 + 0.12 = 0.68
        assert 0.5 < result.score < 0.9
        assert result.total_outcomes == 10

    def test_with_explicit_scores(self, store, sample_experience):
        outcomes = [
            make_outcome(sample_experience.id, result="success", score=0.9),
            make_outcome(sample_experience.id, result="success", score=0.8),
            make_outcome(sample_experience.id, result="partial", score=0.5),
        ]
        for o in outcomes:
            store.put_outcome(o.to_dict())

        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        # feedback_score should be average of explicit scores
        assert abs(result.feedback_score - (0.9 + 0.8 + 0.5) / 3) < 0.01

    def test_error_rate_affects_score(self, store, sample_experience):
        outcomes = [
            make_outcome(sample_experience.id, result="success", error=True),
            make_outcome(sample_experience.id, result="success", error=True),
            make_outcome(sample_experience.id, result="success", error=False),
        ]
        for o in outcomes:
            store.put_outcome(o.to_dict())

        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        assert result.error_rate == pytest.approx(2 / 3, abs=0.01)

    def test_evaluate_from_outcomes_direct(self, store, sample_experience):
        outcomes = [
            Outcome(experience_id=sample_experience.id, result="success"),
            Outcome(experience_id=sample_experience.id, result="success"),
        ]
        engine = EvaluationEngine(store)
        result = engine.evaluate_from_outcomes(sample_experience.id, outcomes)
        assert result.success_rate == 1.0
        assert result.total_outcomes == 2

    def test_avg_latency(self, store, sample_experience):
        outcomes = [
            make_outcome(sample_experience.id, result="success", latency=1.0),
            make_outcome(sample_experience.id, result="success", latency=3.0),
        ]
        for o in outcomes:
            store.put_outcome(o.to_dict())
        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        assert result.avg_latency == pytest.approx(2.0)

    def test_to_dict(self, store, sample_experience):
        for _ in range(3):
            store.put_outcome(make_outcome(sample_experience.id, result="success").to_dict())
        engine = EvaluationEngine(store)
        result = engine.evaluate(sample_experience.id)
        d = result.to_dict()
        assert "score" in d
        assert "status" in d
        assert "recommendation" in d
        assert "total_outcomes" in d


# ─── 3. SafetyGuard ───────────────────────────────────────────────

class TestSafetyGuard:
    def make_eval(self, score=0.8, success_rate=0.8, n=10, rec="reinforce"):
        return EvaluationResult(
            experience_id="exp_test",
            score=score,
            status="good",
            total_outcomes=n,
            success_rate=success_rate,
            error_rate=0.1,
            feedback_score=score,
            avg_latency=1.0,
            recommendation=rec,
        )

    def test_blocked_when_auto_improve_off(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=False))
        result = guard.check_update("exp_1", self.make_eval(), "reinforce")
        assert result.allowed is False
        assert result.reason == "auto_improve_disabled"

    def test_allowed_when_auto_improve_on(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=True, min_outcomes=5,
                                          cooldown_seconds=0))
        result = guard.check_update("exp_1", self.make_eval(n=10), "reinforce")
        assert result.allowed is True

    def test_blocked_insufficient_samples(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=True, min_outcomes=5,
                                          cooldown_seconds=0))
        result = guard.check_update("exp_1", self.make_eval(n=3), "reinforce")
        assert result.allowed is False
        assert result.reason == "insufficient_samples"

    def test_blocked_success_rate_too_low_to_reinforce(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=True, min_outcomes=3,
                                          min_success_rate=0.5, cooldown_seconds=0))
        result = guard.check_update("exp_1", self.make_eval(success_rate=0.3, n=5), "reinforce")
        assert result.allowed is False
        assert result.reason == "success_rate_too_low_to_reinforce"

    def test_blocked_deprecate_too_few_samples(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=True, min_outcomes=3,
                                          deprecation_samples=10, cooldown_seconds=0))
        result = guard.check_update("exp_1",
                                    self.make_eval(n=5, score=0.2, success_rate=0.1, rec="deprecate"),
                                    "deprecate")
        assert result.allowed is False
        assert result.reason == "insufficient_samples_to_deprecate"

    def test_blocked_deprecate_score_too_high(self):
        guard = SafetyGuard(SafetyConfig(auto_improve=True, min_outcomes=3,
                                          deprecation_samples=5,
                                          deprecation_score=0.35, cooldown_seconds=0))
        result = guard.check_update("exp_1",
                                    self.make_eval(n=8, score=0.6, success_rate=0.6),
                                    "deprecate")
        assert result.allowed is False
        assert result.reason == "score_too_high_to_deprecate"

    def test_rollback_always_allowed_without_store(self):
        guard = SafetyGuard(SafetyConfig())
        result = guard.check_rollback("exp_1", target_version=1)
        assert result.allowed is True

    def test_rollback_blocked_when_version_not_found(self, store):
        guard = SafetyGuard(SafetyConfig(), store=store)
        result = guard.check_rollback("exp_nonexistent", target_version=99)
        assert result.allowed is False
        assert result.reason == "version_not_found"


# ─── 4. ExperienceVersioning ──────────────────────────────────────

class TestExperienceVersioning:
    def test_snapshot_creates_version(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        ev = versioning.snapshot(sample_experience, reason="initial")
        assert ev.version == 1
        assert ev.parent_version == 0
        assert ev.experience_id == sample_experience.id
        assert ev.snapshot == sample_experience.to_dict()
        assert ev.reason == "initial"

    def test_snapshot_increments_version(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        v1 = versioning.snapshot(sample_experience, reason="v1")
        v2 = versioning.snapshot(sample_experience, reason="v2")
        assert v1.version == 1
        assert v2.version == 2
        assert v2.parent_version == 1

    def test_snapshot_marks_old_superseded(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        v1 = versioning.snapshot(sample_experience, reason="v1")
        v2 = versioning.snapshot(sample_experience, reason="v2")
        versions = versioning.list_versions(sample_experience.id)
        by_num = {v.version: v for v in versions}
        assert by_num[2].status == "active"
        assert by_num[1].status == "superseded"

    def test_list_versions(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        for i in range(3):
            versioning.snapshot(sample_experience, reason=f"v{i+1}")
        versions = versioning.list_versions(sample_experience.id)
        assert len(versions) == 3
        assert versions[0].version == 3

    def test_get_version(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        versioning.snapshot(sample_experience, reason="snap1")
        v = versioning.get_version(sample_experience.id, 1)
        assert v is not None
        assert v.version == 1

    def test_current_version(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        versioning.snapshot(sample_experience, reason="snap1")
        versioning.snapshot(sample_experience, reason="snap2")
        current = versioning.current_version(sample_experience.id)
        assert current.version == 2

    def test_rollback_restores_state(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        v1_exp = sample_experience
        versioning.snapshot(v1_exp, reason="v1")
        modified = Experience.from_dict(v1_exp.to_dict())
        modified.confidence = 0.1
        store.put_experience(modified.to_dict())
        versioning.snapshot(modified, reason="v2")

        restored = versioning.rollback(sample_experience.id, target_version=1)
        assert restored is not None
        assert restored.confidence == pytest.approx(sample_experience.confidence)

    def test_rollback_marks_rolled_back(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        versioning.snapshot(sample_experience, reason="v1")
        versioning.snapshot(sample_experience, reason="v2")
        versioning.rollback(sample_experience.id, target_version=1)
        versions = versioning.list_versions(sample_experience.id)
        statuses = {v.version: v.status for v in versions}
        assert statuses[1] == "active"
        assert statuses[2] == "rolled_back"

    def test_rollback_nonexistent_version(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        result = versioning.rollback(sample_experience.id, target_version=99)
        assert result is None

    def test_version_from_dict_to_dict_roundtrip(self):
        ev = ExperienceVersion(
            experience_id="exp_abc",
            version=3,
            parent_version=2,
            snapshot={"id": "exp_abc", "confidence": 0.8},
            reason="test snapshot",
        )
        d = ev.to_dict()
        ev2 = ExperienceVersion.from_dict(d)
        assert ev2.version_id == ev.version_id
        assert ev2.version == 3
        assert ev2.snapshot["confidence"] == 0.8

    def test_restore_returns_experience(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        ev = versioning.snapshot(sample_experience)
        restored = ev.restore()
        assert isinstance(restored, Experience)
        assert restored.id == sample_experience.id


# ─── 5. ExperienceUpdater ────────────────────────────────────────

class TestExperienceUpdater:
    def _fill_outcomes(self, store, exp_id, result="success", count=6):
        for _ in range(count):
            o = make_outcome(exp_id, result=result)
            store.put_outcome(o.to_dict())

    def test_blocked_in_lite_mode(self, store, sample_experience):
        updater = ExperienceUpdater(store, config=SafetyConfig(auto_improve=False))
        result = updater.run_improvement_cycle(sample_experience.id)
        assert result["allowed"] is False
        assert result["updated"] is False
        assert result["reason"] == "auto_improve_disabled"

    def test_blocked_insufficient_samples(self, store, sample_experience):
        config = SafetyConfig(auto_improve=True, min_outcomes=5, cooldown_seconds=0)
        updater = ExperienceUpdater(store, config=config)
        self._fill_outcomes(store, sample_experience.id, count=2)
        result = updater.run_improvement_cycle(sample_experience.id)
        assert result["allowed"] is False
        assert result["reason"] == "insufficient_samples"

    def test_reinforce_success(self, store, sample_experience):
        config = SafetyConfig(auto_improve=True, min_outcomes=5, cooldown_seconds=0)
        updater = ExperienceUpdater(store, config=config)
        self._fill_outcomes(store, sample_experience.id, result="success", count=8)
        original_conf = sample_experience.confidence
        result = updater.run_improvement_cycle(sample_experience.id)
        if result["updated"]:
            updated_exp = Experience.from_dict(
                store.get_experience(sample_experience.id)
            )
            assert updated_exp.confidence >= original_conf

    def test_downweight_on_failures(self, store, sample_experience):
        config = SafetyConfig(auto_improve=True, min_outcomes=5, cooldown_seconds=0,
                               min_success_rate=0.0)
        updater = ExperienceUpdater(store, config=config)
        self._fill_outcomes(store, sample_experience.id, result="failure", count=8)
        original_conf = sample_experience.confidence
        result = updater.run_improvement_cycle(sample_experience.id)
        if result["updated"]:
            updated_exp = Experience.from_dict(
                store.get_experience(sample_experience.id)
            )
            assert updated_exp.confidence <= original_conf

    def test_experience_not_found(self, store):
        updater = ExperienceUpdater(store, config=SafetyConfig(auto_improve=True))
        result = updater.run_improvement_cycle("exp_nonexistent")
        assert "error" in result
        assert result["updated"] is False

    def test_rollback_nonexistent_version(self, store, sample_experience):
        updater = ExperienceUpdater(store)
        result = updater.rollback(sample_experience.id, target_version=99)
        assert result["rolled_back"] is False

    def test_rollback_success(self, store, sample_experience):
        versioning = ExperienceVersioning(store)
        versioning.snapshot(sample_experience, reason="initial")
        modified = Experience.from_dict(sample_experience.to_dict())
        modified.confidence = 0.1
        store.put_experience(modified.to_dict())
        versioning.snapshot(modified, reason="downgraded")

        updater = ExperienceUpdater(store)
        result = updater.rollback(sample_experience.id, target_version=1)
        assert result["rolled_back"] is True
        assert result["target_version"] == 1
        restored = Experience.from_dict(store.get_experience(sample_experience.id))
        assert restored.confidence == pytest.approx(sample_experience.confidence)

    def test_improvement_cycle_returns_evaluation(self, store, sample_experience):
        self._fill_outcomes(store, sample_experience.id, count=2)
        updater = ExperienceUpdater(store, config=SafetyConfig(auto_improve=False))
        result = updater.run_improvement_cycle(sample_experience.id)
        assert "evaluation" in result
        assert "score" in result["evaluation"]


# ─── 6. SQLiteStore integration ───────────────────────────────────

class TestSQLiteStorePhase237:
    def test_put_and_list_outcomes(self, store, sample_experience):
        o1 = make_outcome(sample_experience.id, result="success")
        o2 = make_outcome(sample_experience.id, result="failure")
        store.put_outcome(o1.to_dict())
        store.put_outcome(o2.to_dict())
        results = store.list_outcomes(experience_id=sample_experience.id)
        assert len(results) == 2

    def test_list_outcomes_by_agent(self, store, sample_experience):
        o = Outcome(experience_id=sample_experience.id, result="success",
                    agent_id="agent-x")
        store.put_outcome(o.to_dict())
        results = store.list_outcomes(agent_id="agent-x")
        assert len(results) >= 1

    def test_metrics_json_serialized(self, store, sample_experience):
        o = Outcome(experience_id=sample_experience.id,
                    metrics={"latency": 2.5, "tokens": 100})
        store.put_outcome(o.to_dict())
        fetched = store.list_outcomes(experience_id=sample_experience.id)
        assert fetched[0]["metrics"]["latency"] == 2.5

    def test_put_and_get_experience_version(self, store, sample_experience):
        ev = ExperienceVersion(
            experience_id=sample_experience.id,
            version=1,
            snapshot=sample_experience.to_dict(),
        )
        store.put_experience_version(ev.to_dict())
        fetched = store.get_experience_version(sample_experience.id, 1)
        assert fetched is not None
        assert fetched["version"] == 1
        assert isinstance(fetched["snapshot"], dict)

    def test_list_experience_versions(self, store, sample_experience):
        for i in range(3):
            ev = ExperienceVersion(
                experience_id=sample_experience.id,
                version=i + 1,
                snapshot=sample_experience.to_dict(),
            )
            store.put_experience_version(ev.to_dict())
        versions = store.list_experience_versions(sample_experience.id)
        assert len(versions) == 3
        assert versions[0]["version"] == 3

    def test_snapshot_json_roundtrip(self, store, sample_experience):
        ev = ExperienceVersion(
            experience_id=sample_experience.id,
            version=1,
            snapshot=sample_experience.to_dict(),
        )
        store.put_experience_version(ev.to_dict())
        fetched = store.get_experience_version(sample_experience.id, 1)
        assert fetched["snapshot"]["id"] == sample_experience.id


# ─── 7. SDK methods ───────────────────────────────────────────────

class TestSDKImprovementMethods:
    def test_capture_outcome(self, memory_obj, sample_experience):
        result = memory_obj.capture_outcome(
            experience_id=sample_experience.id,
            result="success",
            metrics={"latency": 1.5},
            agent_feedback="all good",
            score=0.9,
        )
        assert result["captured"] is True
        assert result["result"] == "success"
        assert result["outcome_id"].startswith("out_")

    def test_list_outcomes(self, memory_obj, store, sample_experience):
        for _ in range(3):
            memory_obj.capture_outcome(sample_experience.id, "success")
        results = memory_obj.list_outcomes(experience_id=sample_experience.id)
        assert len(results) == 3

    def test_evaluate_experience_no_outcomes(self, memory_obj, sample_experience):
        result = memory_obj.evaluate_experience(sample_experience.id)
        assert result["status"] == "no_data"
        assert result["total_outcomes"] == 0

    def test_evaluate_experience_with_outcomes(self, memory_obj, store, sample_experience):
        for _ in range(5):
            memory_obj.capture_outcome(sample_experience.id, "success")
        result = memory_obj.evaluate_experience(sample_experience.id)
        assert result["success_rate"] == 1.0
        assert result["total_outcomes"] == 5

    def test_run_improvement_cycle_lite_mode(self, memory_obj, store, sample_experience):
        for _ in range(6):
            memory_obj.capture_outcome(sample_experience.id, "success")
        result = memory_obj.run_improvement_cycle(
            sample_experience.id, auto_improve=False
        )
        assert result["allowed"] is False
        assert result["updated"] is False

    def test_run_improvement_cycle_auto_mode(self, memory_obj, store, sample_experience):
        for _ in range(8):
            memory_obj.capture_outcome(sample_experience.id, "success")
        result = memory_obj.run_improvement_cycle(
            sample_experience.id, auto_improve=True
        )
        assert "action" in result
        assert "evaluation" in result

    def test_list_experience_versions(self, memory_obj, store, sample_experience):
        versioning = ExperienceVersioning(store)
        versioning.snapshot(sample_experience, reason="v1")
        results = memory_obj.list_experience_versions(sample_experience.id)
        assert len(results) >= 1
        assert results[0]["experience_id"] == sample_experience.id

    def test_rollback_experience_nonexistent_version(self, memory_obj, sample_experience):
        result = memory_obj.rollback_experience(sample_experience.id, target_version=99)
        assert result["rolled_back"] is False

    def test_improvement_stats_empty(self, memory_obj):
        stats = memory_obj.improvement_stats()
        assert "total_outcomes" in stats
        assert stats["total_outcomes"] == 0

    def test_improvement_stats_with_data(self, memory_obj, store, sample_experience):
        memory_obj.capture_outcome(sample_experience.id, "success")
        memory_obj.capture_outcome(sample_experience.id, "failure")
        stats = memory_obj.improvement_stats()
        assert stats["total_outcomes"] >= 2
        assert stats["success_count"] >= 1
        assert stats["failure_count"] >= 1

    def test_improvement_stats_by_experience(self, memory_obj, store, sample_experience):
        memory_obj.capture_outcome(sample_experience.id, "success")
        stats = memory_obj.improvement_stats(experience_id=sample_experience.id)
        assert "evaluation" in stats
        assert stats["evaluation"]["experience_id"] == sample_experience.id


# ─── 8. REST API ──────────────────────────────────────────────────

class TestRESTImprovement:
    @pytest.fixture
    def rest(self, tmp_path):
        from openmemo.api.rest_server import create_app
        from openmemo.storage.sqlite_store import SQLiteStore
        db_path = str(tmp_path / "rest237.db")
        store = SQLiteStore(db_path=db_path)
        app = create_app(db_path=db_path)
        app.config["TESTING"] = True
        client = app.test_client()
        return client, store

    def test_capture_outcome_missing_experience_id(self, rest):
        client, _ = rest
        resp = client.post("/experience/outcome", json={"result": "success"})
        assert resp.status_code == 400

    def test_capture_outcome_success(self, rest):
        client, store = rest
        exp = Experience(title="Test exp")
        store.put_experience(exp.to_dict())
        resp = client.post("/experience/outcome", json={
            "experience_id": exp.id,
            "result": "success",
            "metrics": {"latency": 1.0},
        })
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert data["captured"] is True
        assert data["outcome_id"].startswith("out_")

    def test_list_outcomes(self, rest):
        client, _ = rest
        resp = client.get("/experience/outcomes")
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert "results" in data

    def test_evaluate_missing_experience_id(self, rest):
        client, _ = rest
        resp = client.post("/experience/evaluate", json={})
        assert resp.status_code == 400

    def test_evaluate_no_outcomes(self, rest):
        client, store = rest
        exp = Experience(title="Eval test")
        store.put_experience(exp.to_dict())
        resp = client.post("/experience/evaluate",
                           json={"experience_id": exp.id})
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert data["status"] == "no_data"

    def test_improve_missing_experience_id(self, rest):
        client, _ = rest
        resp = client.post("/experience/improve", json={})
        assert resp.status_code == 400

    def test_improve_lite_mode(self, rest):
        client, store = rest
        exp = Experience(title="Improve test")
        store.put_experience(exp.to_dict())
        resp = client.post("/experience/improve", json={
            "experience_id": exp.id,
            "auto_improve": False,
        })
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert data["allowed"] is False
        assert data["reason"] == "auto_improve_disabled"

    def test_improve_stats(self, rest):
        client, _ = rest
        resp = client.get("/experience/improve/stats")
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert "total_outcomes" in data

    def test_versions_missing_experience_id(self, rest):
        client, _ = rest
        resp = client.get("/experience/versions")
        assert resp.status_code == 400

    def test_versions_empty(self, rest):
        client, store = rest
        exp = Experience(title="Version test")
        store.put_experience(exp.to_dict())
        resp = client.get(f"/experience/versions?experience_id={exp.id}")
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert data["count"] == 0

    def test_rollback_missing_params(self, rest):
        client, _ = rest
        resp = client.post("/experience/rollback", json={})
        assert resp.status_code == 400

    def test_rollback_nonexistent(self, rest):
        client, _ = rest
        resp = client.post("/experience/rollback", json={
            "experience_id": "exp_xyz",
            "target_version": 1,
        })
        assert resp.status_code == 200
        data = json.loads(resp.data)
        assert data["rolled_back"] is False
