# -*- coding: utf-8 -*-
"""Tests for static_replace"""


import re
from six import BytesIO
from six.moves.urllib.parse import parse_qsl, urlparse, urlunparse

import ddt
import pytest
from django.test import override_settings
from django.utils.http import urlencode, urlquote
from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
from PIL import Image

from static_replace import (
    _url_replace_regex,
    make_static_urls_absolute,
    process_static_urls,
    replace_course_urls,
    replace_static_urls
)
from xmodule.assetstore.assetmgr import AssetManager
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from xmodule.modulestore.xml import XMLModuleStore

DATA_DIRECTORY = 'data_dir'
COURSE_KEY = CourseKey.from_string('org/course/run')
STATIC_SOURCE = '"/static/file.png"'


def encode_unicode_characters_in_url(url):
    """
    Encodes all Unicode characters to their percent-encoding representation
    in both the path portion and query parameter portion of the given URL.
    """
    scheme, netloc, path, params, query, fragment = urlparse(url)
    query_params = parse_qsl(query)
    updated_query_params = []
    for query_name, query_val in query_params:
        updated_query_params.append((query_name, urlquote(query_val)))

    return urlunparse((scheme, netloc, urlquote(path, '/:+@'), params, urlencode(query_params), fragment))


def test_multi_replace():
    course_source = '"/course/file.png"'

    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == \
        replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY)
    assert replace_course_urls(course_source, COURSE_KEY) == \
        replace_course_urls(replace_course_urls(course_source, COURSE_KEY), COURSE_KEY)


def test_process_url():
    def processor(__, prefix, quote, rest):  # pylint: disable=missing-docstring
        return quote + 'test' + prefix + rest + quote

    assert process_static_urls(STATIC_SOURCE, processor) == '"test/static/file.png"'


def test_process_url_data_dir_exists():
    base = '"/static/{data_dir}/file.png"'.format(data_dir=DATA_DIRECTORY)

    def processor(original, prefix, quote, rest):  # pylint: disable=unused-argument,missing-docstring
        return quote + 'test' + rest + quote

    assert process_static_urls(base, processor, data_dir=DATA_DIRECTORY) == base


def test_process_url_no_match():

    def processor(__, prefix, quote, rest):  # pylint: disable=missing-docstring
        return quote + 'test' + prefix + rest + quote

    assert process_static_urls(STATIC_SOURCE, processor) == '"test/static/file.png"'


@patch('django.http.HttpRequest', autospec=True)
def test_static_urls(mock_request):
    mock_request.build_absolute_uri = lambda url: 'http://' + url
    result = make_static_urls_absolute(mock_request, STATIC_SOURCE)
    assert result == '\"http:///static/file.png\"'


@patch('static_replace.staticfiles_storage', autospec=True)
def test_storage_url_exists(mock_storage):
    mock_storage.exists.return_value = True
    mock_storage.url.return_value = '/static/file.png'

    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == '"/static/file.png"'
    mock_storage.exists.assert_called_once_with('file.png')
    mock_storage.url.assert_called_once_with('file.png')


@patch('static_replace.staticfiles_storage', autospec=True)
def test_storage_url_not_exists(mock_storage):
    mock_storage.exists.return_value = False
    mock_storage.url.return_value = '/static/data_dir/file.png'

    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == '"/static/data_dir/file.png"'
    mock_storage.exists.assert_called_once_with('file.png')
    mock_storage.url.assert_called_once_with('data_dir/file.png')


@patch('static_replace.StaticContent', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
@patch('static_replace.models.AssetBaseUrlConfig.get_base_url')
@patch('static_replace.models.AssetExcludedExtensionsConfig.get_excluded_extensions')
def test_mongo_filestore(mock_get_excluded_extensions, mock_get_base_url, mock_modulestore, mock_static_content):

    mock_modulestore.return_value = Mock(MongoModuleStore)
    mock_static_content.get_canonicalized_asset_path.return_value = "c4x://mock_url"
    mock_get_base_url.return_value = u''
    mock_get_excluded_extensions.return_value = ['foobar']

    # No namespace => no change to path
    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == '"/static/data_dir/file.png"'

    # Namespace => content url
    assert '"' + mock_static_content.get_canonicalized_asset_path.return_value + '"' == \
        replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, course_id=COURSE_KEY)

    mock_static_content.get_canonicalized_asset_path.assert_called_once_with(COURSE_KEY, 'file.png', u'', ['foobar'])


@patch('static_replace.settings', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
@patch('static_replace.staticfiles_storage', autospec=True)
def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
    mock_modulestore.return_value = Mock(XMLModuleStore)
    mock_storage.url.side_effect = Exception

    mock_storage.exists.return_value = True
    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == '"/static/data_dir/file.png"'

    mock_storage.exists.return_value = False
    assert replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY) == '"/static/data_dir/file.png"'


def test_raw_static_check():
    """
    Make sure replace_static_urls leaves alone things that end in '.raw'
    """
    path = '"/static/foo.png?raw"'
    assert replace_static_urls(path, DATA_DIRECTORY) == path

    text = 'text <tag a="/static/js/capa/protex/protex.nocache.js?raw"/><div class="'
    assert replace_static_urls(path, text) == path


@pytest.mark.django_db
@patch('static_replace.staticfiles_storage', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
def test_static_url_with_query(mock_modulestore, mock_storage):
    """
    Make sure that for urls with query params:
     query params that contain "^/static/" are converted to full location urls
     query params that do not contain "^/static/" are left unchanged
    """
    mock_storage.exists.return_value = False
    mock_modulestore.return_value = Mock(MongoModuleStore)

    pre_text = 'EMBED src ="/static/LAlec04_controller.swf?csConfigFile=/static/LAlec04_config.xml&name1=value1&name2=value2"'
    post_text = 'EMBED src ="/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2"'
    assert replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY) == post_text


@pytest.mark.django_db
@patch('static_replace.staticfiles_storage', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
def test_static_paths_out(mock_modulestore, mock_storage):
    """
    Tests the side-effect of passing an array to collect static_paths_out.

    * if a static URL is changed, then its changed URL is returned.
    * if a static URL is unchanged, then the unchanged URL is returned.
    * xblock paths are not included in the static_paths_out array.
    """
    mock_storage.exists.return_value = False
    mock_modulestore.return_value = Mock(MongoModuleStore)

    static_url = '/static/LAlec04_controller.swf?csConfigFile=/static/LAlec04_config.xml&name1=value1&name2=value2'
    static_course_url = '/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=%2Fc4x%2Forg%2Fcourse%2Fasset%2FLAlec04_config.xml&name1=value1&name2=value2'
    raw_url = '/static/js/capa/protex/protex.nocache.js?raw'
    xblock_url = '/static/xblock/resources/babys_first.lil_xblock/public/images/pacifier.png'
    # xss-lint: disable=python-wrap-html
    pre_text = 'EMBED src ="{}" xblock={} text <tag a="{}"/><div class="'.format(static_url, xblock_url, raw_url)
    # xss-lint: disable=python-wrap-html
    post_text = 'EMBED src ="{}" xblock={} text <tag a="{}"/><div class="'.format(static_course_url, xblock_url, raw_url)
    static_paths = []
    assert replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY, static_paths_out=static_paths) == post_text
    assert static_paths == [(static_url, static_course_url), (raw_url, raw_url)]


def test_regex():
    yes = ('"/static/foo.png"',
           '"/static/foo.png"',
           "'/static/foo.png'")

    no = ('"/not-static/foo.png"',
          '"/static/foo',  # no matching quote
          )

    regex = _url_replace_regex('/static/')

    for s in yes:
        print('Should match: {0!r}'.format(s))
        assert re.match(regex, s)

    for s in no:
        print('Should not match: {0!r}'.format(s))
        assert not re.match(regex, s)


@patch('static_replace.staticfiles_storage', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
def test_static_url_with_xblock_resource(mock_modulestore, mock_storage):
    """
    Make sure that for URLs with XBlock resource URL, which start with /static/,
    we don't rewrite them.
    """
    mock_storage.exists.return_value = False
    mock_modulestore.return_value = Mock(MongoModuleStore)

    pre_text = 'EMBED src ="/static/xblock/resources/babys_first.lil_xblock/public/images/pacifier.png"'
    post_text = pre_text
    assert replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY) == post_text


@patch('static_replace.staticfiles_storage', autospec=True)
@patch('xmodule.modulestore.django.modulestore', autospec=True)
@override_settings(STATIC_URL='https://example.com/static/')
def test_static_url_with_xblock_resource_on_cdn(mock_modulestore, mock_storage):
    """
    Make sure that for URLs with XBlock resource URL, which start with /static/,
    we don't rewrite them, even if these are served from an absolute URL like a CDN.
    """
    mock_storage.exists.return_value = False
    mock_modulestore.return_value = Mock(MongoModuleStore)

    pre_text = 'EMBED src ="https://example.com/static/xblock/resources/tehehe.xblock/public/images/woo.png"'
    post_text = pre_text
    assert replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY) == post_text


@ddt.ddt
class CanonicalContentTest(SharedModuleStoreTestCase):
    """
    Tests the generation of canonical asset URLs for different types
    of assets: c4x-style, opaque key style, locked, unlocked, CDN
    set, CDN not set, etc.
    """

    @classmethod
    def setUpClass(cls):
        cls.courses = {}

        super(CanonicalContentTest, cls).setUpClass()

        names_and_prefixes = [(ModuleStoreEnum.Type.split, 'split'), (ModuleStoreEnum.Type.mongo, 'old')]
        for store, prefix in names_and_prefixes:
            with cls.store.default_store(store):
                cls.courses[prefix] = CourseFactory.create(org='a', course='b', run=prefix)

                # Create an unlocked image.
                unlock_content = cls.create_image(prefix, (32, 32), 'blue', u'{}_ünlöck.png')

                # Create a locked image.
                lock_content = cls.create_image(prefix, (32, 32), 'green', '{}_lock.png', locked=True)

                # Create a thumbnail of the images.
                contentstore().generate_thumbnail(unlock_content, dimensions=(16, 16))
                contentstore().generate_thumbnail(lock_content, dimensions=(16, 16))

                # Create an unlocked image in a subdirectory.
                cls.create_image(prefix, (1, 1), 'red', u'special/{}_ünlöck.png')

                # Create a locked image in a subdirectory.
                cls.create_image(prefix, (1, 1), 'yellow', 'special/{}_lock.png', locked=True)

                # Create an unlocked image with funky characters in the name.
                cls.create_image(prefix, (1, 1), 'black', u'weird {}_ünlöck.png')
                cls.create_image(prefix, (1, 1), 'black', u'special/weird {}_ünlöck.png')

                # Create an HTML file to test extension exclusion, and create a control file.
                cls.create_arbitrary_content(prefix, '{}_not_excluded.htm')
                cls.create_arbitrary_content(prefix, '{}_excluded.html')
                cls.create_arbitrary_content(prefix, 'special/{}_not_excluded.htm')
                cls.create_arbitrary_content(prefix, 'special/{}_excluded.html')

    @classmethod
    def get_content_digest_for_asset_path(cls, prefix, path):
        """
        Takes an unprocessed asset path, parses it just enough to try and find the
        asset it refers to, and returns the content digest of that asset if it exists.
        """

        # Parse the path as if it was potentially a relative URL with query parameters,
        # or an absolute URL, etc.  Only keep the path because that's all we need.
        _, _, relative_path, _, _, _ = urlparse(path)
        asset_key = StaticContent.get_asset_key_from_path(cls.courses[prefix].id, relative_path)

        try:
            content = AssetManager.find(asset_key, as_stream=True)
            return content.content_digest
        except (ItemNotFoundError, NotFoundError):
            return None

    @classmethod
    def create_image(cls, prefix, dimensions, color, name, locked=False):
        """
        Creates an image.

        Args:
            prefix: the prefix to use e.g. split vs mongo
            dimensions: tuple of (width, height)
            color: the background color of the image
            name: the name of the image; can be a format string
            locked: whether or not the asset should be locked

        Returns:
            StaticContent: the StaticContent object for the created image
        """
        new_image = Image.new('RGB', dimensions, color)
        new_buf = BytesIO()
        new_image.save(new_buf, format='png')
        new_buf.seek(0)
        new_name = name.format(prefix)
        new_key = StaticContent.compute_location(cls.courses[prefix].id, new_name)
        new_content = StaticContent(new_key, new_name, 'image/png', new_buf.getvalue(), locked=locked)
        contentstore().save(new_content)

        return new_content

    @classmethod
    def create_arbitrary_content(cls, prefix, name, locked=False):
        """
        Creates an arbitrary piece of content with a fixed body, for when content doesn't matter.

        Args:
            prefix: the prefix to use e.g. split vs mongo
            name: the name of the content; can be a format string
            locked: whether or not the asset should be locked

        Returns:
            StaticContent: the StaticContent object for the created content

        """
        new_buf = BytesIO(b'testingggggggggggg')
        new_name = name.format(prefix)
        new_key = StaticContent.compute_location(cls.courses[prefix].id, new_name)
        new_content = StaticContent(new_key, new_name, 'application/octet-stream', new_buf.getvalue(), locked=locked)
        contentstore().save(new_content)

        return new_content

    @ddt.data(
        # No leading slash.
        (u'', u'{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1),
        (u'', u'{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'', u'weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'', u'{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'', u'{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1),
        (u'dev', u'{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1),
        (u'dev', u'{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'dev', u'weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'dev', u'{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1),
        # No leading slash with subdirectory.  This ensures we properly substitute slashes.
        (u'', u'special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'', u'special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'', u'special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'', u'special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'dev', u'special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'dev', u'special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'dev', u'special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1),
        # Leading slash.
        (u'', u'/{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1),
        (u'', u'/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'', u'/weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'', u'/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'', u'/{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1),
        (u'dev', u'/{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1),
        (u'dev', u'/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'dev', u'/weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'dev', u'/{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1),
        # Leading slash with subdirectory.  This ensures we properly substitute slashes.
        (u'', u'/special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'', u'/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'', u'/special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'', u'/special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'/special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'dev', u'/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'dev', u'/special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'dev', u'/special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1),
        # Static path.
        (u'', u'/static/{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1),
        (u'', u'/static/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'', u'/static/weird {prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'', u'/static/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'', u'/static/{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1),
        (u'dev', u'/static/{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'dev', u'/static/weird {prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'dev', u'/static/{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1),
        # Static path with subdirectory.  This ensures we properly substitute slashes.
        (u'', u'/static/special/{prfx}_ünlöck.png', u'/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'', u'/static/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'', u'/static/special/weird {prfx}_ünlöck.png', u'/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'/static/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'', u'/static/special/{prfx}_not_excluded.htm', u'/{asset}@special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'/static/special/{prfx}_ünlöck.png', u'//dev/{asset}@special_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/special/{prfx}_lock.png', u'/{asset}@special_{prfx}_lock.png', 1),
        (u'dev', u'/static/special/weird {prfx}_ünlöck.png', u'//dev/{asset}@special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/special/{prfx}_excluded.html', u'/{base_asset}@special_{prfx}_excluded.html', 1),
        (u'dev', u'/static/special/{prfx}_not_excluded.htm', u'//dev/{asset}@special_{prfx}_not_excluded.htm', 1),
        # Static path with query parameter.
        (
            u'',
            u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png',
            u'/{asset}@{prfx}_ünlöck.png?foo={encoded_asset}{prfx}_lock.png',
            2
        ),
        (
            u'',
            u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png',
            u'/{asset}@{prfx}_lock.png?foo={encoded_asset}{prfx}_ünlöck.png',
            2
        ),
        (
            u'',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html',
            u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_asset}{prfx}_excluded.html',
            2
        ),
        (
            u'',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm',
            u'/{base_asset}@{prfx}_excluded.html?foo={encoded_asset}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html',
            u'/{asset}@{prfx}_not_excluded.htm?foo={encoded_base_asset}{prfx}_excluded.html',
            2
        ),
        (
            u'',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_not_excluded.htm',
            u'/{asset}@{prfx}_not_excluded.htm?foo={encoded_asset}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png',
            u'//dev/{asset}@{prfx}_ünlöck.png?foo={encoded_asset}{prfx}_lock.png',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png',
            u'/{asset}@{prfx}_lock.png?foo={encoded_base_url}{encoded_asset}{prfx}_ünlöck.png',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html',
            u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_asset}{prfx}_excluded.html',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm',
            u'/{base_asset}@{prfx}_excluded.html?foo={encoded_base_url}{encoded_asset}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html',
            u'//dev/{asset}@{prfx}_not_excluded.htm?foo={encoded_base_asset}{prfx}_excluded.html',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_not_excluded.htm',
            u'//dev/{asset}@{prfx}_not_excluded.htm?foo={encoded_base_url}{encoded_asset}{prfx}_not_excluded.htm',
            2
        ),
        # Already asset key.
        (u'', u'/{base_asset}@{prfx}_ünlöck.png', u'/{asset}@{prfx}_ünlöck.png', 1),
        (u'', u'/{base_asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'', u'/{base_asset}@weird_{prfx}_ünlöck.png', u'/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'', u'/{base_asset}@{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'', u'/{base_asset}@{prfx}_not_excluded.htm', u'/{asset}@{prfx}_not_excluded.htm', 1),
        (u'dev', u'/{base_asset}@{prfx}_ünlöck.png', u'//dev/{asset}@{prfx}_ünlöck.png', 1),
        (u'dev', u'/{base_asset}@{prfx}_lock.png', u'/{asset}@{prfx}_lock.png', 1),
        (u'dev', u'/{base_asset}@weird_{prfx}_ünlöck.png', u'//dev/{asset}@weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/{base_asset}@{prfx}_excluded.html', u'/{base_asset}@{prfx}_excluded.html', 1),
        (u'dev', u'/{base_asset}@{prfx}_not_excluded.htm', u'//dev/{asset}@{prfx}_not_excluded.htm', 1),
        # Old, c4x-style path.
        (u'', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'', u'/{c4x}/weird_{prfx}_lock.png', u'/{c4x}/weird_{prfx}_lock.png', 1),
        (u'', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1),
        (u'', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        (u'dev', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'dev', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'dev', u'/{c4x}/weird_{prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/{c4x}/{prfx}_excluded.html', u'/{c4x}/{prfx}_excluded.html', 1),
        (u'dev', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        # Thumbnails.
        (u'', u'/{base_th_key}@{prfx}_ünlöck-{th_ext}', u'/{th_key}@{prfx}_ünlöck-{th_ext}', 1),
        (u'', u'/{base_th_key}@{prfx}_lock-{th_ext}', u'/{th_key}@{prfx}_lock-{th_ext}', 1),
        (u'dev', u'/{base_th_key}@{prfx}_ünlöck-{th_ext}', u'//dev/{th_key}@{prfx}_ünlöck-{th_ext}', 1),
        (u'dev', u'/{base_th_key}@{prfx}_lock-{th_ext}', u'//dev/{th_key}@{prfx}_lock-{th_ext}', 1),
    )
    @ddt.unpack
    def test_canonical_asset_path_with_new_style_assets(self, base_url, start, expected, mongo_calls):
        exts = ['.html', '.tm']
        prefix = u'split'
        encoded_base_url = urlquote(u'//' + base_url)
        c4x = u'c4x/a/b/asset'
        base_asset_key = u'asset-v1:a+b+{}+type@asset+block'.format(prefix)
        adjusted_asset_key = base_asset_key
        encoded_asset_key = urlquote(u'/asset-v1:a+b+{}+type@asset+block@'.format(prefix))
        encoded_base_asset_key = encoded_asset_key
        base_th_key = u'asset-v1:a+b+{}+type@thumbnail+block'.format(prefix)
        adjusted_th_key = base_th_key
        th_ext = u'png-16x16.jpg'

        start = start.format(
            prfx=prefix,
            c4x=c4x,
            base_asset=base_asset_key,
            asset=adjusted_asset_key,
            encoded_base_url=encoded_base_url,
            encoded_asset=encoded_asset_key,
            base_th_key=base_th_key,
            th_key=adjusted_th_key,
            th_ext=th_ext
        )

        # Adjust for content digest.  This gets dicey quickly and we have to order our steps:
        # - replace format markets because they have curly braces
        # - encode Unicode characters to percent-encoded
        # - finally shove back in our regex patterns
        digest = CanonicalContentTest.get_content_digest_for_asset_path(prefix, start)
        if digest:
            adjusted_asset_key = u'assets/courseware/VMARK/HMARK/asset-v1:a+b+{}+type@asset+block'.format(prefix)
            adjusted_th_key = u'assets/courseware/VMARK/HMARK/asset-v1:a+b+{}+type@thumbnail+block'.format(prefix)
            encoded_asset_key = u'/assets/courseware/VMARK/HMARK/asset-v1:a+b+{}+type@asset+block@'.format(prefix)
            encoded_asset_key = urlquote(encoded_asset_key)

        expected = expected.format(
            prfx=prefix,
            c4x=c4x,
            base_asset=base_asset_key,
            asset=adjusted_asset_key,
            encoded_base_url=encoded_base_url,
            encoded_asset=encoded_asset_key,
            base_th_key=base_th_key,
            th_key=adjusted_th_key,
            th_ext=th_ext,
            encoded_base_asset=encoded_base_asset_key,
        )

        expected = encode_unicode_characters_in_url(expected)
        expected = expected.replace('VMARK', r'v[\d]')
        expected = expected.replace('HMARK', '[a-f0-9]{32}')
        expected = expected.replace('+', r'\+').replace('?', r'\?')

        with check_mongo_calls(mongo_calls):
            asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts)
            self.assertIsNotNone(re.match(expected, asset_path))

    @ddt.data(
        # No leading slash.
        (u'', u'{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'', u'{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'', u'weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'', u'{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'', u'{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        (u'dev', u'{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1),
        (u'dev', u'{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'dev', u'weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'dev', u'{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1),
        # No leading slash with subdirectory.  This ensures we probably substitute slashes.
        (u'', u'special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'', u'special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'', u'special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'', u'special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'dev', u'special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'dev', u'special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'dev', u'special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1),
        # Leading slash.
        (u'', u'/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'', u'/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'', u'/weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'', u'/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'', u'/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        (u'dev', u'/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1),
        (u'dev', u'/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'dev', u'/weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'dev', u'/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1),
        # Leading slash with subdirectory. This ensures we properly substitute slashes.
        (u'', u'/special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'', u'/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'', u'/special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'', u'/special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'/special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'dev', u'/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'dev', u'/special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'dev', u'/special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1),
        # Static path.
        (u'', u'/static/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'', u'/static/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'', u'/static/weird {prfx}_ünlöck.png', u'/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'', u'/static/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'', u'/static/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        (u'dev', u'/static/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'dev', u'/static/weird {prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'dev', u'/static/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1),
        # Static path with subdirectory.  This ensures we properly substitute slashes.
        (u'', u'/static/special/{prfx}_ünlöck.png', u'/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'', u'/static/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'', u'/static/special/weird {prfx}_ünlöck.png', u'/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'', u'/static/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'', u'/static/special/{prfx}_not_excluded.htm', u'/{c4x}/special_{prfx}_not_excluded.htm', 1),
        (u'dev', u'/static/special/{prfx}_ünlöck.png', u'//dev/{c4x}/special_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/special/{prfx}_lock.png', u'/{c4x}/special_{prfx}_lock.png', 1),
        (u'dev', u'/static/special/weird {prfx}_ünlöck.png', u'//dev/{c4x}/special_weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/static/special/{prfx}_excluded.html', u'/{base_c4x}/special_{prfx}_excluded.html', 1),
        (u'dev', u'/static/special/{prfx}_not_excluded.htm', u'//dev/{c4x}/special_{prfx}_not_excluded.htm', 1),
        # Static path with query parameter.
        (
            u'',
            u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png',
            u'/{c4x}/{prfx}_ünlöck.png?foo={encoded_c4x}{prfx}_lock.png',
            2
        ),
        (
            u'',
            u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png',
            u'/{c4x}/{prfx}_lock.png?foo={encoded_c4x}{prfx}_ünlöck.png',
            2
        ),
        (
            u'',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html',
            u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_c4x}{prfx}_excluded.html',
            2
        ),
        (
            u'',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm',
            u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_c4x}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html',
            u'/{c4x}/{prfx}_not_excluded.htm?foo={encoded_base_c4x}{prfx}_excluded.html',
            2
        ),
        (
            u'',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_not_excluded.htm',
            u'/{c4x}/{prfx}_not_excluded.htm?foo={encoded_c4x}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_ünlöck.png?foo=/static/{prfx}_lock.png',
            u'//dev/{c4x}/{prfx}_ünlöck.png?foo={encoded_c4x}{prfx}_lock.png',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_lock.png?foo=/static/{prfx}_ünlöck.png',
            u'/{c4x}/{prfx}_lock.png?foo={encoded_base_url}{encoded_c4x}{prfx}_ünlöck.png',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_excluded.html',
            u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_c4x}{prfx}_excluded.html',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_excluded.html?foo=/static/{prfx}_not_excluded.htm',
            u'/{base_c4x}/{prfx}_excluded.html?foo={encoded_base_url}{encoded_c4x}{prfx}_not_excluded.htm',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_excluded.html',
            u'//dev/{c4x}/{prfx}_not_excluded.htm?foo={encoded_base_c4x}{prfx}_excluded.html',
            2
        ),
        (
            u'dev',
            u'/static/{prfx}_not_excluded.htm?foo=/static/{prfx}_not_excluded.htm',
            u'//dev/{c4x}/{prfx}_not_excluded.htm?foo={encoded_base_url}{encoded_c4x}{prfx}_not_excluded.htm',
            2
        ),
        # Old, c4x-style path.
        (u'', u'/{c4x}/{prfx}_ünlöck.png', u'/{c4x}/{prfx}_ünlöck.png', 1),
        (u'', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'', u'/{c4x}/weird_{prfx}_lock.png', u'/{c4x}/weird_{prfx}_lock.png', 1),
        (u'', u'/{c4x}/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'', u'/{c4x}/{prfx}_not_excluded.htm', u'/{c4x}/{prfx}_not_excluded.htm', 1),
        (u'dev', u'/{c4x}/{prfx}_ünlöck.png', u'//dev/{c4x}/{prfx}_ünlöck.png', 1),
        (u'dev', u'/{c4x}/{prfx}_lock.png', u'/{c4x}/{prfx}_lock.png', 1),
        (u'dev', u'/{c4x}/weird_{prfx}_ünlöck.png', u'//dev/{c4x}/weird_{prfx}_ünlöck.png', 1),
        (u'dev', u'/{c4x}/{prfx}_excluded.html', u'/{base_c4x}/{prfx}_excluded.html', 1),
        (u'dev', u'/{c4x}/{prfx}_not_excluded.htm', u'//dev/{c4x}/{prfx}_not_excluded.htm', 1),
    )
    @ddt.unpack
    def test_canonical_asset_path_with_c4x_style_assets(self, base_url, start, expected, mongo_calls):
        exts = ['.html', '.tm']
        prefix = 'old'
        base_c4x_block = 'c4x/a/b/asset'
        adjusted_c4x_block = base_c4x_block
        encoded_c4x_block = urlquote('/' + base_c4x_block + '/')
        encoded_base_url = urlquote('//' + base_url)
        encoded_base_c4x_block = encoded_c4x_block

        start = start.format(
            prfx=prefix,
            encoded_base_url=encoded_base_url,
            c4x=base_c4x_block,
            encoded_c4x=encoded_c4x_block
        )

        # Adjust for content digest.  This gets dicey quickly and we have to order our steps:
        # - replace format markets because they have curly braces
        # - encode Unicode characters to percent-encoded
        # - finally shove back in our regex patterns
        digest = CanonicalContentTest.get_content_digest_for_asset_path(prefix, start)
        if digest:
            adjusted_c4x_block = 'assets/courseware/VMARK/HMARK/c4x/a/b/asset'
            encoded_c4x_block = urlquote('/' + adjusted_c4x_block + '/')

        expected = expected.format(
            prfx=prefix,
            encoded_base_url=encoded_base_url,
            base_c4x=base_c4x_block,
            c4x=adjusted_c4x_block,
            encoded_c4x=encoded_c4x_block,
            encoded_base_c4x=encoded_base_c4x_block,
        )

        expected = encode_unicode_characters_in_url(expected)
        expected = expected.replace('VMARK', r'v[\d]')
        expected = expected.replace('HMARK', '[a-f0-9]{32}')
        expected = expected.replace('+', r'\+').replace('?', r'\?')

        with check_mongo_calls(mongo_calls):
            asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts)
            self.assertIsNotNone(re.match(expected, asset_path))
