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

import datetime
from collections import OrderedDict
from enum import Enum
from unittest import TestCase
from unittest.mock import patch

import sqlalchemy as sa

from wuttjamaican.db.model import Base
from wuttjamaican.util import get_timezone_by_name

from wuttaweb.grids import filters as mod
from wuttaweb.testing 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.assertIsNone(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_set_choices(self):
        model = self.app.model

        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
        self.assertEqual(filtr.choices, {})
        self.assertEqual(filtr.data_type, "string")

        class MockEnum(Enum):
            FOO = "foo"
            BAR = "bar"

        filtr.set_choices(MockEnum)
        self.assertEqual(
            filtr.choices,
            OrderedDict(
                [
                    ("FOO", "foo"),
                    ("BAR", "bar"),
                ]
            ),
        )
        self.assertEqual(filtr.data_type, "choice")

        filtr.set_choices(None)
        self.assertEqual(filtr.choices, {})
        self.assertEqual(filtr.data_type, "string")

    def test_normalize_choices(self):
        model = self.app.model
        filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)

        class MockEnum(Enum):
            FOO = "foo"
            BAR = "bar"

        choices = filtr.normalize_choices(MockEnum)
        self.assertEqual(
            choices,
            OrderedDict(
                [
                    ("FOO", "foo"),
                    ("BAR", "bar"),
                ]
            ),
        )

        choices = filtr.normalize_choices(
            OrderedDict(
                [
                    ("first", "1"),
                    ("second", "2"),
                ]
            )
        )
        self.assertEqual(
            choices,
            OrderedDict(
                [
                    ("first", "1"),
                    ("second", "2"),
                ]
            ),
        )

        choices = filtr.normalize_choices(
            {
                "bbb": "b",
                "aaa": "a",
            }
        )
        self.assertEqual(
            choices,
            OrderedDict(
                [
                    ("aaa", "a"),
                    ("bbb", "b"),
                ]
            ),
        )

        choices = filtr.normalize_choices(["one", "two", "three"])
        self.assertEqual(
            choices,
            OrderedDict(
                [
                    ("one", "one"),
                    ("two", "two"),
                    ("three", "three"),
                ]
            ),
        )

    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 TestIntegerAlchemyFilter(WebTestCase):

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

    def test_coerce_value(self):
        model = self.app.model
        filtr = self.make_filter(model.Upgrade.exit_code)

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

        # typical
        self.assertEqual(filtr.coerce_value("42"), 42)
        self.assertEqual(filtr.coerce_value("-42"), -42)

        # invalid
        self.assertIsNone(filtr.coerce_value("42.12"))
        self.assertIsNone(filtr.coerce_value("bogus"))


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.Date(), nullable=True)
    timestamp = sa.Column(sa.DateTime(), nullable=True)


class TestDateAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()

        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 date
        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(), 4)

        # 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 TestDateTimeAlchemyFilter(WebTestCase):

    def setUp(self):
        self.setup_web()
        self.tzlocal = get_timezone_by_name("America/Los_Angeles")

        self.sample_data = [
            {
                "id": 1,
                "timestamp": self.app.make_utc(
                    datetime.datetime(2024, 1, 1, 1, 20, tzinfo=self.tzlocal)
                ),
            },
            {
                "id": 2,
                "timestamp": self.app.make_utc(
                    datetime.datetime(2024, 1, 1, 23, 40, tzinfo=self.tzlocal)
                ),
            },
            {
                "id": 3,
                "timestamp": self.app.make_utc(
                    datetime.datetime(2024, 3, 1, 22, 10, tzinfo=self.tzlocal)
                ),
            },
            {
                "id": 4,
                "timestamp": self.app.make_utc(
                    datetime.datetime(2024, 3, 1, 2, 0, tzinfo=self.tzlocal)
                ),
            },
            {"id": 5, "timestamp": None},
            {"id": 6, "timestamp": 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.DateTimeAlchemyFilter)
        kwargs["model_property"] = model_property
        return factory(self.request, model_property.key, **kwargs)

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

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

        # value as date
        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_equal(self):
        model = self.app.model
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

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

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

            # value as date
            filtered_query = filtr.filter_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_equal(self.sample_query, "2024-02-01")
            self.assertIsNot(filtered_query, self.sample_query)
            self.assertEqual(filtered_query.count(), 0)
            filtered_query = filtr.filter_equal(self.sample_query, "2024-01-01")
            self.assertIsNot(filtered_query, self.sample_query)
            self.assertEqual(filtered_query.count(), 2)

    def test_not_equal(self):
        model = self.app.model
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

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

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

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

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

    def test_greater_than(self):
        model = self.app.model
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

            filtr = self.make_filter(TheLocalThing.timestamp)
            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
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

            filtr = self.make_filter(TheLocalThing.timestamp)
            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
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

            filtr = self.make_filter(TheLocalThing.timestamp)
            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
        with patch.object(self.app, "get_timezone", return_value=self.tzlocal):

            filtr = self.make_filter(TheLocalThing.timestamp)
            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(), 4)

            # 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")
