# -*- coding: utf-8; -*-

import datetime
from unittest import TestCase
from unittest.mock import patch

import sqlalchemy as sa

from wuttjamaican.db.model import Base

from wuttaweb.grids import filters as mod
from tests.util import WebTestCase


class TestGridFilter(WebTestCase):

    def setUp(self):
        self.setup_web()

        model = self.app.model
        self.sample_data = [
            {'name': 'foo1', 'value': 'ONE'},
            {'name': 'foo2', 'value': 'two'},
            {'name': 'foo3', 'value': 'ggg'},
            {'name': 'foo4', 'value': 'ggg'},
            {'name': 'foo5', 'value': 'ggg'},
            {'name': 'foo6', 'value': 'six'},
            {'name': 'foo7', 'value': 'seven'},
            {'name': 'foo8', 'value': 'eight'},
            {'name': 'foo9', 'value': 'nine'},
        ]
        for setting in self.sample_data:
            self.app.save_setting(self.session, setting['name'], setting['value'])
        self.session.commit()
        self.sample_query = self.session.query(model.Setting)

    def make_filter(self, model_property, **kwargs):
        factory = kwargs.pop('factory', mod.GridFilter)
        kwargs['model_property'] = model_property
        return factory(self.request, model_property.key, **kwargs)

    def test_constructor(self):
        model = self.app.model

        # verbs is not set by default, but can be set
        filtr = self.make_filter(model.Setting.name)
        self.assertFalse(hasattr(filtr, 'verbs'))
        filtr = self.make_filter(model.Setting.name, verbs=['foo', 'bar'])
        self.assertEqual(filtr.verbs, ['foo', 'bar'])

        # verb is not set by default, but can be set
        filtr = self.make_filter(model.Setting.name)
        self.assertFalse(hasattr(filtr, 'verb'))
        filtr = self.make_filter(model.Setting.name, verb='foo')
        self.assertEqual(filtr.verb, 'foo')

        # default verb is not set by default, but can be set
        filtr = self.make_filter(model.Setting.name)
        self.assertFalse(hasattr(filtr, 'default_verb'))
        filtr = self.make_filter(model.Setting.name, default_verb='foo')
        self.assertEqual(filtr.default_verb, 'foo')

    def test_repr(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.GridFilter)
        self.assertEqual(repr(filtr), "GridFilter(key='name', active=False, verb=None, value=None)")

    def test_get_verbs(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])

        # by default, returns default verbs (plus 'is_any')
        self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])

        # default verbs can be a callable
        filtr.default_verbs = lambda: ['foo', 'bar']
        self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])

        # uses filtr.verbs if set
        filtr.verbs = ['is_true', 'is_false']
        self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])

        # may add is/null verbs
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter,
                                 nullable=True)
        self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal',
                                             'is_null', 'is_not_null',
                                             'is_any'])

        # filtr.verbs can be a callable
        filtr.nullable = False
        filtr.verbs = lambda: ['baz', 'blarg']
        self.assertEqual(filtr.get_verbs(), ['baz', 'blarg', 'is_any'])

    def test_get_default_verb(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])
        self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])

        # returns first verb by default
        self.assertEqual(filtr.get_default_verb(), 'equal')

        # returns filtr.verb if set
        filtr.verb = 'foo'
        self.assertEqual(filtr.get_default_verb(), 'foo')

        # returns filtr.default_verb if set
        # (nb. this overrides filtr.verb since the point of this
        # method is to return the *default* verb)
        filtr.default_verb = 'bar'
        self.assertEqual(filtr.get_default_verb(), 'bar')

    def test_get_verb_labels(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])

        labels = filtr.get_verb_labels()
        self.assertIsInstance(labels, dict)
        self.assertEqual(labels['equal'], "equal to")
        self.assertEqual(labels['not_equal'], "not equal to")
        self.assertEqual(labels['is_any'], "is any")

    def test_get_valueless_verbs(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])

        verbs = filtr.get_valueless_verbs()
        self.assertIsInstance(verbs, list)
        self.assertIn('is_any', verbs)

    def test_apply_filter(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value, factory=mod.StringAlchemyFilter)

        # default verb used as fallback
        # self.assertEqual(filtr.default_verb, 'contains')
        filtr.default_verb = 'contains'
        filtr.verb = None
        with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
            filtered_query = filtr.apply_filter(self.sample_query, value='foo')
            filter_contains.assert_called_once_with(self.sample_query, 'foo')
        self.assertIsNone(filtr.verb)

        # filter verb used as fallback
        filtr.verb = 'equal'
        with patch.object(filtr, 'filter_equal', create=True, side_effect=lambda q, v: q) as filter_equal:
            filtered_query = filtr.apply_filter(self.sample_query, value='foo')
            filter_equal.assert_called_once_with(self.sample_query, 'foo')

        # filter value used as fallback
        filtr.verb = 'contains'
        filtr.value = 'blarg'
        with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
            filtered_query = filtr.apply_filter(self.sample_query)
            filter_contains.assert_called_once_with(self.sample_query, 'blarg')

        # error if invalid verb
        self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
                          self.sample_query, verb='doesnotexist')
        filtr.verbs = ['doesnotexist']
        self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
                          self.sample_query, verb='doesnotexist')

    def test_filter_is_any(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # nb. value None is ignored
        filtered_query = filtr.filter_is_any(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 9)


class TestAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()

        model = self.app.model
        self.sample_data = [
            {'name': 'foo1', 'value': 'ONE'},
            {'name': 'foo2', 'value': 'two'},
            {'name': 'foo3', 'value': 'ggg'},
            {'name': 'foo4', 'value': 'ggg'},
            {'name': 'foo5', 'value': 'ggg'},
            {'name': 'foo6', 'value': 'six'},
            {'name': 'foo7', 'value': 'seven'},
            {'name': 'foo8', 'value': 'eight'},
            {'name': 'foo9', 'value': None},
        ]
        for setting in self.sample_data:
            self.app.save_setting(self.session, setting['name'], setting['value'])
        self.session.commit()
        self.sample_query = self.session.query(model.Setting)

    def make_filter(self, model_property, **kwargs):
        factory = kwargs.pop('factory', mod.AlchemyFilter)
        kwargs['model_property'] = model_property
        return factory(self.request, model_property.key, **kwargs)

    def test_filter_equal(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # not filtered for null value
        filtered_query = filtr.filter_equal(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)

        # nb. by default, *is filtered* by empty string
        filtered_query = filtr.filter_equal(self.sample_query, '')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 0)

        # filtered by value
        filtered_query = filtr.filter_equal(self.sample_query, 'ggg')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 3)

    def test_filter_not_equal(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # not filtered for empty value
        filtered_query = filtr.filter_not_equal(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)

        # nb. by default, *is filtered* by empty string
        filtered_query = filtr.filter_not_equal(self.sample_query, '')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 9)

        # filtered by value
        filtered_query = filtr.filter_not_equal(self.sample_query, 'ggg')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)

    def test_filter_is_null(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # nb. value None is ignored
        filtered_query = filtr.filter_is_null(self.sample_query, None)
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 1)

    def test_filter_is_not_null(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # nb. value None is ignored
        filtered_query = filtr.filter_is_not_null(self.sample_query, None)
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 8)


class TestStringAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()

        model = self.app.model
        self.sample_data = [
            {'name': 'foo1', 'value': 'ONE'},
            {'name': 'foo2', 'value': 'two'},
            {'name': 'foo3', 'value': 'ggg'},
            {'name': 'foo4', 'value': 'ggg'},
            {'name': 'foo5', 'value': 'ggg'},
            {'name': 'foo6', 'value': 'six'},
            {'name': 'foo7', 'value': 'seven'},
            {'name': 'foo8', 'value': 'eight'},
            {'name': 'foo9', 'value': 'nine'},
        ]
        for setting in self.sample_data:
            self.app.save_setting(self.session, setting['name'], setting['value'])
        self.session.commit()
        self.sample_query = self.session.query(model.Setting)

    def make_filter(self, model_property, **kwargs):
        factory = kwargs.pop('factory', mod.StringAlchemyFilter)
        kwargs['model_property'] = model_property
        return factory(self.request, model_property.key, **kwargs)

    def test_filter_contains(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # not filtered for empty value
        filtered_query = filtr.filter_contains(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        filtered_query = filtr.filter_contains(self.sample_query, '')
        self.assertIs(filtered_query, self.sample_query)

        # filtered by value
        filtered_query = filtr.filter_contains(self.sample_query, 'ggg')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 3)

    def test_filter_does_not_contain(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.value)
        self.assertEqual(self.sample_query.count(), 9)

        # not filtered for empty value
        filtered_query = filtr.filter_does_not_contain(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        filtered_query = filtr.filter_does_not_contain(self.sample_query, '')
        self.assertIs(filtered_query, self.sample_query)

        # filtered by value
        filtered_query = filtr.filter_does_not_contain(self.sample_query, 'ggg')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)


class TestBooleanAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()

        model = self.app.model
        self.sample_data = [
            {'username': 'alice',
             'prevent_edit': False,
             'active': True},
            {'username': 'bob',
             'prevent_edit': True,
             'active': True},
            {'username': 'charlie',
             'active': False,
             'prevent_edit': None},
        ]
        for user in self.sample_data:
            user = model.User(**user)
            self.session.add(user)
        self.session.commit()
        self.sample_query = self.session.query(model.User)

    def make_filter(self, model_property, **kwargs):
        factory = kwargs.pop('factory', mod.BooleanAlchemyFilter)
        kwargs['model_property'] = model_property
        return factory(self.request, model_property.key, **kwargs)

    def test_get_verbs(self):
        model = self.app.model

        # bool field, not nullable
        filtr = self.make_filter(model.User.active,
                                 factory=mod.BooleanAlchemyFilter,
                                 nullable=False)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])

        # by default, returns default verbs (plus 'is_any')
        self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])

        # default verbs can be a callable
        filtr.default_verbs = lambda: ['foo', 'bar']
        self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])

        # bool field, *nullable*
        filtr = self.make_filter(model.User.active,
                                 factory=mod.BooleanAlchemyFilter,
                                 nullable=True)
        self.assertFalse(hasattr(filtr, 'verbs'))
        self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])

        # effective verbs also include is_false_null
        self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_false_null',
                                             'is_null', 'is_not_null', 'is_any'])

    def test_coerce_value(self):
        model = self.app.model
        filtr = self.make_filter(model.User.active)

        self.assertIsNone(filtr.coerce_value(None))

        self.assertTrue(filtr.coerce_value(True))
        self.assertTrue(filtr.coerce_value(1))
        self.assertTrue(filtr.coerce_value('1'))

        self.assertFalse(filtr.coerce_value(False))
        self.assertFalse(filtr.coerce_value(0))
        self.assertFalse(filtr.coerce_value(''))

    def test_filter_is_true(self):
        model = self.app.model
        filtr = self.make_filter(model.User.active)
        self.assertEqual(self.sample_query.count(), 3)

        # nb. value None is ignored
        filtered_query = filtr.filter_is_true(self.sample_query, None)
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

    def test_filter_is_false(self):
        model = self.app.model
        filtr = self.make_filter(model.User.active)
        self.assertEqual(self.sample_query.count(), 3)

        # nb. value None is ignored
        filtered_query = filtr.filter_is_false(self.sample_query, None)
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 1)

    def test_filter_is_false_null(self):
        model = self.app.model
        filtr = self.make_filter(model.User.prevent_edit)
        self.assertEqual(self.sample_query.count(), 3)

        # nb. only one account is marked with "prevent edit"
        filtered_query = filtr.filter_is_false_null(self.sample_query, None)
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)


class TheLocalThing(Base):
    __tablename__ = 'the_local_thing'
    id = sa.Column(sa.Integer(), primary_key=True, autoincrement=False)
    date = sa.Column(sa.DateTime(timezone=True), nullable=True)


class TestDateAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()
        model = self.app.model

        # nb. create table for TheLocalThing
        model.Base.metadata.create_all(bind=self.session.bind)

        self.sample_data = [
            {'id': 1, 'date': datetime.date(2024, 1, 1)},
            {'id': 2, 'date': datetime.date(2024, 1, 1)},
            {'id': 3, 'date': datetime.date(2024, 3, 1)},
            {'id': 4, 'date': datetime.date(2024, 3, 1)},
            {'id': 5, 'date': None},
            {'id': 6, 'date': None},
        ]

        for thing in self.sample_data:
            thing = TheLocalThing(**thing)
            self.session.add(thing)
        self.session.commit()

        self.sample_query = self.session.query(TheLocalThing)

    def make_filter(self, model_property, **kwargs):
        factory = kwargs.pop('factory', mod.DateAlchemyFilter)
        kwargs['model_property'] = model_property
        return factory(self.request, model_property.key, **kwargs)

    def test_coerce_value(self):
        filtr = self.make_filter(TheLocalThing.date)

        # null value
        self.assertIsNone(filtr.coerce_value(None))

        # value as datetime
        value = datetime.date(2024, 1, 1)
        result = filtr.coerce_value(value)
        self.assertIs(value, result)

        # value as string
        result = filtr.coerce_value('2024-04-01')
        self.assertIsInstance(result, datetime.date)
        self.assertEqual(result, datetime.date(2024, 4, 1))

        # invalid
        result = filtr.coerce_value('thisinputisbad')
        self.assertIsNone(result)

    def test_greater_than(self):
        model = self.app.model

        filtr = self.make_filter(TheLocalThing.date)
        self.assertEqual(self.sample_query.count(), 6)

        # null value ignored
        filtered_query = filtr.filter_greater_than(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)

        # value as date
        filtered_query = filtr.filter_greater_than(self.sample_query, datetime.date(2024, 2, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as string
        filtered_query = filtr.filter_greater_than(self.sample_query, '2024-02-01')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

    def test_greater_equal(self):
        model = self.app.model

        filtr = self.make_filter(TheLocalThing.date)
        self.assertEqual(self.sample_query.count(), 6)

        # null value ignored
        filtered_query = filtr.filter_greater_equal(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)

        # value as date (clear of boundary)
        filtered_query = filtr.filter_greater_equal(self.sample_query, datetime.date(2024, 2, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as date (at boundary)
        filtered_query = filtr.filter_greater_equal(self.sample_query, datetime.date(2024, 3, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as string
        filtered_query = filtr.filter_greater_equal(self.sample_query, '2024-01-01')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 4)

    def test_less_than(self):
        model = self.app.model

        filtr = self.make_filter(TheLocalThing.date)
        self.assertEqual(self.sample_query.count(), 6)

        # null value ignored
        filtered_query = filtr.filter_less_than(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)

        # value as date
        filtered_query = filtr.filter_less_than(self.sample_query, datetime.date(2024, 2, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as string
        filtered_query = filtr.filter_less_than(self.sample_query, '2024-04-01')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 4)

    def test_less_equal(self):
        model = self.app.model

        filtr = self.make_filter(TheLocalThing.date)
        self.assertEqual(self.sample_query.count(), 6)

        # null value ignored
        filtered_query = filtr.filter_less_equal(self.sample_query, None)
        self.assertIs(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 6)

        # value as date (clear of boundary)
        filtered_query = filtr.filter_less_equal(self.sample_query, datetime.date(2024, 2, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as date (at boundary)
        filtered_query = filtr.filter_less_equal(self.sample_query, datetime.date(2024, 3, 1))
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 2)

        # value as string
        filtered_query = filtr.filter_less_equal(self.sample_query, '2024-04-01')
        self.assertIsNot(filtered_query, self.sample_query)
        self.assertEqual(filtered_query.count(), 4)


class TestVerbNotSupported(TestCase):

    def test_basic(self):
        error = mod.VerbNotSupported('equal')
        self.assertEqual(str(error), "unknown filter verb not supported: equal")
