# -*- 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 wuttjamaican.util import get_timezone_by_name
from wuttjamaican.testing import DataTestCase
from wuttaweb.forms import schema as mod
from wuttaweb.forms import widgets
from wuttaweb.testing import WebTestCase


class TestWuttaDateTime(WebTestCase):

    def test_serialize(self):
        tzlocal = get_timezone_by_name("America/Los_Angeles")
        with patch.object(self.app, "get_timezone", return_value=tzlocal):
            typ = mod.WuttaDateTime()
            node = colander.SchemaNode(
                typ, widget=widgets.WuttaDateTimeWidget(self.request)
            )

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

            # naive, UTC
            result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
            self.assertEqual(result, "2024-12-11T14:33:00-08:00")

            # aware, UTC
            result = typ.serialize(
                node,
                datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
            )
            self.assertEqual(result, "2024-12-11T14:33:00-08:00")

            # aware, local
            result = typ.serialize(
                node,
                datetime.datetime(2024, 12, 11, 14, 33, tzinfo=tzlocal),
            )
            self.assertEqual(result, "2024-12-11T14:33:00-08:00")

            # custom format
            typ = mod.WuttaDateTime(format="%Y-%m-%d %I:%M %p")
            node = colander.SchemaNode(
                typ, widget=widgets.WuttaDateTimeWidget(self.request)
            )
            result = typ.serialize(
                node,
                datetime.datetime(2024, 12, 11, 22, 33, tzinfo=datetime.timezone.utc),
            )
            self.assertEqual(result, "2024-12-11 02:33 PM")

            # missing widget/request/config
            typ = mod.WuttaDateTime()
            node = colander.SchemaNode(typ)
            result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
            # nb. not possible to know which timezone is system-local
            self.assertTrue(result.startswith("2024-12-"))

    def test_deserialize(self):
        tzlocal = get_timezone_by_name("America/Los_Angeles")
        with patch.object(self.app, "get_timezone", return_value=tzlocal):
            typ = mod.WuttaDateTime()
            node = colander.SchemaNode(
                typ, widget=widgets.WuttaDateTimeWidget(self.request)
            )

            # null
            self.assertIs(typ.deserialize(node, colander.null), colander.null)
            self.assertIs(typ.deserialize(node, None), colander.null)
            self.assertIs(typ.deserialize(node, ""), colander.null)

            # format #1
            result = typ.deserialize(node, "2024-12-11T22:33:00")
            self.assertIsInstance(result, datetime.datetime)
            self.assertEqual(
                result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
            )

            # format #2
            result = typ.deserialize(node, "2024-12-11T10:33 PM")
            self.assertIsInstance(result, datetime.datetime)
            self.assertEqual(
                result, datetime.datetime(2024, 12, 12, 6, 33, tzinfo=None)
            )

            # invalid
            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_serialize(self):

        # null_value is empty string by default
        typ = mod.WuttaDictEnum(self.request, MOCK_STATUS)
        node = colander.SchemaNode(typ)
        self.assertEqual(typ.serialize(node, 1), "1")
        self.assertEqual(typ.serialize(node, colander.null), "")

        # but can override if needed (?)
        typ = mod.WuttaDictEnum(self.request, MOCK_STATUS, null_value="nope")
        node = colander.SchemaNode(typ)
        self.assertEqual(typ.serialize(node, 1), "1")
        self.assertEqual(typ.serialize(node, colander.null), "nope")

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

    def test_constructor(self):
        node = colander.SchemaNode(mod.WuttaList(self.request))
        typ = node.typ
        self.assertIs(typ.request, self.request)
        self.assertIs(typ.config, self.config)
        self.assertIs(typ.app, self.app)


class TestWuttaSet(WebTestCase):

    def test_constructor(self):
        node = colander.SchemaNode(mod.WuttaSet(self.request))
        typ = node.typ
        self.assertIs(typ.request, self.request)
        self.assertIs(typ.config, self.config)
        self.assertIs(typ.app, self.app)


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)
        value = typ.serialize(node, None)
        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 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)
