import os
import re
from collections.abc import Sequence
from datetime import timedelta
from typing import TYPE_CHECKING, Any
from unittest import mock, skipUnless
from urllib.parse import urlsplit

import orjson
from django.conf import settings
from django.test import override_settings
from django.utils.timezone import now as timezone_now

from corporate.models.customers import Customer
from corporate.models.plans import CustomerPlan
from zerver.context_processors import get_apps_page_url
from zerver.lib.integrations import BOT_INTEGRATIONS, CATEGORIES, INTEGRATIONS, META_CATEGORY
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import HostRequestMock
from zerver.lib.url_redirects import INTEGRATION_CATEGORY_SLUGS
from zerver.models import Realm
from zerver.models.realms import get_realm
from zerver.views.documentation import add_api_url_context


def get_canonical_url(path: str) -> str:
    return f'<link rel="canonical" href="https://zulip.com{path}" />'


if TYPE_CHECKING:
    from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse


class DocPageTest(ZulipTestCase):
    def get_doc(self, url: str, subdomain: str) -> "TestHttpResponse":
        return self.client_get(url, subdomain=subdomain)

    def print_msg_if_error(self, url: str, response: "TestHttpResponse") -> None:  # nocoverage
        if response.status_code == 200:
            return
        print("Error processing URL:", url)
        if response.get("Content-Type") == "application/json":
            content = orjson.loads(response.content)
            print()
            print("======================================================================")
            print("ERROR: {}".format(content.get("msg")))
            print()

    def _is_landing_page(self, url: str) -> bool:
        for prefix in [
            "/api/",
            "/devlogin/",
            "/devtools/",
            "/emails/",
            "/errors/",
            "/help",
            "/integrations/",
        ]:
            if url.startswith(prefix):
                return False
        return True

    def _check_basic_fetch(
        self,
        *,
        url: str,
        subdomain: str,
        expected_strings: Sequence[str],
        allow_robots: bool,
    ) -> "TestHttpResponse":
        if url.startswith("/attribution/"):
            allow_robots = False

        # When a raw MDX file is being fetched, the meta tag to
        # disallow robots will be absent.
        if url in ["/help/status-and-availability?raw", "/help/?raw", "/help?raw"]:
            allow_robots = True

        result = self.get_doc(url, subdomain=subdomain)
        self.print_msg_if_error(url, result)
        self.assertEqual(result.status_code, 200)
        for s in expected_strings:
            self.assertIn(s, str(result.content))

        if allow_robots:
            self.assert_not_in_success_response(
                ['<meta name="robots" content="noindex,nofollow" />'], result
            )
        else:
            self.assert_in_success_response(
                ['<meta name="robots" content="noindex,nofollow" />'], result
            )
        return result

    def _test(self, url: str, expected_strings: Sequence[str]) -> "TestHttpResponse":
        # Test the URL on the root subdomain
        response = self._check_basic_fetch(
            url=url,
            subdomain="",
            expected_strings=expected_strings,
            allow_robots=False,
        )

        if not self._is_landing_page(url):
            return response

        with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
            # Test the URL on the root subdomain with the landing page setting
            result = self._check_basic_fetch(
                url=url,
                subdomain="",
                expected_strings=expected_strings,
                allow_robots=True,
            )

            # Confirm page has the following HTML elements:
            # (I have no idea why we don't support this for /attribution/.)
            if not url.startswith("/attribution/"):
                self.assert_in_success_response(
                    [
                        "<title>",
                        '<meta name="description" content="',
                        '<meta property="og:title" content="',
                        '<meta property="og:description" content="',
                    ],
                    result,
                )
        return response

    def test_zephyr_disallows_robots(self) -> None:
        sample_urls = [
            "/apps/",
            "/case-studies/end-point/",
            "/communities/",
            "/devlogin/",
            "/devtools/",
            "/emails/",
            "/errors/404/",
            "/errors/5xx/",
            "/integrations/",
            "/integrations/category/bots",
            "/integrations/github",
            "/team/",
        ]

        for url in sample_urls:
            self._check_basic_fetch(
                url=url,
                subdomain="zephyr",
                expected_strings=[],
                allow_robots=False,
            )

            if self._is_landing_page(url):
                with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
                    self._check_basic_fetch(
                        url=url,
                        subdomain="zephyr",
                        expected_strings=[],
                        allow_robots=False,
                    )

    def test_api_doc_endpoints(self) -> None:
        # We extract the set of /api/ endpoints to check by parsing
        # the /api/ page sidebar for links starting with /api/.
        api_page_raw = str(self.client_get("/api/").content)
        ENDPOINT_REGEXP = re.compile(r"href=\"/api/\s*(.*?)\"")
        endpoint_list_set = set(re.findall(ENDPOINT_REGEXP, api_page_raw))
        endpoint_list = sorted(endpoint_list_set)

        # We want to make sure our regex captured the actual main page.
        assert "" in endpoint_list

        content = {
            "/api/": "The Zulip API",
            "/api/api-keys": "be careful with it",
            "/api/create-user": "zuliprc-admin",
            "/api/delete-queue": "Delete a previously registered queue",
            "/api/get-events": "dont_block",
            "/api/get-own-user": "does not accept any parameters.",
            "/api/get-stream-id": "The name of the channel to access.",
            "/api/get-streams": "include_public",
            "/api/get-subscriptions": "Get all channels that the user is subscribed to.",
            "/api/get-users": "client_gravatar",
            "/api/installation-instructions": "No download required!",
            "/api/register-queue": "apply_markdown",
            "/api/render-message": "**foo**",
            "/api/send-message": "steal away your hearts",
            "/api/subscribe": "authorization_errors_fatal",
            "/api/unsubscribe": "not_removed",
            "/api/update-message": "propagate_mode",
        }

        """
        We have 110 endpoints as of June 2023.  If the
        way we represent links changes, or the way we put links
        into the main /api page changes, or if somebody simply introduces
        a bug into the test, there is a danger of losing coverage,
        although this is mitigated by other factors such as line
        coverage checks.  For that reason, as well as developer convenience,
        we don't make the check here super precise.
        """
        self.assertGreater(len(endpoint_list), 100)

        for endpoint in endpoint_list:
            url = f"/api/{endpoint}"

            if url in content:
                expected_strings = [content[url]]
                del content[url]
            else:
                # TODO: Just fill out dictionary for all ~110 endpoints
                #       with some specific data from the page.
                expected_strings = ["This is an API doc"]

            # Mock OpenGraph call purely to speed up these tests.
            with mock.patch(
                "zerver.lib.html_to_text.html_to_text", return_value="This is an API doc"
            ) as m:
                # Test that canonical URL points to zulip.com
                expected_strings.append(get_canonical_url(url))
                self._test(
                    url=url,
                    expected_strings=expected_strings,
                )
                if url != "/api/":
                    m.assert_called()

        # Make sure we exercised all content checks.
        self.assert_length(content, 0)

    def test_api_doc_404_status_codes(self) -> None:
        result = self.client_get(
            "/api/nonexistent-page",
            follow=True,
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(result.status_code, 404)

        result = self.client_get(
            # This template shouldn't be accessed directly.
            "/api/api-doc-template",
            follow=True,
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(result.status_code, 404)

        result = self.client_get(
            # Non-root API docs pages do not have a trailing slash.
            "/api/changelog/",
            follow=True,
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(result.status_code, 404)

    def test_dev_environment_endpoints(self) -> None:
        self._test("/devlogin/", ["Normal users"])
        self._test("/devtools/", ["Development tools"])
        self._test("/emails/", ["Manually generate most emails"])

    def test_dev_help_default_page_endpoints(self) -> None:
        # View on Zulip.com and View source URLs should be visible.
        self._test(
            "/help/status-and-availability",
            [
                'href="https://zulip.com/help/status-and-availability"',
                'href="/help/status-and-availability?raw"',
            ],
        )

        # Raw MDX file should be shown when `?raw` is present.
        self._test(
            "/help/status-and-availability?raw",
            ["---", "title: Status and availability", "### Set a status"],
        )

        self._test(
            "/help/nonexistent-page-that-does-not-exist",
            ["This is not a valid help path and not a valid MDX file"],
        )

        # `?raw` should have no effect when the page does not exist
        self._test(
            "/help/nonexistent-page-that-does-not-exist?raw",
            ["This is not a valid help path and not a valid MDX file"],
        )

        # Root /help and /help/ is a special case without subpath.
        self._test("/help/?raw", ["---", "title: Zulip help center"])
        self._test("/help?raw", ["---", "title: Zulip help center"])

        with (
            mock.patch("builtins.open", side_effect=OSError("File read error")),
            self.assertLogs("django.request", level="ERROR") as m,
        ):
            result = self.client_get("/help/status-and-availability?raw")
            self.assertEqual(result.status_code, 500)
            self.assertIn("Error reading MDX file", result.content.decode())
            self.assertEqual(
                m.output,
                ["ERROR:django.request:Internal Server Error: /help/status-and-availability"],
            )

    def test_error_endpoints(self) -> None:
        self._test("/errors/404/", ["Page not found"])
        self._test("/errors/5xx/", ["Internal server error"])

    def test_corporate_portico_endpoints(self) -> None:
        self._test("/team/", ["industry veterans"])
        self._test("/apps/", ["Apps for every platform."])

        self._test("/history/", ["Zulip released as open source!"])
        # Test the i18n version of one of these pages.
        self._test("/en/history/", ["Zulip released as open source!"])
        self._test("/values/", ["designed our company"])
        self._test("/hello/", ["remote and flexible work"])
        self._test("/communities/", ["Open communities directory"])
        self._test("/development-community/", ["Zulip development community"])
        self._test("/features/", ["Organized team chat solution"])
        self._test("/jobs/", ["Work with us"])
        self._test("/self-hosting/", ["Self-host Zulip"])
        self._test("/zulip-cloud/", ["Zulip Cloud"])
        self._test("/security/", ["TLS encryption"])
        self._test("/use-cases/", ["Use cases and customer stories"])
        self._test("/why-zulip/", ["Why Zulip?"])
        # /for/... pages
        self._test("/for/open-source/", ["for open source projects"])
        self._test("/for/events/", ["for conferences and events"])
        self._test("/for/education/", ["education pricing"])
        self._test("/for/research/", ["for research"])
        self._test("/for/business/", ["Communication efficiency represents"])
        self._test("/for/communities/", ["Zulip for communities"])
        # case-studies
        self._test("/case-studies/tum/", ["Technical University of Munich"])
        self._test("/case-studies/ucsd/", ["UCSD"])
        self._test(
            "/case-studies/university-of-cordoba/", ["National University of C\\xc3\\xb3rdoba"]
        )
        self._test("/case-studies/rust/", ["Rust programming language"])
        self._test("/case-studies/recurse-center/", ["Recurse Center"])
        self._test("/case-studies/lean/", ["Lean theorem prover"])
        self._test("/case-studies/idrift/", ["Case study: iDrift AS"])
        self._test("/case-studies/end-point/", ["Case study: End Point"])
        self._test("/case-studies/atolio/", ["Case study: Atolio"])
        self._test("/case-studies/asciidoctor/", ["Case study: Asciidoctor"])
        self._test("/case-studies/rush-stack/", ["Case study: Rush Stack"])

    def test_oddball_attributions_page(self) -> None:
        # Look elsewhere in the code--this page never allows robots nor does
        # it provide og data.
        self._test("/attribution/", ["Website attributions"])

    def test_open_organizations_endpoint(self) -> None:
        zulip_dev_info = ["Zulip Dev", "great for testing!"]

        result = self.client_get("/communities/")
        self.assert_not_in_success_response(zulip_dev_info, result)

        realm = get_realm("zulip")
        realm.want_advertise_in_communities_directory = True
        realm.save()

        realm.description = ""
        realm.save()
        result = self.client_get("/communities/")
        # Not shown because the realm has default description set.
        self.assert_not_in_success_response(["Zulip Dev"], result)

        realm.description = "Some description"
        realm.save()
        self._test("/communities/", ["Open communities directory", "Zulip Dev", "Some description"])

        # No org with research type so research category not displayed.
        result = self.client_get("/communities/")
        self.assert_not_in_success_response(['data-category="research"'], result)

        realm.org_type = Realm.ORG_TYPES["research"]["id"]
        realm.save()
        self._test(
            "/communities/", ["Open communities directory", "Zulip Dev", 'data-category="research"']
        )

        # Demo organizations are not shown.
        demo_deletion_date = timezone_now() + timedelta(days=15)
        realm.demo_organization_scheduled_deletion_date = demo_deletion_date
        realm.save()
        result = self.client_get("/communities/")
        self.assert_not_in_success_response(
            ["Zulip Dev", "Some description", 'data-category="research"'], result
        )

    def test_integration_doc_endpoints(self) -> None:
        images_in_docs = set()

        response = self._test(
            "/integrations/",
            expected_strings=[
                "native integrations.",
                "And hundreds more through",
                "Zapier",
                "IFTTT",
            ],
        )
        page = response.content.decode("utf-8")
        for image in re.findall(r"/static/images/integrations/(logos/.*)\"", page):
            images_in_docs.add(image)

        result = self.client_get(
            "/integrations/category/nonexistent_category",
            follow=True,
        )
        self.assertEqual(result.status_code, 404)

        for category_name in INTEGRATION_CATEGORY_SLUGS:
            legacy_category_response = self.client_get(
                f"/integrations/{category_name}",
                follow=False,
            )
            self.assertEqual(legacy_category_response.status_code, 301)
            self.assertEqual(
                legacy_category_response["Location"], f"/integrations/category/{category_name}"
            )

        legacy_doc_response = self.client_get(
            "/integrations/doc/asana?category=project-management",
            follow=False,
        )
        self.assertEqual(legacy_doc_response.status_code, 301)
        self.assertEqual(
            legacy_doc_response["Location"], "/integrations/asana?category=project-management"
        )

        legacy_doc_with_trailing_slash_response = self.client_get(
            "/integrations/doc/asana/",
            follow=False,
        )
        self.assertEqual(legacy_doc_with_trailing_slash_response.status_code, 301)
        self.assertEqual(legacy_doc_with_trailing_slash_response["Location"], "/integrations/asana")

        result = self.client_get(
            "/integrations/nonexistent_integration",
            follow=True,
        )
        self.assertEqual(result.status_code, 404)

        result = self._test(
            "/integrations/asana?category=project-management",
            expected_strings=[
                '<a href="/integrations/category/project-management" id="integration-list-link" class="no-underline">'
            ],
        )

        result = self.client_get(
            "/integrations/asana?category=nonexistent_category",
        )
        self.assert_not_in_success_response(
            [
                '<a href="/integrations/category/project-management" id="integration-list-link" class="no-underline">'
            ],
            response,
        )

        # Mismatched category should also return back to all.
        result = self.client_get(
            "/integrations/asana?category=communication",
        )
        self.assert_not_in_success_response(
            [
                '<a href="/integrations/category/project-management" id="integration-list-link" class="no-underline">'
            ],
            response,
        )

        bot_subdirs = [integration.name for integration in BOT_INTEGRATIONS]
        for integration in INTEGRATIONS:
            url = f"/integrations/{integration}"
            response = self._test(url, expected_strings=[])
            doc = response.content.decode("utf-8")

            patterns = [r"/static/images/integrations/(.*?)\""] + [
                rf"/static/generated/bots/{bot_dir}/(.*?)\"" for bot_dir in bot_subdirs
            ]
            for pattern in patterns:
                for image in re.findall(pattern, doc):
                    images_in_docs.add(image)

        images_in_dir = {
            image_path
            for directory, exclude_md in [
                ("static/images/integrations", False),
            ]
            + [(f"static/generated/bots/{bot_dir}", True) for bot_dir in bot_subdirs]
            for root, _, files in os.walk(directory)
            for file in files
            if "bot_avatars"
            not in (image_path := os.path.relpath(os.path.join(root, file), directory))
            and (not exclude_md or not image_path.endswith(".md"))
        }

        # The integration docs and the screenshot images are in different repos
        # for the PythonAPIIntegrations, so we cannot avoid going out of sync
        # when adding/deleting screenshots.
        # Use this set to temporarily add exclusions to this test.
        exception_images: set[str] = {"jira-plugin/001.png", "git/001.png"}
        images_in_dir.update(exception_images)
        images_in_docs.update(exception_images)

        self.assertEqual(
            images_in_dir,
            images_in_docs,
            (
                "\n\nThe following images are not used in documentation and can be removed:\n"
                + "\n".join(extra_images)
                if (extra_images := images_in_dir - images_in_docs)
                else ""
            )
            + (
                "\n\nThe following images are used in documentation but do not exist:\n"
                + "\n".join(missing_images)
                if (missing_images := images_in_docs - images_in_dir)
                else ""
            ),
        )

    def test_integration_pages_open_graph_metadata(self) -> None:
        og_description = '<meta property="og:description" content="Zulip comes with over'

        # Test a particular integration page
        url = "/integrations/github"
        title = '<meta property="og:title" content="GitHub | Zulip integrations" />'
        description = '<meta property="og:description" content="Zulip comes with over'
        self._test(url, [title, description, get_canonical_url(url)])

        # Test category pages
        for category in CATEGORIES:
            url = f"/integrations/category/{category}"
            if category in META_CATEGORY:
                title = f"<title>{CATEGORIES[category]} | Zulip integrations</title>"
                og_title = f'<meta property="og:title" content="{CATEGORIES[category]} | Zulip integrations" />'
            else:
                title = f"<title>{CATEGORIES[category]} tools | Zulip integrations</title>"
                og_title = f'<meta property="og:title" content="{CATEGORIES[category]} tools | Zulip integrations" />'
            self._test(url, [title, og_title, og_description, get_canonical_url(url)])

        # Test integrations index page
        url = "/integrations/"
        og_title = '<meta property="og:title" content="Zulip integrations" />'
        self._test(url, [og_title, og_description, get_canonical_url(url)])

    def test_integration_404s(self) -> None:
        # We don't need to test all the pages for 404
        for integration in list(INTEGRATIONS.keys())[5]:
            with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
                url = f"/en/integrations/{integration}"
                result = self.client_get(url, subdomain="", follow=True)
                self.assertEqual(result.status_code, 404)
                result = self.client_get(url, subdomain="zephyr", follow=True)
                self.assertEqual(result.status_code, 404)

            url = f"/en/integrations/{integration}"
            result = self.client_get(url, subdomain="", follow=True)
            self.assertEqual(result.status_code, 404)
            result = self.client_get(url, subdomain="zephyr", follow=True)
            self.assertEqual(result.status_code, 404)

        result = self.client_get("/integrations/nonexistent_integration", follow=True)
        self.assertEqual(result.status_code, 404)

    def test_electron_detection(self) -> None:
        result = self.client_get("/accounts/password/reset/")
        # TODO: Ideally, this Mozilla would be the specific browser.
        self.assertTrue('data-platform="Mozilla"' in result.content.decode())

        result = self.client_get("/accounts/password/reset/", HTTP_USER_AGENT="ZulipElectron/1.0.0")
        self.assertTrue('data-platform="ZulipElectron"' in result.content.decode())


class CustomShorthandLinksTest(ZulipTestCase):
    # We do not test relative links here since relative links were only
    # used in the old help pages. We should probably remove the logic
    # for relative links after we have done the official cutover to the
    # starlight help center: https://github.com/zulip/zulip/issues/35654.
    def test_settings_links(self) -> None:
        result = self.client_get("/api/api-keys")
        self.assertEqual(result.status_code, 200)
        self.assertIn(
            'Go to <a href="/#settings/account-and-privacy">Account &amp; privacy</a>',
            str(result.content),
        )
        # Check that the sidebar was rendered properly.
        self.assertIn("API documentation home", str(result.content))

        with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
            result = self.client_get("/api/api-keys", subdomain="")
        self.assertEqual(result.status_code, 200)
        self.assertIn("<strong>Account &amp; privacy</strong>", str(result.content))
        self.assertNotIn("/#settings", str(result.content))


class IntegrationTest(ZulipTestCase):
    def test_check_if_every_integration_has_logo_that_exists(self) -> None:
        for integration in INTEGRATIONS.values():
            assert integration.logo_url is not None
            path = urlsplit(integration.logo_url).path
            self.assertTrue(os.path.isfile(settings.DEPLOY_ROOT + path), integration.name)

    def test_api_url_view_subdomains_base(self) -> None:
        context: dict[str, Any] = {}
        add_api_url_context(context, HostRequestMock())
        self.assertEqual(context["api_url_scheme_relative"], "testserver/api")
        self.assertEqual(context["api_url"], "http://testserver/api")
        self.assertTrue(context["html_settings_links"])

    @override_settings(ROOT_DOMAIN_LANDING_PAGE=True)
    def test_api_url_view_subdomains_homepage_base(self) -> None:
        context: dict[str, Any] = {}
        add_api_url_context(context, HostRequestMock())
        self.assertEqual(context["api_url_scheme_relative"], "your-org.testserver/api")
        self.assertEqual(context["api_url"], "http://your-org.testserver/api")
        self.assertFalse(context["html_settings_links"])

    def test_api_url_view_subdomains_full(self) -> None:
        context: dict[str, Any] = {}
        request = HostRequestMock(host="mysubdomain.testserver")
        add_api_url_context(context, request)
        self.assertEqual(context["api_url_scheme_relative"], "mysubdomain.testserver/api")
        self.assertEqual(context["api_url"], "http://mysubdomain.testserver/api")
        self.assertTrue(context["html_settings_links"])


class AboutPageTest(ZulipTestCase):
    @skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
    def test_endpoint(self) -> None:
        with self.settings(CONTRIBUTOR_DATA_FILE_PATH="zerver/tests/fixtures/authors.json"):
            result = self.client_get("/team/")
        self.assert_in_success_response(["Our amazing community"], result)
        self.assert_in_success_response(["2017-11-20"], result)
        self.assert_in_success_response(["timabbott", "showell", "gnprice", "rishig"], result)

        with mock.patch("corporate.views.portico.open", side_effect=FileNotFoundError) as m:
            result = self.client_get("/team/")
            self.assertEqual(result.status_code, 200)
            self.assert_in_success_response(["Never ran"], result)
            m.assert_called_once()

        with self.settings(ZILENCER_ENABLED=False):
            result = self.client_get("/team/")
            self.assertEqual(result.status_code, 301)
            self.assertEqual(result["Location"], "https://zulip.com/team/")


class SmtpConfigErrorTest(ZulipTestCase):
    def test_smtp_error(self) -> None:
        with self.assertLogs("django.request", level="ERROR") as m:
            result = self.client_get("/config-error/smtp")
            self.assertEqual(result.status_code, 500)
            self.assert_in_response("email configuration", result)
            self.assertEqual(
                m.output,
                ["ERROR:django.request:Internal Server Error: /config-error/smtp"],
            )


class PlansPageTest(ZulipTestCase):
    def test_plans_auth(self) -> None:
        root_domain = ""
        result = self.client_get("/plans/", subdomain=root_domain)
        self.assert_in_success_response(["Self-host Zulip"], result)
        self.assert_not_in_success_response(["/sponsorship/"], result)
        self.assert_in_success_response(["/accounts/go/?next=%2Fsponsorship%2F"], result)

        non_existent_domain = "moo"
        result = self.client_get("/plans/", subdomain=non_existent_domain)
        self.assertEqual(result.status_code, 404)
        self.assert_in_response("There is no Zulip organization at", result)

        realm = get_realm("zulip")
        realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
        realm.save(update_fields=["plan_type"])
        result = self.client_get("/plans/", subdomain="zulip")
        self.assertEqual(result.status_code, 302)
        self.assertEqual(result["Location"], "/accounts/login/?next=/plans/")

        guest_user = "polonius"
        self.login(guest_user)
        result = self.client_get("/plans/", subdomain="zulip", follow=True)
        self.assertEqual(result.status_code, 404)

        organization_member = "hamlet"
        self.login(organization_member)
        result = self.client_get("/plans/", subdomain="zulip")
        self.assertEqual(result.status_code, 302)
        self.assertEqual("/billing/", result["Location"])

        # Test root domain, with login on different domain
        result = self.client_get("/plans/", subdomain="")
        # TODO: works in manual testing, but I suspect something is funny in
        # the test environment
        # self.assert_in_success_response(["Sign up now"], result)

    def test_CTA_text_by_plan_type(self) -> None:
        sign_up_now = "Create organization"
        upgrade_to_standard = "Upgrade to Standard"
        current_plan = "Current plan"
        sponsorship_pending = "Sponsorship requested"

        # Root domain
        result = self.client_get("/plans/", subdomain="")
        self.assert_in_success_response([sign_up_now, upgrade_to_standard], result)
        self.assert_not_in_success_response([current_plan, sponsorship_pending], result)

        realm = get_realm("zulip")
        realm.plan_type = Realm.PLAN_TYPE_SELF_HOSTED
        realm.save(update_fields=["plan_type"])

        with self.settings(PRODUCTION=True):
            result = self.client_get("/plans/", subdomain="zulip")
            self.assertEqual(result.status_code, 302)
            self.assertEqual(result["Location"], "https://zulip.com/plans/")

            self.login("iago")

            # SELF_HOSTED should hide the local plans page, even if logged in
            result = self.client_get("/plans/", subdomain="zulip")
            self.assertEqual(result.status_code, 302)
            self.assertEqual(result["Location"], "https://zulip.com/plans/")

        realm.plan_type = Realm.PLAN_TYPE_LIMITED
        realm.save(update_fields=["plan_type"])
        result = self.client_get("/plans/", subdomain="zulip")
        self.assert_in_success_response([current_plan, upgrade_to_standard], result)
        self.assert_not_in_success_response([sign_up_now, sponsorship_pending], result)

        with self.settings(CLOUD_FREE_TRIAL_DAYS=60):
            result = self.client_get("/plans/", subdomain="zulip")
            self.assert_in_success_response([current_plan, "Start 60-day free trial"], result)
            self.assert_not_in_success_response(
                [sign_up_now, sponsorship_pending, upgrade_to_standard], result
            )

        # Sponsored realms always have Customer entry.
        customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
        realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
        realm.save(update_fields=["plan_type"])
        result = self.client_get("/plans/", subdomain="zulip")
        self.assert_in_success_response([current_plan], result)
        self.assert_not_in_success_response(
            [sign_up_now, upgrade_to_standard, sponsorship_pending], result
        )

        plan = CustomerPlan.objects.create(
            customer=customer,
            tier=CustomerPlan.TIER_CLOUD_STANDARD,
            status=CustomerPlan.ACTIVE,
            billing_cycle_anchor=timezone_now(),
            billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
        )

        realm.plan_type = Realm.PLAN_TYPE_STANDARD
        realm.save(update_fields=["plan_type"])
        result = self.client_get("/plans/", subdomain="zulip")
        self.assert_in_success_response([current_plan], result)
        self.assert_not_in_success_response(
            [sign_up_now, upgrade_to_standard, sponsorship_pending], result
        )

        plan.status = CustomerPlan.FREE_TRIAL
        plan.save(update_fields=["status"])
        result = self.client_get("/plans/", subdomain="zulip")
        self.assert_in_success_response(["Current plan (free trial)"], result)
