"""Tests for the Operators subsystem."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, List, Optional
from unittest.mock import MagicMock, patch

import pytest

from openjarvis.operators.types import OperatorManifest

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


class FakeEngine:
    """Minimal engine stub for tests."""

    def __init__(self, responses: Optional[List[Dict[str, Any]]] = None) -> None:
        self._responses = list(responses or [{"content": "Done."}])
        self._call_idx = 0
        self.calls: List[Any] = []

    def generate(self, messages, **kwargs):
        self.calls.append({"messages": messages, **kwargs})
        if self._call_idx < len(self._responses):
            resp = self._responses[self._call_idx]
            self._call_idx += 1
            return resp
        return {"content": "Fallback."}

    def list_models(self):
        return ["test-model"]

    def health(self):
        return True


class FakeSessionStore:
    """Minimal session store stub."""

    def __init__(self) -> None:
        self._sessions: Dict[str, Any] = {}
        self._messages: Dict[str, List[Dict]] = {}

    def get_or_create(self, session_id: str):
        if session_id not in self._sessions:
            self._sessions[session_id] = MagicMock(messages=[])
        return self._sessions[session_id]

    def save_message(self, session_id: str, message: Dict) -> None:
        self._messages.setdefault(session_id, []).append(message)


class FakeMemoryBackend:
    """Minimal memory backend stub."""

    def __init__(self) -> None:
        self._store: Dict[str, str] = {}

    def store(self, key: str, value: str, **kwargs) -> None:
        self._store[key] = value

    def retrieve(self, key: str, **kwargs) -> str:
        return self._store.get(key, "")

    def search(self, query: str, **kwargs):
        return []


class FakeSchedulerStore:
    """Minimal scheduler store stub."""

    def __init__(self) -> None:
        self._tasks: Dict[str, Dict] = {}
        self._runs: List[Dict] = []

    def save_task(self, task_dict: Dict) -> None:
        self._tasks[task_dict["id"]] = task_dict

    def get_task(self, task_id: str) -> Optional[Dict]:
        return self._tasks.get(task_id)

    def update_task(self, task_dict: Dict) -> None:
        self._tasks[task_dict["id"]] = task_dict

    def list_tasks(self, *, status=None) -> List[Dict]:
        tasks = list(self._tasks.values())
        if status:
            tasks = [t for t in tasks if t.get("status") == status]
        return tasks

    def get_due_tasks(self, now: str) -> List[Dict]:
        return []

    def log_run(self, **kwargs) -> None:
        self._runs.append(kwargs)

    def get_runs(self, task_id: str, limit: int = 10) -> List[Dict]:
        return [r for r in self._runs if r.get("task_id") == task_id][:limit]


def _make_system(
    engine=None,
    scheduler=None,
    session_store=None,
    memory_backend=None,
):
    """Build a minimal mock JarvisSystem."""
    from openjarvis.core.config import JarvisConfig
    from openjarvis.core.events import EventBus

    system = MagicMock()
    system.config = JarvisConfig()
    system.bus = EventBus()
    system.engine = engine or FakeEngine()
    system.engine_key = "test"
    system.model = "test-model"
    system.tools = []
    system.scheduler = scheduler
    system.session_store = session_store
    system.memory_backend = memory_backend
    system.operator_manager = None
    return system


# ---------------------------------------------------------------------------
# TestOperatorManifest
# ---------------------------------------------------------------------------


class TestOperatorManifest:
    def test_fields(self):
        m = OperatorManifest(
            id="test",
            name="Test Op",
            version="2.0.0",
            description="A test",
            tools=["think"],
            schedule_type="cron",
            schedule_value="0 9 * * *",
        )
        assert m.id == "test"
        assert m.name == "Test Op"
        assert m.version == "2.0.0"
        assert m.tools == ["think"]
        assert m.schedule_type == "cron"
        assert m.schedule_value == "0 9 * * *"

    def test_defaults(self):
        m = OperatorManifest(id="x", name="X")
        assert m.version == "0.1.0"
        assert m.temperature == 0.3
        assert m.max_turns == 20
        assert m.schedule_type == "interval"
        assert m.schedule_value == "300"
        assert m.tools == []
        assert m.metrics == []
        assert m.settings == {}
        assert m.metadata == {}


# ---------------------------------------------------------------------------
# TestOperatorLoader
# ---------------------------------------------------------------------------


class TestOperatorLoader:
    def test_load_from_toml(self, tmp_path):
        from openjarvis.operators.loader import load_operator

        toml_content = """\
[operator]
id = "my_op"
name = "My Operator"
version = "1.2.0"
description = "Test operator"
author = "test"

[operator.schedule]
type = "interval"
value = "600"

[operator.agent]
tools = ["think", "web_search"]
max_turns = 15
temperature = 0.5
system_prompt = "You are a test operator."
"""
        p = tmp_path / "my_op.toml"
        p.write_text(toml_content)

        m = load_operator(p)
        assert m.id == "my_op"
        assert m.name == "My Operator"
        assert m.version == "1.2.0"
        assert m.tools == ["think", "web_search"]
        assert m.schedule_type == "interval"
        assert m.schedule_value == "600"
        assert m.temperature == 0.5
        assert m.max_turns == 15
        assert m.system_prompt == "You are a test operator."

    def test_inline_prompt(self, tmp_path):
        from openjarvis.operators.loader import load_operator

        toml_content = """\
[operator]
id = "inline"
name = "Inline"

[operator.agent]
system_prompt = "Do things."
"""
        p = tmp_path / "inline.toml"
        p.write_text(toml_content)

        m = load_operator(p)
        assert m.system_prompt == "Do things."

    def test_external_prompt_file(self, tmp_path):
        from openjarvis.operators.loader import load_operator

        prompt_file = tmp_path / "prompt.md"
        prompt_file.write_text("External prompt content.")

        toml_content = """\
[operator]
id = "external"
name = "External"

[operator.agent]
system_prompt_path = "prompt.md"
"""
        p = tmp_path / "external.toml"
        p.write_text(toml_content)

        m = load_operator(p)
        assert m.system_prompt == "External prompt content."

    def test_missing_file_returns_empty_prompt(self, tmp_path):
        from openjarvis.operators.loader import load_operator

        toml_content = """\
[operator]
id = "missing"
name = "Missing"

[operator.agent]
system_prompt_path = "nonexistent.md"
"""
        p = tmp_path / "missing.toml"
        p.write_text(toml_content)

        m = load_operator(p)
        assert m.system_prompt == ""

    def test_stem_as_default_id(self, tmp_path):
        from openjarvis.operators.loader import load_operator

        toml_content = """\
[operator]
name = "NoID"
"""
        p = tmp_path / "my_operator.toml"
        p.write_text(toml_content)

        m = load_operator(p)
        assert m.id == "my_operator"


# ---------------------------------------------------------------------------
# TestOperatorManager
# ---------------------------------------------------------------------------


class TestOperatorManager:
    def test_register(self):
        from openjarvis.operators.manager import OperatorManager

        system = _make_system()
        mgr = OperatorManager(system)

        m = OperatorManifest(id="test", name="Test")
        mgr.register(m)
        assert mgr.get_manifest("test") is m

    def test_discover(self, tmp_path):
        from openjarvis.operators.manager import OperatorManager

        toml_content = """\
[operator]
id = "discovered"
name = "Discovered"
"""
        (tmp_path / "discovered.toml").write_text(toml_content)

        system = _make_system()
        mgr = OperatorManager(system)
        found = mgr.discover(tmp_path)
        assert len(found) == 1
        assert found[0].id == "discovered"
        assert mgr.get_manifest("discovered") is not None

    def test_activate_creates_scheduler_task(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(
            id="test_op",
            name="Test",
            tools=["think"],
            schedule_type="interval",
            schedule_value="60",
        )
        mgr.register(m)
        task_id = mgr.activate("test_op")

        assert task_id == "operator:test_op"
        # Check the task was persisted with deterministic ID
        task_dict = store.get_task("operator:test_op")
        assert task_dict is not None
        assert task_dict["agent"] == "operative"

    def test_activate_uses_operative_agent(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(id="ag_test", name="Agent Test")
        mgr.register(m)
        mgr.activate("ag_test")

        task_dict = store.get_task("operator:ag_test")
        assert task_dict["agent"] == "operative"

    def test_activate_passes_metadata(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(
            id="meta_test",
            name="Meta",
            system_prompt="Do stuff",
            temperature=0.5,
        )
        mgr.register(m)
        mgr.activate("meta_test")

        task_dict = store.get_task("operator:meta_test")
        meta = task_dict.get("metadata", {})
        assert meta["operator_id"] == "meta_test"
        assert meta["system_prompt"] == "Do stuff"
        assert meta["temperature"] == 0.5

    def test_deactivate(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(id="deact", name="Deact")
        mgr.register(m)
        mgr.activate("deact")
        mgr.deactivate("deact")

        task_dict = store.get_task("operator:deact")
        assert task_dict["status"] == "cancelled"

    def test_pause_resume(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(id="pr_test", name="PR")
        mgr.register(m)
        mgr.activate("pr_test")

        mgr.pause("pr_test")
        task_dict = store.get_task("operator:pr_test")
        assert task_dict["status"] == "paused"

        mgr.resume("pr_test")
        task_dict = store.get_task("operator:pr_test")
        assert task_dict["status"] == "active"

    def test_status(self):
        from openjarvis.operators.manager import OperatorManager

        system = _make_system()
        mgr = OperatorManager(system)

        m = OperatorManifest(id="s1", name="S1", description="Status test")
        mgr.register(m)

        statuses = mgr.status()
        assert len(statuses) == 1
        assert statuses[0]["id"] == "s1"
        assert statuses[0]["name"] == "S1"
        assert statuses[0]["status"] == "registered"

    def test_activate_idempotent(self):
        from openjarvis.operators.manager import OperatorManager
        from openjarvis.scheduler.scheduler import TaskScheduler

        store = FakeSchedulerStore()
        scheduler = TaskScheduler(store)
        system = _make_system(scheduler=scheduler)
        mgr = OperatorManager(system)

        m = OperatorManifest(id="idem", name="Idem")
        mgr.register(m)
        id1 = mgr.activate("idem")
        id2 = mgr.activate("idem")
        assert id1 == id2

    def test_activate_without_scheduler_raises(self):
        from openjarvis.operators.manager import OperatorManager

        system = _make_system(scheduler=None)
        mgr = OperatorManager(system)

        m = OperatorManifest(id="no_sched", name="No Sched")
        mgr.register(m)

        with pytest.raises(RuntimeError, match="TaskScheduler not available"):
            mgr.activate("no_sched")

    def test_run_once(self):
        from openjarvis.operators.manager import OperatorManager

        system = _make_system()
        system.ask = MagicMock(return_value={"content": "Tick done."})

        mgr = OperatorManager(system)
        m = OperatorManifest(
            id="run_test",
            name="Run",
            system_prompt="Test prompt",
            tools=["think"],
        )
        mgr.register(m)

        result = mgr.run_once("run_test")
        assert result == "Tick done."

        # Verify ask was called with correct params
        system.ask.assert_called_once()
        call_kwargs = system.ask.call_args
        assert call_kwargs.kwargs["agent"] == "operative"
        assert call_kwargs.kwargs["system_prompt"] == "Test prompt"
        assert call_kwargs.kwargs["operator_id"] == "run_test"


# ---------------------------------------------------------------------------
# TestOperativeAgent
# ---------------------------------------------------------------------------


class TestOperativeAgent:
    def test_init_defaults(self):
        from openjarvis.agents.operative import OperativeAgent

        engine = FakeEngine()
        with patch(
            "openjarvis.agents._stubs.load_config",
            side_effect=Exception("no config"),
        ):
            agent = OperativeAgent(engine, "test-model")
        assert agent.agent_id == "operative"
        assert agent._temperature == 0.3
        assert agent._max_tokens == 2048
        assert agent._max_turns == 20

    def test_accepts_tools(self):
        from openjarvis.agents.operative import OperativeAgent

        assert OperativeAgent.accepts_tools is True

    def test_run_with_system_prompt(self):
        from openjarvis.agents.operative import OperativeAgent

        engine = FakeEngine([{"content": "Response with prompt."}])
        agent = OperativeAgent(
            engine,
            "test-model",
            system_prompt="You are a test operator.",
        )
        result = agent.run("Execute tick")

        assert result.content == "Response with prompt."
        # Check system prompt was in messages
        call = engine.calls[0]
        messages = call["messages"]
        assert any(
            m.role.value == "system" and "test operator" in m.content for m in messages
        )

    def test_run_loads_session(self):
        from openjarvis.agents.operative import OperativeAgent

        session_store = FakeSessionStore()
        # Pre-populate session with history
        session = session_store.get_or_create("operator:test_op")
        session.messages = [
            {"role": "user", "content": "Previous tick"},
            {"role": "assistant", "content": "Previous response"},
        ]

        engine = FakeEngine([{"content": "New tick done."}])
        agent = OperativeAgent(
            engine,
            "test-model",
            operator_id="test_op",
            session_store=session_store,
        )
        result = agent.run("Execute tick")
        assert result.content == "New tick done."

    def test_run_saves_session(self):
        from openjarvis.agents.operative import OperativeAgent

        session_store = FakeSessionStore()

        engine = FakeEngine([{"content": "Tick response."}])
        agent = OperativeAgent(
            engine,
            "test-model",
            operator_id="save_test",
            session_store=session_store,
        )
        agent.run("Execute tick")

        saved = session_store._messages.get("operator:save_test", [])
        assert len(saved) == 2
        assert saved[0]["role"] == "user"
        assert saved[1]["role"] == "assistant"
        assert saved[1]["content"] == "Tick response."

    def test_run_recalls_state(self):
        from openjarvis.agents.operative import OperativeAgent

        memory = FakeMemoryBackend()
        memory.store("operator:recall_test:state", '{"last_run": "2024-01-01"}')

        engine = FakeEngine([{"content": "State recalled."}])
        agent = OperativeAgent(
            engine,
            "test-model",
            operator_id="recall_test",
            memory_backend=memory,
        )
        result = agent.run("Execute tick")
        assert result.content == "State recalled."

        # Verify state was injected into system prompt
        call = engine.calls[0]
        messages = call["messages"]
        sys_msgs = [m for m in messages if m.role.value == "system"]
        assert any("Previous State" in m.content for m in sys_msgs)

    def test_run_tool_loop(self):
        from openjarvis.agents.operative import OperativeAgent
        from openjarvis.tools._stubs import BaseTool

        # Mock tool
        tool = MagicMock(spec=BaseTool)
        tool.spec = MagicMock()
        tool.spec.name = "think"
        tool.spec.description = "Think about something"
        tool.spec.parameters = {}
        tool.spec.timeout_seconds = 30
        tool.spec.required_capabilities = []
        tool.spec.taint_labels = []
        tool.run = MagicMock(return_value="Thought result")

        engine = FakeEngine(
            [
                {
                    "content": "",
                    "tool_calls": [
                        {
                            "id": "call_1",
                            "name": "think",
                            "arguments": '{"thought": "test"}',
                        },
                    ],
                },
                {"content": "Final answer after tool use."},
            ]
        )

        agent = OperativeAgent(
            engine,
            "test-model",
            tools=[tool],
        )
        result = agent.run("Do something")
        assert result.content == "Final answer after tool use."
        assert result.turns == 2

    def test_run_without_persistence(self):
        from openjarvis.agents.operative import OperativeAgent

        engine = FakeEngine([{"content": "No persistence needed."}])
        agent = OperativeAgent(engine, "test-model")
        result = agent.run("Simple query")
        assert result.content == "No persistence needed."

    def test_run_auto_persists_state(self):
        from openjarvis.agents.operative import OperativeAgent

        memory = FakeMemoryBackend()

        engine = FakeEngine([{"content": "Tick complete."}])
        agent = OperativeAgent(
            engine,
            "test-model",
            operator_id="persist_test",
            memory_backend=memory,
        )
        agent.run("Execute tick")

        # State should be auto-persisted
        state = memory.retrieve("operator:persist_test:state")
        assert "Tick complete." in state

    def test_max_turns_exceeded(self):
        from openjarvis.agents.operative import OperativeAgent

        # Engine always returns tool calls, never a final answer
        responses = [
            {
                "content": "",
                "tool_calls": [
                    {
                        "id": f"call_{i}",
                        "name": "think",
                        "arguments": '{"thought": "loop"}',
                    },
                ],
            }
            for i in range(25)
        ]
        engine = FakeEngine(responses)

        tool = MagicMock()
        tool.spec = MagicMock()
        tool.spec.name = "think"
        tool.spec.description = "Think"
        tool.spec.parameters = {}
        tool.spec.timeout_seconds = 30
        tool.spec.required_capabilities = []
        tool.spec.taint_labels = []
        tool.run = MagicMock(return_value="thought")

        agent = OperativeAgent(
            engine,
            "test-model",
            tools=[tool],
            max_turns=3,
        )
        result = agent.run("Loop forever")
        assert result.turns == 3
        assert result.metadata.get("max_turns_exceeded") is True


# ---------------------------------------------------------------------------
# TestSystemAskPassthrough
# ---------------------------------------------------------------------------


class TestSystemAskPassthrough:
    def test_system_prompt_forwarded(self):
        """system_prompt kwarg reaches the agent."""
        from openjarvis.system import JarvisSystem

        engine = FakeEngine([{"content": "OK"}])
        system = _make_system(engine=engine)

        # Patch _run_agent to capture kwargs
        captured = {}

        def patched_run_agent(
            self,
            query,
            messages,
            agent_name,
            tool_names,
            temperature,
            max_tokens,
            **kwargs,
        ):
            captured.update(kwargs)
            return {"content": "OK"}

        from openjarvis.system import QueryOrchestrator

        with patch.object(QueryOrchestrator, "_run_agent", patched_run_agent):
            real_system = JarvisSystem(
                config=system.config,
                bus=system.bus,
                engine=engine,
                engine_key="test",
                model="test-model",
                agent_name="operative",
            )
            real_system.ask("test", system_prompt="Custom prompt")

        assert captured.get("system_prompt") == "Custom prompt"

    def test_operator_id_forwarded(self):
        """operator_id kwarg reaches the agent."""
        from openjarvis.system import JarvisSystem

        engine = FakeEngine([{"content": "OK"}])
        system = _make_system(engine=engine)

        captured = {}

        def patched_run_agent(
            self,
            query,
            messages,
            agent_name,
            tool_names,
            temperature,
            max_tokens,
            **kwargs,
        ):
            captured.update(kwargs)
            return {"content": "OK"}

        from openjarvis.system import QueryOrchestrator

        with patch.object(QueryOrchestrator, "_run_agent", patched_run_agent):
            real_system = JarvisSystem(
                config=system.config,
                bus=system.bus,
                engine=engine,
                engine_key="test",
                model="test-model",
                agent_name="operative",
            )
            real_system.ask("test", operator_id="my_op")

        assert captured.get("operator_id") == "my_op"


# ---------------------------------------------------------------------------
# TestSchedulerOperatorExecution
# ---------------------------------------------------------------------------


class TestSchedulerOperatorExecution:
    def test_execute_task_with_operator_metadata(self):
        """Scheduler passes operator metadata through to system.ask()."""
        from openjarvis.scheduler.scheduler import ScheduledTask, TaskScheduler

        store = FakeSchedulerStore()
        mock_system = MagicMock()
        mock_system.ask = MagicMock(return_value="Tick result")

        scheduler = TaskScheduler(store, system=mock_system)

        task = ScheduledTask(
            id="operator:test_op",
            prompt="[OPERATOR TICK] Execute.",
            schedule_type="interval",
            schedule_value="300",
            agent="operative",
            tools="think,web_search",
            metadata={
                "operator_id": "test_op",
                "system_prompt": "You are a test operator.",
                "temperature": 0.3,
            },
        )
        store.save_task(task.to_dict())

        scheduler._execute_task(task)

        # Verify system.ask was called
        mock_system.ask.assert_called_once()
        call_kwargs = mock_system.ask.call_args
        # The scheduler passes agent and tools
        assert (
            call_kwargs.kwargs.get("agent") == "operative"
            or call_kwargs[1].get("agent") == "operative"
        )


# ---------------------------------------------------------------------------
# TestConfig
# ---------------------------------------------------------------------------


class TestOperatorsConfig:
    def test_config_defaults(self):
        from openjarvis.core.config import OperatorsConfig

        cfg = OperatorsConfig()
        assert cfg.enabled is False
        assert "operators" in cfg.manifests_dir
        assert cfg.auto_activate == ""

    def test_config_in_jarvis_config(self):
        from openjarvis.core.config import JarvisConfig

        cfg = JarvisConfig()
        assert hasattr(cfg, "operators")
        assert cfg.operators.enabled is False


# ---------------------------------------------------------------------------
# TestEvents
# ---------------------------------------------------------------------------


class TestOperatorEvents:
    def test_event_types_exist(self):
        from openjarvis.core.events import EventType

        assert hasattr(EventType, "OPERATOR_TICK_START")
        assert hasattr(EventType, "OPERATOR_TICK_END")
        assert EventType.OPERATOR_TICK_START.value == "operator_tick_start"
        assert EventType.OPERATOR_TICK_END.value == "operator_tick_end"


# ---------------------------------------------------------------------------
# TestAgentRegistration
# ---------------------------------------------------------------------------


class TestAgentRegistration:
    def test_operative_registered(self):
        from openjarvis.core.registry import AgentRegistry

        # Re-register if cleared by another test
        if not AgentRegistry.contains("operative"):
            from openjarvis.agents.operative import OperativeAgent

            AgentRegistry.register_value("operative", OperativeAgent)

        assert AgentRegistry.contains("operative")
        cls = AgentRegistry.get("operative")
        assert cls.__name__ == "OperativeAgent"


# ---------------------------------------------------------------------------
# TestLoadBundledOperators
# ---------------------------------------------------------------------------


class TestLoadBundledOperators:
    """Test that the bundled operator TOML files parse correctly."""

    @pytest.fixture
    def operators_dir(self):
        # Find the package-level operators/data/ directory
        here = Path(__file__).resolve()
        project_root = here.parent.parent.parent
        ops_dir = project_root / "src" / "openjarvis" / "operators" / "data"
        if not ops_dir.is_dir():
            pytest.skip("operators/data/ directory not found")
        return ops_dir

    def test_researcher_loads(self, operators_dir):
        from openjarvis.operators.loader import load_operator

        m = load_operator(operators_dir / "researcher.toml")
        assert m.id == "researcher"
        assert "web_search" in m.tools
        assert m.schedule_type == "interval"

    def test_news_digest_loads(self, operators_dir):
        from openjarvis.operators.loader import load_operator

        m = load_operator(operators_dir / "news_digest.toml")
        assert m.id == "news_digest"
        assert m.schedule_type == "cron"

    def test_knowledge_curator_loads(self, operators_dir):
        from openjarvis.operators.loader import load_operator

        m = load_operator(operators_dir / "knowledge_curator.toml")
        assert m.id == "knowledge_curator"
        assert "knowledge_add_entity" in m.tools

    def test_system_monitor_loads(self, operators_dir):
        from openjarvis.operators.loader import load_operator

        m = load_operator(operators_dir / "system_monitor.toml")
        assert m.id == "system_monitor"
        assert m.schedule_value == "300"
