from os.path import dirname
import sys

from django.test import TestCase
from django.conf import settings
from django.test.utils import override_settings

import oscar
from oscar.core.loading import (
    get_model, AppNotFoundError, get_classes, get_class, ClassNotFoundError)
from oscar.test.factories import create_product, WishListFactory, UserFactory
from tests import temporary_python_path


class TestClassLoading(TestCase):
    """
    Oscar's class loading utilities
    """

    def test_load_oscar_classes_correctly(self):
        Product, Category = get_classes('catalogue.models', ('Product', 'Category'))
        self.assertEqual('oscar.apps.catalogue.models', Product.__module__)
        self.assertEqual('oscar.apps.catalogue.models', Category.__module__)

    def test_load_oscar_class_correctly(self):
        Product = get_class('catalogue.models', 'Product')
        self.assertEqual('oscar.apps.catalogue.models', Product.__module__)

    def test_load_oscar_class_from_dashboard_subapp(self):
        ReportForm = get_class('dashboard.reports.forms', 'ReportForm')
        self.assertEqual('oscar.apps.dashboard.reports.forms', ReportForm.__module__)

    def test_raise_exception_when_bad_appname_used(self):
        with self.assertRaises(AppNotFoundError):
            get_classes('fridge.models', ('Product', 'Category'))

    def test_raise_exception_when_bad_classname_used(self):
        with self.assertRaises(ClassNotFoundError):
            get_class('catalogue.models', 'Monkey')

    def test_raise_importerror_if_app_raises_importerror(self):
        """
        This tests that Oscar doesn't fall back to using the Oscar catalogue
        app if the overriding app throws an ImportError.
        """
        apps = list(settings.INSTALLED_APPS)
        apps[apps.index('oscar.apps.catalogue')] = 'tests._site.import_error_app.catalogue'
        with override_settings(INSTALLED_APPS=apps):
            with self.assertRaises(ImportError):
                get_class('catalogue.app', 'CatalogueApplication')


class ClassLoadingWithLocalOverrideTests(TestCase):

    def setUp(self):
        self.installed_apps = list(settings.INSTALLED_APPS)
        self.installed_apps[self.installed_apps.index('oscar.apps.shipping')] = 'tests._site.shipping'

    def test_loading_class_defined_in_local_module(self):
        with override_settings(INSTALLED_APPS=self.installed_apps):
            (Free,) = get_classes('shipping.methods', ('Free',))
            self.assertEqual('tests._site.shipping.methods', Free.__module__)

    def test_loading_class_which_is_not_defined_in_local_module(self):
        with override_settings(INSTALLED_APPS=self.installed_apps):
            (FixedPrice,) = get_classes('shipping.methods', ('FixedPrice',))
            self.assertEqual('oscar.apps.shipping.methods', FixedPrice.__module__)

    def test_loading_class_from_module_not_defined_in_local_app(self):
        with override_settings(INSTALLED_APPS=self.installed_apps):
            (Repository,) = get_classes('shipping.repository', ('Repository',))
            self.assertEqual('oscar.apps.shipping.repository', Repository.__module__)

    def test_loading_classes_defined_in_both_local_and_oscar_modules(self):
        with override_settings(INSTALLED_APPS=self.installed_apps):
            (Free, FixedPrice) = get_classes('shipping.methods', ('Free', 'FixedPrice'))
            self.assertEqual('tests._site.shipping.methods', Free.__module__)
            self.assertEqual('oscar.apps.shipping.methods', FixedPrice.__module__)

    def test_loading_classes_with_root_app(self):
        import tests._site.shipping
        path = dirname(dirname(tests._site.shipping.__file__))
        with temporary_python_path([path]):
            self.installed_apps[
                self.installed_apps.index('tests._site.shipping')] = 'shipping'
            with override_settings(INSTALLED_APPS=self.installed_apps):
                (Free,) = get_classes('shipping.methods', ('Free',))
                self.assertEqual('shipping.methods', Free.__module__)

    def test_overriding_view_is_possible_without_overriding_app(self):
        from oscar.apps.customer.app import application, CustomerApplication
        # If test fails, it's helpful to know if it's caused by order of
        # execution
        self.assertEqual(CustomerApplication().summary_view.__module__,
                         'tests._site.apps.customer.views')
        self.assertEqual(application.summary_view.__module__,
                         'tests._site.apps.customer.views')


class ClassLoadingWithLocalOverrideWithMultipleSegmentsTests(TestCase):

    def setUp(self):
        self.installed_apps = list(settings.INSTALLED_APPS)
        self.installed_apps[self.installed_apps.index('oscar.apps.shipping')] = 'tests._site.apps.shipping'

    def test_loading_class_defined_in_local_module(self):
        with override_settings(INSTALLED_APPS=self.installed_apps):
            (Free,) = get_classes('shipping.methods', ('Free',))
            self.assertEqual('tests._site.apps.shipping.methods', Free.__module__)


class TestGetCoreAppsFunction(TestCase):
    """
    oscar.get_core_apps function
    """

    def test_returns_core_apps_when_no_overrides_specified(self):
        apps = oscar.get_core_apps()
        self.assertEqual(oscar.OSCAR_CORE_APPS, apps)

    def test_uses_non_dashboard_override_when_specified(self):
        apps = oscar.get_core_apps(overrides=['apps.shipping'])
        self.assertTrue('apps.shipping' in apps)
        self.assertTrue('oscar.apps.shipping' not in apps)

    def test_uses_dashboard_override_when_specified(self):
        apps = oscar.get_core_apps(overrides=['apps.dashboard.catalogue'])
        self.assertTrue('apps.dashboard.catalogue' in apps)
        self.assertTrue('oscar.apps.dashboard.catalogue' not in apps)
        self.assertTrue('oscar.apps.catalogue' in apps)


class TestOverridingCoreApps(TestCase):

    def test_means_the_overriding_model_is_registered_first(self):
        klass = get_model('partner', 'StockRecord')
        self.assertEqual(
            'tests._site.apps.partner.models', klass.__module__)


class TestAppLabelsForModels(TestCase):

    def test_all_oscar_models_have_app_labels(self):
        from django.apps import apps
        models = apps.get_models()
        missing = []
        for model in models:
            # Ignore non-Oscar models
            if 'oscar' not in repr(model):
                continue
            # Don't know how to get the actual model's Meta class. But if
            # the parent doesn't have a Meta class, it's doesn't have an
            # base in Oscar anyway and is not intended to be overridden
            abstract_model = model.__base__
            meta_class = getattr(abstract_model, 'Meta', None)
            if meta_class is None:
                continue

            if not hasattr(meta_class, 'app_label'):
                missing.append(model)
        if missing:
            self.fail("Those models don't have an app_label set: %s" % missing)


class TestDynamicLoadingOn3rdPartyApps(TestCase):

    core_app_prefix = 'thirdparty_package.apps'

    def setUp(self):
        self.installed_apps = list(settings.INSTALLED_APPS)
        sys.path.append('./tests/_site/')

    def tearDown(self):
        sys.path.remove('./tests/_site/')

    def test_load_core_3rd_party_class_correctly(self):
        self.installed_apps.append('thirdparty_package.apps.myapp')
        with override_settings(INSTALLED_APPS=self.installed_apps):
            Cow, Goat = get_classes('myapp.models', ('Cow', 'Goat'), self.core_app_prefix)
            self.assertEqual('thirdparty_package.apps.myapp.models', Cow.__module__)
            self.assertEqual('thirdparty_package.apps.myapp.models', Goat.__module__)

    def test_load_overriden_3rd_party_class_correctly(self):
        self.installed_apps.append('apps.myapp')
        with override_settings(INSTALLED_APPS=self.installed_apps):
            Cow, Goat = get_classes('myapp.models', ('Cow', 'Goat'), self.core_app_prefix)
            self.assertEqual('thirdparty_package.apps.myapp.models', Cow.__module__)
            self.assertEqual('apps.myapp.models', Goat.__module__)


class TestMovedClasses(TestCase):
    def setUp(self):
        user = UserFactory()
        product = create_product()
        self.wishlist = WishListFactory(owner=user)
        self.wishlist.add(product)

    def test_load_formset_old_destination(self):
        BaseBasketLineFormSet = get_class('basket.forms', 'BaseBasketLineFormSet')
        self.assertEqual('oscar.apps.basket.formsets', BaseBasketLineFormSet.__module__)
        StockRecordFormSet = get_class('dashboard.catalogue.forms', 'StockRecordFormSet')
        self.assertEqual('oscar.apps.dashboard.catalogue.formsets', StockRecordFormSet.__module__)
        OrderedProductFormSet = get_class('dashboard.promotions.forms', 'OrderedProductFormSet')
        OrderedProductForm = get_class('dashboard.promotions.forms', 'OrderedProductForm')
        # Since OrderedProductFormSet created with metaclass, it has __module__
        # attribute pointing to the Django module. Thus, we test if formset was
        # loaded correctly by initiating class instance and checking its forms.
        self.assertTrue(isinstance(OrderedProductFormSet().forms[0], OrderedProductForm))
        LineFormset = get_class('wishlists.forms', 'LineFormset')
        WishListLineForm = get_class('wishlists.forms', 'WishListLineForm')
        self.assertTrue(isinstance(LineFormset(instance=self.wishlist).forms[0], WishListLineForm))

    def test_load_formset_new_destination(self):
        BaseBasketLineFormSet = get_class('basket.formsets', 'BaseBasketLineFormSet')
        self.assertEqual('oscar.apps.basket.formsets', BaseBasketLineFormSet.__module__)
        StockRecordFormSet = get_class('dashboard.catalogue.formsets', 'StockRecordFormSet')
        self.assertEqual('oscar.apps.dashboard.catalogue.formsets', StockRecordFormSet.__module__)
        OrderedProductFormSet = get_class('dashboard.promotions.formsets', 'OrderedProductFormSet')
        OrderedProductForm = get_class('dashboard.promotions.forms', 'OrderedProductForm')
        self.assertTrue(isinstance(OrderedProductFormSet().forms[0], OrderedProductForm))
        LineFormset = get_class('wishlists.formsets', 'LineFormset')
        WishListLineForm = get_class('wishlists.forms', 'WishListLineForm')
        self.assertTrue(isinstance(LineFormset(instance=self.wishlist).forms[0], WishListLineForm))

    def test_load_formsets_mixed_destination(self):
        BaseBasketLineFormSet, BasketLineForm = get_classes('basket.forms', ('BaseBasketLineFormSet', 'BasketLineForm'))
        self.assertEqual('oscar.apps.basket.formsets', BaseBasketLineFormSet.__module__)
        self.assertEqual('oscar.apps.basket.forms', BasketLineForm.__module__)
        StockRecordForm, StockRecordFormSet = get_classes(
            'dashboard.catalogue.forms', ('StockRecordForm', 'StockRecordFormSet')
        )
        self.assertEqual('oscar.apps.dashboard.catalogue.forms', StockRecordForm.__module__)
        OrderedProductForm, OrderedProductFormSet = get_classes(
            'dashboard.promotions.forms', ('OrderedProductForm', 'OrderedProductFormSet')
        )
        self.assertEqual('oscar.apps.dashboard.promotions.forms', OrderedProductForm.__module__)
        self.assertTrue(isinstance(OrderedProductFormSet().forms[0], OrderedProductForm))
        LineFormset, WishListLineForm = get_classes('wishlists.forms', ('LineFormset', 'WishListLineForm'))
        self.assertEqual('oscar.apps.wishlists.forms', WishListLineForm.__module__)
        self.assertTrue(isinstance(LineFormset(instance=self.wishlist).forms[0], WishListLineForm))
