import uuid

from django.core.exceptions import ValidationError
from django.shortcuts import reverse
from django.test import TestCase
from console_base.exceptions import UniqueConstraintValidationError, EXISTING_RECORD_CID_KEY
from rest_framework import exceptions, status
from rest_framework.settings import api_settings
from .base import ConsoleBaseAPITestCase

from sandbox.api.categories.serializers import (
    CategorySerializer,
    PatternSerializer,
    RatingSerializer,
)
from sandbox.categories.choices import PATTERNTYPES, RatingTone
from sandbox.categories.models import Category, Pattern, Rating


class TestUniqueConstraintValidator(TestCase):
    """
    The UniqueConstraintValidationError is to facilitate
    reconciling Sync Publisher's Canonical IDs on Sync Subscribers.
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.silt = Rating.objects.create(code='slt', name='Silt', tone=RatingTone.silt)
        cls.parent = Category.objects.create(
            name='The Parent',
            code='the_parent',
            action='block',
            rating=cls.silt,
        )
        cls.child = Category.objects.create(
            name='The Child',
            code='the_child',
            action='ignore',
            parent=cls.parent,
            rating=cls.silt,
        )
        cls.pattern = Pattern.objects.create(
            name='cabelas.com',
            patterntype=PATTERNTYPES.Domain,
            score=500,
            category=cls.child,
        )

    def test_exception_has_existing_record_cid_attribute(self):
        """
        Ensure that the `existing_record_cid` property is set
        on the exception returned from full_clean.
        """
        try:
            r = Rating(code='slt', name='Silt', tone=RatingTone.silt)
            r.full_clean()
        except ValidationError as e:
            self.assertIsInstance(e, UniqueConstraintValidationError)
            self.assertEqual(getattr(e, EXISTING_RECORD_CID_KEY), self.silt.cid)

        try:
            r = Rating(code='slt', name='Silt', tone=RatingTone.misc, cid=self.silt.cid)
            r.full_clean()
        except ValidationError as e:
            self.assertIsInstance(e, UniqueConstraintValidationError)
            self.assertEqual(getattr(e, EXISTING_RECORD_CID_KEY), self.silt.cid)

    def test_duplicate_record_fails_single_column_no_conditions(self):
        """
        Ensure that creating duplicate records where constraint applies to
        single field, with no conditions includes the existing record cid.
        """
        r = Rating(code='slt', name='Silt', tone=RatingTone.silt)
        with self.assertRaises(ValidationError):
            r.full_clean()

        try:
            error = None
            r.full_clean()
        except Exception as e:
            error = e

        self.assertIsNotNone(error)
        self.assertIsInstance(error, UniqueConstraintValidationError)
        self.assertEqual(getattr(error, EXISTING_RECORD_CID_KEY), self.silt.cid)

    def test_duplicate_record_fails_on_parent_category(self):
        """
        Ensure that creating duplicate records where constraint applies to
        single fields with a conditional on the constraint.
        """
        parent = Category(code=self.parent.code, name='Another Parent', action='allow')
        with self.assertRaises(ValidationError):
            parent.full_clean()

        try:
            error = None
            parent.full_clean()
        except Exception as e:
            error = e

        self.assertIsNotNone(error)
        self.assertIsInstance(error, UniqueConstraintValidationError)
        self.assertEqual(getattr(error, EXISTING_RECORD_CID_KEY), self.parent.cid)

    def test_duplicate_record_fails_on_child_category(self):
        """
        Ensure that creating duplicate records where constraint applies to
        multiple fields with a conditional on the constraint.
        """
        child = Category(
            code=self.child.code,
            name='Another Child',
            action='allow',
            parent=self.parent,
        )
        with self.assertRaises(ValidationError):
            child.full_clean()

        try:
            error = None
            child.full_clean()
        except Exception as e:
            error = e

        self.assertIsNotNone(error)
        self.assertIsInstance(error, UniqueConstraintValidationError)
        self.assertEqual(getattr(error, EXISTING_RECORD_CID_KEY), self.child.cid)

    def test_duplicate_record_fails_on_pattern(self):
        """
        Ensure that creating duplicate records where constraint applies to
        multiple fields but no conditional present on the constraint.
        """
        pat = Pattern(
            name=self.pattern.name,
            patterntype=self.pattern.patterntype,
            category=self.pattern.category,
        )
        with self.assertRaises(ValidationError):
            pat.full_clean()

        try:
            error = None
            pat.full_clean()
        except Exception as e:
            error = e

        self.assertIsNotNone(error)
        self.assertIsInstance(error, UniqueConstraintValidationError)
        self.assertEqual(getattr(error, EXISTING_RECORD_CID_KEY), self.pattern.cid)


class TestUniqueConstraintValidatorRestApi(TestCase):
    """
    Django Rest Framework doesn't call `Model.full_clean` when
    performing validation, the `existing_record_cid` must be
    assigned via DRF machinery.

    The Compass fork of django-rest-framework must be installed
    for these tests to pass.

    https://code.compassfoundation.io/general/django-rest-framework/
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.silt = Rating.objects.create(code='slt', name='Silt', tone=RatingTone.silt)
        cls.parent = Category.objects.create(
            name='The Parent',
            code='the_parent',
            action='block',
            rating=cls.silt,
        )
        cls.child = Category.objects.create(
            name='The Child',
            code='the_child',
            action='ignore',
            parent=cls.parent,
            rating=cls.silt,
        )
        cls.pattern = Pattern.objects.create(
            name='cabelas.com',
            patterntype=PATTERNTYPES.Domain,
            score=500,
            category=cls.child,
        )

    def test_duplicate_record_fails_single_column_no_conditions(self):
        """
        Ensure that creating duplicate records where constraint applies to
        single field, with no conditions includes the existing record cid.
        """
        rating = RatingSerializer(data=dict(code='slt', name='Silt', tone=RatingTone.silt))
        with self.assertRaises(exceptions.ValidationError):
            rating.is_valid(raise_exception=True)

        for column, errors in rating.errors.items():
            error_detail = errors[0]
            self.assertTrue(
                hasattr(error_detail, 'existing_record_cid'),
                msg='Ensure Compass fork of rest_framework is installed',
            )
            self.assertEqual(error_detail.existing_record_cid, self.silt.cid)

    def test_duplicate_record_fails_on_child_category(self):
        """
        Ensure that creating duplicate records where constraint applies to
        multiple fields with a conditional on the constraint.
        """
        category = CategorySerializer(
            data=dict(
                code=self.child.code,
                name='Another Child',
                action='allow',
                parent=self.parent.cid,
            )
        )
        with self.assertRaises(exceptions.ValidationError):
            category.is_valid(raise_exception=True)

        for column, errors in category.errors.items():
            error_detail = errors[0]
            self.assertTrue(
                hasattr(error_detail, 'existing_record_cid'),
                msg='Ensure Compass fork of rest_framework is installed',
            )
            self.assertEqual(error_detail.existing_record_cid, self.child.cid)

    def test_duplicate_record_fails_on_pattern(self):
        """
        Ensure that creating duplicate records where constraint applies to
        multiple fields but no conditional present on the constraint.
        """
        pattern = PatternSerializer(
            data=dict(
                name=self.pattern.name,
                patterntype=self.pattern.patterntype,
                category=self.pattern.category.cid,
            )
        )
        with self.assertRaises(exceptions.ValidationError):
            pattern.is_valid(raise_exception=True)

        for column, errors in pattern.errors.items():
            error_detail = errors[0]
            self.assertTrue(
                hasattr(error_detail, 'existing_record_cid'),
                msg='Ensure Compass fork of rest_framework is installed',
            )
            self.assertEqual(error_detail.existing_record_cid, self.pattern.cid)


class TestUniqueErrorViewsets(ConsoleBaseAPITestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.silt = Rating.objects.create(code='slt', name='Silt', tone=RatingTone.silt)
        cls.parent = Category.objects.create(
            name='The Parent',
            code='the_parent',
            action='block',
            rating=cls.silt,
        )
        cls.child = Category.objects.create(
            name='The Child',
            code='the_child',
            action='ignore',
            parent=cls.parent,
            rating=cls.silt,
        )

    def test_create_duplicate_rating_via_api(self):
        """
        All unique field violations should include the `existing_record_cid` value.
        """
        response = self.client.get(reverse('api-rating-list'))
        self.assertEqual(response.status_code, status.HTTP_200_OK, msg=response.data)

        data = {
            'cid': uuid.uuid1(),
            'is_active': True,
            'name': 'Silt',
            'code': 'slt',
            'tone': 2,
        }
        response = self.client.post(reverse('api-rating-list'), data=data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, msg=response.data)

        for field in ('code', 'name'):
            errors = response.data[field]
            error_detail = errors[0]

            self.assertIsInstance(error_detail, exceptions.UniqueErrorDetail)
            self.assertEqual(
                error_detail.existing_record_cid,
                self.silt.cid,
                msg='Error must include the Canonical ID of the existing unique record',
            )

    def test_create_duplicate_category_via_api(self):
        response = self.client.get(reverse('api-category-list'))
        self.assertEqual(response.status_code, status.HTTP_200_OK, msg=response.data)

        data = {
            'cid': uuid.uuid1(),
            'is_active': True,
            'name': 'The Child',
            'code': 'the_child',
            'action': 'ignore',
            'rating': self.silt.cid,
            'parent': self.parent.cid,
        }
        response = self.client.post(reverse('api-category-list'), data=data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, msg=response.data)
        errors = response.data[api_settings.NON_FIELD_ERRORS_KEY]
        error_detail = errors[0]

        self.assertIsInstance(error_detail, exceptions.UniqueErrorDetail)
        self.assertEqual(
            error_detail.existing_record_cid,
            self.child.cid,
            msg='Error must include the Canonical ID of the existing unique record',
        )
