"""
Tests for manager.py
"""
import ddt
from django.test import TestCase
from nose.plugins.attrib import attr

from ..block_structure import BlockStructureBlockData
from ..config import RAISE_ERROR_WHEN_NOT_FOUND, STORAGE_BACKING_FOR_CACHE, waffle
from ..exceptions import UsageKeyNotInBlockStructure, BlockStructureNotFound
from ..manager import BlockStructureManager
from ..transformers import BlockStructureTransformers
from .helpers import (
    MockModulestoreFactory, MockCache, MockTransformer,
    ChildrenMapTestMixin, UsageKeyFactoryMixin,
    mock_registered_transformers,
)


class TestTransformer1(MockTransformer):
    """
    Test Transformer class with basic functionality to verify collected and
    transformed data.
    """
    collect_data_key = 't1.collect'
    transform_data_key = 't1.transform'
    collect_call_count = 0

    @classmethod
    def collect(cls, block_structure):
        """
        Collects block data for the block structure.
        """
        cls._set_block_values(block_structure, cls.collect_data_key)
        cls.collect_call_count += 1

    def transform(self, usage_info, block_structure):
        """
        Transforms the block structure.
        """
        self._set_block_values(block_structure, self.transform_data_key)

    @classmethod
    def assert_collected(cls, block_structure):
        """
        Asserts data was collected for the block structure.
        """
        cls._assert_block_values(block_structure, cls.collect_data_key)

    @classmethod
    def assert_transformed(cls, block_structure):
        """
        Asserts the block structure was transformed.
        """
        cls._assert_block_values(block_structure, cls.transform_data_key)

    @classmethod
    def _set_block_values(cls, block_structure, data_key):
        """
        Sets a value for each block in the given structure, using the given
        data key.
        """
        for block_key in block_structure.topological_traversal():
            block_structure.set_transformer_block_field(
                block_key, cls, data_key, cls._create_block_value(block_key, data_key)
            )

    @classmethod
    def _assert_block_values(cls, block_structure, data_key):
        """
        Verifies the value for each block in the given structure, for the given
        data key.
        """
        for block_key in block_structure.topological_traversal():
            assert (
                block_structure.get_transformer_block_field(
                    block_key,
                    cls,
                    data_key,
                ) == cls._create_block_value(block_key, data_key)
            )

    @classmethod
    def _create_block_value(cls, block_key, data_key):
        """
        Returns a unique deterministic value for the given block key
        and data key.
        """
        return data_key + 't1.val1.' + unicode(block_key)


@attr(shard=2)
@ddt.ddt
class TestBlockStructureManager(UsageKeyFactoryMixin, ChildrenMapTestMixin, TestCase):
    """
    Test class for BlockStructureManager.
    """
    def setUp(self):
        super(TestBlockStructureManager, self).setUp()

        TestTransformer1.collect_call_count = 0
        self.registered_transformers = [TestTransformer1()]
        with mock_registered_transformers(self.registered_transformers):
            self.transformers = BlockStructureTransformers(self.registered_transformers)

        self.children_map = self.SIMPLE_CHILDREN_MAP
        self.modulestore = MockModulestoreFactory.create(self.children_map, self.block_key_factory)
        self.cache = MockCache()
        self.bs_manager = BlockStructureManager(self.block_key_factory(0), self.modulestore, self.cache)

    def collect_and_verify(self, expect_modulestore_called, expect_cache_updated):
        """
        Calls the manager's get_collected method and verifies its result
        and behavior.
        """
        self.modulestore.get_items_call_count = 0
        self.cache.set_call_count = 0
        with mock_registered_transformers(self.registered_transformers):
            block_structure = self.bs_manager.get_collected()
        self.assert_block_structure(block_structure, self.children_map)
        TestTransformer1.assert_collected(block_structure)
        if expect_modulestore_called:
            self.assertGreater(self.modulestore.get_items_call_count, 0)
        else:
            self.assertEquals(self.modulestore.get_items_call_count, 0)
        self.assertEquals(self.cache.set_call_count, 1 if expect_cache_updated else 0)

    def test_get_transformed(self):
        with mock_registered_transformers(self.registered_transformers):
            block_structure = self.bs_manager.get_transformed(self.transformers)
        self.assert_block_structure(block_structure, self.children_map)
        TestTransformer1.assert_collected(block_structure)
        TestTransformer1.assert_transformed(block_structure)

    def test_get_transformed_with_starting_block(self):
        with mock_registered_transformers(self.registered_transformers):
            block_structure = self.bs_manager.get_transformed(
                self.transformers,
                starting_block_usage_key=self.block_key_factory(1),
            )
        substructure_of_children_map = [[], [3, 4], [], [], []]
        self.assert_block_structure(block_structure, substructure_of_children_map, missing_blocks=[0, 2])
        TestTransformer1.assert_collected(block_structure)
        TestTransformer1.assert_transformed(block_structure)

    def test_get_transformed_with_collected(self):
        with mock_registered_transformers(self.registered_transformers):
            collected_block_structure = self.bs_manager.get_collected()

        # using the same collected block structure,
        # transform at different starting blocks
        for (starting_block, expected_structure, expected_missing_blocks) in [
                (0, [[1, 2], [3, 4], [], [], []], []),
                (1, [[], [3, 4], [], [], []], [0, 2]),
                (2, [[], [], [], [], []], [0, 1, 3, 4]),
        ]:
            block_structure = self.bs_manager.get_transformed(
                self.transformers,
                starting_block_usage_key=self.block_key_factory(starting_block),
                collected_block_structure=collected_block_structure,
            )
            self.assert_block_structure(block_structure, expected_structure, missing_blocks=expected_missing_blocks)

    def test_get_transformed_with_nonexistent_starting_block(self):
        with mock_registered_transformers(self.registered_transformers):
            with self.assertRaises(UsageKeyNotInBlockStructure):
                self.bs_manager.get_transformed(self.transformers, starting_block_usage_key=100)

    def test_get_collected_cached(self):
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
        self.collect_and_verify(expect_modulestore_called=False, expect_cache_updated=False)
        self.assertEquals(TestTransformer1.collect_call_count, 1)

    def test_get_collected_error_raised(self):
        with waffle().override(RAISE_ERROR_WHEN_NOT_FOUND, active=True):
            with mock_registered_transformers(self.registered_transformers):
                with self.assertRaises(BlockStructureNotFound):
                    self.bs_manager.get_collected()

    @ddt.data(True, False)
    def test_update_collected_if_needed(self, with_storage_backing):
        with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
            with mock_registered_transformers(self.registered_transformers):
                self.assertEquals(TestTransformer1.collect_call_count, 0)

                self.bs_manager.update_collected_if_needed()
                self.assertEquals(TestTransformer1.collect_call_count, 1)

                self.bs_manager.update_collected_if_needed()
                self.assertEquals(TestTransformer1.collect_call_count, 1 if with_storage_backing else 2)

                self.collect_and_verify(expect_modulestore_called=False, expect_cache_updated=False)

    def test_get_collected_transformer_version(self):
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)

        # transformer code writes new schema version; data not re-collected
        TestTransformer1.WRITE_VERSION += 1
        self.collect_and_verify(expect_modulestore_called=False, expect_cache_updated=False)

        # transformer code requires new schema version; data re-collected
        TestTransformer1.READ_VERSION += 1
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)

        # old transformer code can read new schema version; data not re-collected
        TestTransformer1.READ_VERSION -= 1
        self.collect_and_verify(expect_modulestore_called=False, expect_cache_updated=False)

        self.assertEquals(TestTransformer1.collect_call_count, 2)

    def test_get_collected_structure_version(self):
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
        BlockStructureBlockData.VERSION += 1
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
        self.assertEquals(TestTransformer1.collect_call_count, 2)

    def test_clear(self):
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
        self.bs_manager.clear()
        self.collect_and_verify(expect_modulestore_called=True, expect_cache_updated=True)
        self.assertEquals(TestTransformer1.collect_call_count, 2)
