"""Tests for authentication client."""

import warnings
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest
import respx
from httpx import Response

from platform_2step_mcp.auth.client import AuthClient
from platform_2step_mcp.auth.exceptions import (
    AuthError,
    DeviceFlowDeniedError,
    DeviceFlowExpiredError,
    InvalidCompletionCodeError,
    RefreshError,
    SessionNotFoundError,
    SessionRevocationForbiddenError,
)
from platform_2step_mcp.auth.models import TokenData


@pytest.fixture
def bff_url() -> str:
    """BFF URL for testing."""
    return "https://plt-2steps-bff.test.agendapro.com"


@pytest.fixture
def temp_storage_path(tmp_path) -> str:
    """Temporary storage path."""
    return str(tmp_path / "tokens.json")


@pytest.fixture
def auth_client(bff_url: str, temp_storage_path: str) -> AuthClient:
    """Create an AuthClient for testing."""
    return AuthClient(
        bff_url=bff_url,
        token_storage_path=temp_storage_path,
    )


@pytest.fixture
def device_auth_response() -> dict:
    """Sample device auth response."""
    return {
        "device_code": "test-device-code",
        "user_code": "TEST-1234",
        "verification_uri": "https://auth.agendapro.com/device",
        "verification_uri_complete": "https://auth.agendapro.com/device?user_code=TEST-1234",
        "expires_in": 1800,
        "interval": 1,  # Short interval for tests
    }


@pytest.fixture
def token_response() -> dict:
    """Sample token response."""
    return {
        "access_token": "test-access-token",
        "refresh_token": "test-refresh-token",
        "token_type": "Bearer",
        "expires_in": 3600,
        "scope": "read create_pending",
    }


class TestAuthClientInit:
    """Tests for AuthClient initialization."""

    def test_init_with_bff_url(self, bff_url: str, temp_storage_path: str):
        """Test initialization with BFF URL."""
        client = AuthClient(bff_url=bff_url, token_storage_path=temp_storage_path)
        assert client.bff_url == bff_url
        assert client.client_id == "platform-mcp"

    def test_init_with_custom_client_id(self, bff_url: str, temp_storage_path: str):
        """Test initialization with custom client ID."""
        client = AuthClient(
            bff_url=bff_url,
            client_id="custom-client",
            token_storage_path=temp_storage_path,
        )
        assert client.client_id == "custom-client"

    def test_init_strips_trailing_slash(self, temp_storage_path: str):
        """Test that trailing slash is stripped from BFF URL."""
        client = AuthClient(
            bff_url="https://example.com/",
            token_storage_path=temp_storage_path,
        )
        assert client.bff_url == "https://example.com"


class TestStartDeviceAuth:
    """Tests for starting device authorization."""

    @respx.mock
    def test_start_device_auth_success(
        self, auth_client: AuthClient, bff_url: str, device_auth_response: dict
    ):
        """Test successful device auth start."""
        respx.post(f"{bff_url}/v1/auth/device").mock(
            return_value=Response(200, json=device_auth_response)
        )

        result = auth_client.start_device_auth()

        assert result.device_code == "test-device-code"
        assert result.user_code == "TEST-1234"
        assert result.expires_in == 1800

    @respx.mock
    def test_start_device_auth_failure(self, auth_client: AuthClient, bff_url: str):
        """Test device auth start failure."""
        respx.post(f"{bff_url}/v1/auth/device").mock(
            return_value=Response(500, json={"error": "server_error"})
        )

        with pytest.raises(AuthError, match="Failed to start device auth"):
            auth_client.start_device_auth()


class TestExchangeCompletionCode:
    """Tests for completion code exchange."""

    @respx.mock
    def test_exchange_success(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test successful completion code exchange."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(200, json=token_response)
        )

        result = auth_client.exchange_completion_code(
            device_code="test-device-code",
            completion_code="COMPLETE-test-code",
        )

        assert result.access_token == "test-access-token"
        assert result.refresh_token == "test-refresh-token"

    @respx.mock
    def test_exchange_invalid_completion_code(
        self, auth_client: AuthClient, bff_url: str
    ):
        """Test exchange with invalid completion code raises InvalidCompletionCodeError."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(400, json={"error": "invalid_grant"})
        )

        with pytest.raises(InvalidCompletionCodeError):
            auth_client.exchange_completion_code(
                device_code="test-device-code",
                completion_code="INVALID-CODE",
            )

    @respx.mock
    def test_exchange_expired_device_code(self, auth_client: AuthClient, bff_url: str):
        """Test exchange with expired device code raises DeviceFlowExpiredError."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(400, json={"error": "expired_token"})
        )

        with pytest.raises(DeviceFlowExpiredError):
            auth_client.exchange_completion_code(
                device_code="expired-device-code",
                completion_code="COMPLETE-test-code",
            )

    @respx.mock
    def test_exchange_access_denied(self, auth_client: AuthClient, bff_url: str):
        """Test exchange with access denied raises DeviceFlowDeniedError."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(400, json={"error": "access_denied"})
        )

        with pytest.raises(DeviceFlowDeniedError):
            auth_client.exchange_completion_code(
                device_code="test-device-code",
                completion_code="COMPLETE-test-code",
            )

    @respx.mock
    def test_exchange_non_json_error_response(self, auth_client: AuthClient, bff_url: str):
        """Test exchange with non-JSON response raises AuthError with details."""
        html_response = "<html><body>404 Not Found</body></html>"
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(404, text=html_response)
        )

        with pytest.raises(AuthError) as exc_info:
            auth_client.exchange_completion_code(
                device_code="test-device-code",
                completion_code="COMPLETE-test-code",
            )

        assert "non-JSON response" in str(exc_info.value)
        assert "404" in str(exc_info.value)


class TestDeviceAuthFlowWithCompletion:
    """Tests for full device auth flow with completion code callback."""

    @respx.mock
    def test_device_auth_flow_with_callback(
        self,
        bff_url: str,
        temp_storage_path: str,
        device_auth_response: dict,
        token_response: dict,
    ):
        """Test full device auth flow using completion code callback."""
        # Create client with mock callback
        mock_callback = lambda: "COMPLETE-mock-code"
        client = AuthClient(
            bff_url=bff_url,
            token_storage_path=temp_storage_path,
            completion_code_callback=mock_callback,
        )

        respx.post(f"{bff_url}/v1/auth/device").mock(
            return_value=Response(200, json=device_auth_response)
        )
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(200, json=token_response)
        )

        result = client.authenticate()

        assert result.access_token == "test-access-token"


class TestPollForToken:
    """Tests for token polling (deprecated)."""

    @respx.mock
    def test_poll_success_immediate(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test immediate token polling success."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(200, json=token_response)
        )

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            result = auth_client.poll_for_token("test-device-code", interval=0, timeout=10)
            # Filter for our specific deprecation warning about poll_for_token
            poll_warnings = [
                x for x in w
                if issubclass(x.category, DeprecationWarning)
                and "poll_for_token" in str(x.message)
            ]
            assert len(poll_warnings) == 1
            assert "exchange_completion_code" in str(poll_warnings[0].message)

        assert result.access_token == "test-access-token"
        assert result.refresh_token == "test-refresh-token"

    @respx.mock
    def test_poll_handles_authorization_pending(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test polling handles authorization_pending."""
        route = respx.post(f"{bff_url}/v1/auth/token")
        route.side_effect = [
            Response(400, json={"error": "authorization_pending"}),
            Response(200, json=token_response),
        ]

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            result = auth_client.poll_for_token("test-device-code", interval=0, timeout=10)

        assert result.access_token == "test-access-token"

    @respx.mock
    def test_poll_handles_slow_down(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test polling handles slow_down by increasing interval."""
        route = respx.post(f"{bff_url}/v1/auth/token")
        route.side_effect = [
            Response(400, json={"error": "slow_down"}),
            Response(200, json=token_response),
        ]

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            result = auth_client.poll_for_token("test-device-code", interval=0, timeout=30)

        assert result.access_token == "test-access-token"

    @respx.mock
    def test_poll_raises_on_expired_token(self, auth_client: AuthClient, bff_url: str):
        """Test polling raises DeviceFlowExpiredError on expired_token."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(400, json={"error": "expired_token"})
        )

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            with pytest.raises(DeviceFlowExpiredError):
                auth_client.poll_for_token("test-device-code", interval=0, timeout=10)

    @respx.mock
    def test_poll_raises_on_access_denied(self, auth_client: AuthClient, bff_url: str):
        """Test polling raises DeviceFlowDeniedError on access_denied."""
        respx.post(f"{bff_url}/v1/auth/token").mock(
            return_value=Response(400, json={"error": "access_denied"})
        )

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            with pytest.raises(DeviceFlowDeniedError):
                auth_client.poll_for_token("test-device-code", interval=0, timeout=10)


class TestRefreshToken:
    """Tests for token refresh."""

    @respx.mock
    def test_refresh_success(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test successful token refresh."""
        respx.post(f"{bff_url}/v1/auth/refresh").mock(
            return_value=Response(200, json=token_response)
        )

        result = auth_client.refresh_token("old-refresh-token")

        assert result.access_token == "test-access-token"

    @respx.mock
    def test_refresh_failure(self, auth_client: AuthClient, bff_url: str):
        """Test token refresh failure."""
        respx.post(f"{bff_url}/v1/auth/refresh").mock(
            return_value=Response(401, json={"error": "invalid_grant"})
        )

        with pytest.raises(RefreshError):
            auth_client.refresh_token("invalid-refresh-token")


class TestAuthenticate:
    """Tests for the authenticate method."""

    def test_authenticate_uses_cached_tokens(
        self, auth_client: AuthClient, temp_storage_path: str
    ):
        """Test that authenticate uses valid cached tokens."""
        # Pre-save valid tokens
        valid_token = TokenData(
            access_token="cached-token",
            refresh_token="cached-refresh",
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client.storage.save(valid_token)

        result = auth_client.authenticate()

        assert result.access_token == "cached-token"

    @respx.mock
    def test_authenticate_refreshes_expired_token(
        self, auth_client: AuthClient, bff_url: str, token_response: dict
    ):
        """Test that authenticate refreshes expired tokens with refresh token."""
        # Pre-save expired tokens with refresh token
        expired_token = TokenData(
            access_token="expired-token",
            refresh_token="valid-refresh",
            expires_at=datetime.utcnow() - timedelta(hours=1),
            scope="read",
        )
        auth_client.storage.save(expired_token)

        respx.post(f"{bff_url}/v1/auth/refresh").mock(
            return_value=Response(200, json=token_response)
        )

        result = auth_client.authenticate()

        assert result.access_token == "test-access-token"


class TestGetValidToken:
    """Tests for get_valid_token method."""

    def test_get_valid_token_returns_cached(self, auth_client: AuthClient):
        """Test that get_valid_token returns cached token when valid."""
        valid_token = TokenData(
            access_token="valid-token",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = valid_token

        result = auth_client.get_valid_token()

        assert result == "valid-token"


class TestClearTokens:
    """Tests for clear_tokens method."""

    def test_clear_tokens(self, auth_client: AuthClient):
        """Test that clear_tokens removes cached and stored tokens."""
        # Pre-save tokens
        token = TokenData(
            access_token="test",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = token
        auth_client.storage.save(token)

        auth_client.clear_tokens()

        assert auth_client._tokens is None
        assert not auth_client.storage.exists()


class TestRevokeSession:
    """Tests for revoke_session method."""

    @respx.mock
    def test_revoke_session_success(
        self, auth_client: AuthClient, bff_url: str
    ):
        """Test successful session revocation."""
        # Set up tokens
        token = TokenData(
            access_token="test-token",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = token

        respx.delete(f"{bff_url}/v1/auth/sessions/test-session-id").mock(
            return_value=Response(200, json={"message": "Session revoked"})
        )

        auth_client.revoke_session("test-session-id")

        # Tokens should be cleared after successful revocation
        assert auth_client._tokens is None

    def test_revoke_session_no_active_session(self, auth_client: AuthClient):
        """Test revoke_session raises error when no active session."""
        auth_client._tokens = None

        with pytest.raises(AuthError, match="No active session"):
            auth_client.revoke_session("test-session-id")

    @respx.mock
    def test_revoke_session_not_found(
        self, auth_client: AuthClient, bff_url: str
    ):
        """Test revoke_session raises SessionNotFoundError for 404."""
        token = TokenData(
            access_token="test-token",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = token

        respx.delete(f"{bff_url}/v1/auth/sessions/unknown-session").mock(
            return_value=Response(404, json={"error": "not_found"})
        )

        with pytest.raises(SessionNotFoundError) as exc_info:
            auth_client.revoke_session("unknown-session")

        assert exc_info.value.session_id == "unknown-session"

    @respx.mock
    def test_revoke_session_forbidden(
        self, auth_client: AuthClient, bff_url: str
    ):
        """Test revoke_session raises SessionRevocationForbiddenError for 403."""
        token = TokenData(
            access_token="test-token",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = token

        respx.delete(f"{bff_url}/v1/auth/sessions/other-user-session").mock(
            return_value=Response(403, json={"error": "forbidden"})
        )

        with pytest.raises(SessionRevocationForbiddenError):
            auth_client.revoke_session("other-user-session")

    @respx.mock
    def test_revoke_session_server_error(
        self, auth_client: AuthClient, bff_url: str
    ):
        """Test revoke_session raises AuthError for server errors."""
        token = TokenData(
            access_token="test-token",
            refresh_token=None,
            expires_at=datetime.utcnow() + timedelta(hours=1),
            scope="read",
        )
        auth_client._tokens = token

        respx.delete(f"{bff_url}/v1/auth/sessions/test-session-id").mock(
            return_value=Response(500, json={"error": "internal_error"})
        )

        with pytest.raises(AuthError, match="Failed to revoke session"):
            auth_client.revoke_session("test-session-id")
