from functools import partial
from operator import attrgetter
from typing import Any, Dict, Optional, Union
from unittest import TestCase

import odin
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase as DjangoTestCase
from odin.codecs import dict_codec
from odin.mapping import FieldMapping

from oscar.core.loading import get_class, get_classes, get_model

from oscar_odin.inheritable import AnnotatedResource, Resource
from oscar_odin.mappings.common import OscarBaseMapping
from oscar_odin.utils import get_mapped_fields

from ocyan.plugin.oscar_odin.models import FieldMappingModel, MappingModel

Product = get_model("catalogue", "Product")
ProductResource = get_class("oscar_odin.resources.catalogue", "ProductResource")
ProductToModel, ProductToResource = get_classes(
    "oscar_odin.mappings.catalogue", ["ProductToModel", "ProductToResource"]
)


class Klazien(AnnotatedResource):
    title: str


class Boerderij(AnnotatedResource):
    street: str
    inhabitants: list[Klazien]


class Book(AnnotatedResource):
    title: str
    chapters: list[str]
    attributes: Dict[str, Union[Any, None]]


class BoerderijBook(odin.Mapping):
    register_mapping = False
    from_obj = Boerderij
    to_obj = Book
    mappings = [
        ("street", None, "title"),
        ("inhabitants", partial(map, attrgetter("title")), "chapters"),
    ]


class Gert(AnnotatedResource):
    title: str
    amount: Optional[int]
    book = odin.DictOf(Book)


class Hendrik(Resource):
    vlaai = odin.StringField()
    koeien = odin.IntegerField(min_value=1)


class GertToHendrik(OscarBaseMapping):
    register_mapping = False
    from_obj = Gert
    to_obj = Hendrik

    @odin.map_field(to_field="vlaai")
    def title(self, value):
        return "Ik ben nu hendrik maar eerst was ik %s" % value


class GertToKlaas(OscarBaseMapping):
    register_mapping = False
    from_obj = Gert
    to_obj = Hendrik

    @odin.map_field(to_field="vlaai")
    def title(self, value):
        return "Ik ben nu klaas maar eerst was ik %s" % value


def add_rules_to_mapping(
    mapping,
    from_field,
    to_field,
    action=None,
    to_list=False,
    bind=False,
    skip_if_none=True,
):
    # pylint: disable=protected-access
    mapping._mapping_rules.append(
        FieldMapping([from_field], action, [to_field], to_list, bind, skip_if_none)
    )


class OdinTest(TestCase):
    """
    It should be possible to modify existing mappings
    """

    def test_resource_mapping_amount_is_unmapped(self):
        "The koeien attribute should initially be unmapped"
        gert = Gert(title="gert", amount=1)
        hendrik = GertToHendrik.apply(gert)
        self.assertEqual(hendrik.vlaai, "Ik ben nu hendrik maar eerst was ik gert")
        self.assertIsNone(hendrik.koeien, "Since koeien is unmapped, it should be None")

    def test_resouce_mapping_dynamic(self):
        "After modifying the mapping with add_rules_to_mapping, koeien should nolonger be unmapped"
        gert = Gert(title="gert", amount=1)

        # because add_rules_to_mapping changes the actual class, we have need two different
        # mappings, because the tests could run in any order
        add_rules_to_mapping(GertToKlaas, "amount", "koeien")
        klaas = GertToKlaas.apply(gert)

        self.assertEqual(klaas.vlaai, "Ik ben nu klaas maar eerst was ik gert")
        self.assertEqual(
            klaas.koeien,
            1,
            "since koeien is now mapped, it should have the same value as amount",
        )

    def test_nesting(self):
        farm = dict_codec.load(
            {
                "street": "henkie",
                "inhabitants": [{"title": "klapoe"}, {"title": "ramoien"}],
            },
            Boerderij,
        )

        self.assertEqual(len(farm.inhabitants), 2)
        one, two = farm.inhabitants
        self.assertEqual(one.title, "klapoe")
        self.assertEqual(two.title, "ramoien")

        lol = BoerderijBook.apply(farm)
        self.assertListEqual(list(lol.chapters), ["klapoe", "ramoien"])

    def test_impacted_fields(self):
        product_to_model_fields = get_mapped_fields(ProductToModel)
        self.assertListEqual(
            sorted(product_to_model_fields),
            [
                "attributes",
                "categories",
                "children",
                "code",
                "date_created",
                "date_updated",
                "description",
                "id",
                "images",
                "is_discountable",
                "is_public",
                "long_description",
                "meta_description",
                "meta_title",
                "parent",
                "priority",
                "product_class",
                "rating",
                "recommended_products",
                "slug",
                "stockrecords",
                "structure",
                "title",
                "upc",
                "vat_rate",
            ],
        )

        model_to_product_fields = get_mapped_fields(ProductToResource)
        self.assertListEqual(
            sorted(model_to_product_fields),
            [
                "attributes",
                "availability",
                "categories",
                "children",
                "code",
                "currency",
                "date_created",
                "date_updated",
                "description",
                "id",
                "images",
                "is_available_to_buy",
                "is_discountable",
                "is_public",
                "long_description",
                "meta_description",
                "meta_title",
                "parent",
                "price",
                "priority",
                "product_class",
                "rating",
                "recommended_products",
                "slug",
                "stockrecords",
                "structure",
                "title",
                "upc",
                "vat_rate",
            ],
        )

        demfields = ProductToModel.get_fields_impacted_by_mapping(
            *model_to_product_fields
        )
        self.assertListEqual(
            sorted(demfields),
            [
                "Category.code",
                "Product.code",
                "Product.description",
                "Product.is_discountable",
                "Product.is_public",
                "Product.long_description",
                "Product.meta_description",
                "Product.meta_title",
                "Product.parent",
                "Product.priority",
                "Product.slug",
                "Product.structure",
                "Product.title",
                "Product.upc",
                "Product.vat_rate",
                "ProductClass.slug",
                "ProductImage.caption",
                "ProductImage.code",
                "ProductImage.display_order",
                "ProductImage.original",
                "StockRecord.num_allocated",
                "StockRecord.num_in_stock",
                "StockRecord.partner",
                "StockRecord.partner_sku",
                "StockRecord.price",
                "StockRecord.price_currency",
            ],
        )


class DynamicModelMappingTest(DjangoTestCase):
    """
    It should be possible to change mappings with models from django
    """

    def test_dynamic_mapping(self):
        ct = ContentType.objects.get_for_model(MappingModel)
        mm = MappingModel(contenttype=ct)
        mm.full_clean()
        mm.save()

        book = Book(title="I am a book about odin jo")
        gert = Gert(title="gert", amount=1, book=book)

        # pylint: disable=no-member
        mm.mappings.add(
            FieldMappingModel.simple_mapping("amount", "koeien"), bulk=False
        )
        mm.mappings.add(
            FieldMappingModel.simple_mapping("book.title", "vlaai"), bulk=False
        )
        mapping = mm.mapping_class(GertToHendrik)
        self.assertEqual(
            mapping.__name__,
            "MappingmodelDynamicGertToHendrik",
            "A new class should have been generated",
        )
        klaas = mapping.apply(gert)

        self.assertEqual(klaas.vlaai, "I am a book about odin jo")
        self.assertEqual(klaas.koeien, 1)

        hendrik = GertToHendrik.apply(gert)
        self.assertEqual(hendrik.vlaai, "Ik ben nu hendrik maar eerst was ik gert")
        self.assertIsNone(
            hendrik.koeien, "The original mapping should be left untouched"
        )
