"""Tests for execution models and executor."""

import pytest
from uuid import uuid4

from xyglue.execute.models import ExecutionResult, ExecutionContext, ExecutionError, StepTrace
from xyglue.execute.executor import Executor
from xyglue.plan.models import Plan, Step
from xyglue.tools.base import MockTool, ToolResult


class TestExecutionResult:
    def test_success_result(self):
        r = ExecutionResult(step_number=1, success=True, output="data")
        assert r.success is True
        assert r.failed is False

    def test_failed_result(self):
        r = ExecutionResult(step_number=1, success=False, error="timeout")
        assert r.failed is True

    def test_to_dict(self):
        r = ExecutionResult(step_number=1, success=True, tool_used="http")
        d = r.to_dict()
        assert d["step_number"] == 1
        assert d["success"] is True

    def test_from_dict(self):
        r = ExecutionResult(step_number=2, success=False, error="oops", tool_used="http")
        d = r.to_dict()
        r2 = ExecutionResult.from_dict(d)
        assert r2.step_number == 2
        assert r2.error == "oops"


class TestExecutionContext:
    def test_default_context(self):
        ctx = ExecutionContext()
        assert ctx.step_number == 0
        assert ctx.budget_limit_usd == 1.0

    def test_budget_remaining(self):
        ctx = ExecutionContext(budget_used_usd=0.3, budget_limit_usd=1.0)
        assert ctx.budget_remaining == pytest.approx(0.7)

    def test_is_over_budget(self):
        ctx = ExecutionContext(budget_used_usd=1.5, budget_limit_usd=1.0)
        assert ctx.is_over_budget is True

    def test_not_over_budget(self):
        ctx = ExecutionContext(budget_used_usd=0.5, budget_limit_usd=1.0)
        assert ctx.is_over_budget is False

    def test_successful_results(self):
        ctx = ExecutionContext(previous_results=[
            ExecutionResult(step_number=1, success=True),
            ExecutionResult(step_number=2, success=False),
            ExecutionResult(step_number=3, success=True),
        ])
        assert len(ctx.successful_results) == 2

    def test_last_output(self):
        ctx = ExecutionContext(previous_results=[
            ExecutionResult(step_number=1, success=True, output="first"),
            ExecutionResult(step_number=2, success=False),
            ExecutionResult(step_number=3, success=True, output="last"),
        ])
        assert ctx.last_output == "last"

    def test_to_dict(self):
        ctx = ExecutionContext(step_number=5)
        d = ctx.to_dict()
        assert d["step_number"] == 5


class TestStepTrace:
    def test_basic_trace(self):
        t = StepTrace(step_number=1, tool="http", action="fetch")
        assert t.step_number == 1
        assert t.success is True

    def test_to_dict(self):
        t = StepTrace(step_number=1, tool="http", success=False, error="timeout")
        d = t.to_dict()
        assert d["success"] is False
        assert d["error"] == "timeout"


class TestExecutionError:
    def test_error_creation(self):
        e = ExecutionError("step failed", step_number=3, recoverable=True)
        assert str(e) == "step failed"
        assert e.step_number == 3
        assert e.recoverable is True


class TestExecutor:
    @pytest.fixture
    def executor(self):
        return Executor()

    def test_register_tool(self, executor):
        tool = MockTool(tool_name="test")
        executor.register_tool(tool)
        assert executor.get_tool("test") is tool

    def test_unregister_tool(self, executor):
        tool = MockTool(tool_name="test")
        executor.register_tool(tool)
        assert executor.unregister_tool("test") is True
        assert executor.get_tool("test") is None

    def test_list_tools(self, executor):
        executor.register_tool(MockTool(tool_name="b"))
        executor.register_tool(MockTool(tool_name="a"))
        assert executor.list_tools() == ["a", "b"]

    async def test_execute_step_simulated(self, executor, execution_context):
        step = Step(number=1, action="test", tool="unknown_tool")
        result = await executor.execute_step(step, execution_context)
        assert result.success is True
        assert "Simulated" in result.output

    async def test_execute_step_with_tool(self, executor, execution_context):
        tool = MockTool(tool_name="real_tool", responses=[ToolResult.ok("real output")])
        executor.register_tool(tool)
        step = Step(number=1, action="test", tool="real_tool")
        result = await executor.execute_step(step, execution_context)
        assert result.success is True
        assert result.output == "real output"

    async def test_execute_step_failure(self, executor, execution_context):
        tool = MockTool(tool_name="fail_tool", responses=[ToolResult.fail("broken")])
        executor.register_tool(tool)
        step = Step(number=1, action="test", tool="fail_tool")
        result = await executor.execute_step(step, execution_context)
        assert result.failed is True

    async def test_execute_plan(self, executor, execution_context):
        plan = Plan(steps=[
            Step(number=1, action="fetch", tool="http"),
            Step(number=2, action="parse", tool="parser"),
        ])
        results = await executor.execute_plan(plan, execution_context)
        assert len(results) == 2
        assert all(r.success for r in results)

    async def test_execute_plan_stops_on_failure(self, executor, execution_context):
        tool = MockTool(tool_name="fail", responses=[ToolResult.fail("error")])
        executor.register_tool(tool)
        plan = Plan(steps=[
            Step(number=1, action="go", tool="fail"),
            Step(number=2, action="next", tool="other"),
        ])
        results = await executor.execute_plan(plan, execution_context)
        assert len(results) == 1
        assert results[0].failed is True

    async def test_budget_exceeded_stops_plan(self, executor):
        ctx = ExecutionContext(budget_used_usd=2.0, budget_limit_usd=1.0)
        plan = Plan(steps=[Step(number=1, action="go", tool="t")])
        results = await executor.execute_plan(plan, ctx)
        assert len(results) == 1
        assert results[0].failed is True
        assert "Budget" in results[0].error

    async def test_execution_log(self, executor, execution_context):
        step = Step(number=1, action="test", tool="sim")
        await executor.execute_step(step, execution_context)
        log = executor.get_execution_log()
        assert len(log) == 1

    async def test_get_stats(self, executor, execution_context):
        step = Step(number=1, action="test", tool="sim")
        await executor.execute_step(step, execution_context)
        stats = executor.get_stats()
        assert stats["total_steps"] == 1
        assert stats["successes"] == 1

    async def test_clear_log(self, executor, execution_context):
        step = Step(number=1, action="test", tool="sim")
        await executor.execute_step(step, execution_context)
        executor.clear_log()
        assert executor.get_execution_log() == []

    async def test_fallback_execution(self, executor, execution_context):
        primary = MockTool(tool_name="primary", responses=[ToolResult.fail("down")])
        fallback = MockTool(tool_name="fallback", responses=[ToolResult.ok("fallback works")])
        executor.register_tool(primary)
        executor.register_tool(fallback)
        step = Step(number=1, action="test", tool="primary", fallback_tool="fallback")
        result = await executor.execute_step(step, execution_context)
        assert result.success is True
        assert result.used_fallback is True
