"""
Tests for LangChain tools.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock

from skillbroker_langchain import (
    SkillBrokerTool,
    SkillBrokerSearchTool,
    SkillBrokerDynamicTool,
    SkillBrokerClient,
    SkillBrokerError,
    Skill,
    SkillResponse,
    SearchResult,
    SkillQuality,
)


class TestSkillBrokerTool:
    """Tests for SkillBrokerTool."""

    def test_init_with_skill_id(self):
        """Test tool initializes with skill ID."""
        tool = SkillBrokerTool(skill_id="tax-advisor")
        assert tool.skill_id == "tax-advisor"
        assert tool.name == "skillbroker_tax_advisor"

    def test_init_with_custom_name(self):
        """Test tool accepts custom name."""
        tool = SkillBrokerTool(
            skill_id="tax-advisor",
            name="my_tax_tool",
            description="Custom description"
        )
        assert tool.name == "my_tax_tool"
        assert tool.description == "Custom description"

    def test_init_creates_client(self):
        """Test tool creates a client instance."""
        tool = SkillBrokerTool(skill_id="test")
        assert tool.client is not None
        assert isinstance(tool.client, SkillBrokerClient)

    def test_init_with_api_config(self):
        """Test tool passes API config to client."""
        tool = SkillBrokerTool(
            skill_id="test",
            api_url="https://custom.api.io",
            api_key="test-key"
        )
        assert tool.client.api_url == "https://custom.api.io"

    @patch.object(SkillBrokerClient, 'invoke')
    def test_run_success(self, mock_invoke):
        """Test successful tool execution."""
        mock_invoke.return_value = SkillResponse(
            success=True,
            response="Here is your tax advice...",
            skill_id="tax-advisor",
            skill_name="Tax Advisor"
        )

        tool = SkillBrokerTool(skill_id="tax-advisor")
        result = tool._run("Can I deduct my home office?")

        assert result == "Here is your tax advice..."
        mock_invoke.assert_called_once_with("tax-advisor", "Can I deduct my home office?")

    @patch.object(SkillBrokerClient, 'invoke')
    def test_run_error(self, mock_invoke):
        """Test tool handles errors gracefully."""
        mock_invoke.side_effect = SkillBrokerError("API Error")

        tool = SkillBrokerTool(skill_id="tax-advisor")
        result = tool._run("query")

        assert "Error querying skill" in result

    def test_args_schema(self):
        """Test tool has correct args schema."""
        tool = SkillBrokerTool(skill_id="test")
        schema = tool.args_schema.model_json_schema()

        assert "query" in schema["properties"]
        assert schema["properties"]["query"]["type"] == "string"


class TestSkillBrokerSearchTool:
    """Tests for SkillBrokerSearchTool."""

    def test_init_default(self):
        """Test search tool initializes with defaults."""
        tool = SkillBrokerSearchTool()
        assert tool.name == "skillbroker_search"
        assert tool.max_results == 5

    def test_init_custom_max_results(self):
        """Test custom max results."""
        tool = SkillBrokerSearchTool(max_results=10)
        assert tool.max_results == 10

    @patch.object(SkillBrokerClient, 'search')
    def test_run_with_results(self, mock_search):
        """Test search returns formatted results."""
        mock_search.return_value = SearchResult(
            skills=[
                Skill(
                    id="tax-advisor",
                    name="Tax Advisor",
                    description="Expert tax advice for freelancers",
                    version="1.0.0",
                    author="expert@example.com",
                    category="Finance",
                    quality=SkillQuality(quality_score=4.5)
                )
            ],
            total_count=1,
            query="tax"
        )

        tool = SkillBrokerSearchTool()
        result = tool._run("tax")

        assert "Found 1 skills" in result
        assert "Tax Advisor" in result
        assert "tax-advisor" in result
        assert "Finance" in result
        assert "4.5" in result

    @patch.object(SkillBrokerClient, 'search')
    def test_run_no_results(self, mock_search):
        """Test search with no results."""
        mock_search.return_value = SearchResult(
            skills=[],
            total_count=0,
            query="nonexistent"
        )

        tool = SkillBrokerSearchTool()
        result = tool._run("nonexistent")

        assert "No skills found" in result

    @patch.object(SkillBrokerClient, 'search')
    def test_run_with_category(self, mock_search):
        """Test search with category filter."""
        mock_search.return_value = SearchResult(skills=[], total_count=0)

        tool = SkillBrokerSearchTool()
        tool._run("query", category="Finance")

        mock_search.assert_called_once_with(
            query="query",
            category="Finance",
            limit=5
        )

    @patch.object(SkillBrokerClient, 'search')
    def test_run_error(self, mock_search):
        """Test search handles errors."""
        mock_search.side_effect = SkillBrokerError("API Error")

        tool = SkillBrokerSearchTool()
        result = tool._run("query")

        assert "Error searching skills" in result


class TestSkillBrokerDynamicTool:
    """Tests for SkillBrokerDynamicTool."""

    def test_init_default(self):
        """Test dynamic tool initializes with defaults."""
        tool = SkillBrokerDynamicTool()
        assert tool.name == "skillbroker_expert"

    def test_init_custom_name(self):
        """Test custom name and description."""
        tool = SkillBrokerDynamicTool(
            name="expert_finder",
            description="Find experts automatically"
        )
        assert tool.name == "expert_finder"

    @patch.object(SkillBrokerClient, 'get_recommendations')
    @patch.object(SkillBrokerClient, 'invoke')
    def test_run_with_recommendations(self, mock_invoke, mock_recommend):
        """Test dynamic tool uses recommendations first."""
        mock_recommend.return_value = [
            Skill(
                id="tax-advisor",
                name="Tax Advisor",
                description="Expert tax advice",
                version="1.0.0",
                author="expert@example.com",
                category="Finance"
            )
        ]
        mock_invoke.return_value = SkillResponse(
            success=True,
            response="Your tax answer",
            skill_id="tax-advisor",
            skill_name="Tax Advisor"
        )

        tool = SkillBrokerDynamicTool()
        result = tool._run("How do I file taxes?")

        assert "[From Tax Advisor]:" in result
        assert "Your tax answer" in result
        mock_recommend.assert_called_once()

    @patch.object(SkillBrokerClient, 'get_recommendations')
    @patch.object(SkillBrokerClient, 'search')
    @patch.object(SkillBrokerClient, 'invoke')
    def test_run_falls_back_to_search(self, mock_invoke, mock_search, mock_recommend):
        """Test dynamic tool falls back to search when recommendations fail."""
        mock_recommend.side_effect = SkillBrokerError("No recommendations")
        mock_search.return_value = SearchResult(
            skills=[
                Skill(
                    id="legal-advisor",
                    name="Legal Advisor",
                    description="Legal advice",
                    version="1.0.0",
                    author="expert@example.com",
                    category="Legal"
                )
            ],
            total_count=1
        )
        mock_invoke.return_value = SkillResponse(
            success=True,
            response="Legal answer",
            skill_id="legal-advisor",
            skill_name="Legal Advisor"
        )

        tool = SkillBrokerDynamicTool()
        result = tool._run("Contract question")

        assert "[From Legal Advisor]:" in result
        mock_search.assert_called_once()

    @patch.object(SkillBrokerClient, 'get_recommendations')
    @patch.object(SkillBrokerClient, 'search')
    def test_run_no_skills_found(self, mock_search, mock_recommend):
        """Test dynamic tool when no skills are found."""
        mock_recommend.side_effect = SkillBrokerError("No recommendations")
        mock_search.return_value = SearchResult(skills=[], total_count=0)

        tool = SkillBrokerDynamicTool()
        result = tool._run("Very obscure question")

        assert "No relevant expert skills found" in result

    @patch.object(SkillBrokerClient, 'get_recommendations')
    @patch.object(SkillBrokerClient, 'invoke')
    def test_run_invocation_error(self, mock_invoke, mock_recommend):
        """Test dynamic tool handles invocation errors."""
        mock_recommend.return_value = [
            Skill(
                id="broken-skill",
                name="Broken Skill",
                description="Does not work",
                version="1.0.0",
                author="expert@example.com",
                category="Test"
            )
        ]
        mock_invoke.side_effect = SkillBrokerError("Invocation failed")

        tool = SkillBrokerDynamicTool()
        result = tool._run("Query")

        assert "Error:" in result


class TestToolIntegration:
    """Integration-style tests for tools."""

    def test_tool_is_langchain_compatible(self):
        """Test tools are proper LangChain tools."""
        from langchain_core.tools import BaseTool

        tool = SkillBrokerTool(skill_id="test")
        assert isinstance(tool, BaseTool)
        assert hasattr(tool, '_run')
        assert hasattr(tool, '_arun')

    def test_search_tool_is_langchain_compatible(self):
        """Test search tool is proper LangChain tool."""
        from langchain_core.tools import BaseTool

        tool = SkillBrokerSearchTool()
        assert isinstance(tool, BaseTool)

    def test_dynamic_tool_is_langchain_compatible(self):
        """Test dynamic tool is proper LangChain tool."""
        from langchain_core.tools import BaseTool

        tool = SkillBrokerDynamicTool()
        assert isinstance(tool, BaseTool)

    def test_tools_have_descriptions(self):
        """Test all tools have descriptions for LLM."""
        tool1 = SkillBrokerTool(skill_id="test")
        tool2 = SkillBrokerSearchTool()
        tool3 = SkillBrokerDynamicTool()

        assert len(tool1.description) > 10
        assert len(tool2.description) > 10
        assert len(tool3.description) > 10
