from datetime import datetime

from pytest import raises

from neomodel import (
    INCOMING, DateTimeProperty, IntegerProperty, RelationshipFrom, RelationshipTo,
    StringProperty, StructuredNode, StructuredRel, Q
)
from neomodel.match import NodeSet, QueryBuilder, Traversal
from neomodel.exceptions import MultipleNodesReturned


class SupplierRel(StructuredRel):
    since = DateTimeProperty(default=datetime.now)
    courier = StringProperty()


class Supplier(StructuredNode):
    name = StringProperty()
    delivery_cost = IntegerProperty()
    coffees = RelationshipTo('Coffee', 'COFFEE SUPPLIERS')  # Space to check for escaping


class Coffee(StructuredNode):
    name = StringProperty(unique_index=True)
    price = IntegerProperty()
    suppliers = RelationshipFrom(Supplier, 'COFFEE SUPPLIERS', model=SupplierRel)
    id_ = IntegerProperty()


def test_filter_exclude_via_labels():
    Coffee(name='Java', price=99).save()

    node_set = NodeSet(Coffee)
    qb = QueryBuilder(node_set).build_ast()

    results = qb._execute()

    assert '(coffee:Coffee)' in qb._ast['match']
    assert 'result_class' in qb._ast
    assert len(results) == 1
    assert isinstance(results[0], Coffee)
    assert results[0].name == 'Java'

    # with filter and exclude
    Coffee(name='Kenco', price=3).save()
    node_set = node_set.filter(price__gt=2).exclude(price__gt=6, name='Java')
    qb = QueryBuilder(node_set).build_ast()

    results = qb._execute()
    assert '(coffee:Coffee)' in qb._ast['match']
    assert 'NOT' in qb._ast['where'][0]
    assert len(results) == 1
    assert results[0].name == 'Kenco'


def test_simple_has_via_label():
    nescafe = Coffee(name='Nescafe', price=99).save()
    tesco = Supplier(name='Tesco', delivery_cost=2).save()
    nescafe.suppliers.connect(tesco)

    ns = NodeSet(Coffee).has(suppliers=True)
    qb = QueryBuilder(ns).build_ast()
    results = qb._execute()
    assert 'COFFEE SUPPLIERS' in qb._ast['where'][0]
    assert len(results) == 1
    assert results[0].name == 'Nescafe'

    Coffee(name='nespresso', price=99).save()
    ns = NodeSet(Coffee).has(suppliers=False)
    qb = QueryBuilder(ns).build_ast()
    results = qb._execute()
    assert len(results) > 0
    assert 'NOT' in qb._ast['where'][0]


def test_get():
    Coffee(name='1', price=3).save()
    assert Coffee.nodes.get(name='1')

    with raises(Coffee.DoesNotExist):
        Coffee.nodes.get(name='2')

    Coffee(name='2', price=3).save()

    with raises(MultipleNodesReturned):
        Coffee.nodes.get(price=3)


def test_simple_traverse_with_filter():
    nescafe = Coffee(name='Nescafe2', price=99).save()
    tesco = Supplier(name='Sainsburys', delivery_cost=2).save()
    nescafe.suppliers.connect(tesco)

    qb = QueryBuilder(NodeSet(source=nescafe).suppliers.match(since__lt=datetime.now()))

    results = qb.build_ast()._execute()

    assert 'lookup' in qb._ast
    assert 'match' in qb._ast
    assert qb._ast['return'] == 'suppliers'
    assert len(results) == 1
    assert results[0].name == 'Sainsburys'


def test_double_traverse():
    nescafe = Coffee(name='Nescafe plus', price=99).save()
    tesco = Supplier(name='Asda', delivery_cost=2).save()
    nescafe.suppliers.connect(tesco)
    tesco.coffees.connect(Coffee(name='Decafe', price=2).save())

    ns = NodeSet(NodeSet(source=nescafe).suppliers.match()).coffees.match()
    qb = QueryBuilder(ns).build_ast()

    results = qb._execute()
    assert len(results) == 1
    assert results[0].name == 'Decafe'


def test_count():
    Coffee(name='Nescafe Gold', price=99).save()
    count = QueryBuilder(NodeSet(source=Coffee)).build_ast()._count()
    assert count > 0


def test_len_and_iter_and_bool():
    iterations = 0

    Coffee(name="Icelands finest").save()

    for c in Coffee.nodes:
        iterations += 1
        c.delete()

    assert iterations > 0

    assert len(Coffee.nodes) == 0


def test_slice():
    for c in Coffee.nodes:
        c.delete()

    Coffee(name="Icelands finest").save()
    Coffee(name="Britains finest").save()
    Coffee(name="Japans finest").save()

    assert len(Coffee.nodes[1:]) == 2
    assert len(Coffee.nodes[:1]) == 1
    assert isinstance(Coffee.nodes[1], Coffee)
    assert isinstance(Coffee.nodes[0], Coffee)
    assert len(Coffee.nodes[1:2]) == 1


def test_issue_208():
    # calls to match persist across queries.

    b = Coffee(name="basics").save()
    l = Supplier(name="lidl").save()
    a = Supplier(name="aldi").save()

    b.suppliers.connect(l, {'courier': 'fedex'})
    b.suppliers.connect(a, {'courier': 'dhl'})

    assert len(b.suppliers.match(courier='fedex'))
    assert len(b.suppliers.match(courier='dhl'))


def test_contains():
    expensive = Coffee(price=1000, name="Pricey").save()
    asda = Coffee(name='Asda', price=1).save()

    assert expensive in Coffee.nodes.filter(price__gt=999)
    assert asda not in Coffee.nodes.filter(price__gt=999)

    # bad value raises
    with raises(ValueError):
        2 in Coffee.nodes

    # unsaved
    with raises(ValueError):
        Coffee() in Coffee.nodes


def test_order_by():
    for c in Coffee.nodes:
        c.delete()

    c1 = Coffee(name="Icelands finest", price=5).save()
    c2 = Coffee(name="Britains finest", price=10).save()
    c3 = Coffee(name="Japans finest", price=35).save()

    assert Coffee.nodes.order_by('price').all()[0].price == 5
    assert Coffee.nodes.order_by('-price').all()[0].price == 35

    ns = Coffee.nodes.order_by('-price')
    qb = QueryBuilder(ns).build_ast()
    assert qb._ast['order_by']
    ns = ns.order_by(None)
    qb = QueryBuilder(ns).build_ast()
    assert not qb._ast['order_by']
    ns = ns.order_by('?')
    qb = QueryBuilder(ns).build_ast()
    assert qb._ast['with'] == 'coffee, rand() as r'
    assert qb._ast['order_by'] == 'r'

    # Test order by on a relationship
    l = Supplier(name="lidl2").save()
    l.coffees.connect(c1)
    l.coffees.connect(c2)
    l.coffees.connect(c3)

    ordered_n = [n for n in l.coffees.order_by('name').all()]
    assert ordered_n[0] == c2
    assert ordered_n[1] == c1
    assert ordered_n[2] == c3


def test_extra_filters():

    for c in Coffee.nodes:
        c.delete()

    c1 = Coffee(name="Icelands finest", price=5, id_=1).save()
    c2 = Coffee(name="Britains finest", price=10, id_=2).save()
    c3 = Coffee(name="Japans finest", price=35, id_=3).save()
    c4 = Coffee(name="US extra-fine", price=None, id_=4).save()

    coffees_5_10 = Coffee.nodes.filter(price__in=[10, 5]).all()
    assert len(coffees_5_10) == 2, "unexpected number of results"
    assert c1 in coffees_5_10, "doesnt contain 5 price coffee"
    assert c2 in coffees_5_10, "doesnt contain 10 price coffee"

    finest_coffees = Coffee.nodes.filter(name__iendswith=' Finest').all()
    assert len(finest_coffees) == 3, "unexpected number of results"
    assert c1 in finest_coffees, "doesnt contain 1st finest coffee"
    assert c2 in finest_coffees, "doesnt contain 2nd finest coffee"
    assert c3 in finest_coffees, "doesnt contain 3rd finest coffee"

    unpriced_coffees = Coffee.nodes.filter(price__isnull=True).all()
    assert len(unpriced_coffees) == 1, "unexpected number of results"
    assert c4 in unpriced_coffees, "doesnt contain unpriced coffee"

    coffees_with_id_gte_3 = Coffee.nodes.filter(id___gte=3).all()
    assert len(coffees_with_id_gte_3) == 2, "unexpected number of results"
    assert c3 in coffees_with_id_gte_3
    assert c4 in coffees_with_id_gte_3


def test_traversal_definition_keys_are_valid():
    muckefuck = Coffee(name='Mukkefuck', price=1)

    with raises(ValueError):
        Traversal(
            muckefuck, 'a_name', {
                'node_class': Supplier, 'direction': INCOMING,
                'relationship_type': 'KNOWS', 'model': None
            }
        )

    Traversal(
        muckefuck, 'a_name', {
            'node_class': Supplier, 'direction': INCOMING,
            'relation_type': 'KNOWS', 'model': None
        }
    )


def test_empty_filters():
    """Test this case:
        ```
            SomeModel.nodes.filter().filter(Q(arg1=val1)).all()
            SomeModel.nodes.exclude().exclude(Q(arg1=val1)).all()
            SomeModel.nodes.filter().filter(arg1=val1).all()
       ```
       In django_rest_framework filter uses such as lazy function and
       ``get_queryset`` function in ``GenericAPIView`` should returns
       ``NodeSet`` object.
    """

    for c in Coffee.nodes:
        c.delete()

    c1 = Coffee(name="Super", price=5, id_=1).save()
    c2 = Coffee(name="Puper", price=10, id_=2).save()

    empty_filter = Coffee.nodes.filter()

    all_coffees = empty_filter.all()
    assert len(all_coffees) == 2, "unexpected number of results"

    filter_empty_filter = empty_filter.filter(price=5)
    assert len(filter_empty_filter.all()) == 1, "unexpected number of results"
    assert c1 in filter_empty_filter.all(), "doesnt contain c1 in ``filter_empty_filter``"

    filter_q_empty_filter = empty_filter.filter(Q(price=5))
    assert len(filter_empty_filter.all()) == 1, "unexpected number of results"
    assert c1 in filter_empty_filter.all(), "doesnt contain c1 in ``filter_empty_filter``"


def test_q_filters():

    for c in Coffee.nodes:
        c.delete()

    c1 = Coffee(name="Icelands finest", price=5, id_=1).save()
    c2 = Coffee(name="Britains finest", price=10, id_=2).save()
    c3 = Coffee(name="Japans finest", price=35, id_=3).save()
    c4 = Coffee(name="US extra-fine", price=None, id_=4).save()
    c5 = Coffee(name="Latte", price=35, id_=5).save()
    c6 = Coffee(name="Cappuccino", price=35, id_=6).save()

    coffees_5_10 = Coffee.nodes.filter(Q(price=10) | Q(price=5)).all()
    assert len(coffees_5_10) == 2, "unexpected number of results"
    assert c1 in coffees_5_10, "doesnt contain 5 price coffee"
    assert c2 in coffees_5_10, "doesnt contain 10 price coffee"

    coffees_5_6 = Coffee.nodes.filter(Q(name="Latte") | Q(name="Cappuccino")).filter(price=35).all()
    assert len(coffees_5_6) == 2, "unexpected number of results"
    assert c5 in coffees_5_6, "doesnt contain 5 coffee"
    assert c6 in coffees_5_6, "doesnt contain 6 coffee"

    coffees_5_6 = Coffee.nodes.filter(price=35).filter(Q(name="Latte") | Q(name="Cappuccino")).all()
    assert len(coffees_5_6) == 2, "unexpected number of results"
    assert c5 in coffees_5_6, "doesnt contain 5 coffee"
    assert c6 in coffees_5_6, "doesnt contain 6 coffee"

    finest_coffees = Coffee.nodes.filter(name__iendswith=' Finest').all()
    assert len(finest_coffees) == 3, "unexpected number of results"
    assert c1 in finest_coffees, "doesnt contain 1st finest coffee"
    assert c2 in finest_coffees, "doesnt contain 2nd finest coffee"
    assert c3 in finest_coffees, "doesnt contain 3rd finest coffee"

    unpriced_coffees = Coffee.nodes.filter(Q(price__isnull=True)).all()
    assert len(unpriced_coffees) == 1, "unexpected number of results"
    assert c4 in unpriced_coffees, "doesnt contain unpriced coffee"

    coffees_with_id_gte_3 = Coffee.nodes.filter(Q(id___gte=3)).all()
    assert len(coffees_with_id_gte_3) == 4, "unexpected number of results"
    assert c3 in coffees_with_id_gte_3
    assert c4 in coffees_with_id_gte_3
    assert c5 in coffees_with_id_gte_3
    assert c6 in coffees_with_id_gte_3

    coffees_5_not_japans = Coffee.nodes.filter(Q(price__gt=5) & ~Q(name="Japans finest")).all()
    assert c3 not in coffees_5_not_japans


def test_traversal_filter_left_hand_statement():
    nescafe = Coffee(name='Nescafe2', price=99).save()
    nescafe_gold = Coffee(name='Nescafe gold', price=11).save()

    tesco = Supplier(name='Sainsburys', delivery_cost=3).save()
    biedronka = Supplier(name='Biedronka', delivery_cost=5).save()
    lidl = Supplier(name='Lidl', delivery_cost=3).save()

    nescafe.suppliers.connect(tesco)
    nescafe_gold.suppliers.connect(biedronka)
    nescafe_gold.suppliers.connect(lidl)

    lidl_supplier = NodeSet(Coffee.nodes.filter(price=11).suppliers).filter(delivery_cost=3).all()

    assert lidl in lidl_supplier