"""
Tests for Feedback Aggregator (v0.22.0)

PRD Coverage:
    §4.1  Task Trace output structure
    §4.2  Memory Profile output structure
    §4.3  Impact Metrics computation
    §4.4  Timeline chronological event stream
    §5.1  Task trace aggregation logic
    §5.2  Memory profile aggregation logic
    §5.3  Timeline build logic
    §6    REST API endpoints (structure)
    §9    Performance: trace<50ms, metrics<30ms, timeline<40ms
"""

import time
import pytest
import tempfile
import os


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

@pytest.fixture
def db_path(tmp_path):
    return str(tmp_path / "test_aggregator.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 agg(store):
    from openmemo.feedback_aggregator import FeedbackAggregator
    return FeedbackAggregator(store)


def _seed_task(store, task_id="task_001", mem_ids=None, success=True):
    """Seed a complete task feedback cycle."""
    from openmemo.memory_feedback.feedback_store import (
        MemoryFeedbackRecord, WorkflowFeedbackRecord
    )
    mem_ids = mem_ids or ["mem_A", "mem_B"]
    for mid in mem_ids:
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id=mid, task_id=task_id,
            memory_layer="experience",
            recalled=True, injected=True, adopted=True,
            success=success, token_size=120,
            position="experience_block",
        ))
    store.record_workflow_feedback(WorkflowFeedbackRecord(
        workflow_id="wf_001", task_id=task_id,
        recommended=True, adopted=True, success=success,
        adoption_rate=0.85, completion_time=42.0,
    ))


def _seed_profile(store, memory_id, usage=20, adoption=14, success=10, noise=2):
    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,
        injection_count=usage, 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


# ── §4.1 Task Trace ───────────────────────────────────────────────────────────

class TestTaskTrace:

    def test_basic_structure(self, agg, store):
        _seed_task(store)
        trace = agg.task_trace("task_001")
        assert "task_id" in trace
        assert trace["task_id"] == "task_001"
        assert "recalled_memories" in trace
        assert "injected_memories" in trace
        assert "adopted_memories" in trace
        assert "workflow_used" in trace
        assert "result" in trace
        assert "stats" in trace

    def test_recalled_count(self, agg, store):
        _seed_task(store, task_id="t_recall", mem_ids=["m1", "m2", "m3"])
        trace = agg.task_trace("t_recall")
        assert len(trace["recalled_memories"]) == 3

    def test_injected_count(self, agg, store):
        _seed_task(store, task_id="t_inject", mem_ids=["m1", "m2"])
        trace = agg.task_trace("t_inject")
        assert len(trace["injected_memories"]) == 2

    def test_adopted_count(self, agg, store):
        _seed_task(store, task_id="t_adopt", mem_ids=["m1", "m2"])
        trace = agg.task_trace("t_adopt")
        assert len(trace["adopted_memories"]) == 2

    def test_workflow_used_populated(self, agg, store):
        _seed_task(store)
        trace = agg.task_trace("task_001")
        wf = trace["workflow_used"]
        assert wf is not None
        assert wf["workflow_id"] == "wf_001"
        assert wf["adopted"] is True

    def test_result_success(self, agg, store):
        _seed_task(store, success=True)
        trace = agg.task_trace("task_001")
        assert trace["result"]["success"] is True

    def test_result_failure(self, agg, store):
        _seed_task(store, task_id="failed_task", success=False)
        trace = agg.task_trace("failed_task")
        assert trace["result"]["success"] is False

    def test_result_summary_nonempty(self, agg, store):
        _seed_task(store)
        trace = agg.task_trace("task_001")
        assert trace["result"]["summary"]

    def test_stats_adoption_rate(self, agg, store):
        _seed_task(store, task_id="t_stats", mem_ids=["m1", "m2"])
        trace = agg.task_trace("t_stats")
        assert 0.0 <= trace["stats"]["adoption_rate"] <= 1.0

    def test_empty_task_returns_valid_structure(self, agg):
        trace = agg.task_trace("nonexistent_task")
        assert trace["task_id"] == "nonexistent_task"
        assert trace["recalled_memories"] == []
        assert trace["result"]["success"] is False

    def test_recalled_memory_has_fields(self, agg, store):
        _seed_task(store)
        trace = agg.task_trace("task_001")
        for r in trace["recalled_memories"]:
            assert "memory_id" in r
            assert "memory_layer" in r
            assert "timestamp" in r

    def test_injected_memory_has_token_size(self, agg, store):
        _seed_task(store)
        trace = agg.task_trace("task_001")
        for r in trace["injected_memories"]:
            assert "token_size" in r
            assert r["token_size"] >= 0

    def test_performance(self, agg, store):
        """§9 — Task trace build < 50ms"""
        _seed_task(store, mem_ids=["m1","m2","m3","m4","m5"])
        t0 = time.time()
        agg.task_trace("task_001")
        assert (time.time() - t0) * 1000 < 50


# ── §4.2 Memory Profile ───────────────────────────────────────────────────────

class TestMemoryProfile:

    def test_basic_structure(self, agg, store):
        _seed_profile(store, "mem_prof_1")
        profile = agg.memory_profile("mem_prof_1")
        assert "memory_id" in profile
        assert "usage_count" in profile
        assert "adoption_rate" in profile
        assert "success_rate" in profile
        assert "quality_score" in profile
        assert "last_used" in profile
        assert "status" in profile

    def test_usage_count(self, agg, store):
        _seed_profile(store, "mem_uc", usage=15)
        profile = agg.memory_profile("mem_uc")
        assert profile["usage_count"] == 15

    def test_adoption_rate_range(self, agg, store):
        _seed_profile(store, "mem_ar")
        profile = agg.memory_profile("mem_ar")
        assert 0.0 <= profile["adoption_rate"] <= 1.0

    def test_success_rate_range(self, agg, store):
        _seed_profile(store, "mem_sr")
        profile = agg.memory_profile("mem_sr")
        assert 0.0 <= profile["success_rate"] <= 1.0

    def test_quality_score_range(self, agg, store):
        _seed_profile(store, "mem_qs")
        profile = agg.memory_profile("mem_qs")
        assert 0.0 <= profile["quality_score"] <= 1.0

    def test_status_promoted(self, agg, store):
        _seed_profile(store, "mem_promo", usage=20, adoption=18, success=16, noise=0)
        profile = agg.memory_profile("mem_promo")
        assert profile["status"] == "promoted"

    def test_status_suppressed(self, agg, store):
        from openmemo.memory_feedback.feedback_store import MemoryQualityProfile
        now = time.time()
        p = MemoryQualityProfile(memory_id="mem_supp",
                                  usage_count=30, adoption_count=1,
                                  noise_count=20, last_updated=now, created_at=now)
        store.save_quality_profile(p)
        profile = agg.memory_profile("mem_supp")
        assert profile["status"] == "suppressed"

    def test_status_new_for_unknown(self, agg):
        profile = agg.memory_profile("brand_new_mem")
        assert profile["status"] == "new"

    def test_noise_count_field(self, agg, store):
        _seed_profile(store, "mem_nc", noise=7)
        profile = agg.memory_profile("mem_nc")
        assert profile["noise_count"] == 7

    def test_memory_layer_inferred(self, agg, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id="layered_mem", memory_layer="user", recalled=True
        ))
        profile = agg.memory_profile("layered_mem")
        assert profile["memory_layer"] == "user"

    def test_batch_profiles(self, agg, store):
        for i in range(3):
            _seed_profile(store, f"batch_{i}")
        profiles = agg.memory_profiles_batch(["batch_0", "batch_1", "batch_2"])
        assert len(profiles) == 3
        for p in profiles:
            assert "quality_score" in p

    def test_top_memories(self, agg, store):
        for i in range(5):
            _seed_profile(store, f"top_{i}", usage=i+5, adoption=i+3, success=i+1)
        top = agg.top_memories(n=3)
        assert len(top) <= 3
        if len(top) >= 2:
            assert top[0]["quality_score"] >= top[1]["quality_score"]

    def test_noisy_memories(self, agg, store):
        _seed_profile(store, "noisy_one", noise=8)
        noisy = agg.noisy_memories(min_noise=5)
        ids = [p["memory_id"] for p in noisy]
        assert "noisy_one" in ids

    def test_performance(self, agg, store):
        """§9 — implied: profile build fast"""
        _seed_profile(store, "perf_mem")
        t0 = time.time()
        agg.memory_profile("perf_mem")
        assert (time.time() - t0) * 1000 < 30


# ── §4.3 Impact Metrics ───────────────────────────────────────────────────────

class TestImpactMetrics:

    def test_basic_structure(self, agg):
        metrics = agg.impact_metrics()
        assert "total_memories" in metrics
        assert "memories_recalled" in metrics
        assert "memories_adopted" in metrics
        assert "workflows_reused" in metrics
        assert "tasks_avoided" in metrics
        assert "tokens_saved" in metrics
        assert "avg_quality_score" in metrics
        assert "promoted_count" in metrics
        assert "suppressed_count" in metrics

    def test_tokens_saved_proportional(self, agg, store):
        """tokens_saved = reused_workflows * 350 (AVG_WORKFLOW_TOKENS)"""
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        for _ in range(4):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="wf_tokens", adopted=True, success=True
            ))
        metrics = agg.impact_metrics()
        assert metrics["tokens_saved"] == metrics["workflows_reused"] * 350

    def test_workflows_reused_count(self, agg, store):
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        for _ in range(3):
            store.record_workflow_feedback(WorkflowFeedbackRecord(
                workflow_id="counted_wf", adopted=True
            ))
        metrics = agg.impact_metrics()
        assert metrics["workflows_reused"] >= 3

    def test_memories_recalled_count(self, agg, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        for i in range(5):
            store.record_memory_feedback(MemoryFeedbackRecord(
                memory_id=f"recalled_{i}", recalled=True
            ))
        metrics = agg.impact_metrics()
        assert metrics["memories_recalled"] >= 5

    def test_tasks_avoided_equals_workflows_reused(self, agg, store):
        """PRD simplified: tasks_avoided ≈ workflows_reused"""
        from openmemo.memory_feedback.feedback_store import WorkflowFeedbackRecord
        store.record_workflow_feedback(WorkflowFeedbackRecord(
            workflow_id="ta_wf", adopted=True
        ))
        metrics = agg.impact_metrics()
        assert metrics["tasks_avoided"] == metrics["workflows_reused"]

    def test_avg_quality_score_range(self, agg, store):
        _seed_profile(store, "q_mem_1")
        metrics = agg.impact_metrics()
        assert 0.0 <= metrics["avg_quality_score"] <= 1.0

    def test_promoted_count(self, agg, store):
        _seed_profile(store, "promoted_qm", usage=20, adoption=18, success=16)
        metrics = agg.impact_metrics()
        assert metrics["promoted_count"] >= 1

    def test_empty_db_returns_zeros(self, agg):
        metrics = agg.impact_metrics()
        assert metrics["total_memories"] == 0
        assert metrics["workflows_reused"] == 0
        assert metrics["tokens_saved"] == 0

    def test_performance(self, agg, store):
        """§9 — Metrics build < 30ms"""
        _seed_task(store, mem_ids=["m1","m2","m3"])
        t0 = time.time()
        agg.impact_metrics()
        assert (time.time() - t0) * 1000 < 30

    def test_window_metrics(self, agg, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id="window_mem", recalled=True
        ))
        since = time.time() - 60
        metrics = agg.impact_metrics_window(since_ts=since)
        assert "window_start" in metrics
        assert "window_end" in metrics
        assert metrics["memories_recalled"] >= 1


# ── §4.4 Timeline ─────────────────────────────────────────────────────────────

class TestTimeline:

    def test_task_timeline_basic_structure(self, agg, store):
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        assert isinstance(events, list)
        assert len(events) > 0

    def test_each_event_has_required_fields(self, agg, store):
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        for e in events:
            assert "timestamp" in e
            assert "event" in e
            assert "summary" in e

    def test_task_timeline_event_types(self, agg, store):
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        event_types = {e["event"] for e in events}
        assert "memory_recalled" in event_types
        assert "memory_injected" in event_types
        assert "memory_adopted" in event_types

    def test_workflow_events_in_timeline(self, agg, store):
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        event_types = {e["event"] for e in events}
        assert "workflow_adopted" in event_types or "workflow_recommended" in event_types

    def test_timeline_sorted_by_time(self, agg, store):
        """Events must be sorted chronologically."""
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        timestamps = [e["timestamp"] for e in events if e["timestamp"]]
        assert timestamps == sorted(timestamps)

    def test_memory_timeline(self, agg, store):
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        store.record_memory_feedback(MemoryFeedbackRecord(
            memory_id="tracked_mem", task_id="t1",
            recalled=True, injected=True, adopted=True
        ))
        events = agg.timeline(memory_id="tracked_mem")
        assert len(events) >= 3
        mem_ids = [e.get("memory_id") for e in events if e.get("memory_id")]
        assert "tracked_mem" in mem_ids

    def test_global_timeline(self, agg, store):
        _seed_task(store, task_id="t_global_1")
        _seed_task(store, task_id="t_global_2")
        events = agg.timeline(limit=50)
        assert isinstance(events, list)

    def test_global_timeline_limit(self, agg, store):
        for i in range(20):
            from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
            store.record_memory_feedback(MemoryFeedbackRecord(
                memory_id=f"m_{i}", recalled=True
            ))
        events = agg.timeline(limit=10)
        assert len(events) <= 10

    def test_empty_task_timeline(self, agg):
        events = agg.timeline(task_id="ghost_task")
        assert events == []

    def test_summary_nonempty(self, agg, store):
        _seed_task(store)
        events = agg.timeline(task_id="task_001")
        for e in events:
            assert e["summary"]

    def test_performance(self, agg, store):
        """§9 — Timeline build < 40ms"""
        _seed_task(store, mem_ids=["m1","m2","m3","m4","m5"])
        t0 = time.time()
        agg.timeline(task_id="task_001")
        assert (time.time() - t0) * 1000 < 40


# ── Inspector Snapshot ────────────────────────────────────────────────────────

class TestInspectorSnapshot:

    def test_snapshot_with_task(self, agg, store):
        _seed_task(store)
        snap = agg.inspector_snapshot(task_id="task_001")
        assert "metrics" in snap
        assert "task_trace" in snap
        assert "timeline" in snap
        assert "top_memories" in snap

    def test_snapshot_without_task(self, agg, store):
        _seed_task(store)
        snap = agg.inspector_snapshot()
        assert "metrics" in snap
        assert "timeline" in snap
        assert "top_memories" in snap
        assert "task_trace" not in snap

    def test_snapshot_metrics_structure(self, agg, store):
        _seed_task(store)
        snap = agg.inspector_snapshot()
        m = snap["metrics"]
        assert "total_memories" in m
        assert "tokens_saved" in m


# ── Module Structure ──────────────────────────────────────────────────────────

class TestModuleStructure:

    def test_all_classes_importable(self):
        from openmemo.feedback_aggregator import (
            FeedbackAggregator, TaskTraceBuilder,
            MemoryProfileBuilder, MetricsBuilder, TimelineBuilder
        )

    def test_top_level_import(self):
        from openmemo import (
            FeedbackAggregator, TaskTraceBuilder,
            MemoryProfileBuilder, MetricsBuilder, TimelineBuilder
        )

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

    def test_utils_importable(self):
        from openmemo.feedback_aggregator.utils import (
            to_iso, safe_div, filter_records, unique_by, avg, clamp
        )

    def test_to_iso(self):
        from openmemo.feedback_aggregator.utils import to_iso
        ts = 1743292800.0  # 2025-03-29T16:00:00Z
        result = to_iso(ts)
        assert "T" in result
        assert result.endswith("Z")

    def test_safe_div(self):
        from openmemo.feedback_aggregator.utils import safe_div
        assert safe_div(7, 10) == 0.7
        assert safe_div(0, 0) == 0.0
        assert safe_div(5, 0, default=99.0) == 99.0

    def test_filter_records(self):
        from openmemo.feedback_aggregator.utils import filter_records
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        recs = [
            MemoryFeedbackRecord(memory_id="a", recalled=True),
            MemoryFeedbackRecord(memory_id="b", recalled=False),
            MemoryFeedbackRecord(memory_id="c", recalled=True),
        ]
        filtered = filter_records(recs, recalled=True)
        assert len(filtered) == 2
        assert all(r.recalled for r in filtered)

    def test_unique_by(self):
        from openmemo.feedback_aggregator.utils import unique_by
        from openmemo.memory_feedback.feedback_store import MemoryFeedbackRecord
        recs = [
            MemoryFeedbackRecord(memory_id="dup"),
            MemoryFeedbackRecord(memory_id="dup"),
            MemoryFeedbackRecord(memory_id="unique"),
        ]
        deduped = unique_by(recs, "memory_id")
        assert len(deduped) == 2

    def test_aggregator_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.feedback_aggregator.aggregator")
        assert spec is not None

    def test_api_file_exists(self):
        import importlib
        spec = importlib.util.find_spec("openmemo.feedback_aggregator.api")
        assert spec is not None
