"""Unit tests for AuthService."""

from pathlib import Path
from unittest.mock import MagicMock, patch
from uuid import UUID

import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta, timezone

from teams_phone.constants import TOKEN_EXPIRY_BUFFER_SECONDS
from teams_phone.exceptions import (
    AuthenticationError,
    ConfigurationError,
    NotFoundError,
)
from teams_phone.infrastructure import CacheManager, ConfigManager
from teams_phone.models import AuthMethod, AuthStatusType, CachedToken, TenantProfile
from teams_phone.services import SCOPES, AuthService
from teams_phone.services.auth_service import CLIENT_SECRET_ENV_VAR


def generate_test_certificate(tmp_path: Path) -> tuple[Path, str]:
    """Generate a self-signed certificate and private key for testing.

    Returns:
        Tuple of (certificate path, expected thumbprint uppercase hex).
    """
    # Generate a private key
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
    )

    # Generate a self-signed certificate
    subject = issuer = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Washington"),
            x509.NameAttribute(NameOID.LOCALITY_NAME, "Seattle"),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test"),
            x509.NameAttribute(NameOID.COMMON_NAME, "test.example.com"),
        ]
    )

    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.now(timezone.utc))
        .not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
        .sign(private_key, hashes.SHA256())
    )

    # Calculate expected thumbprint
    thumbprint = cert.fingerprint(hashes.SHA1()).hex().upper()

    # Serialize to PEM
    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
    key_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )

    # Write combined PEM file
    cert_path = tmp_path / "test_cert.pem"
    cert_path.write_bytes(cert_pem + key_pem)

    return cert_path, thumbprint


class TestAuthServiceInit:
    """Tests for AuthService initialization."""

    @pytest.mark.unit
    def test_accepts_dependencies(self, tmp_path: Path) -> None:
        """AuthService accepts ConfigManager and CacheManager."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))

        service = AuthService(config_manager, cache_manager)

        assert service.config_manager is config_manager
        assert service.cache_manager is cache_manager

    @pytest.mark.unit
    def test_stores_managers(self, tmp_path: Path) -> None:
        """AuthService stores config and cache managers as instance attributes."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))

        service = AuthService(config_manager, cache_manager)

        assert hasattr(service, "config_manager")
        assert hasattr(service, "cache_manager")


class TestScopes:
    """Tests for SCOPES constant."""

    @pytest.mark.unit
    def test_scopes_contains_graph_default(self) -> None:
        """SCOPES contains the Graph API default scope."""
        assert SCOPES == ["https://graph.microsoft.com/.default"]

    @pytest.mark.unit
    def test_scopes_is_list(self) -> None:
        """SCOPES is a list type."""
        assert isinstance(SCOPES, list)


class TestLoadCertificate:
    """Tests for _load_certificate method."""

    @pytest.mark.unit
    def test_loads_valid_certificate(self, tmp_path: Path) -> None:
        """_load_certificate loads valid PEM certificate and extracts thumbprint."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, expected_thumbprint = generate_test_certificate(tmp_path)

        result = service._load_certificate(str(cert_path))

        assert "private_key" in result
        assert "thumbprint" in result
        assert result["thumbprint"] == expected_thumbprint
        assert "-----BEGIN PRIVATE KEY-----" in result["private_key"]

    @pytest.mark.unit
    def test_raises_for_missing_file(self, tmp_path: Path) -> None:
        """_load_certificate raises AuthenticationError for missing file."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(AuthenticationError) as exc_info:
            service._load_certificate("/nonexistent/path/cert.pem")

        assert "Certificate file not found" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "cert_path" in exc_info.value.remediation

    @pytest.mark.unit
    def test_raises_for_permission_error(self, tmp_path: Path) -> None:
        """_load_certificate raises AuthenticationError for permission issues."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path = tmp_path / "restricted.pem"
        cert_path.write_text("dummy")

        with patch.object(
            Path, "read_bytes", side_effect=PermissionError("Access denied")
        ):
            with pytest.raises(AuthenticationError) as exc_info:
                service._load_certificate(str(cert_path))

        assert "Cannot read certificate file" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "permission" in exc_info.value.remediation.lower()

    @pytest.mark.unit
    def test_raises_for_invalid_certificate_format(self, tmp_path: Path) -> None:
        """_load_certificate raises AuthenticationError for invalid certificate."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        invalid_cert = tmp_path / "invalid.pem"
        invalid_cert.write_text("not a valid PEM certificate")

        with pytest.raises(AuthenticationError) as exc_info:
            service._load_certificate(str(invalid_cert))

        # Should raise for either invalid cert or invalid key
        error_msg = str(exc_info.value)
        assert (
            "Invalid certificate format" in error_msg
            or "Invalid private key format" in error_msg
        )
        assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_raises_for_missing_private_key(self, tmp_path: Path) -> None:
        """_load_certificate raises AuthenticationError when private key is missing."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Generate a certificate without private key
        private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        subject = issuer = x509.Name(
            [
                x509.NameAttribute(NameOID.COMMON_NAME, "test.example.com"),
            ]
        )
        cert = (
            x509.CertificateBuilder()
            .subject_name(subject)
            .issuer_name(issuer)
            .public_key(private_key.public_key())
            .serial_number(x509.random_serial_number())
            .not_valid_before(datetime.now(timezone.utc))
            .not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
            .sign(private_key, hashes.SHA256())
        )

        cert_only_path = tmp_path / "cert_only.pem"
        cert_only_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))

        with pytest.raises(AuthenticationError) as exc_info:
            service._load_certificate(str(cert_only_path))

        assert "Invalid private key format" in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "private key" in exc_info.value.remediation.lower()

    @pytest.mark.unit
    def test_expands_tilde_in_path(self, tmp_path: Path) -> None:
        """_load_certificate expands ~ in certificate path."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Mock expanduser to return a nonexistent path
        with patch.object(
            Path, "expanduser", return_value=Path("/nonexistent/cert.pem")
        ):
            with pytest.raises(AuthenticationError) as exc_info:
                service._load_certificate("~/certs/test.pem")

            # Should have attempted to read from expanded path
            assert "Certificate file not found" in str(exc_info.value)

    @pytest.mark.unit
    def test_thumbprint_is_uppercase_hex(self, tmp_path: Path) -> None:
        """_load_certificate returns thumbprint as uppercase hex string."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)

        result = service._load_certificate(str(cert_path))

        thumbprint = result["thumbprint"]
        # Should be uppercase
        assert thumbprint == thumbprint.upper()
        # Should be hex (40 chars for SHA-1)
        assert len(thumbprint) == 40
        assert all(c in "0123456789ABCDEF" for c in thumbprint)


class TestCreateMsalApp:
    """Tests for _create_msal_app method."""

    @pytest.mark.unit
    def test_creates_app_with_certificate_auth(self, tmp_path: Path) -> None:
        """_create_msal_app creates ConfidentialClientApplication for certificate auth."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, expected_thumbprint = generate_test_certificate(tmp_path)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )

        # Mock MSAL to avoid network calls during unit tests
        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_app.return_value = mock_instance

            app = service._create_msal_app(profile)

            assert app is mock_instance
            mock_app.assert_called_once()
            # Verify correct parameters were passed
            call_kwargs = mock_app.call_args[1]
            assert call_kwargs["client_id"] == str(profile.client_id)
            assert (
                call_kwargs["authority"]
                == f"https://login.microsoftonline.com/{profile.tenant_id}"
            )
            assert call_kwargs["client_credential"]["thumbprint"] == expected_thumbprint

    @pytest.mark.unit
    def test_uses_correct_authority_url(self, tmp_path: Path) -> None:
        """_create_msal_app uses correct authority URL format."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        tenant_id = UUID("12345678-1234-1234-1234-123456789012")

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=tenant_id,
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            service._create_msal_app(profile)

            mock_app.assert_called_once()
            call_kwargs = mock_app.call_args[1]
            assert (
                call_kwargs["authority"]
                == f"https://login.microsoftonline.com/{tenant_id}"
            )

    @pytest.mark.unit
    def test_raises_for_missing_cert_path(self, tmp_path: Path) -> None:
        """_create_msal_app raises AuthenticationError when cert_path is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=None,  # Missing cert_path
        )

        with pytest.raises(AuthenticationError) as exc_info:
            service._create_msal_app(profile)

        assert "Certificate path not configured" in str(exc_info.value)
        assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_creates_app_with_client_secret_auth(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_create_msal_app creates ConfidentialClientApplication for client-secret auth."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.setenv(CLIENT_SECRET_ENV_VAR, "test-client-secret")

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CLIENT_SECRET,
        )

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_app.return_value = mock_instance

            app = service._create_msal_app(profile)

            assert app is mock_instance
            mock_app.assert_called_once()
            call_kwargs = mock_app.call_args[1]
            assert call_kwargs["client_id"] == str(profile.client_id)
            assert (
                call_kwargs["authority"]
                == f"https://login.microsoftonline.com/{profile.tenant_id}"
            )
            assert call_kwargs["client_credential"] == "test-client-secret"

    @pytest.mark.unit
    def test_client_secret_raises_when_env_var_missing(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_create_msal_app raises AuthenticationError when client secret env var is not set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.delenv(CLIENT_SECRET_ENV_VAR, raising=False)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CLIENT_SECRET,
        )

        with pytest.raises(AuthenticationError) as exc_info:
            service._create_msal_app(profile)

        assert CLIENT_SECRET_ENV_VAR in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "export" in exc_info.value.remediation.lower()

    @pytest.mark.unit
    def test_client_secret_raises_when_env_var_empty(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_create_msal_app raises AuthenticationError when client secret env var is empty."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.setenv(CLIENT_SECRET_ENV_VAR, "")

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CLIENT_SECRET,
        )

        with pytest.raises(AuthenticationError) as exc_info:
            service._create_msal_app(profile)

        assert CLIENT_SECRET_ENV_VAR in str(exc_info.value)

    @pytest.mark.unit
    def test_passes_client_id_as_string(self, tmp_path: Path) -> None:
        """_create_msal_app passes client_id as string to MSAL."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        client_id = UUID("87654321-4321-4321-4321-210987654321")

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=client_id,
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            service._create_msal_app(profile)

            call_kwargs = mock_app.call_args[1]
            assert call_kwargs["client_id"] == str(client_id)

    @pytest.mark.unit
    def test_passes_certificate_credential(self, tmp_path: Path) -> None:
        """_create_msal_app passes certificate credential dict to MSAL."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, expected_thumbprint = generate_test_certificate(tmp_path)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            service._create_msal_app(profile)

            call_kwargs = mock_app.call_args[1]
            credential = call_kwargs["client_credential"]

            assert "private_key" in credential
            assert "thumbprint" in credential
            assert credential["thumbprint"] == expected_thumbprint
            assert "-----BEGIN PRIVATE KEY-----" in credential["private_key"]


class TestGetClientSecret:
    """Tests for _get_client_secret method."""

    @pytest.mark.unit
    def test_reads_secret_from_env_var(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_get_client_secret reads from TEAMS_PHONE_CLIENT_SECRET env var."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.setenv(CLIENT_SECRET_ENV_VAR, "test-secret-value")

        secret = service._get_client_secret()

        assert secret == "test-secret-value"

    @pytest.mark.unit
    def test_raises_when_env_var_not_set(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_get_client_secret raises AuthenticationError when env var not set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.delenv(CLIENT_SECRET_ENV_VAR, raising=False)

        with pytest.raises(AuthenticationError) as exc_info:
            service._get_client_secret()

        assert CLIENT_SECRET_ENV_VAR in str(exc_info.value)
        assert exc_info.value.remediation is not None
        assert "export" in exc_info.value.remediation.lower()

    @pytest.mark.unit
    def test_raises_when_env_var_empty(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_get_client_secret raises AuthenticationError when env var is empty string."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.setenv(CLIENT_SECRET_ENV_VAR, "")

        with pytest.raises(AuthenticationError) as exc_info:
            service._get_client_secret()

        assert CLIENT_SECRET_ENV_VAR in str(exc_info.value)

    @pytest.mark.unit
    def test_preserves_whitespace_in_secret(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_get_client_secret preserves whitespace in secret value."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Secret with leading/trailing whitespace - should be preserved as-is
        secret_with_whitespace = "  secret-with-spaces  "
        monkeypatch.setenv(CLIENT_SECRET_ENV_VAR, secret_with_whitespace)

        secret = service._get_client_secret()

        assert secret == secret_with_whitespace

    @pytest.mark.unit
    def test_remediation_includes_instructions(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_get_client_secret includes helpful remediation in error."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        monkeypatch.delenv(CLIENT_SECRET_ENV_VAR, raising=False)

        with pytest.raises(AuthenticationError) as exc_info:
            service._get_client_secret()

        assert exc_info.value.remediation is not None
        assert CLIENT_SECRET_ENV_VAR in exc_info.value.remediation
        assert "dev/test" in exc_info.value.remediation.lower()
        assert "certificate" in exc_info.value.remediation.lower()


class TestResolveTenant:
    """Tests for _resolve_tenant method."""

    @pytest.mark.unit
    def test_resolves_explicit_tenant_name(self, tmp_path: Path) -> None:
        """_resolve_tenant returns profile for explicit tenant name."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        result = service._resolve_tenant("test-tenant")

        assert result.name == "test-tenant"
        assert result.tenant_id == profile.tenant_id

    @pytest.mark.unit
    def test_resolves_default_tenant_when_none(self, tmp_path: Path) -> None:
        """_resolve_tenant uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        result = service._resolve_tenant(None)

        assert result.name == "default-tenant"

    @pytest.mark.unit
    def test_raises_configuration_error_when_no_default(self, tmp_path: Path) -> None:
        """_resolve_tenant raises ConfigurationError when None and no default set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(ConfigurationError) as exc_info:
            service._resolve_tenant(None)

        assert "No tenant specified" in str(exc_info.value)
        assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_raises_not_found_for_unknown_tenant(self, tmp_path: Path) -> None:
        """_resolve_tenant raises NotFoundError for non-existent tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(NotFoundError) as exc_info:
            service._resolve_tenant("nonexistent-tenant")

        assert "nonexistent-tenant" in str(exc_info.value)


class TestNeedsRefresh:
    """Tests for _needs_refresh method."""

    @pytest.mark.unit
    def test_returns_true_for_expired_token(self, tmp_path: Path) -> None:
        """_needs_refresh returns True when token is expired."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        expired_token = CachedToken(
            access_token="expired-token",
            expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )

        assert service._needs_refresh(expired_token) is True

    @pytest.mark.unit
    def test_returns_true_for_token_within_buffer(self, tmp_path: Path) -> None:
        """_needs_refresh returns True when token expires within buffer period."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Token expires in 2 minutes (less than 5-minute buffer)
        near_expiry_token = CachedToken(
            access_token="near-expiry-token",
            expires_at=datetime.now(timezone.utc) + timedelta(minutes=2),
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )

        assert service._needs_refresh(near_expiry_token) is True

    @pytest.mark.unit
    def test_returns_false_for_valid_token(self, tmp_path: Path) -> None:
        """_needs_refresh returns False when token is valid and not near expiry."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        valid_token = CachedToken(
            access_token="valid-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )

        assert service._needs_refresh(valid_token) is False

    @pytest.mark.unit
    def test_handles_naive_datetime(self, tmp_path: Path) -> None:
        """_needs_refresh handles naive datetime by assuming UTC."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Create token with naive datetime (no timezone) well in the future
        # Use UTC's concept of "now" + 1 hour to ensure it's valid regardless of local timezone
        utc_now = datetime.now(timezone.utc)
        naive_expires = utc_now.replace(tzinfo=None) + timedelta(hours=1)
        valid_token = CachedToken(
            access_token="valid-token",
            expires_at=naive_expires,
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )

        # Should not raise and should return False (valid token)
        assert service._needs_refresh(valid_token) is False

    @pytest.mark.unit
    def test_uses_correct_buffer_value(self, tmp_path: Path) -> None:
        """_needs_refresh uses TOKEN_EXPIRY_BUFFER_SECONDS (300 seconds)."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Token expires in exactly 5 minutes - should need refresh
        at_buffer_token = CachedToken(
            access_token="at-buffer-token",
            expires_at=datetime.now(timezone.utc)
            + timedelta(seconds=TOKEN_EXPIRY_BUFFER_SECONDS),
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )
        assert service._needs_refresh(at_buffer_token) is True

        # Token expires in 6 minutes - should NOT need refresh
        after_buffer_token = CachedToken(
            access_token="after-buffer-token",
            expires_at=datetime.now(timezone.utc)
            + timedelta(seconds=TOKEN_EXPIRY_BUFFER_SECONDS + 60),
            tenant_id="12345678-1234-1234-1234-123456789012",
            scopes=SCOPES,
        )
        assert service._needs_refresh(after_buffer_token) is False


class TestRefreshToken:
    """Tests for refresh_token method."""

    @pytest.mark.unit
    def test_acquires_and_caches_token(self, tmp_path: Path) -> None:
        """refresh_token acquires token via MSAL and saves to cache."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "test-access-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.refresh_token("test-tenant")

            assert result.access_token == "test-access-token"
            assert result.tenant_id == str(profile.tenant_id)
            assert result.scopes == SCOPES

            # Verify token was cached
            cached = cache_manager.get_token("test-tenant")
            assert cached is not None
            assert cached.access_token == "test-access-token"

    @pytest.mark.unit
    def test_raises_authentication_error_on_msal_failure(self, tmp_path: Path) -> None:
        """refresh_token raises AuthenticationError when MSAL returns error."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "error": "invalid_client",
                "error_description": "The client credentials are invalid",
            }
            mock_app.return_value = mock_instance

            with pytest.raises(AuthenticationError) as exc_info:
                service.refresh_token("test-tenant")

            assert "invalid_client" in str(exc_info.value)
            assert exc_info.value.remediation is not None

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """refresh_token uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "default-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.refresh_token(None)

            assert result.access_token == "default-token"

            # Verify cached under default tenant name
            cached = cache_manager.get_token("default-tenant")
            assert cached is not None

    @pytest.mark.unit
    def test_calculates_expiration_correctly(self, tmp_path: Path) -> None:
        """refresh_token calculates expiration from expires_in."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        before_call = datetime.now(timezone.utc)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "test-token",
                "expires_in": 7200,  # 2 hours
            }
            mock_app.return_value = mock_instance

            result = service.refresh_token("test-tenant")

            after_call = datetime.now(timezone.utc)

            # Expiration should be approximately 2 hours from now
            expected_min = before_call + timedelta(seconds=7200)
            expected_max = after_call + timedelta(seconds=7200)

            assert result.expires_at >= expected_min
            assert result.expires_at <= expected_max

    @pytest.mark.unit
    def test_handles_invalid_grant_error(self, tmp_path: Path) -> None:
        """refresh_token provides specific remediation for invalid_grant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "error": "invalid_grant",
                "error_description": "AADSTS65001: The user or administrator has not consented",
            }
            mock_app.return_value = mock_instance

            with pytest.raises(AuthenticationError) as exc_info:
                service.refresh_token("test-tenant")

            assert "invalid_grant" in str(exc_info.value)
            assert exc_info.value.remediation is not None
            assert "admin consent" in exc_info.value.remediation.lower()


class TestGetToken:
    """Tests for get_token method."""

    @pytest.mark.unit
    def test_returns_cached_token_when_valid(self, tmp_path: Path) -> None:
        """get_token returns cached token when it's still valid."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate cache with valid token
        valid_token = CachedToken(
            access_token="cached-valid-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        # Should not call MSAL
        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            result = service.get_token("test-tenant")

            mock_app.assert_not_called()
            assert result == "cached-valid-token"

    @pytest.mark.unit
    def test_refreshes_expired_token(self, tmp_path: Path) -> None:
        """get_token refreshes token when cached token is expired."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate cache with expired token
        expired_token = CachedToken(
            access_token="expired-token",
            expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", expired_token)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "new-access-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.get_token("test-tenant")

            assert result == "new-access-token"
            mock_instance.acquire_token_for_client.assert_called_once()

    @pytest.mark.unit
    def test_refreshes_token_within_buffer(self, tmp_path: Path) -> None:
        """get_token refreshes token when within buffer period."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate cache with token expiring in 2 minutes (within 5-min buffer)
        near_expiry_token = CachedToken(
            access_token="near-expiry-token",
            expires_at=datetime.now(timezone.utc) + timedelta(minutes=2),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", near_expiry_token)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "proactively-refreshed-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.get_token("test-tenant")

            assert result == "proactively-refreshed-token"
            mock_instance.acquire_token_for_client.assert_called_once()

    @pytest.mark.unit
    def test_acquires_token_when_no_cache(self, tmp_path: Path) -> None:
        """get_token acquires new token when no cached token exists."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "fresh-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.get_token("test-tenant")

            assert result == "fresh-token"
            mock_instance.acquire_token_for_client.assert_called_once()

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """get_token uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        # Pre-populate cache with valid token for default tenant
        valid_token = CachedToken(
            access_token="default-tenant-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("default-tenant", valid_token)

        result = service.get_token(None)

        assert result == "default-tenant-token"

    @pytest.mark.unit
    def test_raises_configuration_error_when_no_default(self, tmp_path: Path) -> None:
        """get_token raises ConfigurationError when None and no default set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(ConfigurationError) as exc_info:
            service.get_token(None)

        assert "No tenant specified" in str(exc_info.value)

    @pytest.mark.unit
    def test_raises_not_found_for_unknown_tenant(self, tmp_path: Path) -> None:
        """get_token raises NotFoundError for non-existent tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(NotFoundError) as exc_info:
            service.get_token("nonexistent-tenant")

        assert "nonexistent-tenant" in str(exc_info.value)


class TestIsAuthenticated:
    """Tests for is_authenticated method."""

    @pytest.mark.unit
    def test_returns_true_for_valid_token(self, tmp_path: Path) -> None:
        """is_authenticated returns True when cached token is valid."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # Cache a valid token
        valid_token = CachedToken(
            access_token="valid-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        assert service.is_authenticated("test-tenant") is True

    @pytest.mark.unit
    def test_returns_false_for_expired_token(self, tmp_path: Path) -> None:
        """is_authenticated returns False when cached token is expired."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # Cache an expired token
        expired_token = CachedToken(
            access_token="expired-token",
            expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", expired_token)

        assert service.is_authenticated("test-tenant") is False

    @pytest.mark.unit
    def test_returns_false_for_no_token(self, tmp_path: Path) -> None:
        """is_authenticated returns False when no cached token exists."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # No token cached
        assert service.is_authenticated("test-tenant") is False

    @pytest.mark.unit
    def test_returns_false_for_token_within_buffer(self, tmp_path: Path) -> None:
        """is_authenticated returns False when token is within refresh buffer."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # Token expires in 2 minutes (within 5-minute buffer)
        near_expiry_token = CachedToken(
            access_token="near-expiry-token",
            expires_at=datetime.now(timezone.utc) + timedelta(minutes=2),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", near_expiry_token)

        assert service.is_authenticated("test-tenant") is False

    @pytest.mark.unit
    def test_does_not_throw_for_configuration_error(self, tmp_path: Path) -> None:
        """is_authenticated returns False (not throws) for ConfigurationError."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # No default tenant, passing None should cause ConfigurationError
        # But is_authenticated should swallow it and return False
        assert service.is_authenticated(None) is False

    @pytest.mark.unit
    def test_does_not_throw_for_not_found_error(self, tmp_path: Path) -> None:
        """is_authenticated returns False (not throws) for NotFoundError."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Nonexistent tenant should cause NotFoundError
        # But is_authenticated should swallow it and return False
        assert service.is_authenticated("nonexistent-tenant") is False

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """is_authenticated uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        # Cache a valid token
        valid_token = CachedToken(
            access_token="valid-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("default-tenant", valid_token)

        assert service.is_authenticated(None) is True


class TestGetStatus:
    """Tests for get_status method."""

    @pytest.mark.unit
    def test_returns_authenticated_with_details(self, tmp_path: Path) -> None:
        """get_status returns AUTHENTICATED with token details when valid."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
        valid_token = CachedToken(
            access_token="valid-token",
            expires_at=expires_at,
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        status = service.get_status("test-tenant")

        assert status.status == AuthStatusType.AUTHENTICATED
        assert status.tenant_name == "test-tenant"
        assert status.tenant_id == str(profile.tenant_id)
        assert status.expires_at == expires_at

    @pytest.mark.unit
    def test_returns_unauthenticated_when_no_token(self, tmp_path: Path) -> None:
        """get_status returns UNAUTHENTICATED when no token exists."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        status = service.get_status("test-tenant")

        assert status.status == AuthStatusType.UNAUTHENTICATED
        assert status.tenant_name == "test-tenant"
        assert status.tenant_id is None
        assert status.expires_at is None

    @pytest.mark.unit
    def test_returns_expired_when_token_expired(self, tmp_path: Path) -> None:
        """get_status returns EXPIRED when token is expired."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        expires_at = datetime.now(timezone.utc) - timedelta(hours=1)
        expired_token = CachedToken(
            access_token="expired-token",
            expires_at=expires_at,
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", expired_token)

        status = service.get_status("test-tenant")

        assert status.status == AuthStatusType.EXPIRED
        assert status.tenant_name == "test-tenant"
        assert status.tenant_id == str(profile.tenant_id)
        assert status.expires_at == expires_at

    @pytest.mark.unit
    def test_raises_configuration_error_when_no_default(self, tmp_path: Path) -> None:
        """get_status raises ConfigurationError when None and no default set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(ConfigurationError) as exc_info:
            service.get_status(None)

        assert "No tenant specified" in str(exc_info.value)

    @pytest.mark.unit
    def test_raises_not_found_for_unknown_tenant(self, tmp_path: Path) -> None:
        """get_status raises NotFoundError for non-existent tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(NotFoundError) as exc_info:
            service.get_status("nonexistent-tenant")

        assert "nonexistent-tenant" in str(exc_info.value)

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """get_status uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        status = service.get_status(None)

        assert status.tenant_name == "default-tenant"


class TestLogin:
    """Tests for login method."""

    @pytest.mark.unit
    def test_returns_cached_token_when_authenticated(self, tmp_path: Path) -> None:
        """login returns cached token when already authenticated."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate with valid token
        valid_token = CachedToken(
            access_token="cached-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        # Should not call MSAL
        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            result = service.login("test-tenant")

            mock_app.assert_not_called()
            assert result.access_token == "cached-token"

    @pytest.mark.unit
    def test_acquires_new_token_when_force_true(self, tmp_path: Path) -> None:
        """login acquires new token when force=True even if authenticated."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate with valid token
        valid_token = CachedToken(
            access_token="old-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "new-forced-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.login("test-tenant", force=True)

            mock_instance.acquire_token_for_client.assert_called_once()
            assert result.access_token == "new-forced-token"

    @pytest.mark.unit
    def test_acquires_token_when_not_authenticated(self, tmp_path: Path) -> None:
        """login acquires new token when no cached token exists."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "fresh-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.login("test-tenant")

            mock_instance.acquire_token_for_client.assert_called_once()
            assert result.access_token == "fresh-token"

    @pytest.mark.unit
    def test_acquires_token_when_cached_expired(self, tmp_path: Path) -> None:
        """login acquires new token when cached token is expired."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("test-tenant", profile)

        # Pre-populate with expired token
        expired_token = CachedToken(
            access_token="expired-token",
            expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", expired_token)

        with patch(
            "teams_phone.services.auth_service.ConfidentialClientApplication"
        ) as mock_app:
            mock_instance = MagicMock()
            mock_instance.acquire_token_for_client.return_value = {
                "access_token": "new-token",
                "expires_in": 3600,
            }
            mock_app.return_value = mock_instance

            result = service.login("test-tenant")

            mock_instance.acquire_token_for_client.assert_called_once()
            assert result.access_token == "new-token"

    @pytest.mark.unit
    def test_raises_configuration_error_when_no_default(self, tmp_path: Path) -> None:
        """login raises ConfigurationError when None and no default set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(ConfigurationError) as exc_info:
            service.login(None)

        assert "No tenant specified" in str(exc_info.value)

    @pytest.mark.unit
    def test_raises_not_found_for_unknown_tenant(self, tmp_path: Path) -> None:
        """login raises NotFoundError for non-existent tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(NotFoundError) as exc_info:
            service.login("nonexistent-tenant")

        assert "nonexistent-tenant" in str(exc_info.value)

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """login uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        cert_path, _ = generate_test_certificate(tmp_path)
        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path=str(cert_path),
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        # Pre-populate with valid token
        valid_token = CachedToken(
            access_token="default-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("default-tenant", valid_token)

        result = service.login(None)

        assert result.access_token == "default-token"


class TestLogout:
    """Tests for logout method."""

    @pytest.mark.unit
    def test_clears_single_tenant_token(self, tmp_path: Path) -> None:
        """logout clears token for specific tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # Cache a token
        valid_token = CachedToken(
            access_token="token-to-clear",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", valid_token)

        # Verify token exists
        assert cache_manager.get_token("test-tenant") is not None

        service.logout("test-tenant")

        # Verify token was cleared
        assert cache_manager.get_token("test-tenant") is None

    @pytest.mark.unit
    def test_clears_all_tenant_tokens(self, tmp_path: Path) -> None:
        """logout clears all tokens when all_tenants=True."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Set up multiple tenants
        for name in ["tenant-1", "tenant-2", "tenant-3"]:
            profile = TenantProfile(
                name=name,
                tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
                client_id=UUID("87654321-4321-4321-4321-210987654321"),
                display_name=name,
                auth_method=AuthMethod.CERTIFICATE,
                cert_path="/path/to/cert.pem",
            )
            config_manager.set_tenant(name, profile)

            token = CachedToken(
                access_token=f"token-{name}",
                expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
                tenant_id=str(profile.tenant_id),
                scopes=SCOPES,
            )
            cache_manager.save_token(name, token)

        # Verify all tokens exist
        for name in ["tenant-1", "tenant-2", "tenant-3"]:
            assert cache_manager.get_token(name) is not None

        service.logout(all_tenants=True)

        # Verify all tokens were cleared
        for name in ["tenant-1", "tenant-2", "tenant-3"]:
            assert cache_manager.get_token(name) is None

    @pytest.mark.unit
    def test_logout_ignores_tenant_name_when_all_tenants_true(
        self, tmp_path: Path
    ) -> None:
        """logout ignores tenant_name parameter when all_tenants=True."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        # Set up a tenant
        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        token = CachedToken(
            access_token="token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("test-tenant", token)

        # Should not raise NotFoundError even though "nonexistent" doesn't exist
        service.logout(tenant_name="nonexistent", all_tenants=True)

        # Verify token was still cleared
        assert cache_manager.get_token("test-tenant") is None

    @pytest.mark.unit
    def test_raises_configuration_error_when_no_default(self, tmp_path: Path) -> None:
        """logout raises ConfigurationError when None and no default set."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(ConfigurationError) as exc_info:
            service.logout(None)

        assert "No tenant specified" in str(exc_info.value)

    @pytest.mark.unit
    def test_raises_not_found_for_unknown_tenant(self, tmp_path: Path) -> None:
        """logout raises NotFoundError for non-existent tenant."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        with pytest.raises(NotFoundError) as exc_info:
            service.logout("nonexistent-tenant")

        assert "nonexistent-tenant" in str(exc_info.value)

    @pytest.mark.unit
    def test_uses_default_tenant_when_none(self, tmp_path: Path) -> None:
        """logout uses default tenant when tenant_name is None."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="default-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Default Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("default-tenant", profile)
        config_manager.set_default_tenant("default-tenant")

        # Cache a token for default tenant
        valid_token = CachedToken(
            access_token="default-token",
            expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )
        cache_manager.save_token("default-tenant", valid_token)

        # Verify token exists
        assert cache_manager.get_token("default-tenant") is not None

        service.logout(None)

        # Verify token was cleared
        assert cache_manager.get_token("default-tenant") is None

    @pytest.mark.unit
    def test_logout_is_idempotent(self, tmp_path: Path) -> None:
        """logout succeeds even if no token exists to clear."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
        service = AuthService(config_manager, cache_manager)

        profile = TenantProfile(
            name="test-tenant",
            tenant_id=UUID("12345678-1234-1234-1234-123456789012"),
            client_id=UUID("87654321-4321-4321-4321-210987654321"),
            display_name="Test Tenant",
            auth_method=AuthMethod.CERTIFICATE,
            cert_path="/path/to/cert.pem",
        )
        config_manager.set_tenant("test-tenant", profile)

        # No token cached - should not raise
        service.logout("test-tenant")
