"""Tests for LSBible API client."""

import pytest
import respx
from httpx import Response

from lsbible.cache import MemoryCacheProvider
from lsbible.client import LSBibleClient
from lsbible.exceptions import APIError, BuildIDError, InvalidReferenceError
from lsbible.models import BookName


class TestLSBibleClient:
    """Test LSBibleClient class."""

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_build_id_from_homepage(self, sample_homepage_html):
        """Test extracting build ID from homepage."""
        # Mock homepage request
        respx.get("https://read.lsbible.org").mock(
            return_value=Response(200, text=sample_homepage_html)
        )

        async with LSBibleClient() as client:
            build_id = await client._get_build_id()
            assert build_id == "test-build-id-123"

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_build_id_caching(self, sample_homepage_html):
        """Test that build ID is cached after first fetch."""
        respx.get("https://read.lsbible.org").mock(
            return_value=Response(200, text=sample_homepage_html)
        )

        async with LSBibleClient() as client:
            build_id1 = await client._get_build_id()
            build_id2 = await client._get_build_id()

            assert build_id1 == build_id2
            # Should only make one request (cached after first)
            assert respx.calls.call_count == 1

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_build_id_with_provided_id(self, sample_homepage_html):
        """Test that provided build ID is used without fetching."""
        async with LSBibleClient(build_id="provided-build-id") as client:
            build_id = await client._get_build_id()

            assert build_id == "provided-build-id"
            # Should not make any HTTP requests
            assert respx.calls.call_count == 0

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_build_id_failure(self):
        """Test that BuildIDError is raised when build ID cannot be found."""
        respx.get("https://read.lsbible.org").mock(
            return_value=Response(200, text="<html>No build ID here</html>")
        )

        async with LSBibleClient() as client:
            with pytest.raises(BuildIDError, match="Could not determine build ID"):
                await client._get_build_id()

    @pytest.mark.asyncio
    @respx.mock
    async def test_search(self, sample_homepage_html, sample_api_response):
        """Test search method."""
        # Mock homepage and API requests
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            response = await client.search("John 3:16")

            assert response.query == "John 3:16"
            assert len(response.passages) == 1
            assert response.passages[0].title == "John 3:16"

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_verse(self, sample_homepage_html, sample_api_response):
        """Test get_verse method."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            passage = await client.get_verse(BookName.JOHN, 3, 16)

            assert passage.title == "John 3:16"
            assert passage.from_ref.book_number == 43
            assert passage.from_ref.chapter == 3
            assert passage.from_ref.verse == 16
            assert passage.is_single_verse is True

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_verse_with_string(self, sample_homepage_html, sample_api_response):
        """Test get_verse with string book name."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            passage = await client.get_verse("John", 3, 16)
            assert passage.from_ref.book_number == 43

    @pytest.mark.asyncio
    async def test_get_verse_validates_reference(self):
        """Test that get_verse validates references."""
        async with LSBibleClient() as client:
            # Invalid chapter
            with pytest.raises(InvalidReferenceError, match="only has 21 chapters"):
                await client.get_verse(BookName.JOHN, 99, 1)

            # Invalid verse
            with pytest.raises(InvalidReferenceError, match="only has 36 verses"):
                await client.get_verse(BookName.JOHN, 3, 999)

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_passage(self, sample_homepage_html, sample_multi_verse_response):
        """Test get_passage method for verse ranges."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_multi_verse_response))

        async with LSBibleClient() as client:
            passage = await client.get_passage(BookName.JOHN, 3, 16, BookName.JOHN, 3, 17)

            assert passage.from_ref.verse == 16
            assert passage.to_ref.verse == 17
            assert passage.is_single_verse is False
            assert passage.verse_count == 2

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_chapter(self, sample_homepage_html, sample_api_response):
        """Test get_chapter method."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            passage = await client.get_chapter(BookName.JOHN, 3)
            assert passage.from_ref.chapter == 3

    @pytest.mark.asyncio
    async def test_get_chapter_validates_chapter(self):
        """Test that get_chapter validates chapter exists."""
        async with LSBibleClient() as client:
            # John only has 21 chapters
            with pytest.raises(APIError, match="only has 21 chapters"):
                await client.get_chapter(BookName.JOHN, 99)

    @pytest.mark.asyncio
    @respx.mock
    async def test_cache_integration(self, sample_homepage_html, sample_api_response):
        """Test that responses are cached."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        api_mock = respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient(cache={"provider": MemoryCacheProvider()}) as client:
            # First request
            response1 = await client.search("John 3:16")
            api_call_count_1 = api_mock.call_count

            # Second request (should be cached)
            response2 = await client.search("John 3:16")
            api_call_count_2 = api_mock.call_count

            # Verify responses are the same
            assert response1.query == response2.query
            # API should only be called once (second was cached)
            assert api_call_count_1 == 1
            assert api_call_count_2 == 1

    @pytest.mark.asyncio
    @respx.mock
    async def test_clear_cache(self, sample_homepage_html, sample_api_response):
        """Test clearing the cache."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        api_mock = respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        cache_provider = MemoryCacheProvider()
        async with LSBibleClient(cache={"provider": cache_provider}) as client:
            # First request
            await client.search("John 3:16")
            assert api_mock.call_count == 1

            # Clear cache (synchronous method)
            cache_provider.clear()

            # Second request (should not be cached)
            await client.search("John 3:16")
            assert api_mock.call_count == 2

    @pytest.mark.asyncio
    @respx.mock
    async def test_api_error_handling(self, sample_homepage_html):
        """Test API error handling."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(500, text="Server error"))

        async with LSBibleClient() as client:
            with pytest.raises(APIError, match="API request failed with status 500"):
                await client.search("test")

    @pytest.mark.asyncio
    @respx.mock
    async def test_context_manager(self, sample_homepage_html, sample_api_response):
        """Test using client as async context manager."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            passage = await client.get_verse(BookName.JOHN, 3, 16)
            assert passage.title == "John 3:16"

        # Client should be closed after exiting context

    @pytest.mark.asyncio
    async def test_client_initialization(self):
        """Test client initialization with custom parameters."""
        cache_provider = MemoryCacheProvider()
        async with LSBibleClient(
            cache={"provider": cache_provider},
            timeout=60,
            build_id="custom-id",
        ) as client:
            assert client._cache_provider is cache_provider
            assert client._build_id == "custom-id"

    @pytest.mark.asyncio
    @respx.mock
    async def test_api_request_headers(self, sample_homepage_html, sample_api_response):
        """Test that API requests include correct headers."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        api_mock = respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        async with LSBibleClient() as client:
            await client.search("test")

            # Verify headers were sent
            request = api_mock.calls.last.request
            assert request.headers.get("accept") == "*/*"
            assert request.headers.get("referer") == "https://read.lsbible.org/"
            assert request.headers.get("x-nextjs-data") == "1"

            # Verify User-Agent header is set
            user_agent = request.headers.get("user-agent")
            assert user_agent is not None
            assert user_agent.startswith("lsbible-python/")
            assert "Python/" in user_agent
            assert "+https://github.com/kdcokenny/lsbible" in user_agent

    @pytest.mark.asyncio
    @respx.mock
    async def test_custom_headers_override(self, sample_homepage_html, sample_api_response):
        """Test that custom headers can override defaults."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        api_mock = respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(return_value=Response(200, json=sample_api_response))

        # Create client with custom User-Agent
        custom_ua = "my-custom-client/1.0"
        async with LSBibleClient(headers={"User-Agent": custom_ua}) as client:
            await client.search("test")

            # Verify custom User-Agent is used
            request = api_mock.calls.last.request
            assert request.headers.get("user-agent") == custom_ua

    @pytest.mark.asyncio
    @respx.mock
    async def test_no_passages_raises_error(self, sample_homepage_html):
        """Test that no passages in response raises APIError."""
        respx.get("https://read.lsbible.org/").mock(
            return_value=Response(200, text=sample_homepage_html)
        )
        respx.route(
            method="GET",
            url__regex=r"https://read\.lsbible\.org/_next/data/test-build-id-123/index\.json.*",
        ).mock(
            return_value=Response(
                200,
                json={
                    "pageProps": {
                        "counter": 0,
                        "q": "nonexistent",
                        "searchMatchCount": 0,
                        "passages": [],
                        "start": 123,
                        "duration": 1,
                    }
                },
            )
        )

        async with LSBibleClient() as client:
            with pytest.raises(APIError, match="No passage found"):
                await client.get_verse(BookName.JOHN, 3, 16)
