import os.path

import pytest
from pympler.asizeof import asizeof

import featuretools as ft
from featuretools import config
from featuretools.feature_base import IdentityFeature
from featuretools.primitives import (
    Diff,
    Last,
    Mode,
    NMostCommon,
    NumUnique,
    Sum,
    TransformPrimitive
)
from featuretools.variable_types import Categorical, Datetime, Id, Numeric


def test_copy_features_does_not_copy_entityset(es):
    agg = ft.Feature(es['log']['value'], parent_entity=es['sessions'], primitive=Sum)
    agg_where = ft.Feature(es['log']['value'], parent_entity=es['sessions'],
                           where=IdentityFeature(es['log']['value']) == 2, primitive=Sum)
    agg_use_previous = ft.Feature(es['log']['value'], parent_entity=es['sessions'],
                                  use_previous='4 days', primitive=Sum)
    agg_use_previous_where = ft.Feature(es['log']['value'], parent_entity=es['sessions'],
                                        where=IdentityFeature(es['log']['value']) == 2,
                                        use_previous='4 days', primitive=Sum)
    features = [agg, agg_where, agg_use_previous, agg_use_previous_where]
    in_memory_size = asizeof(locals())
    copied = [f.copy() for f in features]
    new_in_memory_size = asizeof(locals())
    assert new_in_memory_size < 2 * in_memory_size


def test_get_dependencies(es):
    f = ft.Feature(es['log']['value'])
    agg1 = ft.Feature(f, parent_entity=es['sessions'], primitive=Sum)
    agg2 = ft.Feature(agg1, parent_entity=es['customers'], primitive=Sum)
    d1 = ft.Feature(agg2, es['sessions'])
    shallow = d1.get_dependencies(deep=False, ignored=None)
    deep = d1.get_dependencies(deep=True, ignored=None)
    ignored = set([agg1.unique_name()])
    deep_ignored = d1.get_dependencies(deep=True, ignored=ignored)
    assert [s.unique_name() for s in shallow] == [agg2.unique_name()]
    assert [d.unique_name() for d in deep] == [agg2.unique_name(), agg1.unique_name(), f.unique_name()]
    assert [d.unique_name() for d in deep_ignored] == [agg2.unique_name()]


def test_get_depth(es):
    f = ft.Feature(es['log']['value'])
    g = ft.Feature(es['log']['value'])
    agg1 = ft.Feature(f, parent_entity=es['sessions'], primitive=Last)
    agg2 = ft.Feature(agg1, parent_entity=es['customers'], primitive=Last)
    d1 = ft.Feature(agg2, es['sessions'])
    d2 = ft.Feature(d1, es['log'])
    assert d2.get_depth() == 4
    # Make sure this works if we pass in two of the same
    # feature. This came up when user supplied duplicates
    # in the seed_features of DFS.
    assert d2.get_depth(stop_at=[f, g]) == 4
    assert d2.get_depth(stop_at=[f, g, agg1]) == 3
    assert d2.get_depth(stop_at=[f, g, agg1]) == 3
    assert d2.get_depth(stop_at=[f, g, agg2]) == 2
    assert d2.get_depth(stop_at=[f, g, d1]) == 1
    assert d2.get_depth(stop_at=[f, g, d2]) == 0


def test_squared(es):
    feature = ft.Feature(es['log']['value'])
    squared = feature * feature
    assert len(squared.base_features) == 2
    assert squared.base_features[0].unique_name() == squared.base_features[1].unique_name()


def test_return_type_inference(es):
    mode = ft.Feature(es["log"]["priority_level"], parent_entity=es["customers"], primitive=Mode)
    assert mode.variable_type == es["log"]["priority_level"].__class__


def test_return_type_inference_direct_feature(es):
    mode = ft.Feature(es["log"]["priority_level"], parent_entity=es["customers"], primitive=Mode)
    mode_session = ft.Feature(mode, es["sessions"])
    assert mode_session.variable_type == es["log"]["priority_level"].__class__


def test_return_type_inference_index(es):
    last = ft.Feature(es["log"]["id"], parent_entity=es["customers"], primitive=Last)
    assert last.variable_type == Categorical


def test_return_type_inference_datetime_time_index(es):
    last = ft.Feature(es["log"]["datetime"], parent_entity=es["customers"], primitive=Last)
    assert last.variable_type == Datetime


def test_return_type_inference_numeric_time_index(int_es):
    last = ft.Feature(int_es["log"]["datetime"], parent_entity=int_es["customers"], primitive=Last)
    assert last.variable_type == Numeric


def test_return_type_inference_id(es):
    # direct features should keep Id variable type
    direct_id_feature = ft.Feature(es["sessions"]["customer_id"], es["log"])
    assert direct_id_feature.variable_type == Id

    # aggregations of Id variable types should get converted
    mode = ft.Feature(es["log"]["session_id"], parent_entity=es["customers"], primitive=Mode)
    assert mode.variable_type == Categorical

    # also test direct feature of aggregation
    mode_direct = ft.Feature(mode, es["sessions"])
    assert mode_direct.variable_type == Categorical


def test_set_data_path(es):
    key = "primitive_data_folder"

    # Don't change orig_path
    orig_path = config.get(key)
    new_path = "/example/new/directory"
    filename = "test.csv"

    # Test that default path works
    sum_prim = Sum()
    assert sum_prim.get_filepath(filename) == os.path.join(orig_path, filename)

    # Test that new path works
    config.set({key: new_path})
    assert sum_prim.get_filepath(filename) == os.path.join(new_path, filename)

    # Test that new path with trailing / works
    new_path += "/"
    config.set({key: new_path})
    assert sum_prim.get_filepath(filename) == os.path.join(new_path, filename)

    # Test that the path is correct on newly defined feature
    sum_prim2 = Sum()
    assert sum_prim2.get_filepath(filename) == os.path.join(new_path, filename)

    # Ensure path was reset
    config.set({key: orig_path})
    assert config.get(key) == orig_path


def test_to_dictionary(es):
    direct_feature = ft.Feature(es["sessions"]["customer_id"], es["log"])
    expected = {
        'type': 'DirectFeature',
        'dependencies': [feat.unique_name() for feat in direct_feature.get_dependencies()],
        'arguments': direct_feature.get_arguments()
    }
    assert expected == direct_feature.to_dictionary()


def test_multi_output_base_error_agg(es):
    three_common = NMostCommon(3)
    tc = ft.Feature(es['log']['product_id'], parent_entity=es["sessions"], primitive=three_common)
    error_text = "Cannot stack on whole multi-output feature."
    with pytest.raises(ValueError, match=error_text):
        ft.Feature(tc, parent_entity=es['customers'], primitive=NumUnique)


def test_multi_output_base_error_trans(es):
    class TestTime(TransformPrimitive):
        name = "test_time"
        input_types = [Datetime]
        return_type = Numeric
        number_output_features = 6

    tc = ft.Feature(es['customers']['date_of_birth'], primitive=TestTime)

    error_text = "Cannot stack on whole multi-output feature."
    with pytest.raises(ValueError, match=error_text):
        ft.Feature(tc, primitive=Diff)


def test_multi_output_attributes(es):
    tc = ft.Feature(es['log']['product_id'], parent_entity=es["sessions"], primitive=NMostCommon)

    assert tc.generate_name() == 'N_MOST_COMMON(log.product_id)'
    assert tc.number_output_features == 3
    assert tc.base_features == ['<Feature: product_id>']

    assert tc[0].generate_name() == 'N_MOST_COMMON(log.product_id)[0]'
    assert tc[0].number_output_features == 1
    assert tc[0].base_features == [tc]
    assert tc.relationship_path == tc[0].relationship_path


def test_multi_output_index_error(es):
    error_text = "can only access slice of multi-output feature"
    three_common = ft.Feature(es['log']['product_id'],
                              parent_entity=es["sessions"],
                              primitive=NMostCommon)

    with pytest.raises(AssertionError, match=error_text):
        single = ft.Feature(es['log']['product_id'],
                            parent_entity=es["sessions"],
                            primitive=NumUnique)
        single[0]

    error_text = "Cannot get item from slice of multi output feature"
    with pytest.raises(ValueError, match=error_text):
        three_common[0][0]

    error_text = 'index is higher than the number of outputs'
    with pytest.raises(AssertionError, match=error_text):
        three_common[10]
