"""
Tests for OpenClaw Adapter — Feedback Loop hooks (Inspector PRD §12).

Verifies that all 6 Feedback Loop events are correctly fired:
  memory_stored    → write / on_action / on_observation / on_agent_step
  memory_recalled  → search_memory / search
  memory_injected  → build_orchestrated_prompt
  memory_adopted   → on_agent_step(memory_id=...)
  workflow_used    → build_orchestrated_prompt (workflow_id in result)
  task_completed   → on_task_complete
"""

import pytest
from unittest.mock import MagicMock, patch, call


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

def make_backend(monkeypatch):
    """Create an OpenClawMemoryBackend with all heavy deps mocked directly."""
    from openmemo.adapters.openclaw import OpenClawMemoryBackend

    store_inst = MagicMock()
    fb_inst = MagicMock()

    backend = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
    backend.agent_id = "claw_test"
    backend.adapter_name = "openclaw"
    backend.default_scene = "test"
    backend.recall_limit = 10
    backend.conversation_id = "conv_001"
    backend._memory = store_inst
    backend._feedback = fb_inst
    backend._context_memory = None
    backend._user_memory = None
    backend._orchestrator = None
    backend.metrics = MagicMock()
    backend.metrics.recalls = 0
    backend.metrics.writes = 0
    backend.metrics.injections = 0
    backend.metrics.errors = 0
    backend.metrics.total_recall_ms = 0.0
    backend.metrics.total_write_ms = 0.0

    return backend, fb_inst, store_inst


# ── _task_id helper ───────────────────────────────────────────────────────────

class TestTaskIdHelper:
    def test_uses_conversation_id_first(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = "conv_x"
        assert b._task_id(scene="sc", session_id="si") == "conv_x"

    def test_falls_back_to_session_id(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        assert b._task_id(scene="", session_id="si") == "si"

    def test_falls_back_to_scene(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        assert b._task_id(scene="sc", session_id="") == "sc"

    def test_empty_string_when_all_empty(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        assert b._task_id() == ""


# ── _extract_ids helper ───────────────────────────────────────────────────────

class TestExtractIds:
    def test_extracts_id_field(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        ids = b._extract_ids([{"id": "m1"}, {"id": "m2"}])
        assert ids == ["m1", "m2"]

    def test_extracts_memory_id_field(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        ids = b._extract_ids([{"memory_id": "mx"}])
        assert ids == ["mx"]

    def test_skips_empty_ids(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        ids = b._extract_ids([{}, {"content": "x"}])
        assert ids == []

    def test_empty_list(self):
        from openmemo.adapters.openclaw import OpenClawMemoryBackend
        b = OpenClawMemoryBackend.__new__(OpenClawMemoryBackend)
        b.conversation_id = ""
        assert b._extract_ids([]) == []


# ── search_memory — memory_recalled event ─────────────────────────────────────

class TestSearchMemoryFeedback:
    def test_fires_recall_when_results_found(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = [{"id": "m1"}, {"id": "m2"}]
        backend.search_memory("deploy fastapi", scene="prod")
        fb.on_recall.assert_called_once()
        # memory_ids is the first positional arg to on_recall
        pos_ids = fb.on_recall.call_args[0][0]
        assert "m1" in pos_ids
        assert "m2" in pos_ids

    def test_no_feedback_when_empty_results(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = []
        backend.search_memory("nothing")
        fb.on_recall.assert_not_called()

    def test_no_feedback_when_ids_missing(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = [{"content": "x"}]
        backend.search_memory("x")
        fb.on_recall.assert_not_called()

    def test_returns_results(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = [{"id": "m1", "content": "hello"}]
        results = backend.search_memory("hello")
        assert len(results) == 1
        assert results[0]["id"] == "m1"

    def test_task_id_from_conversation_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        backend.conversation_id = "task_abc"
        store.search_memory.return_value = [{"id": "mx"}]
        backend.search_memory("q")
        kwargs = fb.on_recall.call_args[1]
        assert kwargs["task_id"] == "task_abc"


# ── search — memory_recalled event ───────────────────────────────────────────

class TestSearchFeedback:
    def test_fires_recall_with_ids(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = [{"id": "s1"}]
        backend.search("query")
        fb.on_recall.assert_called_once()

    def test_no_recall_when_empty(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        store.search_memory.return_value = []
        backend.search("q")
        fb.on_recall.assert_not_called()


# ── write — memory_stored tracking ───────────────────────────────────────────

class TestWriteFeedback:
    def test_fires_recall_with_written_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_new"):
            backend.write("deploy script", scene="ci")
        fb.on_recall.assert_called_once()
        pos_ids = fb.on_recall.call_args[0][0]
        assert "mem_new" in pos_ids

    def test_no_feedback_when_write_returns_empty(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value=""):
            backend.write("content")
        fb.on_recall.assert_not_called()

    def test_returns_memory_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_42"):
            result = backend.write("test content")
        assert result == "mem_42"


# ── on_action — memory_stored tracking ───────────────────────────────────────

class TestOnActionFeedback:
    def test_fires_recall_when_write_succeeds(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_act"):
            backend.on_action("run docker build", scene="ci")
        fb.on_recall.assert_called_once()
        pos_ids = fb.on_recall.call_args[0][0]
        assert "mem_act" in pos_ids

    def test_no_feedback_when_write_empty(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value=""):
            backend.on_action("run something")
        fb.on_recall.assert_not_called()


# ── on_observation — memory_stored tracking ───────────────────────────────────

class TestOnObservationFeedback:
    def test_fires_recall_when_write_succeeds(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_obs"):
            backend.on_observation("build succeeded", scene="ci")
        fb.on_recall.assert_called_once()

    def test_no_feedback_when_empty(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value=""):
            backend.on_observation("x")
        fb.on_recall.assert_not_called()


# ── on_agent_step — memory_adopted event ─────────────────────────────────────

class TestOnAgentStepFeedback:
    def test_fires_adopt_with_memory_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_step"), \
             patch.object(backend, "user_memory_store"):
            backend.on_agent_step("deploy done", scene="prod", memory_id="mem_prev")
        fb.on_adopt.assert_called_once()
        kwargs = fb.on_adopt.call_args[1]
        assert kwargs["memory_id"] == "mem_prev"
        assert kwargs["adopted"] is True

    def test_fires_adopt_with_written_id_fallback(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="mem_written"), \
             patch.object(backend, "user_memory_store"):
            backend.on_agent_step("step text", scene="s1")
        fb.on_adopt.assert_called_once()
        kwargs = fb.on_adopt.call_args[1]
        assert kwargs["memory_id"] == "mem_written"

    def test_no_adopt_when_no_id_and_write_empty(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        backend.conversation_id = ""
        with patch.object(backend, "write_memory", return_value=""), \
             patch.object(backend, "user_memory_store"):
            backend.on_agent_step("step", scene="")
        fb.on_adopt.assert_not_called()

    def test_still_calls_user_memory_store(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1") as wm, \
             patch.object(backend, "user_memory_store") as ums:
            backend.on_agent_step("step text")
            ums.assert_called_once_with("step text")

    def test_task_id_from_conversation_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        backend.conversation_id = "task_999"
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_agent_step("step", memory_id="m_prev")
        # task_id is the first positional arg to on_adopt
        task_id_arg = fb.on_adopt.call_args[0][0]
        assert task_id_arg == "task_999"


# ── build_orchestrated_prompt — memory_injected + workflow_used events ────────

class TestBuildOrchestratedPromptFeedback:
    def test_fires_inject_for_each_memory(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        orch_result = {
            "context": "enriched prompt",
            "memories_used": [{"id": "m1"}, {"id": "m2"}],
        }
        with patch.object(backend, "orchestrate_prompt", return_value=orch_result):
            result = backend.build_orchestrated_prompt("deploy fastapi")
        assert fb.on_inject.call_count == 2
        # memory_id is the first positional arg to on_inject
        ids_injected = [c[0][0] for c in fb.on_inject.call_args_list]
        assert "m1" in ids_injected
        assert "m2" in ids_injected

    def test_fires_adopt_for_workflow(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        orch_result = {
            "context": "prompt",
            "memories_used": [],
            "workflow_id": "wf_deploy_001",
        }
        with patch.object(backend, "orchestrate_prompt", return_value=orch_result):
            backend.build_orchestrated_prompt("task")
        fb.on_adopt.assert_called_once()
        kwargs = fb.on_adopt.call_args[1]
        assert kwargs["memory_id"] == "wf_deploy_001"
        assert kwargs["adopted"] is True

    def test_no_inject_when_no_memories(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "orchestrate_prompt", return_value={"context": "p"}):
            backend.build_orchestrated_prompt("task")
        fb.on_inject.assert_not_called()

    def test_no_workflow_adopt_when_no_workflow_id(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "orchestrate_prompt", return_value={"context": "p", "memories_used": []}):
            backend.build_orchestrated_prompt("task")
        fb.on_adopt.assert_not_called()

    def test_returns_orchestrate_result(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        expected = {"context": "my context", "task_type": "deployment"}
        with patch.object(backend, "orchestrate_prompt", return_value=expected):
            result = backend.build_orchestrated_prompt("task")
        assert result == expected

    def test_skips_empty_memory_ids(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        orch_result = {"context": "p", "memories_used": [{"content": "x"}]}
        with patch.object(backend, "orchestrate_prompt", return_value=orch_result):
            backend.build_orchestrated_prompt("task")
        fb.on_inject.assert_not_called()

    def test_handles_string_memory_ids(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        orch_result = {"context": "p", "memories_used": ["mem_str_1", "mem_str_2"]}
        with patch.object(backend, "orchestrate_prompt", return_value=orch_result):
            backend.build_orchestrated_prompt("task")
        assert fb.on_inject.call_count == 2


# ── on_task_complete — task_completed event ───────────────────────────────────

class TestOnTaskCompleteFeedback:
    def test_fires_outcome_on_success(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("deploy", "deployment done", success=True)
        fb.on_outcome.assert_called_once()
        # task_id is the first positional arg, success is a keyword arg
        task_id_arg = fb.on_outcome.call_args[0][0]
        success_arg = fb.on_outcome.call_args[1]["success"]
        assert success_arg is True
        assert task_id_arg == "conv_001"

    def test_fires_outcome_on_failure(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("deploy", "deployment failed", success=False)
        fb.on_outcome.assert_called_once()
        kwargs = fb.on_outcome.call_args[1]
        assert kwargs["success"] is False

    def test_infers_success_from_result_no_error_keywords(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("task", "completed successfully")
        kwargs = fb.on_outcome.call_args[1]
        assert kwargs["success"] is True

    def test_infers_failure_from_error_keyword(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("task", "deployment error occurred")
        kwargs = fb.on_outcome.call_args[1]
        assert kwargs["success"] is False

    def test_infers_failure_from_fail_keyword(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("task", "build failed")
        kwargs = fb.on_outcome.call_args[1]
        assert kwargs["success"] is False

    def test_infers_failure_from_timeout_keyword(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("task", "request timeout")
        kwargs = fb.on_outcome.call_args[1]
        assert kwargs["success"] is False

    def test_result_included_in_outcome(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store"):
            backend.on_task_complete("task", "done successfully")
        task_id_arg = fb.on_outcome.call_args[0][0]
        success_arg = fb.on_outcome.call_args[1]["success"]
        assert task_id_arg == "conv_001"
        assert success_arg is True

    def test_still_calls_user_memory_store(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        with patch.object(backend, "write_memory", return_value="m1"), \
             patch.object(backend, "user_memory_store") as ums:
            backend.on_task_complete("task", "done")
            ums.assert_called_once()


# ── No-feedback paths (graceful degradation) ─────────────────────────────────

class TestFeedbackNullSafety:
    def test_search_no_crash_when_feedback_none(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        backend._feedback = None
        store.search_memory.return_value = [{"id": "m1"}]
        # Should not raise even without _feedback
        result = backend.search_memory("q")
        assert isinstance(result, list)

    def test_write_no_crash_when_feedback_none(self, monkeypatch):
        backend, fb, store = make_backend(monkeypatch)
        backend._feedback = None
        with patch.object(backend, "write_memory", return_value="mid"):
            result = backend.write("content")
        assert result == "mid"
