"""Tests for meta-learning engine."""
from __future__ import annotations

import pytest

from logos_adk.learning.meta_learning import (
    InterventionRecord,
    InterventionStatus,
    InterventionType,
    MetaLearningEngine,
    MetaLearningPolicy,
)


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


@pytest.fixture
def engine(db_path):
    return MetaLearningEngine(db_path=db_path)


class TestLogIntervention:
    def test_log_creates_record(self, engine):
        rec = engine.log_intervention(
            agent_name="agent-1",
            intervention_type=InterventionType.prompt_patch,
            source="self_learning_loop",
            trace_id="trace-abc",
            context={"failure_types": ["hallucination"]},
            cost_estimate=0.05,
        )
        assert isinstance(rec, InterventionRecord)
        assert rec.agent_name == "agent-1"
        assert rec.intervention_type == InterventionType.prompt_patch
        assert rec.source == "self_learning_loop"
        assert rec.trace_id == "trace-abc"
        assert rec.context == {"failure_types": ["hallucination"]}
        assert rec.cost_estimate == 0.05
        assert rec.status == InterventionStatus.applied
        assert rec.uplift is None


class TestGetIntervention:
    def test_get_existing(self, engine):
        rec = engine.log_intervention(
            agent_name="a", intervention_type=InterventionType.prompt_patch,
            source="manual",
        )
        fetched = engine.get_intervention(rec.intervention_id)
        assert fetched is not None
        assert fetched.intervention_id == rec.intervention_id

    def test_get_missing_returns_none(self, engine):
        assert engine.get_intervention("nonexistent") is None


class TestMeasureUplift:
    def test_measure_sets_uplift_and_status(self, engine):
        rec = engine.log_intervention(
            agent_name="a", intervention_type=InterventionType.routing_change,
            source="rca", cost_estimate=0.10,
        )
        engine.measure_uplift(rec.intervention_id, 0.15)
        updated = engine.get_intervention(rec.intervention_id)
        assert updated.uplift == 0.15
        assert updated.status == InterventionStatus.measured
        assert updated.uplift_measured_at is not None


class TestStatusUpdates:
    def test_mark_rolled_back(self, engine):
        rec = engine.log_intervention(
            agent_name="a", intervention_type=InterventionType.prompt_patch,
            source="manual",
        )
        engine.mark_rolled_back(rec.intervention_id)
        updated = engine.get_intervention(rec.intervention_id)
        assert updated.status == InterventionStatus.rolled_back

    def test_mark_failed(self, engine):
        rec = engine.log_intervention(
            agent_name="a", intervention_type=InterventionType.retrieval_change,
            source="manual",
        )
        engine.mark_failed(rec.intervention_id)
        updated = engine.get_intervention(rec.intervention_id)
        assert updated.status == InterventionStatus.failed


def _populate(engine, agent="agent-1"):
    """Helper: create measured interventions across types."""
    # 3 prompt_patch: uplifts 0.2, 0.1, 0.15 — avg 0.15, cost 0.05
    for u in [0.2, 0.1, 0.15]:
        rec = engine.log_intervention(
            agent_name=agent,
            intervention_type=InterventionType.prompt_patch,
            source="self_learning_loop",
            cost_estimate=0.05,
        )
        engine.measure_uplift(rec.intervention_id, u)

    # 3 routing_change: uplifts 0.05, 0.04, 0.06 — avg 0.05, cost 0.01
    for u in [0.05, 0.04, 0.06]:
        rec = engine.log_intervention(
            agent_name=agent,
            intervention_type=InterventionType.routing_change,
            source="rca",
            cost_estimate=0.01,
        )
        engine.measure_uplift(rec.intervention_id, u)

    # 3 retrieval_change: uplifts -0.01, -0.02, 0.01 — avg -0.0067, cost 0.03
    for u in [-0.01, -0.02, 0.01]:
        rec = engine.log_intervention(
            agent_name=agent,
            intervention_type=InterventionType.retrieval_change,
            source="manual",
            cost_estimate=0.03,
        )
        engine.measure_uplift(rec.intervention_id, u)


class TestInterventionStats:
    def test_stats_all_agents(self, engine):
        _populate(engine)
        stats = engine.intervention_stats()
        assert "prompt_patch" in stats
        assert stats["prompt_patch"]["count"] == 3
        assert abs(stats["prompt_patch"]["avg_uplift"] - 0.15) < 0.001
        assert stats["prompt_patch"]["success_rate"] == 1.0

    def test_stats_filtered_by_type(self, engine):
        _populate(engine)
        stats = engine.intervention_stats(intervention_type=InterventionType.routing_change)
        assert "routing_change" in stats
        assert len(stats) == 1

    def test_stats_filtered_by_agent(self, engine):
        _populate(engine, agent="agent-1")
        _populate(engine, agent="agent-2")
        stats = engine.intervention_stats(agent_name="agent-1")
        total = sum(v["count"] for v in stats.values())
        assert total == 9  # only agent-1


class TestROILeaderboard:
    def test_leaderboard_sorted_by_roi_desc(self, engine):
        _populate(engine)
        lb = engine.roi_leaderboard(agent_name="agent-1")
        assert len(lb) >= 2
        for i in range(len(lb) - 1):
            assert lb[i].roi >= lb[i + 1].roi

    def test_leaderboard_prompt_patch_has_highest_uplift(self, engine):
        _populate(engine)
        lb = engine.roi_leaderboard(agent_name="agent-1")
        pp = [e for e in lb if e.intervention_type == InterventionType.prompt_patch][0]
        assert pp.avg_uplift == pytest.approx(0.15, abs=0.001)
        assert pp.count == 3
        assert pp.success_rate == 1.0


class TestRecommendIntervention:
    def test_insufficient_data_returns_none(self, engine):
        # Only 2 interventions, policy requires 5
        for _ in range(2):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual",
            )
            engine.measure_uplift(rec.intervention_id, 0.2)
        result = engine.recommend_intervention(agent_name="a")
        assert result.intervention_type is None
        assert result.action == "human_review"

    def test_selects_highest_roi_type(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        # routing: high uplift, low cost -> best ROI
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.routing_change,
                source="rca", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.20)
        # prompt_patch: same uplift, higher cost -> worse ROI
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="self_learning_loop", cost_estimate=0.10,
            )
            engine.measure_uplift(rec.intervention_id, 0.20)
        result = engine.recommend_intervention(agent_name="a")
        assert result.intervention_type == InterventionType.routing_change
        assert result.action == "promote"

    def test_promote_threshold(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2, promote_threshold=0.10)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.15)
        result = engine.recommend_intervention(agent_name="a")
        assert result.action == "promote"

    def test_experiment_threshold(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2, promote_threshold=0.10)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.06)  # between 0.02 and 0.10
        result = engine.recommend_intervention(agent_name="a")
        assert result.action == "experiment"

    def test_human_review_threshold(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.01)  # below 0.02
        result = engine.recommend_intervention(agent_name="a")
        assert result.action == "human_review"

    def test_rollback_threshold(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, -0.10)
        result = engine.recommend_intervention(agent_name="a")
        assert result.action == "rollback"

    def test_agent_scoped_recommendation(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        # agent-1: prompt_patch works well
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="agent-1", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.20)
        # agent-2: routing_change works well, prompt_patch bad
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="agent-2", intervention_type=InterventionType.routing_change,
                source="rca", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, 0.20)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="agent-2", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.01,
            )
            engine.measure_uplift(rec.intervention_id, -0.10)
        r1 = engine.recommend_intervention(agent_name="agent-1")
        r2 = engine.recommend_intervention(agent_name="agent-2")
        assert r1.intervention_type == InterventionType.prompt_patch
        assert r2.intervention_type == InterventionType.routing_change

    def test_prefer_cheapest_tie_breaking(self, db_path):
        policy = MetaLearningPolicy(min_interventions_for_stats=2, prefer_cheapest=True)
        engine = MetaLearningEngine(db_path=db_path, policy=policy)
        # Both types same uplift, different cost
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.prompt_patch,
                source="manual", cost_estimate=0.10,
            )
            engine.measure_uplift(rec.intervention_id, 0.15)
        for _ in range(3):
            rec = engine.log_intervention(
                agent_name="a", intervention_type=InterventionType.routing_change,
                source="manual", cost_estimate=0.02,
            )
            engine.measure_uplift(rec.intervention_id, 0.15)
        result = engine.recommend_intervention(agent_name="a")
        # routing_change is cheaper
        assert result.intervention_type == InterventionType.routing_change
