"""Tests for the agent testing framework."""
import os
import pytest
from pydantic import BaseModel

from logos_adk.agent import Agent
from logos_adk.guardrails import ContentFilter
from logos_adk.providers.mock import MockProvider
from logos_adk.testing import (
    assert_guardrail_blocked,
    assert_response,
    assert_tool_called,
    assert_tool_not_called,
    mock_agent,
)
from logos_adk.types import Response, StructuredResponse, ToolCall, Usage


class TestMockAgent:
    def test_returns_tuple(self):
        agent, provider = mock_agent("bot", responses=["hi"])
        assert isinstance(agent, Agent)
        assert isinstance(provider, MockProvider)

    @pytest.mark.asyncio
    async def test_runs_and_responds(self):
        agent, provider = mock_agent("bot", responses=["hello world"])
        response = await agent.run("hi")
        assert response.content == "hello world"
        assert len(provider.call_history) == 1


class TestAssertResponse:
    def test_contains_passes(self):
        r = Response(content="hello world", tool_calls=[], usage=Usage(0, 0), raw={})
        assert_response(r, contains=["hello", "world"])

    def test_contains_fails(self):
        r = Response(content="hello", tool_calls=[], usage=Usage(0, 0), raw={})
        with pytest.raises(AssertionError, match="missing"):
            assert_response(r, contains=["goodbye"])

    def test_not_contains_passes(self):
        r = Response(content="hello", tool_calls=[], usage=Usage(0, 0), raw={})
        assert_response(r, not_contains=["evil"])

    def test_not_contains_fails(self):
        r = Response(content="evil content", tool_calls=[], usage=Usage(0, 0), raw={})
        with pytest.raises(AssertionError, match="should not contain"):
            assert_response(r, not_contains=["evil"])

    def test_max_length_passes(self):
        r = Response(content="short", tool_calls=[], usage=Usage(0, 0), raw={})
        assert_response(r, max_length=100)

    def test_max_length_fails(self):
        r = Response(content="x" * 200, tool_calls=[], usage=Usage(0, 0), raw={})
        with pytest.raises(AssertionError, match="exceeds max_length"):
            assert_response(r, max_length=100)

    def test_min_length_passes(self):
        r = Response(content="long enough", tool_calls=[], usage=Usage(0, 0), raw={})
        assert_response(r, min_length=5)

    def test_min_length_fails(self):
        r = Response(content="hi", tool_calls=[], usage=Usage(0, 0), raw={})
        with pytest.raises(AssertionError, match="below min_length"):
            assert_response(r, min_length=10)

    def test_schema_passes(self):
        class MyModel(BaseModel):
            name: str

        r = StructuredResponse(
            content='{"name": "Alice"}', tool_calls=[], usage=Usage(0, 0),
            raw={}, parsed=MyModel(name="Alice"),
        )
        assert_response(r, schema=MyModel)

    def test_schema_none_parsed_fails(self):
        class MyModel(BaseModel):
            name: str

        r = StructuredResponse(
            content="not json", tool_calls=[], usage=Usage(0, 0),
            raw={}, parsed=None,
        )
        with pytest.raises(AssertionError, match="parsed is None"):
            assert_response(r, schema=MyModel)

    def test_schema_on_plain_response_fails(self):
        class MyModel(BaseModel):
            name: str

        r = Response(content="hello", tool_calls=[], usage=Usage(0, 0), raw={})
        with pytest.raises(AssertionError, match="not a StructuredResponse"):
            assert_response(r, schema=MyModel)


class TestAssertToolCalled:
    @pytest.mark.asyncio
    async def test_detects_tool_call(self):
        provider = MockProvider(responses=[
            Response(
                content="",
                tool_calls=[ToolCall(id="1", name="search", arguments={"q": "test"})],
                usage=Usage(0, 0),
                raw={},
            ),
            Response(content="done", tool_calls=[], usage=Usage(0, 0), raw={}),
        ])
        from logos_adk.tools import Tool
        async def search_fn(q: str) -> str:
            return "result"
        tool = Tool(fn=search_fn, name="search", description="Search")
        agent = Agent("bot", model=provider).tools([tool])
        await agent.run("find something")

        assert_tool_called(agent, "search")
        assert_tool_called(agent, "search", times=1)

    @pytest.mark.asyncio
    async def test_not_called(self):
        agent, _ = mock_agent("bot", responses=["hello"])
        await agent.run("hi")
        assert_tool_not_called(agent, "search")

    @pytest.mark.asyncio
    async def test_called_wrong_times_fails(self):
        provider = MockProvider(responses=[
            Response(
                content="",
                tool_calls=[ToolCall(id="1", name="search", arguments={})],
                usage=Usage(0, 0), raw={},
            ),
            Response(content="done", tool_calls=[], usage=Usage(0, 0), raw={}),
        ])
        from logos_adk.tools import Tool
        async def search_fn() -> str:
            return "ok"
        tool = Tool(fn=search_fn, name="search", description="Search")
        agent = Agent("bot", model=provider).tools([tool])
        await agent.run("go")

        with pytest.raises(AssertionError, match="expected 5"):
            assert_tool_called(agent, "search", times=5)


class TestAssertGuardrailBlocked:
    @pytest.mark.asyncio
    async def test_passes_on_block(self):
        agent, _ = mock_agent("bot", responses=["ok"])
        agent.input_guardrail(ContentFilter(blocked=["evil"]))
        with assert_guardrail_blocked("evil"):
            await agent.run("evil prompt")

    @pytest.mark.asyncio
    async def test_fails_when_no_error(self):
        agent, _ = mock_agent("bot", responses=["ok"])
        with pytest.raises(AssertionError, match="GuardrailError was not raised"):
            with assert_guardrail_blocked():
                await agent.run("safe prompt")

    @pytest.mark.asyncio
    async def test_fails_on_wrong_match(self):
        agent, _ = mock_agent("bot", responses=["ok"])
        agent.input_guardrail(ContentFilter(blocked=["evil"]))
        with pytest.raises(AssertionError, match="did not match"):
            with assert_guardrail_blocked("other"):
                await agent.run("evil prompt")


# ---------------------------------------------------------------------------
# YAML Test Runner
# ---------------------------------------------------------------------------

from logos_adk.testing_yaml import run_yaml_tests, TestResult, TestResults


class TestYAMLRunner:
    def test_pass(self):
        path = os.path.join(os.path.dirname(__file__), "fixtures", "test_suite_pass.yaml")
        results = run_yaml_tests(path)
        assert results.total == 1
        assert results.passed == 1
        assert results.failed == 0

    def test_fail(self):
        path = os.path.join(os.path.dirname(__file__), "fixtures", "test_suite_fail.yaml")
        results = run_yaml_tests(path)
        assert results.total == 1
        assert results.failed == 1
        assert "min_length" in results.results[0].error

    def test_guardrail(self):
        path = os.path.join(os.path.dirname(__file__), "fixtures", "test_suite_guardrail.yaml")
        results = run_yaml_tests(path)
        assert results.total == 1
        assert results.passed == 1

    def test_result_dataclass(self):
        r = TestResult(name="test", passed=True, error=None)
        assert r.name == "test"

    def test_results_dataclass(self):
        r = TestResults(results=[], passed=0, failed=0, total=0)
        assert r.total == 0
