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

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

import colander
from pyramid import testing

from sqlalchemy import orm

from wuttjamaican.conf import WuttaConfig
from wuttaweb.forms import schema as mod
from wuttaweb.forms import widgets
from wuttaweb.testing import DataTestCase, WebTestCase


class TestWuttaDateTime(TestCase):

    def test_deserialize(self):
        typ = mod.WuttaDateTime()
        node = colander.SchemaNode(typ)

        result = typ.deserialize(node, colander.null)
        self.assertIs(result, colander.null)

        result = typ.deserialize(node, '2024-12-11T10:33 PM')
        self.assertIsInstance(result, datetime.datetime)
        self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
        self.assertIsNone(result.tzinfo)

        result = typ.deserialize(node, '2024-12-11T22:33:00')
        self.assertIsInstance(result, datetime.datetime)
        self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
        self.assertIsNone(result.tzinfo)

        self.assertRaises(colander.Invalid, typ.deserialize, node, 'bogus')


class TestObjectNode(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_dictify(self):
        model = self.app.model
        person = model.Person(full_name="Betty Boop")

        # unsupported type is converted to string
        node = mod.ObjectNode(colander.String())
        value = node.dictify(person)
        self.assertEqual(value, "Betty Boop")

        # but supported type can dictify
        node = mod.ObjectNode(mod.PersonRef(self.request))
        value = node.dictify(person)
        self.assertIs(value, person)

    def test_objectify(self):
        model = self.app.model
        person = model.Person(full_name="Betty Boop")

        # unsupported type raises error
        node = mod.ObjectNode(colander.String())
        self.assertRaises(NotImplementedError, node.objectify, person)

        # but supported type can objectify
        node = mod.ObjectNode(mod.PersonRef(self.request))
        value = node.objectify(person)
        self.assertIs(value, person)


class TestWuttaEnum(WebTestCase):

    def test_widget_maker(self):
        enum = self.app.enum
        typ = mod.WuttaEnum(self.request, enum.UpgradeStatus)
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.SelectWidget)


MOCK_STATUS_ONE = 1
MOCK_STATUS_TWO = 2
MOCK_STATUS = {
    MOCK_STATUS_ONE: 'one',
    MOCK_STATUS_TWO: 'two',
}

class TestWuttaDictEnum(WebTestCase):

    def test_widget_maker(self):
        typ = mod.WuttaDictEnum(self.request, MOCK_STATUS)
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.SelectWidget)
        self.assertEqual(widget.values, [
            (1, 'one'),
            (2, 'two'),
        ])


class TestWuttaMoney(WebTestCase):

    def test_widget_maker(self):
        enum = self.app.enum

        # default scale
        typ = mod.WuttaMoney(self.request)
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget)
        self.assertEqual(widget.scale, 2)

        # custom scale
        typ = mod.WuttaMoney(self.request, scale=4)
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget)
        self.assertEqual(widget.scale, 4)


class TestWuttaQuantity(WebTestCase):

    def test_serialize(self):
        node = colander.SchemaNode(mod.WuttaQuantity(self.request))
        typ = node.typ

        # null
        result = typ.serialize(node, colander.null)
        self.assertIs(result, colander.null)
        result = typ.serialize(node, None)
        self.assertIs(result, colander.null)

        # quantity
        result = typ.serialize(node, 42)
        self.assertEqual(result, '42')
        result = typ.serialize(node, 42.00)
        self.assertEqual(result, '42')
        result = typ.serialize(node, decimal.Decimal('42.00'))
        self.assertEqual(result, '42')
        result = typ.serialize(node, 42.13)
        self.assertEqual(result, '42.13')


class TestObjectRef(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_empty_option(self):

        # null by default
        typ = mod.ObjectRef(self.request)
        self.assertIsNone(typ.empty_option)

        # passing true yields default empty option
        typ = mod.ObjectRef(self.request, empty_option=True)
        self.assertEqual(typ.empty_option, ('', "(none)"))

        # can set explicitly
        typ = mod.ObjectRef(self.request, empty_option=('foo', 'bar'))
        self.assertEqual(typ.empty_option, ('foo', 'bar'))

        # can set just a label
        typ = mod.ObjectRef(self.request, empty_option="(empty)")
        self.assertEqual(typ.empty_option, ('', "(empty)"))

    def test_model_class(self):
        typ = mod.ObjectRef(self.request)
        self.assertRaises(NotImplementedError, getattr, typ, 'model_class')

    def test_serialize(self):
        model = self.app.model
        node = colander.SchemaNode(colander.String())

        # null
        typ = mod.ObjectRef(self.request)
        value = typ.serialize(node, colander.null)
        self.assertIs(value, colander.null)

        # model instance
        person = model.Person(full_name="Betty Boop")
        self.session.add(person)
        self.session.commit()
        self.assertIsNotNone(person.uuid)
        typ = mod.ObjectRef(self.request)
        value = typ.serialize(node, person)
        self.assertEqual(value, person.uuid.hex)

        # null w/ empty option
        typ = mod.ObjectRef(self.request, empty_option=('bad', 'BAD'))
        value = typ.serialize(node, colander.null)
        self.assertEqual(value, 'bad')

    def test_deserialize(self):
        model = self.app.model
        node = colander.SchemaNode(colander.String())

        # null
        typ = mod.ObjectRef(self.request)
        value = typ.deserialize(node, colander.null)
        self.assertIs(value, colander.null)

        # model instance
        person = model.Person(full_name="Betty Boop")
        self.session.add(person)
        self.session.commit()
        self.assertIsNotNone(person.uuid)
        with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
            with patch.object(mod, 'Session', return_value=self.session):
                typ = mod.ObjectRef(self.request)
                value = typ.deserialize(node, person.uuid)
                self.assertIs(value, person)

    def test_dictify(self):
        model = self.app.model
        node = colander.SchemaNode(colander.String())

        # model instance
        person = model.Person(full_name="Betty Boop")
        self.session.add(person)
        self.session.commit()
        self.assertIsNotNone(person.uuid)
        typ = mod.ObjectRef(self.request)
        value = typ.dictify(person)
        self.assertIs(value, person)

    def test_objectify(self):
        model = self.app.model
        node = colander.SchemaNode(colander.String())

        # null
        typ = mod.ObjectRef(self.request)
        value = typ.objectify(None)
        self.assertIsNone(value)

        with patch.object(mod, 'Session', return_value=self.session):

            # model instance
            person = model.Person(full_name="Betty Boop")
            self.session.add(person)
            self.session.commit()
            self.assertIsNotNone(person.uuid)
            with patch.object(mod.ObjectRef, 'model_class', new=model.Person):

                # can specify as uuid
                typ = mod.ObjectRef(self.request)
                value = typ.objectify(person.uuid)
                self.assertIs(value, person)

                # or can specify object proper
                typ = mod.ObjectRef(self.request)
                value = typ.objectify(person)
                self.assertIs(value, person)

            # error if not found
            with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
                typ = mod.ObjectRef(self.request)
                self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')

    def test_get_query(self):
        model = self.app.model
        with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
            with patch.object(mod, 'Session', return_value=self.session):
                typ = mod.ObjectRef(self.request)
                query = typ.get_query()
                self.assertIsInstance(query, orm.Query)

    def test_sort_query(self):
        model = self.app.model
        with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
            with patch.object(mod, 'Session', return_value=self.session):
                typ = mod.ObjectRef(self.request)
                query = typ.get_query()
                sorted_query = typ.sort_query(query)
                self.assertIs(sorted_query, query)

    def test_widget_maker(self):
        model = self.app.model
        person = model.Person(full_name="Betty Boop")
        self.session.add(person)
        self.session.commit()

        # basic
        with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
            with patch.object(mod, 'Session', return_value=self.session):
                typ = mod.ObjectRef(self.request)
                widget = typ.widget_maker()
                self.assertEqual(len(widget.values), 1)
                self.assertEqual(widget.values[0][1], "Betty Boop")

        # empty option
        with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
            with patch.object(mod, 'Session', return_value=self.session):
                typ = mod.ObjectRef(self.request, empty_option=True)
                widget = typ.widget_maker()
                self.assertEqual(len(widget.values), 2)
                self.assertEqual(widget.values[0][1], "(none)")
                self.assertEqual(widget.values[1][1], "Betty Boop")


class TestPersonRef(WebTestCase):

    def test_sort_query(self):
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.PersonRef(self.request)
            query = typ.get_query()
            self.assertIsInstance(query, orm.Query)
            sorted_query = typ.sort_query(query)
            self.assertIsInstance(sorted_query, orm.Query)
            self.assertIsNot(sorted_query, query)

    def test_get_object_url(self):
        self.pyramid_config.add_route('people.view', '/people/{uuid}')
        model = self.app.model
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.PersonRef(self.request)

            person = model.Person(full_name="Barney Rubble")
            self.session.add(person)
            self.session.commit()

            url = typ.get_object_url(person)
            self.assertIsNotNone(url)
            self.assertIn(f'/people/{person.uuid}', url)


class TestRoleRef(WebTestCase):

    def test_sort_query(self):
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.RoleRef(self.request)
            query = typ.get_query()
            self.assertIsInstance(query, orm.Query)
            sorted_query = typ.sort_query(query)
            self.assertIsInstance(sorted_query, orm.Query)
            self.assertIsNot(sorted_query, query)

    def test_get_object_url(self):
        self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
        model = self.app.model
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.RoleRef(self.request)

            role = model.Role(name='Manager')
            self.session.add(role)
            self.session.commit()

            url = typ.get_object_url(role)
            self.assertIsNotNone(url)
            self.assertIn(f'/roles/{role.uuid}', url)


class TestUserRef(WebTestCase):

    def test_sort_query(self):
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.UserRef(self.request)
            query = typ.get_query()
            self.assertIsInstance(query, orm.Query)
            sorted_query = typ.sort_query(query)
            self.assertIsInstance(sorted_query, orm.Query)
            self.assertIsNot(sorted_query, query)

    def test_get_object_url(self):
        self.pyramid_config.add_route('users.view', '/users/{uuid}')
        model = self.app.model
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.UserRef(self.request)

            user = model.User(username='barney')
            self.session.add(user)
            self.session.commit()

            url = typ.get_object_url(user)
            self.assertIsNotNone(url)
            self.assertIn(f'/users/{user.uuid}', url)


class TestUserRefs(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_widget_maker(self):
        model = self.app.model
        with patch.object(mod, 'Session', return_value=self.session):
            typ = mod.UserRefs(self.request)
            widget = typ.widget_maker()
            self.assertIsInstance(widget, widgets.UserRefsWidget)


class TestRoleRefs(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_widget_maker(self):
        model = self.app.model
        auth = self.app.get_auth_handler()
        admin = auth.get_role_administrator(self.session)
        authed = auth.get_role_authenticated(self.session)
        anon = auth.get_role_anonymous(self.session)
        blokes = model.Role(name="Blokes")
        self.session.add(blokes)
        self.session.commit()

        with patch.object(mod, 'Session', return_value=self.session):

            # with root access, default values include: admin, blokes
            self.request.is_root = True
            typ = mod.RoleRefs(self.request)
            widget = typ.widget_maker()
            self.assertEqual(len(widget.values), 2)
            self.assertEqual(widget.values[0][1], "Administrator")
            self.assertEqual(widget.values[1][1], "Blokes")

            # without root, default values include: blokes
            self.request.is_root = False
            typ = mod.RoleRefs(self.request)
            widget = typ.widget_maker()
            self.assertEqual(len(widget.values), 1)
            self.assertEqual(widget.values[0][1], "Blokes")


class TestPermissions(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_widget_maker(self):

        # no supported permissions
        permissions = {}
        typ = mod.Permissions(self.request, permissions)
        widget = typ.widget_maker()
        self.assertEqual(len(widget.values), 0)

        # supported permissions are morphed to values
        permissions = {
            'widgets': {
                'label': "Widgets",
                'perms': {
                    'widgets.polish': {
                        'label': "Polish the widgets",
                    },
                },
            },
        }
        typ = mod.Permissions(self.request, permissions)
        widget = typ.widget_maker()
        self.assertEqual(len(widget.values), 1)
        self.assertEqual(widget.values[0], ('widgets.polish', "Polish the widgets"))


class TestFileDownload(DataTestCase):

    def setUp(self):
        self.setup_db()
        self.request = testing.DummyRequest(wutta_config=self.config)

    def test_widget_maker(self):

        # sanity / coverage check
        typ = mod.FileDownload(self.request, url='/foo')
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.FileDownloadWidget)
        self.assertEqual(widget.url, '/foo')


class TestEmailRecipients(TestCase):

    def test_serialize(self):
        typ = mod.EmailRecipients()
        node = colander.SchemaNode(typ)

        recips = [
            'alice@example.com',
            'bob@example.com',
        ]
        recips_str = ', '.join(recips)

        # values
        result = typ.serialize(node, recips_str)
        self.assertEqual(result, '\n'.join(recips))

        # null
        result = typ.serialize(node, colander.null)
        self.assertIs(result, colander.null)

    def test_deserialize(self):
        typ = mod.EmailRecipients()
        node = colander.SchemaNode(typ)

        recips = [
            'alice@example.com',
            'bob@example.com',
        ]
        recips_str = ', '.join(recips)

        # values
        result = typ.deserialize(node, recips_str)
        self.assertEqual(result, recips_str)

        # null
        result = typ.deserialize(node, colander.null)
        self.assertIs(result, colander.null)

    def test_widget_maker(self):
        typ = mod.EmailRecipients()
        widget = typ.widget_maker()
        self.assertIsInstance(widget, widgets.EmailRecipientsWidget)
