# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
# SPDX-License-Identifier: Apache-2.0

"""End-to-end tests for agent providers with real API calls.

These tests require:
- Claude CLI installed (`npm i -g @anthropic-ai/claude-code`)
- Gemini CLI installed (if testing Gemini)

Run with: pytest tests/agents/test_providers_e2e.py -v -s
"""

from __future__ import annotations

import shutil

import pytest

# Skip all tests if providers not available
pytest.importorskip("krons.agent.providers")


def _has_claude_cli() -> bool:
    """Check if Claude CLI is installed."""
    return shutil.which("claude") is not None


def _has_gemini_cli() -> bool:
    """Check if Gemini CLI is installed."""
    return shutil.which("gemini") is not None


class TestClaudeCodeEndpoint:
    """End-to-end tests for Claude Code CLI endpoint."""

    @pytest.fixture
    def endpoint(self):
        """Create Claude Code endpoint."""
        from krons.agent.providers.claude_code import ClaudeCodeEndpoint

        return ClaudeCodeEndpoint()

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_simple_query(self, endpoint):
        """Test a simple query to Claude Code CLI."""
        response = await endpoint.call(
            request={
                "messages": [
                    {
                        "role": "user",
                        "content": "What is 2 + 2? Reply with just the number.",
                    }
                ],
                "max_turns": 1,
                "model": "haiku",  # Use haiku for cost efficiency
            }
        )

        assert response.status == "success"
        assert response.data is not None
        # Should contain "4" somewhere in the response
        assert "4" in str(response.data)

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_json_response(self, endpoint):
        """Test getting JSON response from Claude Code CLI."""
        response = await endpoint.call(
            request={
                "messages": [
                    {
                        "role": "user",
                        "content": 'Return a JSON object with keys "name" and "value". '
                        'Use name="test" and value=42. Reply with ONLY the JSON, no markdown.',
                    }
                ],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        assert response.status == "success"
        # Should be able to parse as JSON (fuzzy)
        from krons.utils.fuzzy import extract_json

        data = extract_json(response.data)
        assert data is not None
        assert "name" in data or "value" in data

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_metadata_populated(self, endpoint):
        """Test that response metadata is populated."""
        response = await endpoint.call(
            request={
                "messages": [{"role": "user", "content": "Say hello"}],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        assert response.status == "success"
        assert response.metadata is not None
        # Should have usage info
        assert (
            "usage" in response.metadata or response.metadata.get("usage") is not None
        )


class TestGeminiCodeEndpoint:
    """End-to-end tests for Gemini Code CLI endpoint."""

    @pytest.fixture
    def endpoint(self):
        """Create Gemini Code endpoint."""
        from krons.agent.providers.gemini import GeminiCodeEndpoint

        return GeminiCodeEndpoint()

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_gemini_cli(),
        reason="Gemini CLI not installed",
    )
    async def test_simple_query(self, endpoint):
        """Test a simple query to Gemini Code CLI."""
        response = await endpoint.call(
            request={
                "messages": [
                    {
                        "role": "user",
                        "content": "What is 3 + 3? Reply with just the number.",
                    }
                ],
                "max_turns": 1,
            }
        )

        assert response.status == "success"
        assert response.data is not None
        assert "6" in str(response.data)


class TestClaudeCodeStructuredOutput:
    """E2E tests for Claude Code with structured output (Pydantic models)."""

    @pytest.fixture
    def endpoint(self):
        """Create Claude Code endpoint."""
        from krons.agent.providers.claude_code import ClaudeCodeEndpoint

        return ClaudeCodeEndpoint()

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_structured_json_output(self, endpoint):
        """Test getting structured JSON output from Claude Code.

        This test verifies the agentic capability to produce structured output.
        """
        from pydantic import BaseModel

        class Person(BaseModel):
            name: str
            age: int
            occupation: str

        prompt = """Return a JSON object representing a person with these exact fields:
- name: string (use "Alice")
- age: integer (use 30)
- occupation: string (use "Engineer")

Return ONLY the JSON object, no markdown code blocks, no explanation."""

        response = await endpoint.call(
            request={
                "messages": [{"role": "user", "content": prompt}],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        assert response.status == "success"

        # Extract and validate JSON
        from krons.utils.fuzzy import extract_json

        data = extract_json(response.data)

        assert data is not None, f"Failed to extract JSON from: {response.data}"

        # Validate against Pydantic model
        try:
            person = Person(**data)
            assert person.name == "Alice"
            assert person.age == 30
            assert person.occupation == "Engineer"
        except Exception as e:
            # Allow flexible matching - just verify structure
            assert "name" in data
            assert "age" in data
            assert "occupation" in data

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_structured_list_output(self, endpoint):
        """Test getting structured list output from Claude Code."""
        prompt = """Return a JSON array with exactly 3 objects, each having:
- id: integer (1, 2, 3)
- name: string ("item_1", "item_2", "item_3")

Return ONLY the JSON array, no markdown, no explanation."""

        response = await endpoint.call(
            request={
                "messages": [{"role": "user", "content": prompt}],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        assert response.status == "success"

        from krons.utils.fuzzy import extract_json

        data = extract_json(response.data)

        assert data is not None
        assert isinstance(data, list)
        assert len(data) >= 3

        # Verify structure
        for item in data[:3]:
            assert "id" in item or "name" in item

    @pytest.mark.anyio
    @pytest.mark.skipif(
        not _has_claude_cli(),
        reason="Claude CLI not installed",
    )
    async def test_multi_turn_conversation(self, endpoint):
        """Test multi-turn conversation capability."""
        # First turn
        response1 = await endpoint.call(
            request={
                "messages": [
                    {
                        "role": "user",
                        "content": "Remember this number: 42. Just say 'OK, noted.'",
                    }
                ],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        assert response1.status == "success"

        # Note: True multi-turn requires session management
        # This test verifies the endpoint can handle conversation structure


# Simple standalone test for quick verification
if __name__ == "__main__":
    import asyncio

    async def main():
        if not _has_claude_cli():
            print("Claude CLI not installed, skipping test")
            return

        from krons.agent.providers.claude_code import ClaudeCodeEndpoint

        endpoint = ClaudeCodeEndpoint()
        print("Testing Claude Code endpoint with structured output...")

        # Test structured output
        from pydantic import BaseModel

        class TestOutput(BaseModel):
            result: int
            explanation: str

        response = await endpoint.call(
            request={
                "messages": [
                    {
                        "role": "user",
                        "content": 'Calculate 2+2 and return JSON: {"result": <number>, "explanation": "<text>"}. ONLY JSON, no markdown.',
                    }
                ],
                "max_turns": 1,
                "model": "haiku",
            }
        )

        print(f"Status: {response.status}")
        print(f"Data: {response.data}")
        print(f"Metadata: {response.metadata}")

        if response.status == "success":
            from krons.utils.fuzzy import extract_json

            data = extract_json(response.data)
            if data:
                print(f"\nExtracted JSON: {data}")
                try:
                    output = TestOutput(**data)
                    print(
                        f"Validated output: result={output.result}, explanation={output.explanation}"
                    )
                    print("\nStructured output test PASSED!")
                except Exception as e:
                    print(f"Validation warning: {e}")
                    print(
                        "\nBasic test PASSED (JSON extracted but validation flexible)"
                    )
            else:
                print("\nTest PASSED (response received, JSON extraction optional)")
        else:
            print(f"\nTest FAILED: {response.error}")

    asyncio.run(main())
