# coding: utf-8

"""
Unit tests for template_spec.py.

"""

import os.path
import sys
import unittest

from pystache import Renderer, TemplateSpec
from pystache.common import TemplateNotFoundError
from pystache.loader import Loader
from pystache.locator import Locator
from pystache.specloader import SpecLoader
from pystache.tests.common import (
    DATA_DIR,
    EXAMPLES_DIR,
    AssertIsMixin,
    AssertStringMixin,
)
from pystache.tests.data.views import NonAscii, SampleView

from . import examples
from .examples.complex import Complex
from .examples.inverted import Inverted, InvertedLists
from .examples.lambdas import Lambdas
from .examples.simple import Simple


class Thing(object):
    pass


class AssertPathsMixin:

    """A unittest.TestCase mixin to check path equality."""

    def assertPaths(self, actual, expected):
        self.assertEqual(actual, expected)


class ViewTestCase(unittest.TestCase, AssertStringMixin):

    def test_template_rel_directory(self):
        """
        Test that View.template_rel_directory is respected.

        """
        class Tagless(TemplateSpec):
            pass

        view = Tagless()
        renderer = Renderer()

        self.assertRaises(TemplateNotFoundError, renderer.render, view)

        # TODO: change this test to remove the following brittle line.
        view.template_rel_directory = "examples"
        actual = renderer.render(view)
        self.assertEqual(actual, "No tags...")

    def test_template_path_for_partials(self):
        """
        Test that View.template_rel_path is respected for partials.

        """
        spec = TemplateSpec()
        spec.template = "Partial: {{>tagless}}"

        renderer1 = Renderer()
        renderer2 = Renderer(search_dirs=EXAMPLES_DIR)

        actual = renderer1.render(spec)
        self.assertString(actual, "Partial: ")

        actual = renderer2.render(spec)
        self.assertEqual(actual, "Partial: No tags...")

    def test_basic_method_calls(self):
        renderer = Renderer()
        actual = renderer.render(Simple())

        self.assertString(actual, "Hi pizza!")

    def test_non_callable_attributes(self):
        view = Simple()
        view.thing = 'Chris'

        renderer = Renderer()
        actual = renderer.render(view)
        self.assertEqual(actual, "Hi Chris!")

    def test_complex(self):
        renderer = Renderer()
        actual = renderer.render(Complex())
        self.assertString(actual, """\
<h1>Colors</h1>
<ul>
<li><strong>red</strong></li>
<li><a href="#Green">green</a></li>
<li><a href="#Blue">blue</a></li>
</ul>""")

    def test_higher_order_replace(self):
        renderer = Renderer()
        actual = renderer.render(Lambdas())
        self.assertEqual(actual, 'bar != bar. oh, it does!')

    def test_higher_order_rot13(self):
        view = Lambdas()
        view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'

        renderer = Renderer()
        actual = renderer.render(view)
        self.assertString(actual, 'nopqrstuvwxyz')

    def test_higher_order_lambda(self):
        view = Lambdas()
        view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'

        renderer = Renderer()
        actual = renderer.render(view)
        self.assertString(actual, 'abcdefghijklmnopqrstuvwxyz')

    def test_partials_with_lambda(self):
        view = Lambdas()
        view.template = '{{>partial_with_lambda}}'

        renderer = Renderer(search_dirs=EXAMPLES_DIR)
        actual = renderer.render(view)
        self.assertEqual(actual, 'nopqrstuvwxyz')

    def test_hierarchical_partials_with_lambdas(self):
        view = Lambdas()
        view.template = '{{>partial_with_partial_and_lambda}}'

        renderer = Renderer(search_dirs=EXAMPLES_DIR)
        actual = renderer.render(view)
        self.assertString(actual, 'nopqrstuvwxyznopqrstuvwxyz')

    def test_inverted(self):
        renderer = Renderer()
        actual = renderer.render(Inverted())
        self.assertString(actual, """one, two, three, empty list""")

    def test_accessing_properties_on_parent_object_from_child_objects(self):
        parent = Thing()
        parent.this = 'derp'
        parent.children = [Thing()]
        view = Simple()
        view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"

        renderer = Renderer()
        actual = renderer.render(view, {'parent': parent})

        self.assertString(actual, 'derp')

    def test_inverted_lists(self):
        renderer = Renderer()
        actual = renderer.render(InvertedLists())
        self.assertString(actual, """one, two, three, empty list""")


def _make_specloader():
    """
    Return a default SpecLoader instance for testing purposes.

    """
    # Python 2 and 3 have different default encodings.  Thus, to have
    # consistent test results across both versions, we need to specify
    # the string and file encodings explicitly rather than relying on
    # the defaults.
    def to_unicode(s, encoding=None):
        """
        Raises a TypeError exception if the given string is already unicode.

        """
        if encoding is None:
            encoding = 'ascii'
        return str(s, encoding, 'strict')

    loader = Loader(file_encoding='ascii', to_unicode=to_unicode)
    return SpecLoader(loader=loader)


class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin,
                      AssertPathsMixin):

    """
    Tests template_spec.SpecLoader.

    """

    def _make_specloader(self):
        return _make_specloader()

    def test_init__defaults(self):
        spec_loader = SpecLoader()

        # Check the loader attribute.
        loader = spec_loader.loader
        self.assertEqual(loader.extension, 'mustache')
        self.assertEqual(loader.file_encoding, sys.getdefaultencoding())
        # TODO: finish testing the other Loader attributes.
        to_unicode = loader.to_unicode

    def test_init__loader(self):
        loader = Loader()
        custom = SpecLoader(loader=loader)

        self.assertIs(custom.loader, loader)

    # TODO: rename to something like _assert_load().
    def _assert_template(self, loader, custom, expected):
        self.assertString(loader.load(custom), expected)

    def test_load__template__type_str(self):
        """
        Test the template attribute: str string.

        """
        custom = TemplateSpec()
        custom.template = "abc"

        spec_loader = self._make_specloader()
        self._assert_template(spec_loader, custom, "abc")

    def test_load__template__type_unicode(self):
        """
        Test the template attribute: unicode string.

        """
        custom = TemplateSpec()
        custom.template = "abc"

        spec_loader = self._make_specloader()
        self._assert_template(spec_loader, custom, "abc")

    def test_load__template__unicode_non_ascii(self):
        """
        Test the template attribute: non-ascii unicode string.

        """
        custom = TemplateSpec()
        custom.template = "é"

        spec_loader = self._make_specloader()
        self._assert_template(spec_loader, custom, "é")

    def test_load__template__with_template_encoding(self):
        """
        Test the template attribute: with template encoding attribute.

        """
        custom = TemplateSpec()
        custom.template = 'é'.encode('utf-8')

        spec_loader = self._make_specloader()

        self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, 'é')

        custom.template_encoding = 'utf-8'
        self._assert_template(spec_loader, custom, 'é')

    # TODO: make this test complete.
    def test_load__template__correct_loader(self):
        """
        Test that reader.unicode() is called correctly.

        This test tests that the correct reader is called with the correct
        arguments.  This is a catch-all test to supplement the other
        test cases.  It tests SpecLoader.load() independent of reader.unicode()
        being implemented correctly (and tested).

        """
        class MockLoader(Loader):

            def __init__(self):
                self.s = None
                self.encoding = None

            # Overrides the existing method.
            def str(self, s, encoding=None):
                self.s = s
                self.encoding = encoding
                return "foo"

        loader = MockLoader()
        custom_loader = SpecLoader()
        custom_loader.loader = loader

        view = TemplateSpec()
        view.template = "template-foo"
        view.template_encoding = "encoding-foo"

        # Check that our unicode() above was called.
        self._assert_template(custom_loader, view, 'foo')
        self.assertEqual(loader.s, "template-foo")
        self.assertEqual(loader.encoding, "encoding-foo")

    def test_find__template_path(self):
        """Test _find() with TemplateSpec.template_path."""
        loader = self._make_specloader()
        custom = TemplateSpec()
        custom.template_path = "path/foo"
        actual = loader._find(custom)
        self.assertPaths(actual, "path/foo")


# TODO: migrate these tests into the SpecLoaderTests class.
# TODO: rename the get_template() tests to test load().
# TODO: condense, reorganize, and rename the tests so that it is
#   clear whether we have full test coverage (e.g. organized by
#   TemplateSpec attributes or something).
class TemplateSpecTests(unittest.TestCase, AssertPathsMixin):

    def _make_loader(self):
        return _make_specloader()

    def _assert_template_location(self, view, expected):
        loader = self._make_loader()
        actual = loader._find_relative(view)
        self.assertEqual(actual, expected)

    def test_find_relative(self):
        """
        Test _find_relative(): default behavior (no attributes set).

        """
        view = SampleView()
        self._assert_template_location(view, (None, 'sample_view.mustache'))

    def test_find_relative__template_rel_path__file_name_only(self):
        """
        Test _find_relative(): template_rel_path attribute.

        """
        view = SampleView()
        view.template_rel_path = 'template.txt'
        self._assert_template_location(view, ('', 'template.txt'))

    def test_find_relative__template_rel_path__file_name_with_directory(self):
        """
        Test _find_relative(): template_rel_path attribute.

        """
        view = SampleView()
        view.template_rel_path = 'foo/bar/template.txt'
        self._assert_template_location(view, ('foo/bar', 'template.txt'))

    def test_find_relative__template_rel_directory(self):
        """
        Test _find_relative(): template_rel_directory attribute.

        """
        view = SampleView()
        view.template_rel_directory = 'foo'

        self._assert_template_location(view, ('foo', 'sample_view.mustache'))

    def test_find_relative__template_name(self):
        """
        Test _find_relative(): template_name attribute.

        """
        view = SampleView()
        view.template_name = 'new_name'
        self._assert_template_location(view, (None, 'new_name.mustache'))

    def test_find_relative__template_extension(self):
        """
        Test _find_relative(): template_extension attribute.

        """
        view = SampleView()
        view.template_extension = 'txt'
        self._assert_template_location(view, (None, 'sample_view.txt'))

    def test_find__with_directory(self):
        """
        Test _find() with a view that has a directory specified.

        """
        loader = self._make_loader()

        view = SampleView()
        view.template_rel_path = os.path.join('foo', 'bar.txt')
        self.assertTrue(loader._find_relative(view)[0] is not None)

        actual = loader._find(view)
        expected = os.path.join(DATA_DIR, 'foo', 'bar.txt')

        self.assertPaths(actual, expected)

    def test_find__without_directory(self):
        """
        Test _find() with a view that doesn't have a directory specified.

        """
        loader = self._make_loader()

        view = SampleView()
        self.assertTrue(loader._find_relative(view)[0] is None)

        actual = loader._find(view)
        expected = os.path.join(DATA_DIR, 'sample_view.mustache')

        self.assertPaths(actual, expected)

    def _assert_get_template(self, custom, expected):
        loader = self._make_loader()
        actual = loader.load(custom)

        self.assertEqual(type(actual), str)
        self.assertEqual(actual, expected)

    def test_get_template(self):
        """
        Test get_template(): default behavior (no attributes set).

        """
        view = SampleView()

        self._assert_get_template(view, "ascii: abc")

    def test_get_template__template_encoding(self):
        """
        Test get_template(): template_encoding attribute.

        """
        view = NonAscii()

        self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo')

        view.template_encoding = 'utf-8'
        self._assert_get_template(view, "non-ascii: é")
