import pytest
from arcade_tdk.errors import RetryableToolError, ToolExecutionError

from arcade_zendesk.enums import ArticleSortBy, SortOrder
from arcade_zendesk.tools.search_articles import search_articles


class TestSearchArticlesValidation:
    """Test input validation for search_articles."""

    @pytest.mark.asyncio
    async def test_missing_subdomain(self, mock_context):
        """Test error when subdomain is not configured."""
        mock_context.get_secret.side_effect = ValueError("Secret not found")

        with pytest.raises(ToolExecutionError) as exc_info:
            await search_articles(context=mock_context, query="test")

        assert "subdomain is not set" in str(exc_info.value.message)

    @pytest.mark.asyncio
    async def test_missing_search_params(self, mock_context):
        """Test error when no search parameters provided."""
        mock_context.get_secret.return_value = "test-subdomain"

        with pytest.raises(RetryableToolError) as exc_info:
            await search_articles(context=mock_context)

        assert "At least one search parameter" in str(exc_info.value.message)

    @pytest.mark.parametrize(
        "date_param,date_value",
        [
            ("created_after", "2024/01/01"),
            ("created_before", "01-15-2024"),
            ("created_at", "2024-1-15"),
            ("created_after", "2024-01-1"),
            ("created_before", "20240115"),
            ("created_at", "not-a-date"),
        ],
    )
    @pytest.mark.asyncio
    async def test_invalid_date_format(self, mock_context, date_param, date_value):
        """Test validation of date format parameters."""
        mock_context.get_secret.return_value = "test-subdomain"

        with pytest.raises(RetryableToolError) as exc_info:
            await search_articles(context=mock_context, query="test", **{date_param: date_value})

        assert "Invalid date format" in str(exc_info.value.message)
        assert "YYYY-MM-DD" in str(exc_info.value.message)
        assert date_param in str(exc_info.value.message)

    @pytest.mark.parametrize("limit", [0, -1, -10])
    @pytest.mark.asyncio
    async def test_invalid_limit(self, mock_context, limit):
        """Test validation of limit parameter."""
        mock_context.get_secret.return_value = "test-subdomain"

        with pytest.raises(RetryableToolError) as exc_info:
            await search_articles(context=mock_context, query="test", limit=limit)

        assert "at least 1" in str(exc_info.value.message)

    @pytest.mark.parametrize("offset", [-1, -10])
    @pytest.mark.asyncio
    async def test_invalid_offset(self, mock_context, offset):
        """Test validation of offset parameter."""
        mock_context.get_secret.return_value = "test-subdomain"

        with pytest.raises(RetryableToolError) as exc_info:
            await search_articles(context=mock_context, query="test", offset=offset)

        assert "cannot be negative" in str(exc_info.value.message)


class TestSearchArticlesSuccess:
    """Test successful search scenarios."""

    @pytest.mark.asyncio
    async def test_basic_search(
        self, mock_context, mock_httpx_client, build_search_response, mock_http_response
    ):
        """Test basic search with query parameter."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup mock response
        search_response = build_search_response()
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(context=mock_context, query="password reset")

        assert "results" in result
        assert len(result["results"]) == 1
        assert result["results"][0]["metadata"]["title"] == "How to reset your password"

        mock_httpx_client.get.assert_called_once()
        call_args = mock_httpx_client.get.call_args
        assert (
            call_args[0][0]
            == "https://test-subdomain.zendesk.com/api/v2/help_center/articles/search"
        )
        assert call_args[1]["params"]["query"] == "password reset"
        # Check that pagination params were set correctly
        assert call_args[1]["params"]["page"] == 1
        assert call_args[1]["params"]["per_page"] == 100

    @pytest.mark.asyncio
    async def test_search_with_filters(
        self, mock_context, mock_httpx_client, build_search_response, mock_http_response
    ):
        """Test search with multiple filter parameters."""
        mock_context.get_secret.return_value = "test-subdomain"

        search_response = build_search_response()
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(
            context=mock_context,
            query="API",
            created_after="2024-01-01",
            sort_by=ArticleSortBy.CREATED_AT,
            sort_order=SortOrder.DESC,
            limit=25,
        )

        assert "results" in result

        # Verify all parameters were passed
        call_params = mock_httpx_client.get.call_args[1]["params"]
        assert call_params["query"] == "API"
        assert call_params["created_after"] == "2024-01-01"
        assert call_params["sort_by"] == "created_at"
        assert call_params["sort_order"] == "desc"
        # Should fetch first page with 100 items per page
        assert call_params["page"] == 1
        assert call_params["per_page"] == 100

    @pytest.mark.asyncio
    async def test_search_without_body(
        self,
        mock_context,
        mock_httpx_client,
        sample_article_response,
        mock_http_response,
    ):
        """Test search with include_body=False."""
        mock_context.get_secret.return_value = "test-subdomain"

        search_response = {"results": [sample_article_response], "next_page": None}
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(context=mock_context, query="test", include_body=False)

        assert result["results"][0]["content"] is None
        assert result["results"][0]["metadata"]["title"] == sample_article_response["title"]

    @pytest.mark.asyncio
    async def test_search_by_labels(
        self, mock_context, mock_httpx_client, build_search_response, mock_http_response
    ):
        """Test search by label names."""
        mock_context.get_secret.return_value = "test-subdomain"

        search_response = build_search_response()
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(context=mock_context, label_names=["password", "security"])

        assert "results" in result
        assert mock_httpx_client.get.call_args[1]["params"]["label_names"] == "password,security"


class TestSearchArticlesPagination:
    """Test pagination scenarios."""

    @pytest.mark.asyncio
    async def test_single_page_default(
        self, mock_context, mock_httpx_client, build_search_response, mock_http_response
    ):
        """Test default behavior returns single page."""
        mock_context.get_secret.return_value = "test-subdomain"

        search_response = build_search_response(count=100)
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(context=mock_context, query="test")

        assert len(result["results"]) == 1
        assert mock_httpx_client.get.call_count == 1

    @pytest.mark.asyncio
    async def test_fetch_with_limit_across_pages(
        self, mock_context, mock_httpx_client, mock_http_response
    ):
        """Test fetching results across multiple pages with limit."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup pagination responses - 100 items per page
        articles_page1 = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(1, 101)
        ]
        articles_page2 = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(101, 201)
        ]

        page1 = {"results": articles_page1, "next_page": "page2"}
        page2 = {"results": articles_page2, "next_page": "page3"}

        mock_httpx_client.get.side_effect = [
            mock_http_response(page1),
            mock_http_response(page2),
        ]

        # Request 150 items starting from offset 0
        result = await search_articles(context=mock_context, query="test", limit=150)

        assert result["count"] == 150
        assert "next_offset" in result  # More results available
        assert result["next_offset"] == 150
        assert mock_httpx_client.get.call_count == 2  # Fetched 2 pages

    @pytest.mark.asyncio
    async def test_fetch_with_offset(self, mock_context, mock_httpx_client, mock_http_response):
        """Test fetching with offset parameter."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup response - page 2 would have items 101-200
        # We want items starting from offset 150 (which is item 151, at index 50 on page 2)
        articles_page2 = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(101, 201)
        ]
        response = {"results": articles_page2, "next_page": "page3"}

        mock_httpx_client.get.return_value = mock_http_response(response)

        # Request 30 items starting from offset 150
        result = await search_articles(context=mock_context, query="test", offset=150, limit=30)

        assert result["count"] == 30
        assert "next_offset" in result
        assert result["next_offset"] == 180

        # Should request page 2 (offset 150 = page 2, starting at index 50)
        call_params = mock_httpx_client.get.call_args[1]["params"]
        assert call_params["page"] == 2

    @pytest.mark.asyncio
    async def test_no_next_offset_when_no_more_results(
        self, mock_context, mock_httpx_client, build_search_response, mock_http_response
    ):
        """Test that next_offset is not included when no more results."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup response with no next page
        articles = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(1, 21)
        ]
        response = {"results": articles, "next_page": None}

        mock_httpx_client.get.return_value = mock_http_response(response)

        result = await search_articles(context=mock_context, query="test", limit=20)

        assert result["count"] == 20
        assert "next_offset" not in result  # No more results

    @pytest.mark.asyncio
    async def test_partial_page_with_more_items(
        self, mock_context, mock_httpx_client, mock_http_response
    ):
        """Test that next_offset is included when there are more items on the current page."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup response with 50 items on a page, but we only request 30
        articles = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(1, 51)
        ]
        response = {"results": articles, "next_page": None}

        mock_httpx_client.get.return_value = mock_http_response(response)

        # Request only 30 items when page has 50
        result = await search_articles(context=mock_context, query="test", limit=30)

        assert result["count"] == 30
        assert "next_offset" in result  # More items available on current page
        assert result["next_offset"] == 30

    @pytest.mark.asyncio
    async def test_request_more_than_available(
        self, mock_context, mock_httpx_client, mock_http_response
    ):
        """Test when requesting more items than are available returns only what's available."""
        mock_context.get_secret.return_value = "test-subdomain"

        # Setup response with only 15 items total
        articles = [
            {"id": i, "title": f"Article {i}", "body": f"Content {i}"} for i in range(1, 16)
        ]
        response = {"results": articles, "next_page": None}

        mock_httpx_client.get.return_value = mock_http_response(response)

        # Request 30 items when only 15 are available
        result = await search_articles(context=mock_context, query="test", limit=30)

        assert result["count"] == 15  # Only returns what's available
        assert "next_offset" not in result  # No more results


class TestSearchArticlesContentProcessing:
    """Test content processing and formatting."""

    @pytest.mark.asyncio
    async def test_html_cleaning(self, mock_context, mock_httpx_client, mock_http_response):
        """Test HTML content is properly cleaned."""
        mock_context.get_secret.return_value = "test-subdomain"

        article_with_html = {
            "id": 1,
            "title": "Test Article",
            "body": "<h1>Header</h1><p>Paragraph with <strong>bold</strong> and "
            "<em>italic</em>.</p><br/><div>Div content</div>",
            "url": "https://example.com/article/1",
        }

        search_response = {"results": [article_with_html], "next_page": None}
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        result = await search_articles(context=mock_context, query="test", include_body=True)

        content = result["results"][0]["content"]
        assert content == "Header Paragraph with bold and italic . Div content"

    @pytest.mark.asyncio
    async def test_max_article_length(self, mock_context, mock_httpx_client, mock_http_response):
        """Test article length limiting."""
        mock_context.get_secret.return_value = "test-subdomain"

        long_article = {
            "id": 1,
            "title": "Long Article",
            "body": "A" * 1000,  # 1000 character body
        }

        search_response = {"results": [long_article], "next_page": None}
        mock_httpx_client.get.return_value = mock_http_response(search_response)

        # Test with default 500 char limit
        result = await search_articles(context=mock_context, query="test")
        assert len(result["results"][0]["content"]) < 520  # 500 + truncation suffix

        # Test with custom limit
        result = await search_articles(context=mock_context, query="test", max_article_length=100)
        assert len(result["results"][0]["content"]) < 120  # 100 + truncation suffix

        # Test with no limit
        result = await search_articles(context=mock_context, query="test", max_article_length=None)
        assert len(result["results"][0]["content"]) == 1000
