"""
Tests for Memory Feedback Loop (v0.21.0)

PRD Coverage:
    §7   — 4 feedback signal types
    §8   — Data models
    §9   — Quality scoring formulas
    §10  — Ranking adjustment
    §11  — Promotion / Suppression
    §12  — Integration timing
    §14  — Module design
    §18  — Performance targets
"""

import time
import math
import pytest
import tempfile
import os


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

@pytest.fixture
def db_path(tmp_path):
    return str(tmp_path / "test_feedback.db")


@pytest.fixture
def store(db_path):
    from openmemo.memory_feedback.feedback_store import FeedbackStore
    s = FeedbackStore(db_path=db_path)
    yield s
    s.close()


@pytest.fixture
def scorer(store):
    from openmemo.memory_feedback.quality_scorer import QualityScorer
    return QualityScorer(store)


@pytest.fixture
def collector(store, scorer):
    from openmemo.memory_feedback.feedback_collector import FeedbackCollector
    return FeedbackCollector(store, scorer)


@pytest.fixture
def adjuster(store, scorer):
    from openmemo.memory_feedback.ranking_adjuster import RankingAdjuster
    return RankingAdjuster(store, scorer)


@pytest.fixture
def wf_feedback(store, scorer):
    from openmemo.memory_feedback.workflow_feedback import WorkflowFeedback
    return WorkflowFeedback(store, scorer)


@pytest.fixture
def facade(db_path):
    from openmemo.memory_feedback import MemoryFeedback
    fb = MemoryFeedback(db_path=db_path)
    yield fb
    fb.close()


# ── §8 Data Models ────────────────────────────────────────────────────────────

class TestDataModels:

    def test_memory_feedback_record_defaults(self):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        fb = MemoryFeedbackRecord(memory_id="mem_1")
        assert fb.memory_id == "mem_1"
        assert fb.feedback_id != ""
        assert fb.recalled is False
        assert fb.injected is False
        assert fb.adopted is False
        assert fb.success is False

    def test_memory_feedback_record_to_dict(self):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        fb = MemoryFeedbackRecord(memory_id="mem_1", recalled=True,
                                   task_id="t1", token_size=120)
        d = fb.to_dict()
        assert d["memory_id"] == "mem_1"
        assert d["recalled"] is True
        assert d["task_id"] == "t1"

    def test_workflow_feedback_record_defaults(self):
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        wfb = WorkflowFeedbackRecord(workflow_id="wf_1")
        assert wfb.workflow_id == "wf_1"
        assert wfb.recommended is True
        assert wfb.adopted is False
        assert 0.0 <= wfb.timestamp <= time.time() + 1

    def test_quality_profile_defaults(self):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        p = MemoryQualityProfile(memory_id="mem_1")
        assert p.usage_count == 0
        assert p.adoption_count == 0
        assert p.quality_score == 0.0

    def test_quality_profile_rates(self):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        p = MemoryQualityProfile(memory_id="mem_1",
                                  usage_count=10, adoption_count=7, success_count=5)
        assert abs(p.adoption_rate - 0.7) < 0.001
        assert abs(p.success_rate - 5/7) < 0.001

    def test_quality_profile_zero_division_safe(self):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        p = MemoryQualityProfile(memory_id="mem_1", usage_count=0)
        assert p.adoption_rate == 0.0
        assert p.success_rate == 0.0

    def test_quality_profile_to_dict_includes_rates(self):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        p = MemoryQualityProfile(memory_id="mem_1", usage_count=10, adoption_count=8)
        d = p.to_dict()
        assert "adoption_rate" in d
        assert "success_rate" in d


# ── §8 FeedbackStore Persistence ─────────────────────────────────────────────

class TestFeedbackStore:

    def test_record_and_get_memory_feedback(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        fb = MemoryFeedbackRecord(memory_id="mem_10", task_id="t1", recalled=True)
        store.record_memory_feedback(fb)
        rows = store.get_memory_feedback("mem_10")
        assert len(rows) == 1
        assert rows[0].recalled is True

    def test_multiple_records_for_same_memory(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        for i in range(5):
            store.record_memory_feedback(MemoryFeedbackRecord(
                memory_id="mem_20", recalled=True
            ))
        rows = store.get_memory_feedback("mem_20")
        assert len(rows) == 5

    def test_record_workflow_feedback(self, store):
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        wfb = WorkflowFeedbackRecord(workflow_id="wf_1", task_id="t1",
                                      adopted=True, success=True)
        store.record_workflow_feedback(wfb)
        rows = store.get_workflow_feedback("wf_1")
        assert len(rows) == 1
        assert rows[0].adopted is True
        assert rows[0].success is True

    def test_quality_profile_upsert(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        p = MemoryQualityProfile(memory_id="mem_30", usage_count=5,
                                  quality_score=0.8,
                                  last_updated=time.time(),
                                  created_at=time.time())
        store.save_quality_profile(p)
        loaded = store.get_quality_profile("mem_30")
        assert loaded.usage_count == 5
        assert abs(loaded.quality_score - 0.8) < 0.001

    def test_quality_profile_update(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(memory_id="mem_31", usage_count=3,
                                  last_updated=now, created_at=now)
        store.save_quality_profile(p)
        p.usage_count = 10
        store.save_quality_profile(p)
        loaded = store.get_quality_profile("mem_31")
        assert loaded.usage_count == 10

    def test_get_nonexistent_profile_returns_default(self, store):
        p = store.get_quality_profile("never_seen")
        assert p.memory_id == "never_seen"
        assert p.usage_count == 0

    def test_list_profiles(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        for i, score in enumerate([0.9, 0.3, 0.7]):
            p = MemoryQualityProfile(memory_id=f"mem_list_{i}",
                                      quality_score=score,
                                      last_updated=now, created_at=now)
            store.save_quality_profile(p)
        profiles = store.list_profiles(min_quality=0.5)
        scores = [p.quality_score for p in profiles]
        assert all(s >= 0.5 for s in scores)
        assert len(profiles) >= 2

    def test_list_noisy_profiles(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(memory_id="noisy_mem", noise_count=8,
                                  last_updated=now, created_at=now)
        store.save_quality_profile(p)
        noisy = store.list_noisy_profiles(min_noise=5)
        ids = [n.memory_id for n in noisy]
        assert "noisy_mem" in ids

    def test_task_feedback_query(self, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id="mA", task_id="task_X", recalled=True
        ))
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id="mB", task_id="task_X", recalled=True
        ))
        rows = store.get_task_feedback("task_X")
        assert len(rows) == 2


# ── §9 Quality Scoring ────────────────────────────────────────────────────────

class TestQualityScorer:

    def test_score_zero_usage(self, scorer):
        assert scorer.score_memory("unknown_mem") == 0.0

    def test_score_formula(self, scorer):
        """§9.1: quality = adoption * 0.4 + success * 0.4 + recency * 0.2"""
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(
            memory_id="m1",
            usage_count=10, adoption_count=8, success_count=6,
            last_updated=now, created_at=now
        )
        score = scorer.score_memory_from_profile(p)
        # adoption_rate = 0.8, success_rate = 0.75, recency ≈ 1.0
        expected = 0.8 * 0.4 + 0.75 * 0.4 + 1.0 * 0.2
        assert abs(score - expected) < 0.05

    def test_score_bounds(self, scorer):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(
            memory_id="perf",
            usage_count=100, adoption_count=100, success_count=100,
            last_updated=now, created_at=now
        )
        assert scorer.score_memory_from_profile(p) <= 1.0
        assert scorer.score_memory_from_profile(p) >= 0.0

    def test_update_memory_profile_recall(self, scorer):
        profile = scorer.update_memory_profile("mem_up", recalled=True)
        assert profile.usage_count == 1

    def test_update_memory_profile_inject_no_adopt_increments_noise(self, scorer):
        profile = scorer.update_memory_profile("mem_noise",
                                                injected=True, adopted=False)
        assert profile.noise_count == 1

    def test_update_memory_profile_adopt_no_noise(self, scorer):
        profile = scorer.update_memory_profile("mem_good",
                                                injected=True, adopted=True)
        assert profile.noise_count == 0
        assert profile.adoption_count == 1

    def test_archive_candidate_triggered(self, scorer):
        """§11.3 — archive after 10+ noise + low adoption"""
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile, FeedbackStore
        from openmemo.memory_feedback.quality_scorer import QualityScorer
        s = scorer.store
        now = time.time()
        p = MemoryQualityProfile(
            memory_id="archive_me",
            usage_count=50, adoption_count=2, noise_count=12,
            last_updated=now, created_at=now
        )
        s.save_quality_profile(p)
        updated = scorer.update_memory_profile("archive_me", injected=True, adopted=False)
        assert updated.archive_candidate is True

    def test_recency_score_fresh(self):
        from openmemo.memory_feedback.quality_scorer import recency_score
        score = recency_score(time.time())
        assert 0.99 <= score <= 1.0

    def test_recency_score_half_life(self):
        from openmemo.memory_feedback.quality_scorer import recency_score
        half_life = 7 * 24 * 3600
        old_ts = time.time() - half_life
        score = recency_score(old_ts)
        assert 0.45 <= score <= 0.55

    def test_recency_score_very_old(self):
        from openmemo.memory_feedback.quality_scorer import recency_score
        old_ts = time.time() - 365 * 24 * 3600  # 1 year old
        score = recency_score(old_ts)
        assert score < 0.01

    def test_workflow_score_no_history(self, scorer):
        result = scorer.score_workflow("new_wf")
        assert result["total"] == 0
        assert result["score"] == 0.0

    def test_workflow_score_formula(self, scorer):
        """§9.2: workflow_score = success*0.5 + adoption*0.3 + recency*0.2"""
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        store = scorer.store
        for i in range(10):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="wf_score",
                adopted=True, success=(i < 8)
            ))
        result = scorer.score_workflow("wf_score")
        assert result["adoption_rate"] == 1.0
        # 8/10 success
        assert abs(result["success_rate"] - 0.8) < 0.001
        assert result["score"] > 0.8

    def test_refresh_all_profiles(self, scorer):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        store = scorer.store
        for i in range(5):
            p = MemoryQualityProfile(
                memory_id=f"bulk_{i}",
                usage_count=i + 1,
                adoption_count=i,
                last_updated=now, created_at=now
            )
            store.save_quality_profile(p)
        count = scorer.refresh_all_profiles()
        assert count >= 5


# ── §7 Feedback Collector (4 signal types) ────────────────────────────────────

class TestFeedbackCollector:

    def test_on_recall_writes_records(self, collector):
        """§7.1 Recall Signal"""
        ids = collector.on_recall(["m1", "m2"], task_id="t1", agent_id="ag1")
        assert len(ids) == 2
        assert all(len(fid) > 0 for fid in ids)

    def test_on_recall_updates_profile(self, collector):
        collector.on_recall(["mem_recall_test"], task_id="t1")
        profile = collector.store.get_quality_profile("mem_recall_test")
        assert profile.usage_count == 1

    def test_on_inject_writes_record(self, collector):
        """§7.2 Injection Signal"""
        fid = collector.on_inject("m1", task_id="t1", token_size=120,
                                   position="experience_block")
        assert len(fid) > 0

    def test_on_inject_increments_noise_when_not_adopted(self, collector):
        collector.on_inject("noise_mem", task_id="t1", token_size=50)
        profile = collector.store.get_quality_profile("noise_mem")
        assert profile.noise_count == 1

    def test_on_inject_block(self, collector):
        """Bulk injection."""
        ids = collector.on_inject_block("t2", {"m1": 100, "m2": 200, "m3": 50})
        assert len(ids) == 3

    def test_on_adopt_memory(self, collector):
        """§7.3 Adoption Signal — memory"""
        collector.on_adopt("t1", memory_id="m1", adopted=True)
        profile = collector.store.get_quality_profile("m1")
        assert profile.adoption_count == 1

    def test_on_adopt_workflow(self, collector):
        """§7.3 Adoption Signal — workflow"""
        fid = collector.on_adopt("t1", workflow_id="wf_1",
                                  adopted=True, adoption_rate=0.8)
        assert fid  # feedback_id returned

    def test_on_outcome_success(self, collector):
        """§7.4 Outcome Signal"""
        ids = collector.on_outcome("t1", success=True,
                                    memory_ids=["m1", "m2"],
                                    workflow_id="wf_1",
                                    execution_time=35.0)
        assert len(ids) >= 2

    def test_on_outcome_updates_success_count(self, collector):
        collector.on_recall(["m_outcome"], task_id="t1")
        collector.on_adopt("t1", memory_id="m_outcome", adopted=True)
        collector.on_outcome("t1", success=True, memory_ids=["m_outcome"])
        profile = collector.store.get_quality_profile("m_outcome")
        assert profile.success_count >= 1

    def test_adoption_detection_positive(self, collector):
        """§7.3 keyword matching"""
        steps = ["docker build", "docker compose up", "health check"]
        adopted = collector.detect_adoption(
            "ran docker build and docker compose up successfully", steps
        )
        assert adopted is True

    def test_adoption_detection_negative(self, collector):
        steps = ["docker build", "docker compose up", "health check"]
        adopted = collector.detect_adoption("I did nothing relevant", steps)
        assert adopted is False

    def test_adoption_rate_calculation(self, collector):
        steps = ["step_a", "step_b", "step_c", "step_d"]
        rate = collector.detect_adoption_rate("I did step_a and step_b", steps)
        assert abs(rate - 0.5) < 0.01

    def test_empty_recall_list(self, collector):
        ids = collector.on_recall([])
        assert ids == []

    def test_feedback_write_latency(self, collector):
        """§18 — feedback write latency < 20ms"""
        t0 = time.time()
        collector.on_recall(["latency_mem"], task_id="t_lat")
        elapsed_ms = (time.time() - t0) * 1000
        assert elapsed_ms < 20.0


# ── §10 Ranking Adjuster ──────────────────────────────────────────────────────

class TestRankingAdjuster:

    def _seed_profile(self, store, memory_id, usage=20, adoption=15, success=12, noise=0):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        from openmemo.memory_feedback.quality_scorer import QualityScorer
        now = time.time()
        p = MemoryQualityProfile(
            memory_id=memory_id,
            usage_count=usage, adoption_count=adoption, success_count=success,
            noise_count=noise, last_updated=now, created_at=now
        )
        scorer = QualityScorer(store)
        p.quality_score = scorer.score_memory_from_profile(p)
        store.save_quality_profile(p)
        return p

    def test_adjust_recall_preserves_order_by_score(self, adjuster, store):
        """§10.1 — Higher quality memories should rank higher."""
        self._seed_profile(store, "high_q", adoption=18, success=15)
        self._seed_profile(store, "low_q", adoption=2, success=1, noise=8)

        results = [
            {"id": "high_q", "score": 0.7},
            {"id": "low_q", "score": 0.8},
        ]
        adjusted = adjuster.adjust_recall(results)
        ids = [r["id"] for r in adjusted]
        # high_q should move up due to quality
        assert "high_q" in ids

    def test_adjust_recall_adds_quality_score_field(self, adjuster):
        results = [{"id": "m_new", "score": 0.5}]
        adjusted = adjuster.adjust_recall(results)
        assert "quality_score" in adjusted[0]
        assert "ranking_action" in adjusted[0]

    def test_adjust_recall_empty(self, adjuster):
        assert adjuster.adjust_recall([]) == []

    def test_adjust_recall_neutral_for_unknown(self, adjuster):
        results = [{"id": "unknown_mem", "score": 0.6}]
        adjusted = adjuster.adjust_recall(results)
        assert adjusted[0]["ranking_action"] == "neutral"

    def test_promotion_action(self, adjuster, store):
        """§11.1 — High adoption + success → promote"""
        self._seed_profile(store, "promo_mem", usage=20, adoption=18, success=16)
        results = [{"id": "promo_mem", "score": 0.6}]
        adjusted = adjuster.adjust_recall(results)
        action = adjusted[0]["ranking_action"]
        assert action == "promote"

    def test_suppression_action(self, adjuster, store):
        """§11.2 — High noise + low adoption → suppress"""
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(
            memory_id="suppress_me",
            usage_count=30, adoption_count=1, noise_count=20,
            last_updated=now, created_at=now
        )
        store.save_quality_profile(p)
        results = [{"id": "suppress_me", "score": 0.7}]
        adjusted = adjuster.adjust_recall(results)
        action = adjusted[0]["ranking_action"]
        assert action == "suppress"
        # Suppressed score should be lower than original
        assert adjusted[0]["score"] < 0.7

    def test_promote_boosts_score(self, adjuster, store):
        self._seed_profile(store, "boost_mem", usage=20, adoption=18, success=16)
        results = [{"id": "boost_mem", "score": 0.6}]
        adjusted = adjuster.adjust_recall(results)
        assert adjusted[0]["score"] >= 0.6 * 0.80

    def test_adjust_workflows_empty(self, adjuster):
        assert adjuster.adjust_workflows([]) == []

    def test_adjust_workflows_adds_adoption_rate(self, adjuster, store):
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        for _ in range(5):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="wf_adj", adopted=True, success=True
            ))
        workflows = [{"id": "wf_adj", "score": 0.6}]
        adjusted = adjuster.adjust_workflows(workflows)
        assert "adoption_rate" in adjusted[0]

    def test_is_promoted(self, adjuster, store):
        self._seed_profile(store, "is_promo", usage=20, adoption=18, success=16)
        assert adjuster.is_promoted("is_promo") is True

    def test_is_suppressed(self, adjuster, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(memory_id="is_supp",
                                  usage_count=30, adoption_count=1, noise_count=20,
                                  last_updated=now, created_at=now)
        store.save_quality_profile(p)
        assert adjuster.is_suppressed("is_supp") is True

    def test_ranking_adjustment_latency(self, adjuster):
        """§18 — Ranking overhead < 15ms"""
        results = [{"id": f"m{i}", "score": 0.5} for i in range(20)]
        t0 = time.time()
        adjuster.adjust_recall(results)
        elapsed_ms = (time.time() - t0) * 1000
        assert elapsed_ms < 15.0


# ── §5.2 Workflow Feedback ─────────────────────────────────────────────────────

class TestWorkflowFeedback:

    def test_recommend_and_get_history(self, wf_feedback):
        wf_feedback.on_recommend("wf_1", task_id="t1", agent_id="ag1")
        history = wf_feedback.get_history("wf_1")
        assert len(history) >= 1

    def test_on_step_detects_adoption(self, wf_feedback):
        wf_feedback.on_recommend("wf_deploy", task_id="t2")
        result = wf_feedback.on_step(
            task_id="t2",
            step_text="docker build && docker compose up && health check passed",
            workflow_id="wf_deploy",
            workflow_steps=["docker build", "docker compose", "health check"]
        )
        assert result["adopted"] is True
        assert result["adoption_rate"] > 0.3

    def test_on_step_no_adoption(self, wf_feedback):
        result = wf_feedback.on_step(
            task_id="t3",
            step_text="I did something completely unrelated",
            workflow_id="wf_x",
            workflow_steps=["step_a", "step_b", "step_c"]
        )
        assert result["adopted"] is False

    def test_on_complete_records_success(self, wf_feedback):
        wf_feedback.on_recommend("wf_2", task_id="t4")
        fid = wf_feedback.on_complete("t4", workflow_id="wf_2",
                                       success=True, execution_time=42.0)
        assert len(fid) > 0
        q = wf_feedback.get_quality("wf_2")
        assert q["total"] >= 2

    def test_on_complete_failure(self, wf_feedback):
        wf_feedback.on_recommend("wf_3", task_id="t5")
        wf_feedback.on_complete("t5", workflow_id="wf_3", success=False)
        q = wf_feedback.get_quality("wf_3")
        assert q["success_rate"] == 0.0

    def test_workflow_quality_empty(self, wf_feedback):
        q = wf_feedback.get_quality("nonexistent_wf")
        assert q["total"] == 0
        assert q["score"] == 0.0

    def test_workflow_quality_improves_with_success(self, wf_feedback, store):
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        for _ in range(10):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="improving_wf", adopted=True, success=True
            ))
        q = wf_feedback.get_quality("improving_wf")
        assert q["score"] > 0.7


# ── MemoryFeedback Facade ─────────────────────────────────────────────────────

class TestMemoryFeedbackFacade:

    def test_full_lifecycle(self, facade):
        """Complete recall → inject → adopt → outcome loop."""
        task_id = "full_loop_task"
        mem_id = "mem_loop"

        # Recall
        ids = facade.on_recall([mem_id], task_id=task_id)
        assert ids

        # Inject
        fid = facade.on_inject(mem_id, task_id=task_id, token_size=150,
                                position="experience_block")
        assert fid

        # Adopt
        facade.on_adopt(task_id, memory_id=mem_id, adopted=True, adoption_rate=1.0)

        # Outcome
        facade.on_outcome(task_id, success=True, memory_ids=[mem_id])

        # Quality should be > 0
        profile = facade.get_quality_profile(mem_id)
        assert profile.usage_count >= 1

    def test_adjust_recall(self, facade):
        results = [{"id": "r1", "score": 0.7}, {"id": "r2", "score": 0.5}]
        adjusted = facade.adjust_recall(results)
        assert len(adjusted) == 2
        assert all("quality_score" in r for r in adjusted)

    def test_adjust_workflows(self, facade):
        workflows = [{"id": "wf_a", "score": 0.6}]
        adjusted = facade.adjust_workflows(workflows)
        assert len(adjusted) == 1

    def test_workflow_recommend_and_complete(self, facade):
        facade.workflow_recommend("facade_wf", task_id="ft1")
        result = facade.workflow_on_step("ft1",
            "running docker build and compose up",
            workflow_id="facade_wf",
            workflow_steps=["docker build", "compose up"])
        facade.workflow_on_complete("ft1", workflow_id="facade_wf",
                                     success=True, execution_time=20.0)
        q = facade.get_workflow_quality("facade_wf")
        assert q["total"] >= 2

    def test_get_quality_score_default(self, facade):
        score = facade.get_quality_score("never_seen_mem")
        assert 0.0 <= score <= 1.0

    def test_is_suppressed_new_memory(self, facade):
        assert facade.is_suppressed("brand_new") is False

    def test_is_promoted_new_memory(self, facade):
        assert facade.is_promoted("brand_new_2") is False

    def test_list_noisy_memories(self, facade):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(memory_id="noisy_facade", noise_count=10,
                                  last_updated=now, created_at=now)
        facade._store.save_quality_profile(p)
        noisy = facade.list_noisy_memories(min_noise=5)
        ids = [n.memory_id for n in noisy]
        assert "noisy_facade" in ids

    def test_noise_not_counting_adopted(self, facade):
        """Adopted memories should NOT increment noise_count."""
        facade.on_inject("clean_mem", task_id="t1", token_size=100)
        facade.on_adopt("t1", memory_id="clean_mem", adopted=True)
        profile = facade.get_quality_profile("clean_mem")
        # adoption_count up, noise should not be counted in scorer after adopt
        assert profile.adoption_count >= 1


# ── §14 Module Structure ──────────────────────────────────────────────────────

class TestModuleStructure:

    def test_all_modules_importable(self):
        from openmemo.memory_feedback import (
            MemoryFeedback, FeedbackStore, MemoryFeedbackRecord,
            WorkflowFeedbackRecord, MemoryQualityProfile,
            QualityScorer, FeedbackCollector, RankingAdjuster, WorkflowFeedback,
        )

    def test_top_level_import(self):
        from openmemo import (
            MemoryFeedback, FeedbackStore, MemoryFeedbackRecord,
            WorkflowFeedbackRecord, MemoryQualityProfile,
        )

    def test_version_updated(self):
        import openmemo
        assert openmemo.__version__ == "0.21.0"

    def test_feedback_store_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.memory_feedback.feedback_store")
        assert spec is not None

    def test_quality_scorer_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.memory_feedback.quality_scorer")
        assert spec is not None

    def test_feedback_collector_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.memory_feedback.feedback_collector")
        assert spec is not None

    def test_ranking_adjuster_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.memory_feedback.ranking_adjuster")
        assert spec is not None

    def test_workflow_feedback_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.memory_feedback.workflow_feedback")
        assert spec is not None


# ── §18 Performance Targets ───────────────────────────────────────────────────

class TestPerformance:

    def test_quality_score_update_latency(self, scorer):
        """§18 — quality score update < 50ms"""
        t0 = time.time()
        scorer.update_memory_profile("perf_mem", recalled=True, injected=True)
        elapsed_ms = (time.time() - t0) * 1000
        assert elapsed_ms < 50.0

    def test_workflow_score_latency(self, scorer, store):
        """§18 — quality score computation < 50ms"""
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        for _ in range(20):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="perf_wf", adopted=True, success=True
            ))
        t0 = time.time()
        scorer.score_workflow("perf_wf")
        elapsed_ms = (time.time() - t0) * 1000
        assert elapsed_ms < 50.0

    def test_ranking_adjuster_10_items_latency(self, adjuster):
        """§18 — ranking overhead < 15ms for 10 items"""
        results = [{"id": f"m{i}", "score": 0.5 + i * 0.01} for i in range(10)]
        t0 = time.time()
        adjuster.adjust_recall(results)
        elapsed_ms = (time.time() - t0) * 1000
        assert elapsed_ms < 15.0
