"""
Test suite for email verification authentication flows.

This module tests the email code login mechanism including:
- Email code sending with rate limiting
- Code verification and validation
- Account creation via email verification
- Workspace creation for new users
"""

import base64
from unittest.mock import MagicMock, patch

import pytest
from flask import Flask
from pydantic import ValidationError

from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError
from controllers.console.auth.login import EmailCodeLoginApi, EmailCodeLoginPayload, EmailCodeLoginSendEmailApi
from controllers.console.error import (
    AccountInFreezeError,
    AccountNotFound,
    EmailSendIpLimitError,
    NotAllowedCreateWorkspace,
    WorkspacesLimitExceeded,
)
from services.errors.account import AccountRegisterError


def encode_code(code: str) -> str:
    """Helper to encode verification code as Base64 for testing."""
    return base64.b64encode(code.encode("utf-8")).decode()


def test_email_code_login_payload_rejects_invalid_timezone():
    with pytest.raises(ValidationError):
        EmailCodeLoginPayload.model_validate(
            {
                "email": "newuser@example.com",
                "code": "123456",
                "token": "token-123",
                "timezone": "",
            }
        )


class TestEmailCodeLoginSendEmailApi:
    """Test cases for sending email verification codes."""

    @pytest.fixture
    def app(self):
        """Create Flask test application."""
        app = Flask(__name__)
        app.config["TESTING"] = True
        return app

    @pytest.fixture
    def mock_account(self):
        """Create mock account object."""
        account = MagicMock()
        account.email = "test@example.com"
        account.name = "Test User"
        return account

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.AccountService.send_email_code_login_email")
    def test_send_email_code_existing_user(
        self, mock_send_email, mock_get_user, mock_is_ip_limit, mock_db, app, mock_account
    ):
        """
        Test sending email code to existing user.

        Verifies that:
        - Email code is sent to existing account
        - Token is generated and returned
        - IP rate limiting is checked
        """
        # Arrange
        mock_is_ip_limit.return_value = False
        mock_get_user.return_value = mock_account
        mock_send_email.return_value = "email_token_123"

        # Act
        with app.test_request_context(
            "/email-code-login", method="POST", json={"email": "test@example.com", "language": "en-US"}
        ):
            api = EmailCodeLoginSendEmailApi()
            response = api.post()

        # Assert
        assert response["result"] == "success"
        assert response["data"] == "email_token_123"
        mock_send_email.assert_called_once_with(account=mock_account, language="en-US")

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.FeatureService.get_system_features")
    @patch("controllers.console.auth.login.AccountService.send_email_code_login_email")
    def test_send_email_code_new_user_registration_allowed(
        self, mock_send_email, mock_get_features, mock_get_user, mock_is_ip_limit, mock_db, app
    ):
        """
        Test sending email code to new user when registration is allowed.

        Verifies that:
        - Email code is sent even for non-existent accounts
        - Registration is allowed by system features
        """
        # Arrange
        mock_is_ip_limit.return_value = False
        mock_get_user.return_value = None
        mock_get_features.return_value.is_allow_register = True
        mock_send_email.return_value = "email_token_123"

        # Act
        with app.test_request_context(
            "/email-code-login", method="POST", json={"email": "newuser@example.com", "language": "en-US"}
        ):
            api = EmailCodeLoginSendEmailApi()
            response = api.post()

        # Assert
        assert response["result"] == "success"
        mock_send_email.assert_called_once_with(email="newuser@example.com", language="en-US")

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.FeatureService.get_system_features")
    def test_send_email_code_new_user_registration_disabled(
        self, mock_get_features, mock_get_user, mock_is_ip_limit, mock_db, app
    ):
        """
        Test sending email code to new user when registration is disabled.

        Verifies that:
        - AccountNotFound is raised for non-existent accounts
        - Registration is blocked by system features
        """
        # Arrange
        mock_is_ip_limit.return_value = False
        mock_get_user.return_value = None
        mock_get_features.return_value.is_allow_register = False

        # Act & Assert
        with app.test_request_context("/email-code-login", method="POST", json={"email": "newuser@example.com"}):
            api = EmailCodeLoginSendEmailApi()
            with pytest.raises(AccountNotFound):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    def test_send_email_code_ip_rate_limited(self, mock_is_ip_limit, mock_db, app: Flask):
        """
        Test email code sending blocked by IP rate limit.

        Verifies that:
        - EmailSendIpLimitError is raised when IP limit exceeded
        - Prevents spam and abuse
        """
        # Arrange
        mock_is_ip_limit.return_value = True

        # Act & Assert
        with app.test_request_context("/email-code-login", method="POST", json={"email": "test@example.com"}):
            api = EmailCodeLoginSendEmailApi()
            with pytest.raises(EmailSendIpLimitError):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    def test_send_email_code_frozen_account(self, mock_get_user, mock_is_ip_limit, mock_db, app: Flask):
        """
        Test email code sending to frozen account.

        Verifies that:
        - AccountInFreezeError is raised for frozen accounts
        """
        # Arrange
        mock_is_ip_limit.return_value = False
        mock_get_user.side_effect = AccountRegisterError("Account frozen")

        # Act & Assert
        with app.test_request_context("/email-code-login", method="POST", json={"email": "frozen@example.com"}):
            api = EmailCodeLoginSendEmailApi()
            with pytest.raises(AccountInFreezeError):
                api.post()

    @pytest.mark.parametrize(
        ("language_input", "expected_language"),
        [
            ("zh-Hans", "zh-Hans"),
            ("en-US", "en-US"),
            (None, "en-US"),
        ],
    )
    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.is_email_send_ip_limit")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.AccountService.send_email_code_login_email")
    def test_send_email_code_language_handling(
        self,
        mock_send_email,
        mock_get_user,
        mock_is_ip_limit,
        mock_db,
        app: Flask,
        mock_account,
        language_input,
        expected_language,
    ):
        """
        Test email code sending with different language preferences.

        Verifies that:
        - Language parameter is correctly processed
        - Defaults to en-US when not specified
        """
        # Arrange
        mock_is_ip_limit.return_value = False
        mock_get_user.return_value = mock_account
        mock_send_email.return_value = "token"

        # Act
        with app.test_request_context(
            "/email-code-login", method="POST", json={"email": "test@example.com", "language": language_input}
        ):
            api = EmailCodeLoginSendEmailApi()
            api.post()

        # Assert
        call_args = mock_send_email.call_args
        assert call_args.kwargs["language"] == expected_language


class TestEmailCodeLoginApi:
    """Test cases for email code verification and login."""

    @pytest.fixture
    def app(self):
        """Create Flask test application."""
        app = Flask(__name__)
        app.config["TESTING"] = True
        return app

    @pytest.fixture
    def mock_account(self):
        """Create mock account object."""
        account = MagicMock()
        account.email = "test@example.com"
        account.name = "Test User"
        return account

    @pytest.fixture
    def mock_token_pair(self):
        """Create mock token pair object."""
        token_pair = MagicMock()
        token_pair.access_token = "access_token"
        token_pair.refresh_token = "refresh_token"
        token_pair.csrf_token = "csrf_token"
        return token_pair

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.TenantService.get_join_tenants")
    @patch("controllers.console.auth.login.AccountService.login")
    @patch("controllers.console.auth.login.AccountService.reset_login_error_rate_limit")
    def test_email_code_login_existing_user(
        self,
        mock_reset_rate_limit,
        mock_login,
        mock_get_tenants,
        mock_get_user,
        mock_revoke_token,
        mock_get_data,
        mock_db,
        app: Flask,
        mock_account,
        mock_token_pair,
    ):
        """
        Test successful email code login for existing user.

        Verifies that:
        - Email and code are validated
        - Token is revoked after use
        - User is logged in with token pair
        """
        # Arrange
        mock_get_data.return_value = {"email": "test@example.com", "code": "123456"}
        mock_get_user.return_value = mock_account
        mock_get_tenants.return_value = [MagicMock()]
        mock_login.return_value = mock_token_pair

        # Act
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": encode_code("123456"), "token": "valid_token"},
        ):
            api = EmailCodeLoginApi()
            response = api.post()

        # Assert
        assert response.json["result"] == "success"
        mock_revoke_token.assert_called_once_with("valid_token")
        mock_login.assert_called_once()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.AccountService.create_account_and_tenant")
    @patch("controllers.console.auth.login.AccountService.login")
    @patch("controllers.console.auth.login.AccountService.reset_login_error_rate_limit")
    def test_email_code_login_new_user_creates_account(
        self,
        mock_reset_rate_limit,
        mock_login,
        mock_create_account,
        mock_get_user,
        mock_revoke_token,
        mock_get_data,
        mock_db,
        app: Flask,
        mock_account,
        mock_token_pair,
    ):
        """
        Test email code login creates new account for new user.

        Verifies that:
        - New account is created when user doesn't exist
        - Workspace is created for new user
        - User is logged in after account creation
        """
        # Arrange
        mock_get_data.return_value = {"email": "newuser@example.com", "code": "123456"}
        mock_get_user.return_value = None
        mock_create_account.return_value = mock_account
        mock_login.return_value = mock_token_pair

        # Act
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={
                "email": "newuser@example.com",
                "code": encode_code("123456"),
                "token": "valid_token",
                "language": "en-US",
                "timezone": "Asia/Shanghai",
            },
        ):
            api = EmailCodeLoginApi()
            response = api.post()

        # Assert
        assert response.json["result"] == "success"
        mock_create_account.assert_called_once_with(
            email="newuser@example.com",
            name="newuser@example.com",
            interface_language="en-US",
            timezone="Asia/Shanghai",
        )

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    def test_email_code_login_invalid_token(self, mock_get_data, mock_db, app: Flask):
        """
        Test email code login with invalid token.

        Verifies that:
        - InvalidTokenError is raised for invalid/expired tokens
        """
        # Arrange
        mock_get_data.return_value = None

        # Act & Assert
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": encode_code("123456"), "token": "invalid_token"},
        ):
            api = EmailCodeLoginApi()
            with pytest.raises(InvalidTokenError):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    def test_email_code_login_email_mismatch(self, mock_get_data, mock_db, app: Flask):
        """
        Test email code login with mismatched email.

        Verifies that:
        - InvalidEmailError is raised when email doesn't match token
        """
        # Arrange
        mock_get_data.return_value = {"email": "original@example.com", "code": "123456"}

        # Act & Assert
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "different@example.com", "code": encode_code("123456"), "token": "token"},
        ):
            api = EmailCodeLoginApi()
            with pytest.raises(InvalidEmailError):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    def test_email_code_login_wrong_code(self, mock_get_data, mock_db, app: Flask):
        """
        Test email code login with incorrect code.

        Verifies that:
        - EmailCodeError is raised for wrong verification code
        """
        # Arrange
        mock_get_data.return_value = {"email": "test@example.com", "code": "123456"}

        # Act & Assert
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": encode_code("wrong_code"), "token": "token"},
        ):
            api = EmailCodeLoginApi()
            with pytest.raises(EmailCodeError):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.TenantService.get_join_tenants")
    @patch("controllers.console.auth.login.FeatureService.get_system_features")
    def test_email_code_login_creates_workspace_for_user_without_tenant(
        self,
        mock_get_features,
        mock_get_tenants,
        mock_get_user,
        mock_revoke_token,
        mock_get_data,
        mock_db,
        app: Flask,
        mock_account,
    ):
        """
        Test email code login creates workspace for user without tenant.

        Verifies that:
        - Workspace is created when user has no tenants
        - User is added as owner of new workspace
        """
        # Arrange
        mock_get_data.return_value = {"email": "test@example.com", "code": "123456"}
        mock_get_user.return_value = mock_account
        mock_get_tenants.return_value = []
        mock_features = MagicMock()
        mock_features.is_allow_create_workspace = True
        mock_features.license.workspaces.is_available.return_value = True
        mock_get_features.return_value = mock_features

        # Act & Assert - Should not raise WorkspacesLimitExceeded
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": "123456", "token": "token"},
        ):
            api = EmailCodeLoginApi()
            # This would complete the flow, but we're testing workspace creation logic
            # In real implementation, TenantService.create_tenant would be called

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.TenantService.get_join_tenants")
    @patch("controllers.console.auth.login.FeatureService.get_system_features")
    def test_email_code_login_workspace_limit_exceeded(
        self,
        mock_get_features,
        mock_get_tenants,
        mock_get_user,
        mock_revoke_token,
        mock_get_data,
        mock_db,
        app: Flask,
        mock_account,
    ):
        """
        Test email code login fails when workspace limit exceeded.

        Verifies that:
        - WorkspacesLimitExceeded is raised when limit reached
        """
        # Arrange
        mock_get_data.return_value = {"email": "test@example.com", "code": "123456"}
        mock_get_user.return_value = mock_account
        mock_get_tenants.return_value = []
        mock_features = MagicMock()
        mock_features.license.workspaces.is_available.return_value = False
        mock_get_features.return_value = mock_features

        # Act & Assert
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": encode_code("123456"), "token": "token"},
        ):
            api = EmailCodeLoginApi()
            with pytest.raises(WorkspacesLimitExceeded):
                api.post()

    @patch("controllers.console.wraps.db")
    @patch("controllers.console.auth.login.AccountService.get_email_code_login_data")
    @patch("controllers.console.auth.login.AccountService.revoke_email_code_login_token")
    @patch("controllers.console.auth.login.AccountService.get_user_through_email")
    @patch("controllers.console.auth.login.TenantService.get_join_tenants")
    @patch("controllers.console.auth.login.FeatureService.get_system_features")
    def test_email_code_login_workspace_creation_not_allowed(
        self,
        mock_get_features,
        mock_get_tenants,
        mock_get_user,
        mock_revoke_token,
        mock_get_data,
        mock_db,
        app: Flask,
        mock_account,
    ):
        """
        Test email code login fails when workspace creation not allowed.

        Verifies that:
        - NotAllowedCreateWorkspace is raised when creation disabled
        """
        # Arrange
        mock_get_data.return_value = {"email": "test@example.com", "code": "123456"}
        mock_get_user.return_value = mock_account
        mock_get_tenants.return_value = []
        mock_features = MagicMock()
        mock_features.is_allow_create_workspace = False
        mock_get_features.return_value = mock_features

        # Act & Assert
        with app.test_request_context(
            "/email-code-login/validity",
            method="POST",
            json={"email": "test@example.com", "code": encode_code("123456"), "token": "token"},
        ):
            api = EmailCodeLoginApi()
            with pytest.raises(NotAllowedCreateWorkspace):
                api.post()
