"""Unit tests for CLI auth commands."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest
from tests.conftest import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import (
    AuthenticationError,
    ConfigurationError,
    NotFoundError,
)
from teams_phone.models import AuthStatus, AuthStatusType, CachedToken

if TYPE_CHECKING:
    pass

runner = CliRunner()


class TestAuthStatusCommand:
    """Tests for auth status command."""

    @pytest.mark.unit
    def test_status_shows_in_help(self) -> None:
        """status command appears in auth help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "status" in result.stdout

    @pytest.mark.unit
    def test_status_unauthenticated(self) -> None:
        """status shows unauthenticated when no token exists."""
        mock_auth_status = AuthStatus(
            status=AuthStatusType.UNAUTHENTICATED,
            tenant_name="test-tenant",
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "status"])
            assert result.exit_code == 0
            assert "unauthenticated" in result.stdout.lower()
            assert "test-tenant" in result.stdout

    @pytest.mark.unit
    def test_status_authenticated(self) -> None:
        """status shows authenticated when valid token exists."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_auth_status = AuthStatus(
            status=AuthStatusType.AUTHENTICATED,
            tenant_name="test-tenant",
            tenant_id="test-tenant-id-abc",
            expires_at=expires,
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--no-color", "auth", "status"])
            assert result.exit_code == 0
            assert "authenticated" in result.stdout.lower()
            assert "test-tenant" in result.stdout
            assert "test-tenant-id-abc" in result.stdout

    @pytest.mark.unit
    def test_status_expired(self) -> None:
        """status shows expired when token is expired."""
        expires = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
        mock_auth_status = AuthStatus(
            status=AuthStatusType.EXPIRED,
            tenant_name="test-tenant",
            tenant_id="12345-tenant-id",
            expires_at=expires,
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "status"])
            assert result.exit_code == 0
            assert "expired" in result.stdout.lower()

    @pytest.mark.unit
    def test_status_json_output(self) -> None:
        """status outputs valid JSON with --json flag."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_auth_status = AuthStatus(
            status=AuthStatusType.AUTHENTICATED,
            tenant_name="test-tenant",
            tenant_id="12345-tenant-id",
            expires_at=expires,
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "status"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "authenticated"
            assert data["tenant_name"] == "test-tenant"
            assert data["tenant_id"] == "12345-tenant-id"

    @pytest.mark.unit
    def test_status_respects_tenant_override(self) -> None:
        """status uses --tenant override from global options."""
        mock_auth_status = AuthStatus(
            status=AuthStatusType.UNAUTHENTICATED,
            tenant_name="override-tenant",
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(
                app, ["--tenant", "override-tenant", "auth", "status"]
            )
            assert result.exit_code == 0
            mock_auth_service.get_status.assert_called_once_with("override-tenant")

    @pytest.mark.unit
    def test_status_config_error_exit_code(self) -> None:
        """status exits with correct code on configuration error."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.side_effect = ConfigurationError(
                "No default tenant",
                remediation="Set a default tenant",
            )
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "status"])
            assert result.exit_code == 7  # ConfigurationError exit code

    @pytest.mark.unit
    def test_status_not_found_error_exit_code(self) -> None:
        """status exits with correct code on not found error."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.side_effect = NotFoundError(
                "Tenant not found",
                remediation="Check tenant name",
            )
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "status"])
            assert result.exit_code == 4  # NotFoundError exit code


class TestAuthLoginCommand:
    """Tests for auth login command."""

    @pytest.mark.unit
    def test_login_shows_in_help(self) -> None:
        """login command appears in auth help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "login" in result.stdout

    @pytest.mark.unit
    def test_login_help_shows_force_option(self) -> None:
        """login --help shows --force option."""
        result = runner.invoke(app, ["auth", "login", "--help"])
        assert result.exit_code == 0
        assert "--force" in result.plain_stdout
        assert "-f" in result.plain_stdout

    @pytest.mark.unit
    def test_login_success(self) -> None:
        """login authenticates successfully."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="test-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.login.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "login"])
            assert result.exit_code == 0
            assert "authenticated" in result.stdout.lower()
            mock_auth_service.login.assert_called_once_with(None, force=False)

    @pytest.mark.unit
    def test_login_force_flag(self) -> None:
        """login --force forces new authentication."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="test-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.login.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "login", "--force"])
            assert result.exit_code == 0
            mock_auth_service.login.assert_called_once_with(None, force=True)

    @pytest.mark.unit
    def test_login_json_output(self) -> None:
        """login outputs valid JSON with --json flag."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="test-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.login.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "login"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "authenticated"
            assert data["tenant_id"] == "12345-tenant-id"

    @pytest.mark.unit
    def test_login_auth_error_exit_code(self) -> None:
        """login exits with correct code on authentication error."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.login.side_effect = AuthenticationError(
                "Invalid certificate",
                remediation="Check certificate path",
            )
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "login"])
            assert result.exit_code == 2  # AuthenticationError exit code

    @pytest.mark.unit
    def test_login_respects_tenant_override(self) -> None:
        """login uses --tenant override from global options."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="test-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.login.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--tenant", "my-tenant", "auth", "login"])
            assert result.exit_code == 0
            mock_auth_service.login.assert_called_once_with("my-tenant", force=False)


class TestAuthLogoutCommand:
    """Tests for auth logout command."""

    @pytest.mark.unit
    def test_logout_shows_in_help(self) -> None:
        """logout command appears in auth help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "logout" in result.stdout

    @pytest.mark.unit
    def test_logout_help_shows_all_option(self) -> None:
        """logout --help shows --all option."""
        result = runner.invoke(app, ["auth", "logout", "--help"])
        assert result.exit_code == 0
        assert "--all" in result.plain_stdout
        assert "-a" in result.plain_stdout

    @pytest.mark.unit
    def test_logout_success(self) -> None:
        """logout clears tokens successfully."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "logout"])
            assert result.exit_code == 0
            assert "logged out" in result.stdout.lower()
            mock_auth_service.logout.assert_called_once_with(None, all_tenants=False)

    @pytest.mark.unit
    def test_logout_all_tenants(self) -> None:
        """logout --all clears all tokens."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "logout", "--all"])
            assert result.exit_code == 0
            assert "all tenants" in result.stdout.lower()
            mock_auth_service.logout.assert_called_once_with(None, all_tenants=True)

    @pytest.mark.unit
    def test_logout_json_output(self) -> None:
        """logout outputs valid JSON with --json flag."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "logout"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "logged_out"

    @pytest.mark.unit
    def test_logout_json_all_tenants(self) -> None:
        """logout --all outputs valid JSON with --json flag."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "logout", "--all"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "logged_out"
            assert data["all_tenants"] is True

    @pytest.mark.unit
    def test_logout_respects_tenant_override(self) -> None:
        """logout uses --tenant override from global options."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--tenant", "my-tenant", "auth", "logout"])
            assert result.exit_code == 0
            mock_auth_service.logout.assert_called_once_with(
                "my-tenant", all_tenants=False
            )

    @pytest.mark.unit
    def test_logout_config_error_exit_code(self) -> None:
        """logout exits with correct code on configuration error."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.logout.side_effect = ConfigurationError(
                "No default tenant",
                remediation="Set a default tenant",
            )
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "logout"])
            assert result.exit_code == 7  # ConfigurationError exit code


class TestAuthRefreshCommand:
    """Tests for auth refresh command."""

    @pytest.mark.unit
    def test_refresh_shows_in_help(self) -> None:
        """refresh command appears in auth help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "refresh" in result.stdout

    @pytest.mark.unit
    def test_refresh_help_shows_force_option(self) -> None:
        """refresh --help shows --force option."""
        result = runner.invoke(app, ["auth", "refresh", "--help"])
        assert result.exit_code == 0
        assert "--force" in result.plain_stdout
        assert "-f" in result.plain_stdout

    @pytest.mark.unit
    def test_refresh_success_when_needed(self) -> None:
        """refresh refreshes token when needed."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="new-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )
        mock_auth_status = AuthStatus(
            status=AuthStatusType.EXPIRED,
            tenant_name="test-tenant",
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service.refresh_token.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "refresh"])
            assert result.exit_code == 0
            assert "refreshed" in result.stdout.lower()
            mock_auth_service.refresh_token.assert_called_once_with(None)

    @pytest.mark.unit
    def test_refresh_skips_when_valid(self) -> None:
        """refresh skips when token is still valid."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_auth_status = AuthStatus(
            status=AuthStatusType.AUTHENTICATED,
            tenant_name="test-tenant",
            tenant_id="12345-tenant-id",
            expires_at=expires,
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "refresh"])
            assert result.exit_code == 0
            assert "still valid" in result.stdout.lower()
            mock_auth_service.refresh_token.assert_not_called()

    @pytest.mark.unit
    def test_refresh_force_flag(self) -> None:
        """refresh --force forces token refresh."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="new-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.refresh_token.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "refresh", "--force"])
            assert result.exit_code == 0
            assert "refreshed" in result.stdout.lower()
            # With --force, get_status should not be called
            mock_auth_service.get_status.assert_not_called()
            mock_auth_service.refresh_token.assert_called_once_with(None)

    @pytest.mark.unit
    def test_refresh_json_output_when_refreshed(self) -> None:
        """refresh outputs valid JSON when token is refreshed."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="new-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.refresh_token.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "refresh", "--force"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "refreshed"
            assert data["tenant_id"] == "12345-tenant-id"

    @pytest.mark.unit
    def test_refresh_json_output_when_valid(self) -> None:
        """refresh outputs valid JSON when token is already valid."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_auth_status = AuthStatus(
            status=AuthStatusType.AUTHENTICATED,
            tenant_name="test-tenant",
            tenant_id="12345-tenant-id",
            expires_at=expires,
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.get_status.return_value = mock_auth_status
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["--json", "auth", "refresh"])
            assert result.exit_code == 0
            data = json.loads(result.stdout)
            assert data["status"] == "already_valid"
            assert data["tenant_id"] == "12345-tenant-id"

    @pytest.mark.unit
    def test_refresh_auth_error_exit_code(self) -> None:
        """refresh exits with correct code on authentication error."""
        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.refresh_token.side_effect = AuthenticationError(
                "Token refresh failed",
                remediation="Re-authenticate with login",
            )
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(app, ["auth", "refresh", "--force"])
            assert result.exit_code == 2  # AuthenticationError exit code

    @pytest.mark.unit
    def test_refresh_respects_tenant_override(self) -> None:
        """refresh uses --tenant override from global options."""
        expires = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
        mock_token = CachedToken(
            access_token="new-token",
            expires_at=expires,
            tenant_id="12345-tenant-id",
            scopes=["https://graph.microsoft.com/.default"],
        )

        with (
            patch("teams_phone.cli.auth.CacheManager"),
            patch("teams_phone.cli.auth.AuthService") as mock_auth_service_class,
        ):
            mock_auth_service = MagicMock()
            mock_auth_service.refresh_token.return_value = mock_token
            mock_auth_service_class.return_value = mock_auth_service

            result = runner.invoke(
                app, ["--tenant", "my-tenant", "auth", "refresh", "--force"]
            )
            assert result.exit_code == 0
            mock_auth_service.refresh_token.assert_called_once_with("my-tenant")


class TestAuthCommandIntegration:
    """Integration tests for auth command group."""

    @pytest.mark.unit
    def test_auth_commands_appear_in_help(self) -> None:
        """All auth commands appear in auth --help."""
        result = runner.invoke(app, ["auth", "--help"])
        assert result.exit_code == 0
        assert "login" in result.stdout
        assert "logout" in result.stdout
        assert "status" in result.stdout
        assert "refresh" in result.stdout

    @pytest.mark.unit
    def test_auth_no_args_shows_help(self) -> None:
        """auth with no args shows help (exit code 2)."""
        result = runner.invoke(app, ["auth"])
        assert result.exit_code == 2
        assert "Usage:" in result.stdout
