"""Unit tests for navari_cli.auth module."""

from unittest.mock import MagicMock, patch

import msal
import pytest
from navari_cli.auth import (
    _build_msal_app,
    _extract_token,
    _login_callback_html,
    acquire_token_silent,
    device_code_flow,
    password_flow,
)

AUTH_CONFIG = {
    "client_id": "test-client-id",
    "authority": "https://login.microsoftonline.com/test-tenant",
    "scopes": ["api://test-api/user_impersonation"],
}


# ============================================================================
# _extract_token
# ============================================================================


class TestExtractToken:
    def test_success(self):
        result = {"access_token": "at-123", "id_token": "id-456"}
        token, id_token = _extract_token(result)
        assert token == "at-123"
        assert id_token == "id-456"

    def test_success_no_id_token(self):
        result = {"access_token": "at-123"}
        token, id_token = _extract_token(result)
        assert token == "at-123"
        assert id_token is None

    def test_error_with_description(self):
        result = {"error": "invalid_grant", "error_description": "Bad credentials"}
        with pytest.raises(ValueError, match="Bad credentials"):
            _extract_token(result)

    def test_error_without_description(self):
        result = {"error": "invalid_grant"}
        with pytest.raises(ValueError, match="invalid_grant"):
            _extract_token(result)

    def test_empty_result(self):
        with pytest.raises(ValueError, match="unknown_error"):
            _extract_token({})


# ============================================================================
# _build_msal_app
# ============================================================================


class TestBuildMsalApp:
    OIDC_DISCOVERY = {
        "authorization_endpoint": "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/authorize",
        "token_endpoint": "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
    }

    def test_creates_app_with_config(self):
        with patch("msal.authority.tenant_discovery", return_value=self.OIDC_DISCOVERY):
            app = _build_msal_app(AUTH_CONFIG)
        assert app.client_id == AUTH_CONFIG["client_id"]

    def test_creates_app_with_cache(self):
        cache = msal.SerializableTokenCache()
        with patch("msal.authority.tenant_discovery", return_value=self.OIDC_DISCOVERY):
            app = _build_msal_app(AUTH_CONFIG, token_cache=cache)
        assert app.token_cache is cache


# ============================================================================
# acquire_token_silent
# ============================================================================


class TestAcquireTokenSilent:
    async def test_no_accounts_returns_none(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.get_accounts.return_value = []
            mock_build.return_value = mock_app

            result = await acquire_token_silent(AUTH_CONFIG, msal.SerializableTokenCache())
        assert result is None

    async def test_silent_success(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.get_accounts.return_value = [{"username": "user@test.com"}]
            mock_app.acquire_token_silent.return_value = {"access_token": "refreshed-token"}
            mock_build.return_value = mock_app

            result = await acquire_token_silent(AUTH_CONFIG, msal.SerializableTokenCache())
        assert result == "refreshed-token"

    async def test_silent_failure_returns_none(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.get_accounts.return_value = [{"username": "user@test.com"}]
            mock_app.acquire_token_silent.return_value = None
            mock_build.return_value = mock_app

            result = await acquire_token_silent(AUTH_CONFIG, msal.SerializableTokenCache())
        assert result is None

    async def test_filters_by_username(self):
        """When username is provided, only matching accounts are considered."""
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.get_accounts.return_value = [{"username": "correct@test.com"}]
            mock_app.acquire_token_silent.return_value = {"access_token": "correct-token"}
            mock_build.return_value = mock_app

            result = await acquire_token_silent(AUTH_CONFIG, msal.SerializableTokenCache(), username="correct@test.com")
        assert result == "correct-token"
        mock_app.get_accounts.assert_called_once_with(username="correct@test.com")

    async def test_username_filter_no_match(self):
        """When username filter matches no accounts, returns None."""
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.get_accounts.return_value = []
            mock_build.return_value = mock_app

            result = await acquire_token_silent(
                AUTH_CONFIG, msal.SerializableTokenCache(), username="nonexistent@test.com"
            )
        assert result is None


# ============================================================================
# password_flow
# ============================================================================


class TestPasswordFlow:
    async def test_success(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.acquire_token_by_username_password.return_value = {
                "access_token": "pw-token",
                "id_token": "pw-id",
            }
            mock_build.return_value = mock_app

            token, id_token = await password_flow(AUTH_CONFIG, "user@test.com", "secret")
        assert token == "pw-token"
        assert id_token == "pw-id"
        mock_app.acquire_token_by_username_password.assert_called_once_with(
            username="user@test.com",
            password="secret",
            scopes=AUTH_CONFIG["scopes"],
        )

    async def test_failure(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.acquire_token_by_username_password.return_value = {
                "error": "invalid_grant",
                "error_description": "Wrong password",
            }
            mock_build.return_value = mock_app

            with pytest.raises(ValueError, match="Wrong password"):
                await password_flow(AUTH_CONFIG, "user@test.com", "wrong")


# ============================================================================
# device_code_flow
# ============================================================================


class TestDeviceCodeFlow:
    async def test_success(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.initiate_device_flow.return_value = {
                "user_code": "ABC123",
                "verification_uri": "https://login.example.com/devicelogin",
            }
            mock_app.acquire_token_by_device_flow.return_value = {
                "access_token": "dc-token",
                "id_token": "dc-id",
            }
            mock_build.return_value = mock_app

            with patch("navari_cli.auth.console"):
                token, id_token = await device_code_flow(AUTH_CONFIG)
        assert token == "dc-token"
        assert id_token == "dc-id"

    async def test_initiation_failure(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.initiate_device_flow.return_value = {
                "error_description": "Device code flow not enabled",
            }
            mock_build.return_value = mock_app

            with pytest.raises(ValueError, match="Device code flow not enabled"):
                await device_code_flow(AUTH_CONFIG)

    async def test_token_error(self):
        with patch("navari_cli.auth._build_msal_app") as mock_build:
            mock_app = MagicMock()
            mock_app.initiate_device_flow.return_value = {
                "user_code": "ABC123",
                "verification_uri": "https://login.example.com/devicelogin",
            }
            mock_app.acquire_token_by_device_flow.return_value = {
                "error": "authorization_declined",
                "error_description": "User cancelled",
            }
            mock_build.return_value = mock_app

            with (
                patch("navari_cli.auth.console"),
                pytest.raises(ValueError, match="User cancelled"),
            ):
                await device_code_flow(AUTH_CONFIG)


# ============================================================================
# _login_callback_html
# ============================================================================


class TestLoginCallbackHtml:
    def test_success_page_content(self):
        html = _login_callback_html(True, "https://navari.example.com")
        assert "Welcome to Navari" in html
        assert "Login Successful!" in html
        assert "return to the CLI" in html

    def test_failure_page_content(self):
        html = _login_callback_html(False, "https://navari.example.com")
        assert "Welcome to Navari" in html
        assert "Authentication Failed" in html
        assert "device code flow" in html

    def test_server_link_on_success(self):
        html = _login_callback_html(True, "https://navari.example.com")
        assert 'href="https://navari.example.com/"' in html
        assert "navari.example.com/" in html

    def test_no_server_link_on_failure(self):
        html = _login_callback_html(False, "https://navari.example.com")
        assert 'href="https://navari.example.com/"' not in html

    def test_trailing_slash_normalized(self):
        html = _login_callback_html(True, "https://navari.example.com/")
        # Should not produce double slash in the link
        assert 'href="https://navari.example.com/"' in html
        assert "navari.example.com//" not in html

    def test_logos_loaded_from_server(self):
        html = _login_callback_html(True, "https://navari.example.com")
        assert 'src="https://navari.example.com/logo-light.png"' in html
        assert 'src="https://navari.example.com/logo-dark.png"' in html

    def test_favicon_loaded_from_server(self):
        html = _login_callback_html(True, "https://navari.example.com")
        assert 'href="https://navari.example.com/fav.png"' in html

    def test_dark_mode_support(self):
        html = _login_callback_html(True, "https://navari.example.com")
        assert "prefers-color-scheme: dark" in html
        assert "logo-light" in html
        assert "logo-dark" in html

    def test_no_server_url(self):
        """Page works without a server URL — no logos, no link, no favicon."""
        html = _login_callback_html(True, "")
        assert "Welcome to Navari" in html
        assert "Login Successful!" in html
        assert "logo-light.png" not in html
        assert "<a href" not in html
        assert "fav.png" not in html
