import datetime
import locale
import logging
import os.path
from posixpath import join as posix_join
from sys import platform

from jinja2.utils import generate_lorem_ipsum

from pelican.contents import Article, Author, Category, Page, Static, logger
from pelican.plugins.signals import content_object_init
from pelican.settings import DEFAULT_CONFIG
from pelican.tests.support import LoggedTestCase, get_context, get_settings, unittest
from pelican.utils import path_to_url, posixize_path, truncate_html_words

# generate 3 test paragraphs, each enclosed with <p>
# save the first paragraph separately for testing the summary generation algorithm
# NOTE: these values are nondeterministic between test runs
TEST_CONTENT_FIRST_PARAGRAPH = str(generate_lorem_ipsum(n=1))
TEST_CONTENT_REMAINING_PARAGRAPHS = str(generate_lorem_ipsum(n=2))
TEST_CONTENT = TEST_CONTENT_FIRST_PARAGRAPH + TEST_CONTENT_REMAINING_PARAGRAPHS
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)


class TestBase(LoggedTestCase):
    def setUp(self):
        super().setUp()
        self.old_locale = locale.setlocale(locale.LC_ALL)
        locale.setlocale(locale.LC_ALL, "C")
        self.page_kwargs = {
            "content": TEST_CONTENT,
            "context": {
                "localsiteurl": "",
                "generated_content": {},
                "static_content": {},
                "static_links": set(),
            },
            "metadata": {
                "summary": TEST_SUMMARY,
                "title": "foo bar",
                "author": Author("Blogger", DEFAULT_CONFIG),
            },
            "source_path": "/path/to/file/foo.ext",
        }
        self._disable_limit_filter()

    def tearDown(self):
        locale.setlocale(locale.LC_ALL, self.old_locale)
        self._enable_limit_filter()

    def _disable_limit_filter(self):
        logger.disable_filter()

    def _enable_limit_filter(self):
        logger.enable_filter()

    def _copy_page_kwargs(self):
        # copy page_kwargs
        page_kwargs = dict(self.page_kwargs)
        for key, val in page_kwargs.items():
            if not isinstance(val, dict):
                break
            page_kwargs[key] = {subkey: val[subkey] for subkey in val}

        return page_kwargs


class TestPage(TestBase):
    def test_use_args(self):
        # Creating a page with arguments passed to the constructor should use
        # them to initialise object's attributes.
        metadata = {
            "foo": "bar",
            "foobar": "baz",
            "title": "foobar",
        }
        page = Page(TEST_CONTENT, metadata=metadata, context={"localsiteurl": ""})
        for key, value in metadata.items():
            self.assertTrue(hasattr(page, key))
            self.assertEqual(value, getattr(page, key))
        self.assertEqual(page.content, TEST_CONTENT)

    def test_mandatory_properties(self):
        # If the title is not set, must throw an exception.
        page = Page("content")
        self.assertFalse(page._has_valid_mandatory_properties())
        self.assertLogCountEqual(
            count=1,
            msg="Skipping .*: could not find information about 'title'",
            level=logging.ERROR,
        )
        page = Page("content", metadata={"title": "foobar"})
        self.assertTrue(page._has_valid_mandatory_properties())

    def test_summary_from_metadata(self):
        # If a :summary: metadata is given, it should be used
        page = Page(**self.page_kwargs)
        self.assertEqual(page.summary, TEST_SUMMARY)

    def test_summary_max_length(self):
        # If a :SUMMARY_MAX_LENGTH: is set, and there is no other summary,
        # generated summary should not exceed the given length.
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        del page_kwargs["metadata"]["summary"]
        settings["SUMMARY_MAX_LENGTH"] = None
        page = Page(**page_kwargs)
        self.assertEqual(page.summary, TEST_CONTENT)
        settings["SUMMARY_MAX_LENGTH"] = 10
        page = Page(**page_kwargs)
        self.assertEqual(page.summary, truncate_html_words(TEST_CONTENT, 10))
        settings["SUMMARY_MAX_LENGTH"] = 0
        page = Page(**page_kwargs)
        self.assertEqual(page.summary, "")

    def test_summary_paragraph(self):
        # If SUMMARY_MAX_PARAGRAPHS is set, the generated summary should
        # not exceed the given paragraph count.
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        del page_kwargs["metadata"]["summary"]
        settings["SUMMARY_MAX_PARAGRAPHS"] = 1
        settings["SUMMARY_MAX_LENGTH"] = None
        page = Page(**page_kwargs)
        self.assertEqual(page.summary, TEST_CONTENT_FIRST_PARAGRAPH)

    def test_summary_paragraph_max_length(self):
        # If both SUMMARY_MAX_PARAGRAPHS and SUMMARY_MAX_LENGTH are set,
        # the generated summary should not exceed the given paragraph count and
        # not exceed the given length.
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        del page_kwargs["metadata"]["summary"]
        settings["SUMMARY_MAX_PARAGRAPHS"] = 1
        settings["SUMMARY_MAX_LENGTH"] = 10
        page = Page(**page_kwargs)
        self.assertEqual(
            page.summary, truncate_html_words(TEST_CONTENT_FIRST_PARAGRAPH, 10)
        )

    def test_summary_paragraph_long_max_length(self):
        # If both SUMMARY_MAX_PARAGRAPHS and SUMMARY_MAX_LENGTH are set,
        # and the first SUMMARY_MAX_PARAGRAPHS paragraphs have fewer words
        # than SUMMARY_MAX_LENGTH, the generated summary should still only
        # contain the given number of paragraphs.
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        del page_kwargs["metadata"]["summary"]
        settings["SUMMARY_MAX_PARAGRAPHS"] = 1
        settings["SUMMARY_MAX_LENGTH"] = 50
        page = Page(**page_kwargs)
        self.assertEqual(
            page.summary, truncate_html_words(TEST_CONTENT_FIRST_PARAGRAPH, 50)
        )

    def test_summary_end_suffix(self):
        # If a :SUMMARY_END_SUFFIX: is set, and there is no other summary,
        # generated summary should contain the specified marker at the end.
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        del page_kwargs["metadata"]["summary"]
        settings["SUMMARY_END_SUFFIX"] = "test_marker"
        settings["SUMMARY_MAX_LENGTH"] = 10
        page = Page(**page_kwargs)
        self.assertEqual(
            page.summary, truncate_html_words(TEST_CONTENT, 10, "test_marker")
        )
        self.assertIn("test_marker", page.summary)

    def test_summary_get_summary_warning(self):
        """calling ._get_summary() should issue a warning"""
        page_kwargs = self._copy_page_kwargs()
        page = Page(**page_kwargs)
        self.assertEqual(page.summary, TEST_SUMMARY)
        self.assertEqual(page._get_summary(), TEST_SUMMARY)
        self.assertLogCountEqual(
            count=1,
            msg=r"_get_summary\(\) has been deprecated since 3\.6\.4\. "
            "Use the summary decorator instead",
            level=logging.WARNING,
        )

    def test_slug(self):
        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        page_kwargs["settings"] = settings
        settings["SLUGIFY_SOURCE"] = "title"
        page = Page(**page_kwargs)
        self.assertEqual(page.slug, "foo-bar")
        settings["SLUGIFY_SOURCE"] = "basename"
        page = Page(**page_kwargs)
        self.assertEqual(page.slug, "foo")

        # test slug from title with unicode and case

        inputs = (
            # (title, expected, preserve_case, use_unicode)
            ("指導書", "zhi-dao-shu", False, False),
            ("指導書", "Zhi-Dao-Shu", True, False),
            ("指導書", "指導書", False, True),
            ("指導書", "指導書", True, True),
            ("Çığ", "cig", False, False),
            ("Çığ", "Cig", True, False),
            ("Çığ", "çığ", False, True),
            ("Çığ", "Çığ", True, True),
        )

        settings = get_settings()
        page_kwargs = self._copy_page_kwargs()
        page_kwargs["settings"] = settings

        for title, expected, preserve_case, use_unicode in inputs:
            settings["SLUGIFY_PRESERVE_CASE"] = preserve_case
            settings["SLUGIFY_USE_UNICODE"] = use_unicode
            page_kwargs["metadata"]["title"] = title
            page = Page(**page_kwargs)
            self.assertEqual(page.slug, expected, (title, preserve_case, use_unicode))

    def test_defaultlang(self):
        # If no lang is given, default to the default one.
        page = Page(**self.page_kwargs)
        self.assertEqual(page.lang, DEFAULT_CONFIG["DEFAULT_LANG"])

        # it is possible to specify the lang in the metadata infos
        self.page_kwargs["metadata"].update(
            {
                "lang": "fr",
            }
        )
        page = Page(**self.page_kwargs)
        self.assertEqual(page.lang, "fr")

    def test_save_as(self):
        # If a lang is not the default lang, save_as should be set
        # accordingly.

        # if a title is defined, save_as should be set
        page = Page(**self.page_kwargs)
        self.assertEqual(page.save_as, "pages/foo-bar.html")

        # if a language is defined, save_as should include it accordingly
        self.page_kwargs["metadata"].update(
            {
                "lang": "fr",
            }
        )
        page = Page(**self.page_kwargs)
        self.assertEqual(page.save_as, "pages/foo-bar-fr.html")

    def test_relative_source_path(self):
        # 'relative_source_path' should be the relative path
        # from 'PATH' to 'source_path'
        page_kwargs = self._copy_page_kwargs()

        # If 'source_path' is None, 'relative_source_path' should
        # also return None
        page_kwargs["source_path"] = None
        page = Page(**page_kwargs)
        self.assertIsNone(page.relative_source_path)

        page_kwargs = self._copy_page_kwargs()
        settings = get_settings()
        full_path = page_kwargs["source_path"]

        settings["PATH"] = os.path.dirname(full_path)
        page_kwargs["settings"] = settings
        page = Page(**page_kwargs)

        # if 'source_path' is set, 'relative_source_path' should
        # return the relative path from 'PATH' to 'source_path'
        self.assertEqual(
            page.relative_source_path,
            os.path.relpath(full_path, os.path.dirname(full_path)),
        )

    def test_metadata_url_format(self):
        # Arbitrary metadata should be passed through url_format()
        page = Page(**self.page_kwargs)
        self.assertIn("summary", page.url_format.keys())
        page.metadata["directory"] = "test-dir"
        page.settings = get_settings(PAGE_SAVE_AS="{directory}/{slug}")
        self.assertEqual(page.save_as, "test-dir/foo-bar")

    def test_datetime(self):
        # If DATETIME is set to a tuple, it should be used to override LOCALE
        dt = datetime.datetime(2015, 9, 13)

        page_kwargs = self._copy_page_kwargs()

        # set its date to dt
        page_kwargs["metadata"]["date"] = dt
        page = Page(**page_kwargs)

        # page.locale_date is a unicode string in both python2 and python3
        dt_date = dt.strftime(DEFAULT_CONFIG["DEFAULT_DATE_FORMAT"])

        self.assertEqual(page.locale_date, dt_date)
        page_kwargs["settings"] = get_settings()

        # I doubt this can work on all platforms ...
        if platform == "win32":
            the_locale = "jpn"
        else:
            the_locale = "ja_JP.utf8"
        page_kwargs["settings"]["DATE_FORMATS"] = {"jp": (the_locale, "%Y-%m-%d(%a)")}
        page_kwargs["metadata"]["lang"] = "jp"

        try:
            page = Page(**page_kwargs)
            self.assertEqual(page.locale_date, "2015-09-13(\u65e5)")
        except locale.Error:
            # The constructor of ``Page`` will try to set the locale to
            # ``ja_JP.utf8``. But this attempt will failed when there is no
            # such locale in the system. You can see which locales there are
            # in your system with ``locale -a`` command.
            #
            # Until we find some other method to test this functionality, we
            # will simply skip this test.
            unittest.skip(f"There is no locale {the_locale} in this system.")

    def test_template(self):
        # Pages default to page, metadata overwrites
        default_page = Page(**self.page_kwargs)
        self.assertEqual("page", default_page.template)
        page_kwargs = self._copy_page_kwargs()
        page_kwargs["metadata"]["template"] = "custom"
        custom_page = Page(**page_kwargs)
        self.assertEqual("custom", custom_page.template)

    def test_signal(self):
        def receiver_test_function(_sender):
            receiver_test_function.has_been_called = True

        receiver_test_function.has_been_called = False

        content_object_init.connect(receiver_test_function)
        self.assertIn(receiver_test_function, content_object_init.receivers_for(Page))

        self.assertFalse(receiver_test_function.has_been_called)
        Page(**self.page_kwargs)
        self.assertTrue(receiver_test_function.has_been_called)

    def test_get_content(self):
        # Test that the content is updated with the relative links to
        # filenames, tags and categories.
        settings = get_settings()
        args = self.page_kwargs.copy()
        args["settings"] = settings

        # Tag
        args["content"] = 'A simple test, with a <a href="|tag|tagname">link</a>'
        page = Page(**args)
        content = page.get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            (
                "A simple test, with a "
                '<a href="http://notmyidea.org/tag/tagname.html">link</a>'
            ),
        )

        # Category
        args["content"] = 'A simple test, with a <a href="|category|category">link</a>'
        page = Page(**args)
        content = page.get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            (
                "A simple test, with a "
                '<a href="http://notmyidea.org/category/category.html">link</a>'
            ),
        )

    def test_intrasite_link(self):
        cls_name = "_DummyArticle"
        article = type(cls_name, (object,), {"url": "article.html"})

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["generated_content"] = {"article.rst": article}

        # Classic intrasite link via filename
        args["content"] = (
            'A simple test, with a <a href="|filename|article.rst">link</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a "
            '<a href="http://notmyidea.org/article.html">link</a>',
        )

        # fragment
        args["content"] = (
            'A simple test, with a <a href="|filename|article.rst#section-2">link</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a "
            '<a href="http://notmyidea.org/article.html#section-2">link</a>',
        )

        # query
        args["content"] = (
            "A simple test, with a "
            '<a href="|filename|article.rst'
            '?utm_whatever=234&highlight=word">link</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a "
            '<a href="http://notmyidea.org/article.html'
            '?utm_whatever=234&highlight=word">link</a>',
        )

        # combination
        args["content"] = (
            "A simple test, with a "
            '<a href="|filename|article.rst'
            '?utm_whatever=234&highlight=word#section-2">link</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a "
            '<a href="http://notmyidea.org/article.html'
            '?utm_whatever=234&highlight=word#section-2">link</a>',
        )

        # also test for summary in metadata
        parsed = (
            'A simple summary test, with a <a href="|filename|article.rst">link</a>'
        )
        linked = (
            "A simple summary test, with a "
            '<a href="http://notmyidea.org/article.html">link</a>'
        )
        args["settings"]["FORMATTED_FIELDS"] = ["summary", "custom"]
        args["metadata"]["summary"] = parsed
        args["metadata"]["custom"] = parsed
        args["context"]["localsiteurl"] = "http://notmyidea.org"
        p = Page(**args)
        # This is called implicitly from all generators and Pelican.run() once
        # all files are processed. Here we process just one page so it needs
        # to be called explicitly.
        p.refresh_metadata_intersite_links()
        self.assertEqual(p.summary, linked)
        self.assertEqual(p.custom, linked)

    def test_intrasite_link_more(self):
        cls_name = "_DummyAsset"

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["static_content"] = {
            "images/poster.jpg": type(
                cls_name, (object,), {"url": "images/poster.jpg"}
            ),
            "assets/video.mp4": type(cls_name, (object,), {"url": "assets/video.mp4"}),
            "images/graph.svg": type(cls_name, (object,), {"url": "images/graph.svg"}),
        }
        args["context"]["generated_content"] = {
            "reference.rst": type(cls_name, (object,), {"url": "reference.html"}),
        }

        # video.poster
        args["content"] = (
            "There is a video with poster "
            '<video controls poster="{static}/images/poster.jpg">'
            '<source src="|static|/assets/video.mp4" type="video/mp4">'
            "</video>"
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "There is a video with poster "
            '<video controls poster="http://notmyidea.org/images/poster.jpg">'
            '<source src="http://notmyidea.org/assets/video.mp4"'
            ' type="video/mp4">'
            "</video>",
        )

        # object.data
        args["content"] = (
            "There is a svg object "
            '<object data="{static}/images/graph.svg"'
            ' type="image/svg+xml">'
            "</object>"
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "There is a svg object "
            '<object data="http://notmyidea.org/images/graph.svg"'
            ' type="image/svg+xml">'
            "</object>",
        )

        # blockquote.cite
        args["content"] = (
            "There is a blockquote with cite attribute "
            '<blockquote cite="{filename}reference.rst">blah blah</blockquote>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "There is a blockquote with cite attribute "
            '<blockquote cite="http://notmyidea.org/reference.html">'
            "blah blah"
            "</blockquote>",
        )

    def test_intrasite_link_absolute(self):
        """Test that absolute URLs are merged properly."""

        args = self.page_kwargs.copy()
        args["settings"] = get_settings(
            STATIC_URL="http://static.cool.site/{path}",
            ARTICLE_URL="http://blog.cool.site/{slug}.html",
        )
        args["source_path"] = "content"
        args["context"]["static_content"] = {
            "images/poster.jpg": Static(
                "", settings=args["settings"], source_path="images/poster.jpg"
            ),
        }
        args["context"]["generated_content"] = {
            "article.rst": Article(
                "",
                settings=args["settings"],
                metadata={"slug": "article", "title": "Article"},
            )
        }

        # Article link will go to blog
        args["content"] = '<a href="{filename}article.rst">Article</a>'
        content = Page(**args).get_content("http://cool.site")
        self.assertEqual(
            content, '<a href="http://blog.cool.site/article.html">Article</a>'
        )

        # Page link will go to the main site
        args["content"] = '<a href="{index}">Index</a>'
        content = Page(**args).get_content("http://cool.site")
        self.assertEqual(content, '<a href="http://cool.site/index.html">Index</a>')

        # Image link will go to static
        args["content"] = '<img src="{static}/images/poster.jpg"/>'
        content = Page(**args).get_content("http://cool.site")
        self.assertEqual(
            content, '<img src="http://static.cool.site/images/poster.jpg"/>'
        )

        # Image link will go to static
        args["content"] = '<meta content="{static}/images/poster.jpg"/>'
        content = Page(**args).get_content("http://cool.site")
        self.assertEqual(
            content, '<meta content="http://static.cool.site/images/poster.jpg"/>'
        )

    def test_intrasite_link_escape(self):
        article = type("_DummyArticle", (object,), {"url": "article-spaces.html"})
        asset = type("_DummyAsset", (object,), {"url": "name@example.com"})

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["generated_content"] = {"article spaces.rst": article}
        args["context"]["static_content"] = {"name@example.com": asset}

        expected_output = (
            "A simple test with a "
            '<a href="http://notmyidea.org/article-spaces.html#anchor">link</a> '
            '<a href="http://notmyidea.org/name@example.com#anchor">file</a>'
        )

        # not escaped
        args["content"] = (
            "A simple test with a "
            '<a href="{filename}article spaces.rst#anchor">link</a> '
            '<a href="{static}name@example.com#anchor">file</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(content, expected_output)

        # html escaped
        args["content"] = (
            "A simple test with a "
            '<a href="{filename}article spaces.rst#anchor">link</a> '
            '<a href="{static}name&#64;example.com#anchor">file</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(content, expected_output)

        # url escaped
        args["content"] = (
            "A simple test with a "
            '<a href="{filename}article%20spaces.rst#anchor">link</a> '
            '<a href="{static}name%40example.com#anchor">file</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(content, expected_output)

        # html and url escaped
        args["content"] = (
            "A simple test with a "
            '<a href="{filename}article%20spaces.rst#anchor">link</a> '
            '<a href="{static}name&#64;example.com#anchor">file</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(content, expected_output)

    def test_intrasite_link_markdown_spaces(self):
        cls_name = "_DummyArticle"
        article = type(cls_name, (object,), {"url": "article-spaces.html"})

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["generated_content"] = {"article spaces.rst": article}

        # An intrasite link via filename with %20 as a space
        args["content"] = (
            'A simple test, with a <a href="|filename|article%20spaces.rst">link</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a "
            '<a href="http://notmyidea.org/article-spaces.html">link</a>',
        )

    def test_intrasite_link_source_and_generated(self):
        """Test linking both to the source and the generated article"""
        cls_name = "_DummyAsset"

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["generated_content"] = {
            "article.rst": type(cls_name, (object,), {"url": "article.html"})
        }
        args["context"]["static_content"] = {
            "article.rst": type(cls_name, (object,), {"url": "article.rst"})
        }

        args["content"] = (
            "A simple test, with a link to an"
            '<a href="{filename}article.rst">article</a> and its'
            '<a href="{static}article.rst">source</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a link to an"
            '<a href="http://notmyidea.org/article.html">article</a> and its'
            '<a href="http://notmyidea.org/article.rst">source</a>',
        )

    def test_intrasite_link_to_static_content_with_filename(self):
        """Test linking to a static resource with deprecated {filename}"""
        cls_name = "_DummyAsset"

        args = self.page_kwargs.copy()
        args["settings"] = get_settings()
        args["source_path"] = "content"
        args["context"]["static_content"] = {
            "poster.jpg": type(cls_name, (object,), {"url": "images/poster.jpg"})
        }

        args["content"] = (
            'A simple test, with a link to a<a href="{filename}poster.jpg">poster</a>'
        )
        content = Page(**args).get_content("http://notmyidea.org")
        self.assertEqual(
            content,
            "A simple test, with a link to a"
            '<a href="http://notmyidea.org/images/poster.jpg">poster</a>',
        )

    def test_multiple_authors(self):
        """Test article with multiple authors."""
        args = self.page_kwargs.copy()
        content = Page(**args)
        assert content.authors == [content.author]
        args["metadata"].pop("author")
        args["metadata"]["authors"] = [
            Author("First Author", DEFAULT_CONFIG),
            Author("Second Author", DEFAULT_CONFIG),
        ]
        content = Page(**args)
        assert content.authors
        assert content.author == content.authors[0]


class TestArticle(TestBase):
    def test_template(self):
        # Articles default to article, metadata overwrites
        default_article = Article(**self.page_kwargs)
        self.assertEqual("article", default_article.template)
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["template"] = "custom"
        custom_article = Article(**article_kwargs)
        self.assertEqual("custom", custom_article.template)

    def test_slugify_category_author(self):
        settings = get_settings()
        settings["SLUG_REGEX_SUBSTITUTIONS"] = [
            (r"C#", "csharp"),
            (r"[^\w\s-]", ""),
            (r"(?u)\A\s*", ""),
            (r"(?u)\s*\Z", ""),
            (r"[-\s]+", "-"),
        ]
        settings["ARTICLE_URL"] = "{author}/{category}/{slug}/"
        settings["ARTICLE_SAVE_AS"] = "{author}/{category}/{slug}/index.html"
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["author"] = Author("O'Brien", settings)
        article_kwargs["metadata"]["category"] = Category("C# & stuff", settings)
        article_kwargs["metadata"]["title"] = "fnord"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertEqual(article.url, "obrien/csharp-stuff/fnord/")
        self.assertEqual(article.save_as, "obrien/csharp-stuff/fnord/index.html")

    def test_slugify_with_author_substitutions(self):
        settings = get_settings()
        settings["AUTHOR_REGEX_SUBSTITUTIONS"] = [
            ("Alexander Todorov", "atodorov"),
            ("Krasimir Tsonev", "krasimir"),
            (r"[^\w\s-]", ""),
            (r"(?u)\A\s*", ""),
            (r"(?u)\s*\Z", ""),
            (r"[-\s]+", "-"),
        ]
        settings["ARTICLE_URL"] = "blog/{author}/{slug}/"
        settings["ARTICLE_SAVE_AS"] = "blog/{author}/{slug}/index.html"
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["author"] = Author("Alexander Todorov", settings)
        article_kwargs["metadata"]["title"] = "fnord"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertEqual(article.url, "blog/atodorov/fnord/")
        self.assertEqual(article.save_as, "blog/atodorov/fnord/index.html")

    def test_slugify_category_with_dots(self):
        settings = get_settings()
        settings["CATEGORY_REGEX_SUBSTITUTIONS"] = [
            ("Fedora QA", "fedora.qa"),
        ]
        settings["ARTICLE_URL"] = "{category}/{slug}/"
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["category"] = Category("Fedora QA", settings)
        article_kwargs["metadata"]["title"] = "This Week in Fedora QA"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertEqual(article.url, "fedora.qa/this-week-in-fedora-qa/")

    def test_valid_save_as_detects_breakout(self):
        settings = get_settings()
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["slug"] = "../foo"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertFalse(article._has_valid_save_as())

    def test_valid_save_as_detects_breakout_to_root(self):
        settings = get_settings()
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["slug"] = "/foo"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertFalse(article._has_valid_save_as())

    def test_valid_save_as_passes_valid(self):
        settings = get_settings()
        article_kwargs = self._copy_page_kwargs()
        article_kwargs["metadata"]["slug"] = "foo"
        article_kwargs["settings"] = settings
        article = Article(**article_kwargs)
        self.assertTrue(article._has_valid_save_as())


class TestStatic(LoggedTestCase):
    def setUp(self):
        super().setUp()
        self.settings = get_settings(
            STATIC_SAVE_AS="{path}",
            STATIC_URL="{path}",
            PAGE_SAVE_AS=os.path.join("outpages", "{slug}.html"),
            PAGE_URL="outpages/{slug}.html",
        )
        self.context = get_context(self.settings)

        self.static = Static(
            content=None,
            metadata={},
            settings=self.settings,
            source_path=posix_join("dir", "foo.jpg"),
            context=self.context,
        )

        self.context["static_content"][self.static.source_path] = self.static

    def tearDown(self):
        pass

    def test_attach_to_same_dir(self):
        """attach_to() overrides a static file's save_as and url."""
        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "fakepage.md"),
        )
        self.static.attach_to(page)

        expected_save_as = os.path.join("outpages", "foo.jpg")
        self.assertEqual(self.static.save_as, expected_save_as)
        self.assertEqual(self.static.url, path_to_url(expected_save_as))

    def test_attach_to_parent_dir(self):
        """attach_to() preserves dirs inside the linking document dir."""
        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path="fakepage.md",
        )
        self.static.attach_to(page)

        expected_save_as = os.path.join("outpages", "dir", "foo.jpg")
        self.assertEqual(self.static.save_as, expected_save_as)
        self.assertEqual(self.static.url, path_to_url(expected_save_as))

    def test_attach_to_other_dir(self):
        """attach_to() ignores dirs outside the linking document dir."""
        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
        )
        self.static.attach_to(page)

        expected_save_as = os.path.join("outpages", "foo.jpg")
        self.assertEqual(self.static.save_as, expected_save_as)
        self.assertEqual(self.static.url, path_to_url(expected_save_as))

    def test_attach_to_ignores_subsequent_calls(self):
        """attach_to() does nothing when called a second time."""
        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "fakepage.md"),
        )

        self.static.attach_to(page)

        otherdir_settings = self.settings.copy()
        otherdir_settings.update(
            {
                "PAGE_SAVE_AS": os.path.join("otherpages", "{slug}.html"),
                "PAGE_URL": "otherpages/{slug}.html",
            }
        )
        otherdir_page = Page(
            content="other page",
            metadata={"title": "otherpage"},
            settings=otherdir_settings,
            source_path=os.path.join("dir", "otherpage.md"),
        )

        self.static.attach_to(otherdir_page)

        otherdir_save_as = os.path.join("otherpages", "foo.jpg")
        self.assertNotEqual(self.static.save_as, otherdir_save_as)
        self.assertNotEqual(self.static.url, path_to_url(otherdir_save_as))

    def test_attach_to_does_nothing_after_save_as_referenced(self):
        """attach_to() does nothing if the save_as was already referenced.
        (For example, by a {static} link an a document processed earlier.)
        """
        original_save_as = self.static.save_as

        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "fakepage.md"),
        )
        self.static.attach_to(page)

        self.assertEqual(self.static.save_as, original_save_as)
        self.assertEqual(self.static.url, path_to_url(original_save_as))

    def test_attach_to_does_nothing_after_url_referenced(self):
        """attach_to() does nothing if the url was already referenced.
        (For example, by a {static} link an a document processed earlier.)
        """
        original_url = self.static.url

        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "fakepage.md"),
        )
        self.static.attach_to(page)

        self.assertEqual(self.static.save_as, self.static.source_path)
        self.assertEqual(self.static.url, original_url)

    def test_attach_to_does_not_override_an_override(self):
        """attach_to() does not override paths that were overridden elsewhere.
        (For example, by the user with EXTRA_PATH_METADATA)
        """
        customstatic = Static(
            content=None,
            metadata={"save_as": "customfoo.jpg", "url": "customfoo.jpg"},
            settings=self.settings,
            source_path=os.path.join("dir", "foo.jpg"),
            context=self.settings.copy(),
        )

        page = Page(
            content="fake page",
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "fakepage.md"),
        )

        customstatic.attach_to(page)

        self.assertEqual(customstatic.save_as, "customfoo.jpg")
        self.assertEqual(customstatic.url, "customfoo.jpg")

    def test_attach_link_syntax(self):
        """{attach} link syntax triggers output path override & url replacement."""
        html = '<a href="{attach}../foo.jpg">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(
            content, html, "{attach} link syntax did not trigger URL replacement."
        )

        expected_save_as = os.path.join("outpages", "foo.jpg")
        self.assertEqual(self.static.save_as, expected_save_as)
        self.assertEqual(self.static.url, path_to_url(expected_save_as))

    def test_tag_link_syntax(self):
        "{tag} link syntax triggers url replacement."

        html = '<a href="{tag}foo">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(content, html)

    def test_category_link_syntax(self):
        "{category} link syntax triggers url replacement."

        html = '<a href="{category}foo">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(content, html)

    def test_author_link_syntax(self):
        "{author} link syntax triggers url replacement."

        html = '<a href="{author}foo">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(content, html)

    def test_index_link_syntax(self):
        "{index} link syntax triggers url replacement."

        html = '<a href="{index}">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(content, html)

        expected_html = (
            '<a href="'
            + "/".join((self.settings["SITEURL"], self.settings["INDEX_SAVE_AS"]))
            + '">link</a>'
        )
        self.assertEqual(content, expected_html)

    def test_unknown_link_syntax(self):
        "{unknown} link syntax should trigger warning."

        html = '<a href="{unknown}foo">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertEqual(content, html)
        self.assertLogCountEqual(
            count=1,
            msg="Replacement Indicator 'unknown' not recognized in '{unknown}foo', "
            "skipping replacement",
            level=logging.WARNING,
        )

    def test_link_to_unknown_file(self):
        "{filename} link to unknown file should trigger warning."

        html = '<a href="{filename}foo">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertEqual(content, html)
        self.assertLogCountEqual(
            count=1,
            msg="Unable to find 'foo', skipping url replacement.",
            level=logging.WARNING,
        )

    def test_index_link_syntax_with_spaces(self):
        """{index} link syntax triggers url replacement
        with spaces around the equal sign."""

        html = '<a href = "{index}">link</a>'
        page = Page(
            content=html,
            metadata={"title": "fakepage"},
            settings=self.settings,
            source_path=os.path.join("dir", "otherdir", "fakepage.md"),
            context=self.context,
        )
        content = page.get_content("")

        self.assertNotEqual(content, html)

        expected_html = (
            '<a href = "'
            + "/".join((self.settings["SITEURL"], self.settings["INDEX_SAVE_AS"]))
            + '">link</a>'
        )
        self.assertEqual(content, expected_html)

    def test_not_save_as_draft(self):
        """Static.save_as is not affected by draft status."""

        static = Static(
            content=None,
            metadata={
                "status": "draft",
            },
            settings=self.settings,
            source_path=os.path.join("dir", "foo.jpg"),
            context=self.settings.copy(),
        )

        expected_save_as = posixize_path(os.path.join("dir", "foo.jpg"))
        self.assertEqual(static.status, "draft")
        self.assertEqual(static.save_as, expected_save_as)
        self.assertEqual(static.url, path_to_url(expected_save_as))
