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

import datetime
import decimal
from unittest.mock import patch

import colander
import deform
from pyramid import testing

from wuttaweb import grids
from wuttaweb.forms import widgets as mod
from wuttaweb.forms import schema
from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions,
                                   WuttaDateTime, EmailRecipients)
from wuttaweb.testing import WebTestCase


class TestObjectRefWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def make_widget(self, **kwargs):
        return mod.ObjectRefWidget(self.request, **kwargs)

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

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

            # standard (editable)
            node = colander.SchemaNode(PersonRef(self.request))
            widget = self.make_widget()
            field = self.make_field(node)
            html = widget.serialize(field, person.uuid)
            self.assertIn('<b-select ', html)

            # readonly
            node = colander.SchemaNode(PersonRef(self.request))
            node.model_instance = person
            widget = self.make_widget()
            field = self.make_field(node)
            html = widget.serialize(field, person.uuid, readonly=True)
            self.assertIn('Betty Boop', html)
            self.assertNotIn('<a', html)

            # with hyperlink
            node = colander.SchemaNode(PersonRef(self.request))
            node.model_instance = person
            widget = self.make_widget(url=lambda p: '/foo')
            field = self.make_field(node)
            html = widget.serialize(field, person.uuid, readonly=True)
            self.assertIn('Betty Boop', html)
            self.assertIn('<a', html)
            self.assertIn('href="/foo"', html)

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

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

            # standard
            node = colander.SchemaNode(PersonRef(self.request))
            widget = self.make_widget()
            field = self.make_field(node)
            values = widget.get_template_values(field, person.uuid, {})
            self.assertIn('cstruct', values)
            self.assertNotIn('url', values)

            # readonly w/ empty option
            node = colander.SchemaNode(PersonRef(self.request,
                                                 empty_option=('_empty_', '(empty)')))
            widget = self.make_widget(readonly=True, url=lambda obj: '/foo')
            field = self.make_field(node)
            values = widget.get_template_values(field, '_empty_', {})
            self.assertIn('cstruct', values)
            self.assertNotIn('url', values)


class TestWuttaDateWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def make_widget(self, **kwargs):
        return mod.WuttaDateWidget(self.request, **kwargs)

    def test_serialize(self):
        node = colander.SchemaNode(colander.Date())
        field = self.make_field(node)

        # first try normal date
        widget = self.make_widget()
        dt = datetime.date(2025, 1, 15)

        # editable widget has normal picker html
        result = widget.serialize(field, str(dt))
        self.assertIn('<wutta-datepicker', result)

        # readonly is rendered per app convention
        result = widget.serialize(field, str(dt), readonly=True)
        self.assertEqual(result, '2025-01-15')

        # now try again with datetime
        widget = self.make_widget()
        dt = datetime.datetime(2025, 1, 15, 8, 35)

        # editable widget has normal picker html
        result = widget.serialize(field, str(dt))
        self.assertIn('<wutta-datepicker', result)

        # readonly is rendered per app convention
        result = widget.serialize(field, str(dt), readonly=True)
        self.assertEqual(result, '2025-01-15')


class TestWuttaDateTimeWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def make_widget(self, **kwargs):
        return mod.WuttaDateTimeWidget(self.request, **kwargs)

    def test_serialize(self):
        node = colander.SchemaNode(WuttaDateTime())
        field = self.make_field(node)
        widget = self.make_widget()
        dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=datetime.timezone.utc)

        # editable widget has normal picker html
        result = widget.serialize(field, str(dt))
        self.assertIn('<wutta-datepicker', result)

        # readonly is rendered per app convention
        result = widget.serialize(field, str(dt), readonly=True)
        self.assertEqual(result, '2024-12-12 13:49+0000')


class TestWuttaMoneyInputWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def make_widget(self, **kwargs):
        return mod.WuttaMoneyInputWidget(self.request, **kwargs)

    def test_serialize(self):
        node = colander.SchemaNode(schema.WuttaMoney(self.request))
        field = self.make_field(node)
        widget = self.make_widget()
        amount = decimal.Decimal('12.34')

        # editable widget has normal text input
        result = widget.serialize(field, str(amount))
        self.assertIn('<b-input', result)

        # readonly is rendered per app convention
        result = widget.serialize(field, str(amount), readonly=True)
        self.assertEqual(result, '<span>$12.34</span>')

        # readonly w/ null value
        result = widget.serialize(field, None, readonly=True)
        self.assertEqual(result, '<span></span>')


class TestFileDownloadWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):

        # nb. we let the field construct the widget via our type
        # (nb. at first we do not provide a url)
        node = colander.SchemaNode(FileDownload(self.request))
        field = self.make_field(node)
        widget = field.widget

        # null value
        html = widget.serialize(field, None, readonly=True)
        self.assertNotIn('<a ', html)
        self.assertIn('<span>', html)

        # path to nonexistent file
        html = widget.serialize(field, '/this/path/does/not/exist', readonly=True)
        self.assertNotIn('<a ', html)
        self.assertIn('<span>', html)

        # path to actual file
        datfile = self.write_file('data.txt', "hello\n" * 1000)
        html = widget.serialize(field, datfile, readonly=True)
        self.assertNotIn('<a ', html)
        self.assertIn('<span>', html)
        self.assertIn('data.txt', html)
        self.assertIn('kB)', html)

        # path to file, w/ url
        node = colander.SchemaNode(FileDownload(self.request, url='/download/blarg'))
        field = self.make_field(node)
        widget = field.widget
        html = widget.serialize(field, datfile, readonly=True)
        self.assertNotIn('<span>', html)
        self.assertIn('<a href="/download/blarg">', html)
        self.assertIn('data.txt', html)
        self.assertIn('kB)', html)

        # nb. same readonly output even if we ask for editable
        html2 = widget.serialize(field, datfile, readonly=False)
        self.assertEqual(html2, html)


class TestGridWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):
        grid = grids.Grid(self.request,
                          columns=['foo', 'bar'],
                          data=[{'foo': 1, 'bar': 2}, {'foo': 3, 'bar': 4}])

        node = colander.SchemaNode(colander.String())
        widget = mod.GridWidget(self.request, grid)
        field = self.make_field(node)

        # readonly works okay
        html = widget.serialize(field, None, readonly=True)
        self.assertIn('<b-table ', html)

        # but otherwise, error
        self.assertRaises(NotImplementedError, widget.serialize, field, None)


class TestRoleRefsWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):
        self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
        model = self.app.model
        auth = self.app.get_auth_handler()
        admin = auth.get_role_administrator(self.session)
        blokes = model.Role(name="Blokes")
        self.session.add(blokes)
        self.session.commit()

        # nb. we let the field construct the widget via our type
        with patch.object(schema, 'Session', return_value=self.session):
            node = colander.SchemaNode(RoleRefs(self.request))
            field = self.make_field(node)
            widget = field.widget

            # readonly values list includes admin
            html = widget.serialize(field, {admin.uuid, blokes.uuid}, readonly=True)
            self.assertIn(admin.name, html)
            self.assertIn(blokes.name, html)

            # editable values list *excludes* admin (by default)
            html = widget.serialize(field, {admin.uuid, blokes.uuid})
            self.assertNotIn(str(admin.uuid.hex), html)
            self.assertIn(str(blokes.uuid.hex), html)

            # but admin is included for root user
            self.request.is_root = True
            node = colander.SchemaNode(RoleRefs(self.request))
            field = self.make_field(node)
            widget = field.widget
            html = widget.serialize(field, {admin.uuid, blokes.uuid})
            self.assertIn(str(admin.uuid.hex), html)
            self.assertIn(str(blokes.uuid.hex), html)


class TestUserRefsWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

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

        # nb. we let the field construct the widget via our type
        # node = colander.SchemaNode(UserRefs(self.request, session=self.session))
        with patch.object(schema, 'Session', return_value=self.session):
            node = colander.SchemaNode(UserRefs(self.request))
            field = self.make_field(node)
            widget = field.widget

            # readonly is required
            self.assertRaises(NotImplementedError, widget.serialize, field, set())
            self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False)

            # empty
            html = widget.serialize(field, set(), readonly=True)
            self.assertEqual(html, '<span></span>')

            # with data, no actions
            user = model.User(username='barney')
            self.session.add(user)
            self.session.commit()
            html = widget.serialize(field, {user.uuid}, readonly=True)
            self.assertIn('<b-table ', html)
            self.assertNotIn('Actions', html)
            self.assertNotIn('View', html)
            self.assertNotIn('Edit', html)

            # with view/edit actions
            with patch.object(self.request, 'is_root', new=True):
                html = widget.serialize(field, {user.uuid}, readonly=True)
            self.assertIn('<b-table ', html)
            self.assertIn('Actions', html)
            self.assertIn('View', html)
            self.assertIn('Edit', html)


class TestPermissionsWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):
        permissions = {
            'widgets': {
                'label': "Widgets",
                'perms': {
                    'widgets.polish': {
                        'label': "Polish the widgets",
                    },
                },
            },
        }

        # nb. we let the field construct the widget via our type
        node = colander.SchemaNode(Permissions(self.request, permissions))
        field = self.make_field(node)
        widget = field.widget

        # readonly output does *not* include the perm by default
        html = widget.serialize(field, set(), readonly=True)
        self.assertNotIn("Polish the widgets", html)

        # readonly output includes the perm if set
        html = widget.serialize(field, {'widgets.polish'}, readonly=True)
        self.assertIn("Polish the widgets", html)

        # editable output always includes the perm
        html = widget.serialize(field, set())
        self.assertIn("Polish the widgets", html)


class TestEmailRecipientsWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):
        node = colander.SchemaNode(EmailRecipients())
        field = self.make_field(node)
        widget = mod.EmailRecipientsWidget()

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

        # readonly
        result = widget.serialize(field, recips_str, readonly=True)
        self.assertIn('<ul>', result)
        self.assertIn('<li>alice@example.com</li>', result)

        # editable
        result = widget.serialize(field, recips_str)
        self.assertIn('<b-input', result)
        self.assertIn('type="textarea"', result)

    def test_deserialize(self):
        node = colander.SchemaNode(EmailRecipients())
        field = self.make_field(node)
        widget = mod.EmailRecipientsWidget()

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

        # values
        result = widget.deserialize(field, recips_str)
        self.assertEqual(result, recips_str)

        # null
        result = widget.deserialize(field, colander.null)
        self.assertIs(result, colander.null)


class TestBatchIdWidget(WebTestCase):

    def make_field(self, node, **kwargs):
        # TODO: not sure why default renderer is in use even though
        # pyramid_deform was included in setup?  but this works..
        kwargs.setdefault('renderer', deform.Form.default_renderer)
        return deform.Field(node, **kwargs)

    def test_serialize(self):
        node = colander.SchemaNode(colander.Integer())
        field = self.make_field(node)
        widget = mod.BatchIdWidget()

        result = widget.serialize(field, colander.null)
        self.assertIs(result, colander.null)

        result = widget.serialize(field, 42)
        self.assertEqual(result, '00000042')