"""
Tests for search API functions.
"""
import itertools
from unittest.mock import patch

from ddt import (
    data,
    ddt,
    unpack,
)
from django.conf import settings
from django.db.models.signals import post_save
from elasticsearch.exceptions import NotFoundError
from factory.django import mute_signals

from dashboard.factories import (
    CachedCertificateFactory,
    CachedEnrollmentFactory,
    CachedCurrentGradeFactory,
    ProgramEnrollmentFactory
)
from dashboard.models import ProgramEnrollment
from dashboard.serializers import UserProgramSearchSerializer
from courses.factories import (
    ProgramFactory,
    CourseFactory,
    CourseRunFactory,
)
from profiles.factories import (
    EducationFactory,
    EmploymentFactory,
    ProfileFactory,
)
from profiles.ser
ializers import (
    ProfileSerializer
)
from roles.models import Role
from roles.roles import (
    Instructor,
    Staff
)
from search.base import ESTestCase, reindex_test_es_data
from search.c
onnection import (
    ALL_INDEX_TYPES,
    get_aliases,
    get_default_alias,
    make_alias_name,
    make_backing_index_name,
    GLOBAL_DOC_TYPE,
    PERCOLATE_INDEX_TYPE,
    PUBLIC_ENROLLMENT_INDEX_TYPE,
    PRIVATE_ENROLLMENT_INDEX_TYPE,
)
from search.exceptions import ReindexException
from search.factories import PercolateQueryFactory
from search.indexing_api import (
    clear_and_create_index,
    delete_indices,
    get_conn,
    refresh_index,
    index_program_enrolled_users,
    remove_program_enrolled_user,
    serialize_program_enrolled_user,
    serialize_public_enrolled_user,
    filter_current_work,
    index_percolate_queries,
    delete_percolate_query, create_backing_indices,
)

from search.models import PercolateQuery
from search.util import traverse_mapping

DOC_TYPES_PER_ENROLLMENT = 1


# pylint: disable=too-many-lines
class ESTestActions:
    """
    Provides helper functions for tests to communicate with ES
    """
    def __init__(self):
        self.conn = get_conn(verify=False)

    def search(self, index_type):
        """Gets full index data from the _search endpoint"""
        alias = get_default_alias(index_type)
        refresh_index(alias)
        return self.conn.search(index=alias)['hits']

    def get_percolate_query(self, _id):
        """Get percolate query"""
        index = get_default_alias(PERCOLATE_INDEX_TYPE)
        return self.conn.get(id=_id, doc_type=GLOBAL_DOC_TYPE, index=index)

    def get_mappings(self, index_type):
        """Gets mapping data"""
        alias = get_default_alias(index_type)
        refresh_index(alias)
        mapping = self.conn.indices.get_mapping(index=alias)
        return list(mapping.values())[0]['mappings']

    def get_default_backing_index(self, index_type):
        """Get the default backing index"""
        alias = get_default_alias(index_type)
        return list(self.conn.indices.get_alias(name=alias).keys())[0]


es = ESTestActions()


def get_sources(results):
    """
    Get sources from hits, sorted by source id

    Args:
        results (dict): Elasticsearch results

    Returns:
        list of dict: The list of source dicts
    """
    sorted_hits = sorted(results['hits'], key=lambda hit: hit['_source']['id'])
    return [hit['_source'] for hit in sorted_hits]


def remove_es_keys(hit):
    """
    Removes ES keys from a hit object in-place

    Args:
        hit (dict): Elasticsearch hit object

    Returns:
        dict: modified Elasticsearch hit object
    """
    del hit['_id']
    if '_type' in hit:
        del hit['_type']
    return hit


def assert_search(results, program_enrollments, *, index_type):
    """
    Assert that search results match program-enrolled users
    """
    assert results['total'] == len(program_enrollments) * DOC_TYPES_PER_ENROLLMENT
    sources_advanced = get_sources(results)
    sorted_program_enrollments = sorted(program_enrollments, key=lambda program_enrollment: program_enrollment.id)

    if index_type == PRIVATE_ENROLLMENT_INDEX_TYPE:
        serialized = [
            remove_es_keys(serialize_program_enrolled_user(program_enrollment))
            for program_enrollment in sorted_program_enrollments
        ]
    elif index_type == PUBLIC_ENROLLMENT_INDEX_TYPE:
        serialized = [
            remove_es_keys(serialize_public_enrolled_user(
                serialize_program_enrolled_user(program_enrollment)
            ))
            for program_enrollment in sorted_program_enrollments
        ]
    else:
        raise Exception("Unexpected index type")

    assert serialized == sources_advanced


# pylint: disable=unused-argument
@ddt
@patch('search.signals.transaction.on_commit', side_effect=lambda callback: callback())
class IndexTests(ESTestCase):
    """
    Tests for indexing
    """
    # pylint: disable=too-many-public-methods
    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_program_enrollment_add(self, index_type, mock_on_commit):
        """
        Test that a newly created ProgramEnrollment is indexed properly
        """
        assert es.search(index_type)['total'] == 0
        program_enrollment = ProgramEnrollmentFactory.create()
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_program_enrollment_delete(self, index_type, mock_on_commit):
        """
        Test that ProgramEnrollment is removed from index after the user is removed
        """
        program_enrollment = ProgramEnrollmentFactory.create()
        assert es.search(index_type)['total'] == DOC_TYPES_PER_ENROLLMENT
        program_enrollment.user.delete()
        assert es.search(index_type)['total'] == 0

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_profile_update(self, index_type, mock_on_commit):
        """
        Test that ProgramEnrollment is reindexed after the User's Profile has been updated
        """
        program_enrollment = ProgramEnrollmentFactory.create()
        assert es.search(index_type)['total'] == DOC_TYPES_PER_ENROLLMENT
        profile = program_enrollment.user.profile
        profile.first_name = 'updated'
        profile.save()
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_education_add(self, index_type, mock_on_commit):
        """
        Test that Education is indexed after being added
        """
        program_enrollment = ProgramEnrollmentFactory.create()
        assert es.search(index_type)['total'] == DOC_TYPES_PER_ENROLLMENT
        EducationFactory.create(profile=program_enrollment.user.profile)
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_education_update(self, index_type, mock_on_commit):
        """
        Test that Education is reindexed after being updated
        """
        program_enrollment = ProgramEnrollmentFactory.create()
        assert es.search(index_type)['total'] == DOC_TYPES_PER_ENROLLMENT
        education = EducationFactory.create(profile=program_enrollment.user.profile)
        education.school_city = 'city'
        education.save()
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_education_delete(self, index_type, mock_on_commit):
        """
        Test that Education is removed from index after being deleted
        """
        program_enrollment = ProgramEnrollmentFactory.create()
        education = EducationFactory.create(profile=program_enrollment.user.profile)
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)
        education.delete()
        assert_search(es.search(index_type), [program_enrollment], index_type=index_type)

    @data(PRIVATE_ENROLLMENT_INDEX_TYPE, PUBLIC_ENROLLMENT_INDEX_TYPE)
    def test_employment_add(self, index_type, mock_on_commit):
       