"""Tests for custom pages functionality (SPA).""" import json import tempfile from collections.abc import Generator from pathlib import Path from typing import Any import pytest from fastapi.testclient import TestClient from meshcore_hub.web.pages import CustomPage, PageLoader class TestCustomPage: """Tests for the CustomPage dataclass.""" def test_url_property(self) -> None: """Test that url property returns correct path.""" page = CustomPage( slug="about", title="About Us", menu_order=10, content_html="

Content

", file_path="/pages/about.md", ) assert page.url == "/pages/about" def test_url_property_with_hyphenated_slug(self) -> None: """Test url property with hyphenated slug.""" page = CustomPage( slug="terms-of-service", title="Terms of Service", menu_order=50, content_html="

Terms

", file_path="/pages/terms-of-service.md", ) assert page.url == "/pages/terms-of-service" class TestPageLoader: """Tests for the PageLoader class.""" def test_load_pages_nonexistent_directory(self) -> None: """Test loading from a non-existent directory.""" loader = PageLoader("/nonexistent/path") loader.load_pages() assert loader.get_menu_pages() == [] def test_load_pages_empty_directory(self) -> None: """Test loading from an empty directory.""" with tempfile.TemporaryDirectory() as tmpdir: loader = PageLoader(tmpdir) loader.load_pages() assert loader.get_menu_pages() == [] def test_load_pages_with_frontmatter(self) -> None: """Test loading a page with full frontmatter.""" with tempfile.TemporaryDirectory() as tmpdir: page_path = Path(tmpdir) / "about.md" page_path.write_text( """--- title: About Us slug: about menu_order: 10 --- # About This is the about page. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].slug == "about" assert pages[0].title == "About Us" assert pages[0].menu_order == 10 assert "About" in pages[0].content_html assert "

This is the about page.

" in pages[0].content_html def test_load_pages_default_slug_from_filename(self) -> None: """Test that slug defaults to filename when not specified.""" with tempfile.TemporaryDirectory() as tmpdir: page_path = Path(tmpdir) / "my-custom-page.md" page_path.write_text( """--- title: My Custom Page --- Content here. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].slug == "my-custom-page" def test_load_pages_default_title_from_slug(self) -> None: """Test that title defaults to titlecased slug when not specified.""" with tempfile.TemporaryDirectory() as tmpdir: page_path = Path(tmpdir) / "terms-of-service.md" page_path.write_text("Just content, no frontmatter.") loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].title == "Terms Of Service" def test_load_pages_default_menu_order(self) -> None: """Test that menu_order defaults to 100.""" with tempfile.TemporaryDirectory() as tmpdir: page_path = Path(tmpdir) / "page.md" page_path.write_text( """--- title: Test Page --- Content. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].menu_order == 100 def test_load_pages_sorted_by_menu_order(self) -> None: """Test that pages are sorted by menu_order then title.""" with tempfile.TemporaryDirectory() as tmpdir: # Create pages with different menu_order values (Path(tmpdir) / "page-z.md").write_text( """--- title: Z Page menu_order: 30 --- Content. """ ) (Path(tmpdir) / "page-a.md").write_text( """--- title: A Page menu_order: 10 --- Content. """ ) (Path(tmpdir) / "page-m.md").write_text( """--- title: M Page menu_order: 20 --- Content. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 3 assert [p.title for p in pages] == ["A Page", "M Page", "Z Page"] def test_load_pages_secondary_sort_by_title(self) -> None: """Test that pages with same menu_order are sorted by title.""" with tempfile.TemporaryDirectory() as tmpdir: (Path(tmpdir) / "zebra.md").write_text( """--- title: Zebra menu_order: 10 --- Content. """ ) (Path(tmpdir) / "apple.md").write_text( """--- title: Apple menu_order: 10 --- Content. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 2 assert [p.title for p in pages] == ["Apple", "Zebra"] def test_get_page_returns_correct_page(self) -> None: """Test that get_page returns the page with the given slug.""" with tempfile.TemporaryDirectory() as tmpdir: (Path(tmpdir) / "about.md").write_text( """--- title: About slug: about --- About content. """ ) (Path(tmpdir) / "contact.md").write_text( """--- title: Contact slug: contact --- Contact content. """ ) loader = PageLoader(tmpdir) loader.load_pages() page = loader.get_page("about") assert page is not None assert page.slug == "about" assert page.title == "About" page = loader.get_page("contact") assert page is not None assert page.slug == "contact" def test_get_page_returns_none_for_unknown_slug(self) -> None: """Test that get_page returns None for unknown slugs.""" loader = PageLoader("/nonexistent") loader.load_pages() assert loader.get_page("unknown") is None def test_reload_clears_and_reloads(self) -> None: """Test that reload() clears existing pages and reloads from disk.""" with tempfile.TemporaryDirectory() as tmpdir: page_path = Path(tmpdir) / "page.md" page_path.write_text( """--- title: Original --- Content. """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].title == "Original" # Update the file page_path.write_text( """--- title: Updated --- New content. """ ) loader.reload() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].title == "Updated" def test_load_pages_ignores_non_md_files(self) -> None: """Test that only .md files are loaded.""" with tempfile.TemporaryDirectory() as tmpdir: (Path(tmpdir) / "page.md").write_text("# Valid Page") (Path(tmpdir) / "readme.txt").write_text("Not a markdown file") (Path(tmpdir) / "data.json").write_text('{"key": "value"}') loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert pages[0].slug == "page" def test_markdown_tables_rendered(self) -> None: """Test that markdown tables are rendered to HTML.""" with tempfile.TemporaryDirectory() as tmpdir: (Path(tmpdir) / "tables.md").write_text( """--- title: Tables --- | Header 1 | Header 2 | |----------|----------| | Cell 1 | Cell 2 | """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert "" in pages[0].content_html assert "
" in pages[0].content_html def test_markdown_fenced_code_rendered(self) -> None: """Test that fenced code blocks are rendered.""" with tempfile.TemporaryDirectory() as tmpdir: (Path(tmpdir) / "code.md").write_text( """--- title: Code --- ```python def hello(): print("Hello!") ``` """ ) loader = PageLoader(tmpdir) loader.load_pages() pages = loader.get_menu_pages() assert len(pages) == 1 assert "
" in pages[0].content_html
            assert "def hello():" in pages[0].content_html


class TestPagesRoute:
    """Tests for the custom pages routes (SPA).

    In the SPA architecture:
    - /pages/{slug} returns the SPA shell HTML (catch-all)
    - /spa/pages/{slug} returns page content as JSON
    """

    @pytest.fixture
    def pages_dir(self) -> Generator[str, None, None]:
        """Create a temporary content directory with test pages."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create pages subdirectory (CONTENT_HOME/pages)
            pages_subdir = Path(tmpdir) / "pages"
            pages_subdir.mkdir()
            (pages_subdir / "about.md").write_text(
                """---
title: About Us
slug: about
menu_order: 10
---

# About Our Network

Welcome to the network.
"""
            )
            (pages_subdir / "faq.md").write_text(
                """---
title: FAQ
slug: faq
menu_order: 20
---

# Frequently Asked Questions

Here are some answers.
"""
            )
            yield tmpdir

    @pytest.fixture
    def web_app_with_pages(
        self, pages_dir: str, mock_http_client: Any
    ) -> Generator[Any, None, None]:
        """Create a web app with custom pages configured."""
        import os

        # Temporarily set CONTENT_HOME environment variable
        os.environ["CONTENT_HOME"] = pages_dir

        from meshcore_hub.web.app import create_app

        app = create_app(
            api_url="http://localhost:8000",
            api_key="test-api-key",
            network_name="Test Network",
        )
        app.state.http_client = mock_http_client

        yield app

        # Cleanup
        del os.environ["CONTENT_HOME"]

    @pytest.fixture
    def client_with_pages(
        self, web_app_with_pages: Any, mock_http_client: Any
    ) -> TestClient:
        """Create a test client with custom pages."""
        web_app_with_pages.state.http_client = mock_http_client
        return TestClient(web_app_with_pages, raise_server_exceptions=True)

    def test_page_route_returns_spa_shell(self, client_with_pages: TestClient) -> None:
        """Test that /pages/{slug} returns the SPA shell HTML."""
        response = client_with_pages.get("/pages/about")
        assert response.status_code == 200
        assert "window.__APP_CONFIG__" in response.text

    def test_page_route_nonexistent_returns_spa_shell(
        self, client_with_pages: TestClient
    ) -> None:
        """Test that /pages/{slug} returns SPA shell even for nonexistent pages.

        The SPA catch-all serves the shell for all routes.
        Client-side code fetches page content via /spa/pages/{slug}.
        """
        response = client_with_pages.get("/pages/nonexistent")
        assert response.status_code == 200
        assert "window.__APP_CONFIG__" in response.text

    def test_spa_page_api_returns_json(self, client_with_pages: TestClient) -> None:
        """Test that /spa/pages/{slug} returns page content as JSON."""
        response = client_with_pages.get("/spa/pages/about")
        assert response.status_code == 200
        assert "application/json" in response.headers["content-type"]

        data = response.json()
        assert data["slug"] == "about"
        assert data["title"] == "About Us"
        assert "About Our Network" in data["content_html"]
        assert "Welcome to the network" in data["content_html"]

    def test_spa_page_api_not_found(self, client_with_pages: TestClient) -> None:
        """Test that /spa/pages/{slug} returns 404 for unknown page."""
        response = client_with_pages.get("/spa/pages/nonexistent")
        assert response.status_code == 404
        data = response.json()
        assert data["detail"] == "Page not found"

    def test_spa_page_api_faq(self, client_with_pages: TestClient) -> None:
        """Test that /spa/pages/faq returns FAQ page content."""
        response = client_with_pages.get("/spa/pages/faq")
        assert response.status_code == 200
        data = response.json()
        assert data["slug"] == "faq"
        assert data["title"] == "FAQ"
        assert "Frequently Asked Questions" in data["content_html"]

    def test_pages_in_navigation(self, client_with_pages: TestClient) -> None:
        """Test that custom pages appear in navigation."""
        response = client_with_pages.get("/")
        assert response.status_code == 200
        # Check for navigation links
        assert 'href="/pages/about"' in response.text
        assert 'href="/pages/faq"' in response.text

    def test_pages_sorted_in_navigation(self, client_with_pages: TestClient) -> None:
        """Test that pages are sorted by menu_order in navigation."""
        response = client_with_pages.get("/")
        assert response.status_code == 200
        # About (order 10) should appear before FAQ (order 20)
        about_pos = response.text.find('href="/pages/about"')
        faq_pos = response.text.find('href="/pages/faq"')
        assert about_pos < faq_pos

    def test_pages_in_config(self, client_with_pages: TestClient) -> None:
        """Test that custom pages are included in SPA config."""
        response = client_with_pages.get("/")
        text = response.text
        config_start = text.find("window.__APP_CONFIG__ = ") + len(
            "window.__APP_CONFIG__ = "
        )
        config_end = text.find(";", config_start)
        config = json.loads(text[config_start:config_end])

        custom_pages = config["custom_pages"]
        assert len(custom_pages) == 2
        slugs = [p["slug"] for p in custom_pages]
        assert "about" in slugs
        assert "faq" in slugs


class TestPagesInSitemap:
    """Tests for custom pages in sitemap."""

    @pytest.fixture
    def pages_dir(self) -> Generator[str, None, None]:
        """Create a temporary content directory with test pages."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create pages subdirectory (CONTENT_HOME/pages)
            pages_subdir = Path(tmpdir) / "pages"
            pages_subdir.mkdir()
            (pages_subdir / "about.md").write_text(
                """---
title: About
slug: about
---

About page.
"""
            )
            yield tmpdir

    @pytest.fixture
    def client_with_pages_for_sitemap(
        self, pages_dir: str, mock_http_client: Any
    ) -> Generator[TestClient, None, None]:
        """Create a test client with custom pages for sitemap testing."""
        import os

        os.environ["CONTENT_HOME"] = pages_dir

        from meshcore_hub.web.app import create_app

        app = create_app(
            api_url="http://localhost:8000",
            api_key="test-api-key",
            network_name="Test Network",
        )
        app.state.http_client = mock_http_client

        client = TestClient(app, raise_server_exceptions=True)
        yield client

        del os.environ["CONTENT_HOME"]

    def test_pages_included_in_sitemap(
        self, client_with_pages_for_sitemap: TestClient
    ) -> None:
        """Test that custom pages are included in sitemap.xml."""
        response = client_with_pages_for_sitemap.get("/sitemap.xml")
        assert response.status_code == 200
        assert "/pages/about" in response.text
        assert "weekly" in response.text