import json
import os
from datetime import timedelta
from unittest.mock import MagicMock, patch

from django.conf import settings
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings, tag
from django.urls import reverse
from django.utils import timezone

from api.models import (
    AgentEmailAccount,
    AgentEmailOAuthCredential,
    AgentEmailOAuthSession,
    BrowserUseAgent,
    CommsChannel,
    PersistentAgent,
    PersistentAgentCommsEndpoint,
)
from console.email_settings.views import _format_email_connection_error, _normalize_email_error_text
from api.services.persistent_agents import ensure_default_agent_email_endpoint


@override_settings(PERSONAL_FREE_TRIAL_ENFORCEMENT_ENABLED=False)
@tag("batch_console_email_oauth")
class AgentEmailOAuthApiTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        User = get_user_model()
        cls.user = User.objects.create_user(
            username="email-oauth-user",
            email="email-oauth@example.com",
            password="password123",
        )
        cls.other_user = User.objects.create_user(
            username="email-oauth-other",
            email="other@example.com",
            password="password123",
        )

        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=cls.user, name="BA")

        cls.agent = PersistentAgent.objects.create(
            user=cls.user,
            name="OAuth Agent",
            charter="c",
            browser_use_agent=browser_agent,
        )
        cls.endpoint = PersistentAgentCommsEndpoint.objects.create(
            owner_agent=cls.agent,
            channel=CommsChannel.EMAIL,
            address="agent@example.com",
            is_primary=True,
        )
        cls.account = AgentEmailAccount.objects.create(endpoint=cls.endpoint)

    def setUp(self):
        self.client.force_login(self.user)

    def test_start_creates_session(self):
        url = reverse("console-email-oauth-start")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "account_id": str(self.account.pk),
                    "scope": "mail.read",
                    "token_endpoint": "https://oauth.example.com/token",
                    "code_verifier": "secret-verifier",
                    "state": "custom-state",
                    "client_id": "abc123",
                    "client_secret": "shhh",
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 201, response.content)
        payload = response.json()
        self.assertIn("session_id", payload)
        self.assertEqual(payload["state"], "custom-state")
        session = AgentEmailOAuthSession.objects.get(id=payload["session_id"])
        self.assertEqual(session.scope, "mail.read")
        self.assertEqual(session.code_verifier, "secret-verifier")
        self.assertEqual(session.client_id, "abc123")
        self.assertEqual(session.client_secret, "shhh")

    def test_start_requires_permission(self):
        self.client.force_login(self.other_user)
        url = reverse("console-email-oauth-start")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "account_id": str(self.account.pk),
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 404)

    @patch.dict(
        os.environ,
        {
            "GOOGLE_CLIENT_ID": "managed-client-id",
            "GOOGLE_CLIENT_SECRET": "managed-secret",
        },
        clear=False,
    )
    def test_start_uses_managed_app(self):
        url = reverse("console-email-oauth-start")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "account_id": str(self.account.pk),
                    "provider": "gmail",
                    "scope": "mail.read",
                    "token_endpoint": "https://oauth.example.com/token",
                    "use_gobii_app": True,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 201, response.content)
        payload = response.json()
        self.assertEqual(payload["client_id"], "managed-client-id")
        session = AgentEmailOAuthSession.objects.get(id=payload["session_id"])
        self.assertEqual(session.client_id, "managed-client-id")
        self.assertEqual(session.client_secret, "managed-secret")

    @patch.dict(
        os.environ,
        {
            "MICROSOFT_CLIENT_ID": "managed-ms-client-id",
            "MICROSOFT_CLIENT_SECRET": "managed-ms-secret",
        },
        clear=False,
    )
    def test_start_uses_managed_microsoft_app(self):
        url = reverse("console-email-oauth-start")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "account_id": str(self.account.pk),
                    "provider": "microsoft",
                    "scope": "offline_access",
                    "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
                    "use_gobii_app": True,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 201, response.content)
        payload = response.json()
        self.assertEqual(payload["client_id"], "managed-ms-client-id")
        session = AgentEmailOAuthSession.objects.get(id=payload["session_id"])
        self.assertEqual(session.client_id, "managed-ms-client-id")
        self.assertEqual(session.client_secret, "managed-ms-secret")

    @patch.dict(
        os.environ,
        {
            "MICROSOFT_CLIENT_ID": "managed-ms-client-id",
            "MICROSOFT_CLIENT_SECRET": "managed-ms-secret",
        },
        clear=False,
    )
    def test_start_uses_managed_outlook_app(self):
        url = reverse("console-email-oauth-start")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "account_id": str(self.account.pk),
                    "provider": "outlook",
                    "scope": "offline_access",
                    "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
                    "use_gobii_app": True,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 201, response.content)
        payload = response.json()
        self.assertEqual(payload["client_id"], "managed-ms-client-id")
        session = AgentEmailOAuthSession.objects.get(id=payload["session_id"])
        self.assertEqual(session.client_id, "managed-ms-client-id")
        self.assertEqual(session.client_secret, "managed-ms-secret")

    @patch("console.api_views.httpx.post")
    def test_callback_stores_credentials(self, mock_httpx_post):
        session = AgentEmailOAuthSession.objects.create(
            account=self.account,
            initiated_by=self.user,
            user=self.user,
            state="state-123",
            token_endpoint="https://oauth.example.com/token",
            expires_at=timezone.now() + timedelta(minutes=5),
        )
        session.code_verifier = "verifier-xyz"
        session.client_secret = "secret"
        session.client_id = "client-id"
        session.save()

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "access123",
            "refresh_token": "refresh123",
            "token_type": "Bearer",
            "expires_in": 3600,
            "scope": "read",
        }
        mock_httpx_post.return_value = mock_response

        url = reverse("console-email-oauth-callback")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "session_id": str(session.id),
                    "authorization_code": "code-abc",
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertTrue(payload["connected"])
        credential = AgentEmailOAuthCredential.objects.get(account=self.account)
        self.assertEqual(credential.access_token, "access123")
        self.assertEqual(credential.refresh_token, "refresh123")
        self.assertEqual(credential.client_id, "client-id")
        self.assertEqual(credential.client_secret, "secret")
        self.assertFalse(
            AgentEmailOAuthSession.objects.filter(id=session.id).exists(),
            "OAuth session should be removed after callback completion",
        )

    @patch("console.api_views.httpx.post")
    def test_callback_persists_microsoft_provider_metadata(self, mock_httpx_post):
        session = AgentEmailOAuthSession.objects.create(
            account=self.account,
            initiated_by=self.user,
            user=self.user,
            state="state-msft",
            scope="offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
            token_endpoint="https://login.microsoftonline.com/common/oauth2/v2.0/token",
            client_id="client-id",
            expires_at=timezone.now() + timedelta(minutes=5),
            metadata={
                "provider": "microsoft",
                "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
            },
        )

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "access123",
            "refresh_token": "refresh123",
            "token_type": "Bearer",
            "expires_in": 3600,
            "scope": "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
        }
        mock_httpx_post.return_value = mock_response

        url = reverse("console-email-oauth-callback")
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "session_id": str(session.id),
                    "authorization_code": "code-abc",
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)

        credential = AgentEmailOAuthCredential.objects.get(account=self.account)
        self.assertEqual(credential.provider, "microsoft")
        self.assertEqual(
            credential.scope,
            "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send",
        )
        self.assertEqual(
            credential.metadata["token_endpoint"],
            "https://login.microsoftonline.com/common/oauth2/v2.0/token",
        )

    def test_status_without_credentials(self):
        url = reverse("console-email-oauth-status", args=[self.account.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        payload = response.json()
        self.assertFalse(payload["connected"])

    def test_revoke_deletes_credentials(self):
        credential = AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
        )
        credential.access_token = "value"
        credential.save()

        url = reverse("console-email-oauth-revoke", args=[self.account.pk])
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        payload = response.json()
        self.assertTrue(payload["revoked"])
        self.assertFalse(AgentEmailOAuthCredential.objects.filter(id=credential.id).exists())

    def test_callback_page_includes_completion_script(self):
        url = reverse("console-email-oauth-callback-view")
        response = self.client.get(url, {"code": "abc", "state": "xyz"})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "js/agent_email_oauth_callback.js")

    def test_settings_page_mounts_react_email_app(self):
        url = reverse("agent_email_settings", args=[self.agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'data-app="agent-email-settings"')
        self.assertContains(response, reverse("console_agent_email_settings", args=[self.agent.pk]))

    def test_settings_page_creates_account_with_imap_idle_enabled_by_default(self):
        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=self.user, name="BA-page-defaults")
        second_agent = PersistentAgent.objects.create(
            user=self.user,
            name="Page Defaults Agent",
            charter="c",
            browser_use_agent=browser_agent,
        )
        endpoint = PersistentAgentCommsEndpoint.objects.create(
            owner_agent=second_agent,
            channel=CommsChannel.EMAIL,
            address="page-defaults@example.com",
            is_primary=True,
        )

        url = reverse("agent_email_settings", args=[second_agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        account = AgentEmailAccount.objects.get(endpoint=endpoint)
        self.assertTrue(account.imap_idle_enabled)

    def test_email_settings_api_get(self):
        url = reverse("console_agent_email_settings", args=[self.agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        payload = response.json()
        self.assertEqual(payload["agent"]["id"], str(self.agent.pk))
        self.assertEqual(payload["endpoint"]["address"], self.endpoint.address)
        self.assertTrue(payload["account"]["exists"])
        self.assertFalse(payload["account"]["hasSmtpPassword"])
        self.assertFalse(payload["account"]["hasImapPassword"])
        self.assertTrue(payload["account"]["imapIdleEnabled"])
        self.assertIn("defaultEmailDomain", payload)
        self.assertEqual(payload["defaultEmailDomain"], (settings.DEFAULT_AGENT_EMAIL_DOMAIN or "").lower())
        self.assertIn("defaultEndpoint", payload)
        self.assertIn("exists", payload["defaultEndpoint"])
        self.assertIn("address", payload["defaultEndpoint"])
        self.assertIn("isInboundAliasActive", payload["defaultEndpoint"])
        self.assertIn("gmail", payload["providerDefaults"])
        self.assertIn("microsoft", payload["providerDefaults"])
        self.assertIn("outlook", payload["providerDefaults"])

    def test_email_settings_api_get_includes_default_endpoint_payload(self):
        with override_settings(ENABLE_DEFAULT_AGENT_EMAIL=True):
            default_endpoint = ensure_default_agent_email_endpoint(self.agent, is_primary=False)
            self.assertIsNotNone(default_endpoint)

        url = reverse("console_agent_email_settings", args=[self.agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertTrue(payload["defaultEndpoint"]["exists"])
        self.assertEqual(payload["defaultEndpoint"]["address"], default_endpoint.address)
        self.assertTrue(payload["defaultEndpoint"]["isInboundAliasActive"])

    def test_email_settings_api_get_defaults_imap_idle_enabled_for_unconfigured_agent(self):
        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=self.user, name="BA-no-email")
        unconfigured_agent = PersistentAgent.objects.create(
            user=self.user,
            name="No Email Agent",
            charter="c",
            browser_use_agent=browser_agent,
        )
        url = reverse("console_agent_email_settings", args=[unconfigured_agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertFalse(payload["account"]["exists"])
        self.assertTrue(payload["account"]["imapIdleEnabled"])

    def test_email_settings_api_get_preserves_imap_idle_value_for_configured_account(self):
        self.account.imap_host = "imap.example.com"
        self.account.is_inbound_enabled = True
        self.account.imap_idle_enabled = False
        self.account.save()

        url = reverse("console_agent_email_settings", args=[self.agent.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertFalse(payload["account"]["imapIdleEnabled"])

    def test_email_settings_ensure_account_creates_endpoint(self):
        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=self.user, name="BA-2")
        second_agent = PersistentAgent.objects.create(
            user=self.user,
            name="OAuth Agent Two",
            charter="c",
            browser_use_agent=browser_agent,
        )
        ensure_url = reverse("console_agent_email_settings_ensure_account", args=[second_agent.pk])
        response = self.client.post(
            ensure_url,
            data=json.dumps({"endpointAddress": "second-agent@example.com"}),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertTrue(payload["settings"]["endpoint"]["exists"])
        self.assertTrue(payload["settings"]["account"]["exists"])
        self.assertTrue(payload["settings"]["account"]["imapIdleEnabled"])
        created_endpoint = PersistentAgentCommsEndpoint.objects.get(
            owner_agent=second_agent,
            channel=CommsChannel.EMAIL,
            address="second-agent@example.com",
        )
        self.assertEqual(created_endpoint.address, "second-agent@example.com")

    def test_email_settings_ensure_account_rebinds_oauth_usernames_when_switching_endpoints(self):
        self.account.connection_mode = AgentEmailAccount.ConnectionMode.OAUTH2
        self.account.smtp_auth = AgentEmailAccount.AuthMode.OAUTH2
        self.account.imap_auth = AgentEmailAccount.ImapAuthMode.OAUTH2
        self.account.smtp_username = self.endpoint.address
        self.account.imap_username = self.endpoint.address
        self.account.save()

        credential = AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="gmail",
        )

        new_endpoint = PersistentAgentCommsEndpoint.objects.create(
            channel=CommsChannel.EMAIL,
            address="david.weigelt@c12forums.com",
            is_primary=False,
        )

        ensure_url = reverse("console_agent_email_settings_ensure_account", args=[self.agent.pk])
        response = self.client.post(
            ensure_url,
            data=json.dumps({"endpointAddress": new_endpoint.address}),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)

        payload = response.json()
        self.assertEqual(payload["settings"]["endpoint"]["address"], new_endpoint.address)
        self.assertEqual(payload["settings"]["account"]["smtpUsername"], new_endpoint.address)
        self.assertEqual(payload["settings"]["account"]["imapUsername"], new_endpoint.address)

        new_endpoint.refresh_from_db()
        self.assertEqual(new_endpoint.owner_agent_id, self.agent.id)
        self.assertTrue(new_endpoint.is_primary)

        moved_account = AgentEmailAccount.objects.get(endpoint=new_endpoint)
        self.assertEqual(moved_account.smtp_username, new_endpoint.address)
        self.assertEqual(moved_account.imap_username, new_endpoint.address)
        self.assertFalse(AgentEmailAccount.objects.filter(endpoint=self.endpoint).exists())

        credential.refresh_from_db()
        self.assertEqual(credential.account_id, new_endpoint.id)

    def test_email_settings_ensure_account_preserves_default_alias_when_switching_to_custom(self):
        default_domain = settings.DEFAULT_AGENT_EMAIL_DOMAIN
        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=self.user, name="BA-default-switch")
        alias_agent = PersistentAgent.objects.create(
            user=self.user,
            name="Alias Switch Agent",
            charter="c",
            browser_use_agent=browser_agent,
        )
        default_endpoint = PersistentAgentCommsEndpoint.objects.create(
            owner_agent=alias_agent,
            channel=CommsChannel.EMAIL,
            address=f"alias.switch@{default_domain}",
            is_primary=True,
        )
        account = AgentEmailAccount.objects.create(endpoint=default_endpoint, imap_idle_enabled=True)
        account.smtp_host = "smtp.example.com"
        account.smtp_port = 587
        account.smtp_security = "starttls"
        account.smtp_auth = "login"
        account.smtp_username = default_endpoint.address
        account.smtp_password_encrypted = b"encrypted"
        account.is_outbound_enabled = True
        account.save()

        ensure_url = reverse("console_agent_email_settings_ensure_account", args=[alias_agent.pk])
        response = self.client.post(
            ensure_url,
            data=json.dumps({"endpointAddress": "custom.switch@example.com"}),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertEqual(payload["settings"]["endpoint"]["address"], "custom.switch@example.com")

        default_endpoint.refresh_from_db()
        self.assertEqual(default_endpoint.address, f"alias.switch@{default_domain}")
        self.assertFalse(default_endpoint.is_primary)

        custom_endpoint = PersistentAgentCommsEndpoint.objects.get(
            owner_agent=alias_agent,
            channel=CommsChannel.EMAIL,
            address="custom.switch@example.com",
        )
        self.assertTrue(custom_endpoint.is_primary)
        self.assertFalse(AgentEmailAccount.objects.filter(endpoint=default_endpoint).exists())
        self.assertTrue(AgentEmailAccount.objects.filter(endpoint=custom_endpoint).exists())

    def test_ensure_default_agent_email_endpoint_creates_alias_for_custom_only_agent(self):
        with patch.object(BrowserUseAgent, "select_random_proxy", return_value=None):
            browser_agent = BrowserUseAgent.objects.create(user=self.user, name="BA-custom-only")
        custom_only_agent = PersistentAgent.objects.create(
            user=self.user,
            name="Custom Only Agent",
            charter="c",
            browser_use_agent=browser_agent,
        )
        PersistentAgentCommsEndpoint.objects.create(
            owner_agent=custom_only_agent,
            channel=CommsChannel.EMAIL,
            address="custom.only@example.com",
            is_primary=True,
        )

        with override_settings(ENABLE_DEFAULT_AGENT_EMAIL=True):
            default_endpoint = ensure_default_agent_email_endpoint(custom_only_agent, is_primary=False)
            self.assertIsNotNone(default_endpoint)
            self.assertTrue(default_endpoint.address.endswith(f"@{settings.DEFAULT_AGENT_EMAIL_DOMAIN}".lower()))
            self.assertFalse(default_endpoint.is_primary)

            second_call_endpoint = ensure_default_agent_email_endpoint(custom_only_agent, is_primary=False)
            self.assertEqual(second_call_endpoint.id, default_endpoint.id)

    def test_email_settings_reset_to_default_restores_default_alias_and_removes_custom_config(self):
        with override_settings(ENABLE_DEFAULT_AGENT_EMAIL=True):
            default_endpoint = ensure_default_agent_email_endpoint(self.agent, is_primary=False)
            self.assertIsNotNone(default_endpoint)

        self.account.smtp_host = "smtp.gmail.com"
        self.account.smtp_port = 587
        self.account.smtp_security = "starttls"
        self.account.smtp_auth = "login"
        self.account.smtp_username = self.endpoint.address
        self.account.is_outbound_enabled = True
        self.account.save()
        credential = AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="gmail",
        )

        save_url = reverse("console_agent_email_settings", args=[self.agent.pk])
        with override_settings(ENABLE_DEFAULT_AGENT_EMAIL=True):
            response = self.client.post(
                save_url,
                data=json.dumps({"action": "reset_to_default"}),
                content_type="application/json",
            )
        self.assertEqual(response.status_code, 200, response.content)

        payload = response.json()
        self.assertTrue(payload["ok"])
        self.assertEqual(payload["settings"]["endpoint"]["address"], default_endpoint.address)
        self.assertFalse(payload["settings"]["account"]["exists"])

        default_endpoint.refresh_from_db()
        self.assertEqual(default_endpoint.owner_agent_id, self.agent.id)
        self.assertTrue(default_endpoint.is_primary)

        self.endpoint.refresh_from_db()
        self.assertIsNone(self.endpoint.owner_agent_id)
        self.assertFalse(self.endpoint.is_primary)
        self.assertFalse(AgentEmailAccount.objects.filter(endpoint=self.endpoint).exists())
        self.assertFalse(AgentEmailOAuthCredential.objects.filter(id=credential.id).exists())

    def test_email_settings_reset_to_default_requires_default_alias_feature(self):
        save_url = reverse("console_agent_email_settings", args=[self.agent.pk])
        with override_settings(ENABLE_DEFAULT_AGENT_EMAIL=False):
            response = self.client.post(
                save_url,
                data=json.dumps({"action": "reset_to_default"}),
                content_type="application/json",
            )
        self.assertEqual(response.status_code, 400, response.content)
        payload = response.json()
        self.assertIn("errors", payload)
        self.assertIn("default_endpoint", payload["errors"])

    def test_email_settings_ensure_account_rejects_invalid_endpoint(self):
        ensure_url = reverse("console_agent_email_settings_ensure_account", args=[self.agent.pk])
        response = self.client.post(
            ensure_url,
            data=json.dumps({"endpointAddress": "not-an-email"}),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 400, response.content)
        payload = response.json()
        self.assertIn("errors", payload)
        self.assertIn("endpoint_address", payload["errors"])

    @patch("console.email_settings.views._validate_agent_smtp_connection", return_value=(True, ""))
    def test_email_settings_test_endpoint_runs_smtp(self, _mock_validate_smtp):
        url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "endpointAddress": self.endpoint.address,
                    "connectionMode": "custom",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": False,
                    "testOutbound": True,
                    "testInbound": False,
                    "smtpHost": "smtp.gmail.com",
                    "smtpPort": 587,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "login",
                    "smtpUsername": self.endpoint.address,
                    "smtpPassword": "app-password",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "login",
                    "imapUsername": "",
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertTrue(payload["ok"])
        self.assertTrue(payload["results"]["smtp"]["ok"])

    @patch("console.email_settings.views._validate_agent_imap_connection", return_value=(True, ""))
    @patch("console.email_settings.views._validate_agent_smtp_connection", return_value=(True, ""))
    def test_email_settings_test_endpoint_applies_microsoft_oauth_defaults(
        self,
        mock_validate_smtp,
        mock_validate_imap,
    ):
        url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="microsoft",
        )
        mock_validate_smtp.side_effect = lambda account: (
            self.assertEqual(account.smtp_host, "smtp.office365.com") or True,
            "",
        )
        mock_validate_imap.side_effect = lambda account: (
            self.assertEqual(account.imap_host, "outlook.office365.com") or True,
            "",
        )

        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "endpointAddress": self.endpoint.address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "microsoft",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "testOutbound": True,
                    "testInbound": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": self.endpoint.address,
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": self.endpoint.address,
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertTrue(payload["ok"])
        self.assertEqual(mock_validate_smtp.call_count, 1)
        self.assertEqual(mock_validate_imap.call_count, 1)

    @patch("console.email_settings.views._validate_agent_smtp_connection", return_value=(False, "mock-smtp-error"))
    def test_email_settings_test_endpoint_does_not_persist_draft_settings(self, _mock_validate_smtp):
        self.account.smtp_host = "saved.smtp.example.com"
        self.account.smtp_port = 587
        self.account.smtp_security = "starttls"
        self.account.smtp_auth = "login"
        self.account.smtp_username = "saved-user@example.com"
        self.account.is_outbound_enabled = True
        self.account.imap_host = "saved.imap.example.com"
        self.account.imap_port = 993
        self.account.imap_security = "ssl"
        self.account.imap_auth = "login"
        self.account.imap_username = "saved-user@example.com"
        self.account.is_inbound_enabled = True
        self.account.save()

        url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "endpointAddress": self.endpoint.address,
                    "connectionMode": "custom",
                    "isOutboundEnabled": False,
                    "isInboundEnabled": False,
                    "testOutbound": True,
                    "testInbound": False,
                    "smtpHost": "new.smtp.example.com",
                    "smtpPort": 465,
                    "smtpSecurity": "ssl",
                    "smtpAuth": "none",
                    "smtpUsername": "new-user@example.com",
                    "smtpPassword": "new-password",
                    "imapHost": "new.imap.example.com",
                    "imapPort": 143,
                    "imapSecurity": "starttls",
                    "imapAuth": "none",
                    "imapUsername": "new-user@example.com",
                    "imapPassword": "new-password",
                    "imapFolder": "NEWFOLDER",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 180,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        payload = response.json()
        self.assertFalse(payload["ok"])

        self.account.refresh_from_db()
        self.assertEqual(self.account.smtp_host, "saved.smtp.example.com")
        self.assertEqual(self.account.smtp_port, 587)
        self.assertEqual(self.account.smtp_security, "starttls")
        self.assertEqual(self.account.smtp_auth, "login")
        self.assertEqual(self.account.smtp_username, "saved-user@example.com")
        self.assertEqual(self.account.imap_host, "saved.imap.example.com")
        self.assertEqual(self.account.imap_port, 993)
        self.assertEqual(self.account.imap_security, "ssl")
        self.assertEqual(self.account.imap_auth, "login")
        self.assertEqual(self.account.imap_username, "saved-user@example.com")
        self.assertTrue(self.account.is_outbound_enabled)
        self.assertTrue(self.account.is_inbound_enabled)
        self.assertIn("SMTP test failed: mock-smtp-error", self.account.connection_error)

    @patch("console.email_settings.views._validate_agent_imap_connection")
    @patch("console.email_settings.views._validate_agent_smtp_connection")
    def test_email_settings_test_endpoint_rebinds_oauth_usernames_for_connection_checks(
        self,
        mock_validate_smtp,
        mock_validate_imap,
    ):
        self.account.connection_mode = AgentEmailAccount.ConnectionMode.OAUTH2
        self.account.smtp_auth = AgentEmailAccount.AuthMode.OAUTH2
        self.account.imap_auth = AgentEmailAccount.ImapAuthMode.OAUTH2
        self.account.smtp_username = self.endpoint.address
        self.account.imap_username = self.endpoint.address
        self.account.save()

        AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="gmail",
        )

        url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        new_address = "renamed-agent@example.com"

        def smtp_side_effect(account):
            self.assertEqual(account.endpoint.address, new_address)
            self.assertEqual(account.smtp_username, new_address)
            return True, ""

        def imap_side_effect(account):
            self.assertEqual(account.endpoint.address, new_address)
            self.assertEqual(account.imap_username, new_address)
            return True, ""

        mock_validate_smtp.side_effect = smtp_side_effect
        mock_validate_imap.side_effect = imap_side_effect

        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "endpointAddress": new_address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "gmail",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "testOutbound": True,
                    "testInbound": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": self.endpoint.address,
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": self.endpoint.address,
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)

        payload = response.json()
        self.assertTrue(payload["ok"])
        self.assertEqual(payload["settings"]["endpoint"]["address"], new_address)
        self.assertEqual(mock_validate_smtp.call_count, 1)
        self.assertEqual(mock_validate_imap.call_count, 1)

        self.account.refresh_from_db()
        self.assertEqual(self.account.endpoint.address, new_address)

    @patch("console.email_settings.views._validate_agent_imap_connection")
    @patch("console.email_settings.views._validate_agent_smtp_connection")
    def test_email_settings_test_endpoint_replaces_stale_first_party_oauth_usernames(
        self,
        mock_validate_smtp,
        mock_validate_imap,
    ):
        self.account.connection_mode = AgentEmailAccount.ConnectionMode.OAUTH2
        self.account.smtp_auth = AgentEmailAccount.AuthMode.OAUTH2
        self.account.imap_auth = AgentEmailAccount.ImapAuthMode.OAUTH2
        self.account.smtp_host = "smtp.office365.com"
        self.account.imap_host = "imap.gmail.com"
        self.account.smtp_username = "stale-gmail@example.com"
        self.account.imap_username = "stale-gmail@example.com"
        self.account.save()

        AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="outlook",
        )

        mailbox_address = "matt.greathouse@outlook.com"

        def smtp_side_effect(account):
            self.assertEqual(account.smtp_username, mailbox_address)
            self.assertEqual(account.smtp_host, "smtp-mail.outlook.com")
            return True, ""

        def imap_side_effect(account):
            self.assertEqual(account.imap_username, mailbox_address)
            self.assertEqual(account.imap_host, "outlook.office365.com")
            return True, ""

        mock_validate_smtp.side_effect = smtp_side_effect
        mock_validate_imap.side_effect = imap_side_effect

        url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        response = self.client.post(
            url,
            data=json.dumps(
                {
                    "endpointAddress": mailbox_address,
                    "previousEndpointAddress": self.endpoint.address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "outlook",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "testOutbound": True,
                    "testInbound": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": "stale-gmail@example.com",
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": "stale-gmail@example.com",
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(response.status_code, 200, response.content)
        self.assertTrue(response.json()["ok"])

    @patch("console.email_settings.views._validate_agent_imap_connection")
    @patch("console.email_settings.views._validate_agent_smtp_connection")
    def test_email_settings_save_rebinds_oauth_usernames_after_test_address_change(
        self,
        mock_validate_smtp,
        mock_validate_imap,
    ):
        old_address = self.endpoint.address
        self.account.connection_mode = AgentEmailAccount.ConnectionMode.OAUTH2
        self.account.smtp_auth = AgentEmailAccount.AuthMode.OAUTH2
        self.account.imap_auth = AgentEmailAccount.ImapAuthMode.OAUTH2
        self.account.smtp_username = old_address
        self.account.imap_username = old_address
        self.account.connection_last_ok_at = timezone.now()
        self.account.save()

        AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="gmail",
        )

        mock_validate_smtp.return_value = True, ""
        mock_validate_imap.return_value = True, ""

        new_address = "renamed-agent-save@example.com"
        test_url = reverse("console_agent_email_settings_test", args=[self.agent.pk])
        save_url = reverse("console_agent_email_settings", args=[self.agent.pk])

        test_response = self.client.post(
            test_url,
            data=json.dumps(
                {
                    "endpointAddress": new_address,
                    "previousEndpointAddress": old_address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "gmail",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "testOutbound": True,
                    "testInbound": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": old_address,
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": old_address,
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(test_response.status_code, 200, test_response.content)

        self.account.refresh_from_db()
        self.assertEqual(self.account.endpoint.address, new_address)
        self.assertEqual(self.account.smtp_username, old_address)
        self.assertEqual(self.account.imap_username, old_address)

        save_response = self.client.post(
            save_url,
            data=json.dumps(
                {
                    "endpointAddress": new_address,
                    "previousEndpointAddress": old_address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "gmail",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": old_address,
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": old_address,
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(save_response.status_code, 200, save_response.content)

        self.account.refresh_from_db()
        self.assertEqual(self.account.endpoint.address, new_address)
        self.assertEqual(self.account.smtp_username, new_address)
        self.assertEqual(self.account.imap_username, new_address)

    @patch("console.email_settings.views._validate_agent_imap_connection")
    @patch("console.email_settings.views._validate_agent_smtp_connection")
    def test_email_settings_save_rebinds_microsoft_oauth_usernames_after_test_address_change(
        self,
        mock_validate_smtp,
        mock_validate_imap,
    ):
        old_address = self.endpoint.address
        self.account.connection_mode = AgentEmailAccount.ConnectionMode.OAUTH2
        self.account.smtp_auth = AgentEmailAccount.AuthMode.OAUTH2
        self.account.imap_auth = AgentEmailAccount.ImapAuthMode.OAUTH2
        self.account.smtp_username = old_address
        self.account.imap_username = old_address
        self.account.connection_last_ok_at = timezone.now()
        self.account.save()

        AgentEmailOAuthCredential.objects.create(
            account=self.account,
            user=self.user,
            provider="microsoft",
        )

        mock_validate_smtp.return_value = True, ""
        mock_validate_imap.return_value = True, ""

        new_address = "renamed-agent-microsoft@example.com"
        save_url = reverse("console_agent_email_settings", args=[self.agent.pk])

        save_response = self.client.post(
            save_url,
            data=json.dumps(
                {
                    "endpointAddress": new_address,
                    "previousEndpointAddress": old_address,
                    "connectionMode": "oauth2",
                    "oauthProvider": "microsoft",
                    "isOutboundEnabled": True,
                    "isInboundEnabled": True,
                    "smtpHost": "",
                    "smtpPort": None,
                    "smtpSecurity": "starttls",
                    "smtpAuth": "oauth2",
                    "smtpUsername": old_address,
                    "smtpPassword": "",
                    "imapHost": "",
                    "imapPort": None,
                    "imapSecurity": "ssl",
                    "imapAuth": "oauth2",
                    "imapUsername": old_address,
                    "imapPassword": "",
                    "imapFolder": "INBOX",
                    "imapIdleEnabled": False,
                    "pollIntervalSec": 120,
                }
            ),
            content_type="application/json",
        )
        self.assertEqual(save_response.status_code, 200, save_response.content)

        self.account.refresh_from_db()
        self.assertEqual(self.account.endpoint.address, new_address)
        self.assertEqual(self.account.smtp_username, new_address)
        self.assertEqual(self.account.imap_username, new_address)
        self.assertEqual(self.account.smtp_host, "smtp.office365.com")
        self.assertEqual(self.account.imap_host, "outlook.office365.com")

    def test_normalize_email_error_text_flattens_tuple_and_bytes(self):
        normalized = _normalize_email_error_text(
            "(535, b'5.7.8 Username and Password not accepted. Learn more at https://support.google.com/mail/?p=BadCredentials x - gsmtp')"
        )
        self.assertNotIn("b'", normalized)
        self.assertIn("Username and Password not accepted", normalized)

    def test_format_email_connection_error_humanizes_missing_credentials(self):
        message = _format_email_connection_error("b'Empty username or password. af79cd13be357-8cb2e53b218mb5783094385a'")
        self.assertEqual(message, "Username or password is missing. Enter both values and try again.")

    def test_format_email_connection_error_humanizes_microsoft_smtp_auth_disabled(self):
        message = _format_email_connection_error("535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant.")
        self.assertEqual(
            message,
            "Microsoft says SMTP AUTH is disabled for this tenant. Enable authenticated SMTP, or use a different outbound mail provider.",
        )

    def test_format_email_connection_error_humanizes_microsoft_mailbox_smtp_auth_disabled(self):
        message = _format_email_connection_error(
            "535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox."
        )
        self.assertEqual(
            message,
            "Microsoft says SMTP AUTH is disabled for this mailbox. Enable authenticated SMTP for the mailbox, or use a different outbound mail provider.",
        )

    def test_format_email_connection_error_humanizes_microsoft_smtp_oauth_failure(self):
        message = _format_email_connection_error(
            "535 5.7.3 Authentication unsuccessful",
            channel="smtp",
            auth_mode="oauth2",
            provider="outlook",
        )
        self.assertEqual(
            message,
            "Microsoft rejected SMTP OAuth for this mailbox. Confirm Authenticated SMTP is enabled for the mailbox and try reconnecting OAuth.",
        )

    def test_format_email_connection_error_humanizes_generic_oauth_failure(self):
        message = _format_email_connection_error(
            "Authentication failed",
            channel="imap",
            auth_mode="oauth2",
            provider="custom",
        )
        self.assertEqual(message, "OAuth authentication failed. Reconnect this email account and try again.")

    def test_format_email_connection_error_humanizes_disabled_imap(self):
        message = _format_email_connection_error("IMAP is disabled for this account.")
        self.assertEqual(
            message,
            "IMAP access is disabled for this mailbox. Enable IMAP for the account and try again.",
        )
