"""
Tests for oaa.modules.base_url_session

Tests the BaseURLSession class which provides a requests.Session
with automatic base URL joining.

This test suite validates:
- Initialization and configuration validation
- URL path joining behavior
- HTTP method functionality
- Query parameter handling
- Session features (headers, cookies)
- Edge cases and unusual inputs

Tests use requests_mock to avoid requiring a real API endpoint.
"""

import pytest
import requests_mock
from requests import Session
from oaa.modules.base_url_session import BaseURLSession
from oaa.modules.exceptions import ConfigurationError


# ============================================================================
# Initialization Tests
# ============================================================================


class TestInitialization:
    """Test BaseURLSession initialization and validation."""

    def test_valid_base_url_without_trailing_slash(self):
        """Base URL without trailing slash should have one added."""
        session = BaseURLSession("https://api.example.com/v1")
        assert session.base_url == "https://api.example.com/v1/"

    def test_valid_base_url_with_trailing_slash(self):
        """Base URL with trailing slash should not be duplicated."""
        session = BaseURLSession("https://api.example.com/v1/")
        assert session.base_url == "https://api.example.com/v1/"

    def test_none_base_url_raises_configuration_error(self):
        """Initialization with None should raise ConfigurationError."""
        with pytest.raises(ConfigurationError) as exc_info:
            BaseURLSession(None)
        assert "base_url is required" in str(exc_info.value)

    def test_empty_string_base_url_raises_configuration_error(self):
        """Initialization with empty string should raise ConfigurationError."""
        with pytest.raises(ConfigurationError) as exc_info:
            BaseURLSession("")
        assert "base_url is required" in str(exc_info.value)

    def test_inherits_from_requests_session(self):
        """BaseURLSession should be a subclass of requests.Session."""
        session = BaseURLSession("https://api.example.com")
        assert isinstance(session, Session)


# ============================================================================
# URL Joining Tests
# ============================================================================


class TestURLJoining:
    """Test URL path joining behavior."""

    def test_url_with_leading_slash(self):
        """URL with leading slash should be stripped and joined correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users")
            assert response.status_code == 200
            assert m.last_request.url == "https://api.example.com/v1/users"

    def test_url_without_leading_slash(self):
        """URL without leading slash should be joined directly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("users")
            assert response.status_code == 200
            assert m.last_request.url == "https://api.example.com/v1/users"

    def test_empty_url(self):
        """Empty URL should just use base_url."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/", json={})
            response = session.get("")
            assert response.status_code == 200
            assert m.last_request.url == "https://api.example.com/v1/"

    def test_nested_path(self):
        """Nested paths should be joined correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/users/123/roles",
                json={"roles": []},
            )
            response = session.get("/users/123/roles")
            assert response.status_code == 200
            assert m.last_request.url == "https://api.example.com/v1/users/123/roles"

    def test_url_with_query_string(self):
        """Query string in URL should be preserved."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/users?active=true",
                json={"users": []},
            )
            response = session.get("/users?active=true")
            assert response.status_code == 200
            assert "active=true" in m.last_request.url

    def test_url_with_multiple_query_params(self):
        """Multiple query parameters should be preserved."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/users?page=1&limit=10",
                json={"users": []},
            )
            response = session.get("/users?page=1&limit=10")
            assert response.status_code == 200
            assert "page=1" in m.last_request.url
            assert "limit=10" in m.last_request.url

    def test_url_with_encoded_query_params(self):
        """URL encoding in query params should be preserved."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/search?q=hello%20world",
                json={"results": []},
            )
            response = session.get("/search?q=hello%20world")
            assert response.status_code == 200
            assert "hello%20world" in m.last_request.url


# ============================================================================
# HTTP Method Tests
# ============================================================================


class TestHTTPMethods:
    """Test all HTTP methods work correctly with mocked responses."""

    def test_get_request(self):
        """GET request should work correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users")
            assert response.status_code == 200
            assert response.json() == {"users": []}

    def test_post_request_with_json(self):
        """POST request with JSON body should work correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.post(
                "https://api.example.com/v1/users",
                json={"id": 1, "name": "test"},
            )
            response = session.post("/users", json={"name": "test"})
            assert response.status_code == 200
            assert response.json() == {"id": 1, "name": "test"}

    def test_put_request(self):
        """PUT request should work correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.put(
                "https://api.example.com/v1/users/1",
                json={"id": 1, "name": "updated"},
            )
            response = session.put("/users/1", json={"name": "updated"})
            assert response.status_code == 200
            assert response.json()["name"] == "updated"

    def test_delete_request(self):
        """DELETE request should work correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.delete("https://api.example.com/v1/users/1", status_code=204)
            response = session.delete("/users/1")
            assert response.status_code == 204

    def test_patch_request(self):
        """PATCH request should work correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.patch(
                "https://api.example.com/v1/users/1",
                json={"id": 1, "status": "active"},
            )
            response = session.patch("/users/1", json={"status": "active"})
            assert response.status_code == 200
            assert response.json()["status"] == "active"


# ============================================================================
# Query Parameters Tests
# ============================================================================


class TestQueryParameters:
    """Test query parameter handling via params kwarg."""

    def test_params_kwarg_added_to_url(self):
        """Query params via kwargs should be added to URL."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users", params={"active": "true"})
            assert response.status_code == 200
            assert "active=true" in m.last_request.url

    def test_params_kwarg_with_existing_query_string(self):
        """URL query string and params kwarg should both be present."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users?foo=bar", params={"baz": "qux"})
            assert response.status_code == 200
            # Both parameters should be in the final URL
            assert "foo=bar" in m.last_request.url
            assert "baz=qux" in m.last_request.url

    def test_params_kwarg_with_list_values(self):
        """List values in params should be encoded correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users", params={"ids": [1, 2, 3]})
            assert response.status_code == 200
            # List params are encoded as repeated keys
            assert "ids" in m.last_request.url

    def test_params_kwarg_with_none_values(self):
        """None values in params should be handled correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            # requests library drops None values from params
            response = session.get("/users", params={"key": None, "other": "value"})
            assert response.status_code == 200
            assert "other=value" in m.last_request.url


# ============================================================================
# Session Features Tests
# ============================================================================


class TestSessionFeatures:
    """Test session features like headers and cookies."""

    def test_headers_persist_across_requests(self):
        """Headers set on session should be sent with requests."""
        session = BaseURLSession("https://api.example.com/v1")
        session.headers.update({"X-Custom-Header": "custom-value"})

        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/users", json={"users": []})
            response = session.get("/users")
            assert response.status_code == 200
            assert m.last_request.headers["X-Custom-Header"] == "custom-value"

    def test_authorization_header(self):
        """Authorization header should be sent correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        session.headers.update({"Authorization": "Bearer token123"})

        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/v1/secure", json={"data": "secret"})
            response = session.get("/secure")
            assert response.status_code == 200
            assert m.last_request.headers["Authorization"] == "Bearer token123"

    def test_session_cookies_persist(self):
        """Cookies should be managed by session (BaseURLSession is a Session subclass)."""
        session = BaseURLSession("https://api.example.com/v1")

        with requests_mock.Mocker() as m:
            # Mock with a Set-Cookie header
            m.get(
                "https://api.example.com/v1/login",
                json={"success": True},
                headers={"Set-Cookie": "session_id=abc123; Path=/"},
            )
            # Second request should have access to session cookies
            m.get(
                "https://api.example.com/v1/users",
                json={"users": []},
            )

            # Make first request
            response1 = session.get("/login")
            assert response1.status_code == 200

            # Make second request
            response2 = session.get("/users")
            assert response2.status_code == 200

            # Session should have cookie jar (even if requests_mock doesn't populate it)
            # The important thing is that BaseURLSession inherits Session's cookie handling
            assert hasattr(session, "cookies")
            assert isinstance(session.cookies, type(session.cookies))


# ============================================================================
# Edge Cases
# ============================================================================


class TestEdgeCases:
    """Test edge cases and unusual inputs."""

    def test_base_url_with_path_component(self):
        """Base URL with path component should work correctly."""
        session = BaseURLSession("https://api.example.com/api/v1")
        assert session.base_url == "https://api.example.com/api/v1/"

        with requests_mock.Mocker() as m:
            m.get("https://api.example.com/api/v1/users", json={"users": []})
            response = session.get("/users")
            assert response.status_code == 200

    def test_multiple_leading_slashes_in_url(self):
        """Multiple leading slashes should be handled (only first is stripped)."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            # The code strips leading '/', so //users becomes /users
            m.get("https://api.example.com/v1//users", json={"users": []})
            response = session.get("//users")
            assert response.status_code == 200

    def test_base_url_with_port(self):
        """Base URL with explicit port should work correctly."""
        session = BaseURLSession("https://api.example.com:8080/v1")
        assert session.base_url == "https://api.example.com:8080/v1/"

        with requests_mock.Mocker() as m:
            m.get("https://api.example.com:8080/v1/users", json={"users": []})
            response = session.get("/users")
            assert response.status_code == 200

    def test_url_with_special_characters(self):
        """URLs with special characters should be handled correctly."""
        session = BaseURLSession("https://api.example.com/v1")
        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/search?name=john%2Bdoe",
                json={"results": []},
            )
            response = session.get("/search?name=john%2Bdoe")
            assert response.status_code == 200


# ============================================================================
# Integration Tests
# ============================================================================


class TestIntegration:
    """Test realistic integration scenarios."""

    def test_realistic_api_workflow(self):
        """Test a realistic multi-step API workflow."""
        session = BaseURLSession("https://api.example.com/v1")
        session.headers.update(
            {
                "Authorization": "Bearer token123",
                "Content-Type": "application/json",
            }
        )

        with requests_mock.Mocker() as m:
            # Step 1: Create a user
            m.post(
                "https://api.example.com/v1/users",
                json={"id": 1, "name": "John"},
            )
            # Step 2: Get the user
            m.get(
                "https://api.example.com/v1/users/1",
                json={"id": 1, "name": "John"},
            )
            # Step 3: Update the user
            m.put(
                "https://api.example.com/v1/users/1",
                json={"id": 1, "name": "John Updated"},
            )

            # Execute workflow
            create_response = session.post("/users", json={"name": "John"})
            assert create_response.json()["id"] == 1

            get_response = session.get("/users/1")
            assert get_response.json()["name"] == "John"

            update_response = session.put("/users/1", json={"name": "John Updated"})
            assert update_response.json()["name"] == "John Updated"

    def test_error_responses_still_work(self):
        """Test that error responses are handled correctly."""
        session = BaseURLSession("https://api.example.com/v1")

        with requests_mock.Mocker() as m:
            m.get(
                "https://api.example.com/v1/users/999",
                status_code=404,
                json={"error": "Not found"},
            )
            response = session.get("/users/999")
            assert response.status_code == 404
            assert response.json()["error"] == "Not found"
