"""Integration tests — full task lifecycle, plugin loading, event flow."""

from __future__ import annotations

from typing import Any

import pytest

from agent_scout.agent import Agent
from agent_scout.db.models import AutonomyLevel, Task, TaskStatus
from agent_scout.events import Event, EventBus, EventType
from agent_scout.llm.budget import BudgetConfig, BudgetEngine
from agent_scout.llm.estimator import CostEstimator
from agent_scout.llm.router import ModelRouter, RouterConfig
from agent_scout.plugins.base import Plugin, ToolSpec
from agent_scout.plugins.registry import PluginRegistry
from agent_scout.react_loop import Action, ActionType, Step

# --- Helpers ---


class MockLLM:
    """LLM that returns a FINISH action after N steps."""

    def __init__(self, steps_before_finish: int = 0):
        self._call_count = 0
        self._steps_before_finish = steps_before_finish

    async def think(
        self,
        task: str,
        steps: list[Step],
        available_tools: list[dict[str, Any]],
    ) -> tuple[str, Action]:
        self._call_count += 1
        if self._call_count > self._steps_before_finish:
            return "I have the answer", Action(
                action_type=ActionType.FINISH,
                message="Task completed",
                result={"answer": "Done"},
            )
        return "Let me search", Action(
            action_type=ActionType.TOOL_CALL,
            tool_name="test_tool",
            tool_input={"query": "test"},
        )

    async def reflect(
        self,
        task: str,
        steps: list[Step],
        latest_observation: str,
    ) -> str:
        return "Reflection: looks good"


class MockToolExecutor:
    async def execute(
        self, tool_name: str, tool_input: dict[str, Any], task_id: str | None = None
    ) -> str:
        return f"Result from {tool_name}: {tool_input}"

    def get_available_tools(self) -> list[dict[str, Any]]:
        return [{"name": "test_tool", "description": "A test tool"}]


# --- Tests ---


@pytest.mark.asyncio
async def test_full_task_lifecycle():
    """Test creating and running a task through the agent."""
    bus = EventBus()
    events: list[Event] = []

    async def collect(event):
        events.append(event)

    bus.subscribe(collect)

    agent = Agent(
        llm=MockLLM(),
        tools=MockToolExecutor(),
        event_bus=bus,
        autonomy=AutonomyLevel.FULL,
    )

    task = Task(description="Find best deals")
    result = await agent.run_task(task)

    assert result.status == TaskStatus.COMPLETED
    assert result.result is not None

    event_types = [e.event_type for e in events]
    assert EventType.TASK_STATE_CHANGE in event_types
    assert EventType.AGENT_STARTED in event_types
    assert EventType.TASK_COMPLETED in event_types
    assert EventType.AGENT_STOPPED in event_types


@pytest.mark.asyncio
async def test_task_with_tool_calls():
    """Test a task that uses tools before finishing."""
    bus = EventBus()
    agent = Agent(
        llm=MockLLM(steps_before_finish=2),
        tools=MockToolExecutor(),
        event_bus=bus,
    )

    task = Task(description="Research laptops")
    result = await agent.run_task(task)

    assert result.status == TaskStatus.COMPLETED


@pytest.mark.asyncio
async def test_task_failure_handling():
    """Test that exceptions in the LLM are handled gracefully."""
    bus = EventBus()

    class FailingLLM:
        async def think(self, task, steps, available_tools):
            raise RuntimeError("LLM API unavailable")

        async def reflect(self, task, steps, latest_observation):
            return ""

    agent = Agent(
        llm=FailingLLM(),
        tools=MockToolExecutor(),
        event_bus=bus,
    )

    task = Task(description="This will fail")
    result = await agent.run_task(task)

    assert result.status == TaskStatus.FAILED
    assert "LLM API unavailable" in result.error


@pytest.mark.asyncio
async def test_plugin_registry_lifecycle():
    """Test loading, enabling, executing, and unloading plugins."""

    class TestPlugin(Plugin):
        @property
        def name(self):
            return "test_plugin"

        @property
        def description(self):
            return "A test plugin"

        def get_tools(self):
            return [ToolSpec(name="test_tool", description="Test")]

        async def execute(self, tool_name, tool_input):
            return "test result"

    registry = PluginRegistry()
    plugin = TestPlugin()

    # Register (sync)
    registry.register(plugin)
    assert plugin.name in registry.plugins

    # Load and enable
    await plugin.load()
    await plugin.enable()
    assert plugin.enabled

    tools = registry.get_available_tools()
    assert any(t["name"] == "test_tool" for t in tools)

    result = await registry.execute("test_tool", {})
    assert result == "test result"

    await plugin.disable()
    assert not plugin.enabled

    registry.unregister(plugin.name)
    assert plugin.name not in registry.plugins


@pytest.mark.asyncio
async def test_database_task_lifecycle(db):
    """Test full database task CRUD cycle."""
    task = Task(description="DB test task")

    created = await db.create_task(task)
    assert created.id == task.id

    fetched = await db.get_task(task.id)
    assert fetched is not None
    assert fetched.description == "DB test task"

    await db.update_task_status(task.id, TaskStatus.COMPLETED, result={"done": True})
    updated = await db.get_task(task.id)
    assert updated.status == TaskStatus.COMPLETED
    assert updated.result == {"done": True}

    tasks = await db.list_tasks()
    assert len(tasks) >= 1


@pytest.mark.asyncio
async def test_event_bus_pub_sub():
    """Test EventBus pub/sub with filtering."""
    bus = EventBus()
    all_events: list[Event] = []
    thought_events: list[Event] = []

    async def all_handler(event):
        all_events.append(event)

    async def thought_handler(event):
        thought_events.append(event)

    bus.subscribe(all_handler)
    bus.subscribe(thought_handler, event_type=EventType.THOUGHT)

    await bus.emit(Event(event_type=EventType.THOUGHT, data={"content": "thinking"}))
    await bus.emit(Event(event_type=EventType.ACTION, data={"tool": "search"}))

    assert len(all_events) == 2
    assert len(thought_events) == 1
    assert thought_events[0].event_type == EventType.THOUGHT


@pytest.mark.asyncio
async def test_budget_enforcement_in_agent():
    """Test that budget engine integrates with event bus."""
    bus = EventBus()
    budget_events: list[Event] = []

    async def budget_handler(event):
        budget_events.append(event)

    bus.subscribe(budget_handler, event_type=EventType.BUDGET_WARNING)
    bus.subscribe(budget_handler, event_type=EventType.BUDGET_EXCEEDED)

    config = BudgetConfig(task_limit=0.01)
    engine = BudgetEngine(config=config, event_bus=bus)

    engine.record_cost("test-model", 1000, 500, 0.008, "task-1")
    await engine.check_budget("task-1")
    assert len(budget_events) == 1  # 80% warning


@pytest.mark.asyncio
async def test_model_router_integration():
    """Test router picks appropriate model for different tasks."""
    config = RouterConfig(
        fast_model="fast/model",
        balanced_model="balanced/model",
        best_model="best/model",
    )
    router = ModelRouter(config=config)

    # Complex reasoning with many steps → best model
    complex_task = router.route(
        "analyze and compare multiple options",
        step_count=10,
        tool_count=5,
    )
    assert complex_task == "best/model"


@pytest.mark.asyncio
async def test_cost_estimator_integration():
    """Test cost estimator produces reasonable estimates."""
    estimator = CostEstimator()

    estimate = estimator.estimate(
        model="anthropic/claude-sonnet-4-20250514",
        input_tokens=1000,
        expected_output_tokens=500,
    )
    assert estimate.estimated_cost_usd > 0
    assert estimate.estimated_input_tokens == 1000

    comparison = estimator.compare(estimate, 1000, 500)
    assert comparison.actual_cost > 0


@pytest.mark.asyncio
async def test_database_price_history(db):
    """Test price history storage and retrieval."""
    from agent_scout.db.models import PriceSnapshot

    for i, price in enumerate([100.0, 95.0, 90.0]):
        snap = PriceSnapshot(
            item_name="Laptop X",
            source=f"Store{i}",
            price=price,
            currency="USD",
        )
        await db.store_price(snap)

    history = await db.get_price_history("Laptop X")
    assert len(history) == 3
    # Most recent first
    assert history[0].price == 90.0


@pytest.mark.asyncio
async def test_database_memory_lifecycle(db):
    """Test memory store, search, touch, and decay."""
    from agent_scout.db.models import Memory

    mem = Memory(
        content="User prefers cheap flights",
        memory_type="preference",
        tags=["flights", "budget"],
        relevance_score=1.0,
    )
    await db.store_memory(mem)

    results = await db.search_memories(memory_type="preference")
    assert len(results) == 1
    assert results[0].content == "User prefers cheap flights"

    tag_results = await db.search_memories(tags=["flights"])
    assert len(tag_results) == 1

    await db.touch_memory(mem.id)
    updated = await db.search_memories(memory_type="preference")
    assert updated[0].access_count == 1

    await db.decay_memories(0.5)
    decayed = await db.search_memories(memory_type="preference")
    assert decayed[0].relevance_score < 1.0
