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

from unittest import TestCase
from unittest.mock import patch

from sqlalchemy import orm
from paginate import Page
from paginate_sqlalchemy import SqlalchemyOrmPage
from pyramid import testing

from wuttjamaican.conf import WuttaConfig
from wuttaweb.grids import base as mod
from wuttaweb.forms import FieldList
from tests.util import WebTestCase


class TestGrid(WebTestCase):

    def make_grid(self, request=None, **kwargs):
        return mod.Grid(request or self.request, **kwargs)

    def test_constructor(self):

        # empty
        grid = self.make_grid()
        self.assertIsNone(grid.key)
        self.assertEqual(grid.columns, [])
        self.assertIsNone(grid.data)

        # now with columns
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertIsInstance(grid.columns, FieldList)
        self.assertEqual(grid.columns, ['foo', 'bar'])

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

        # defaults, not sortable
        grid = self.make_grid()
        self.assertFalse(grid.sortable)
        self.assertTrue(grid.sort_on_backend)
        self.assertEqual(grid.sorters, {})
        self.assertEqual(grid.sort_defaults, [])

        # defaults, sortable
        grid = self.make_grid(sortable=True)
        self.assertTrue(grid.sortable)
        self.assertTrue(grid.sort_on_backend)
        self.assertEqual(grid.sorters, {})
        self.assertEqual(grid.sort_defaults, [])

        # sorters may be pre-populated
        grid = self.make_grid(model_class=model.Setting, sortable=True)
        self.assertEqual(len(grid.sorters), 2)
        self.assertIn('name', grid.sorters)
        self.assertIn('value', grid.sorters)
        self.assertEqual(grid.sort_defaults, [])

        # sort defaults as str
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_defaults='name')
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])

        # sort defaults as tuple
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_defaults=('name', 'desc'))
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])

        # sort defaults as list w/ single tuple
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_defaults=[('name', 'desc')])
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])

        # multi-column defaults
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_multiple=True,
                              sort_defaults=[('name', 'desc'), ('value', 'asc')])
        self.assertTrue(grid.sort_multiple)
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc'),
                                              mod.SortInfo('value', 'asc')])

        # multi-column sort disabled for oruga
        self.request.use_oruga = True
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_multiple=True)
        self.assertFalse(grid.sort_multiple)

    def test_vue_tagname(self):
        grid = self.make_grid()
        self.assertEqual(grid.vue_tagname, 'wutta-grid')

    def test_vue_component(self):
        grid = self.make_grid()
        self.assertEqual(grid.vue_component, 'WuttaGrid')

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

        # empty
        grid = self.make_grid()
        self.assertEqual(grid.columns, [])
        self.assertEqual(grid.get_columns(), [])

        # explicit
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertEqual(grid.columns, ['foo', 'bar'])
        self.assertEqual(grid.get_columns(), ['foo', 'bar'])

        # derived from model
        grid = self.make_grid(model_class=model.Setting)
        self.assertEqual(grid.columns, ['name', 'value'])
        self.assertEqual(grid.get_columns(), ['name', 'value'])

    def test_append(self):
        grid = self.make_grid(columns=['one', 'two'])
        self.assertEqual(grid.columns, ['one', 'two'])
        grid.append('one', 'two', 'three')
        self.assertEqual(grid.columns, ['one', 'two', 'three'])

    def test_remove(self):
        grid = self.make_grid(columns=['one', 'two', 'three', 'four'])
        self.assertEqual(grid.columns, ['one', 'two', 'three', 'four'])
        grid.remove('two', 'three')
        self.assertEqual(grid.columns, ['one', 'four'])

    def test_set_label(self):
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertEqual(grid.labels, {})

        # basic
        grid.set_label('foo', "Foo Fighters")
        self.assertEqual(grid.labels['foo'], "Foo Fighters")

        # can replace label
        grid.set_label('foo', "Different")
        self.assertEqual(grid.labels['foo'], "Different")
        self.assertEqual(grid.get_label('foo'), "Different")

    def test_get_label(self):
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertEqual(grid.labels, {})

        # default derived from key
        self.assertEqual(grid.get_label('foo'), "Foo")

        # can override
        grid.set_label('foo', "Different")
        self.assertEqual(grid.get_label('foo'), "Different")

    def test_set_renderer(self):
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertEqual(grid.renderers, {})

        def render1(record, key, value):
            pass

        # basic
        grid.set_renderer('foo', render1)
        self.assertIs(grid.renderers['foo'], render1)

        def render2(record, key, value, extra=None):
            return extra

        # can pass kwargs to get a partial
        grid.set_renderer('foo', render2, extra=42)
        self.assertIsNot(grid.renderers['foo'], render2)
        self.assertEqual(grid.renderers['foo'](None, None, None), 42)

    def test_linked_columns(self):
        grid = self.make_grid(columns=['foo', 'bar'])
        self.assertEqual(grid.linked_columns, [])
        self.assertFalse(grid.is_linked('foo'))

        grid.set_link('foo')
        self.assertEqual(grid.linked_columns, ['foo'])
        self.assertTrue(grid.is_linked('foo'))
        self.assertFalse(grid.is_linked('bar'))

        grid.set_link('bar')
        self.assertEqual(grid.linked_columns, ['foo', 'bar'])
        self.assertTrue(grid.is_linked('foo'))
        self.assertTrue(grid.is_linked('bar'))

        grid.set_link('foo', False)
        self.assertEqual(grid.linked_columns, ['bar'])
        self.assertFalse(grid.is_linked('foo'))
        self.assertTrue(grid.is_linked('bar'))

    def test_get_pagesize_options(self):
        grid = self.make_grid()

        # default
        options = grid.get_pagesize_options()
        self.assertEqual(options, [5, 10, 20, 50, 100, 200])

        # override default
        options = grid.get_pagesize_options(default=[42])
        self.assertEqual(options, [42])

        # from config
        self.config.setdefault('wuttaweb.grids.default_pagesize_options', '1 2 3')
        options = grid.get_pagesize_options()
        self.assertEqual(options, [1, 2, 3])

    def test_get_pagesize(self):
        grid = self.make_grid()

        # default
        size = grid.get_pagesize()
        self.assertEqual(size, 20)

        # override default
        size = grid.get_pagesize(default=42)
        self.assertEqual(size, 42)

        # override default options
        self.config.setdefault('wuttaweb.grids.default_pagesize_options', '10 15 30')
        grid = self.make_grid()
        size = grid.get_pagesize()
        self.assertEqual(size, 10)

        # from config
        self.config.setdefault('wuttaweb.grids.default_pagesize', '15')
        size = grid.get_pagesize()
        self.assertEqual(size, 15)

    ##############################
    # configuration methods
    ##############################

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

        # nb. first use a paging grid
        grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True,
                              pagesize=20, page=1)

        # settings are loaded, applied, saved
        self.assertEqual(grid.page, 1)
        self.assertNotIn('grid.foo.page', self.request.session)
        self.request.GET = {'pagesize': '10', 'page': '2'}
        grid.load_settings()
        self.assertEqual(grid.page, 2)
        self.assertEqual(self.request.session['grid.foo.page'], 2)

        # can skip the saving step
        self.request.GET = {'pagesize': '10', 'page': '3'}
        grid.load_settings(persist=False)
        self.assertEqual(grid.page, 3)
        self.assertEqual(self.request.session['grid.foo.page'], 2)

        # no error for non-paginated grid
        grid = self.make_grid(key='foo', paginated=False)
        grid.load_settings()
        self.assertFalse(grid.paginated)

        # nb. next use a sorting grid
        grid = self.make_grid(key='settings', model_class=model.Setting,
                              sortable=True, sort_on_backend=True)

        # settings are loaded, applied, saved
        self.assertEqual(grid.sort_defaults, [])
        self.assertFalse(hasattr(grid, 'active_sorters'))
        self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'}
        grid.load_settings()
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
        self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
        self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
        self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')

        # can skip the saving step
        self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'}
        grid.load_settings(persist=False)
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
        self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
        self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
        self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')

        # no error for non-sortable grid
        grid = self.make_grid(key='foo', sortable=False)
        grid.load_settings()
        self.assertFalse(grid.sortable)

        # with sort defaults
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_on_backend=True, sort_defaults='name')
        self.assertFalse(hasattr(grid, 'active_sorters'))
        grid.load_settings()
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])

        # with multi-column sort defaults
        grid = self.make_grid(model_class=model.Setting, sortable=True,
                              sort_on_backend=True)
        grid.sort_defaults = [
            mod.SortInfo('name', 'asc'),
            mod.SortInfo('value', 'desc'),
        ]
        self.assertFalse(hasattr(grid, 'active_sorters'))
        grid.load_settings()
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])

        # load settings from session when nothing is in request
        self.request.GET = {}
        self.request.session.invalidate()
        self.assertNotIn('grid.settings.sorters.length', self.request.session)
        self.request.session['grid.settings.sorters.length'] = 1
        self.request.session['grid.settings.sorters.1.key'] = 'name'
        self.request.session['grid.settings.sorters.1.dir'] = 'desc'
        grid = self.make_grid(key='settings', model_class=model.Setting,
                              sortable=True, sort_on_backend=True,
                              paginated=True, paginate_on_backend=True)
        self.assertFalse(hasattr(grid, 'active_sorters'))
        grid.load_settings()
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])

    def test_request_has_settings(self):
        grid = self.make_grid(key='foo')

        # paging
        self.assertFalse(grid.request_has_settings('page'))
        with patch.object(self.request, 'GET', new={'pagesize': '20'}):
            self.assertTrue(grid.request_has_settings('page'))
        with patch.object(self.request, 'GET', new={'page': '1'}):
            self.assertTrue(grid.request_has_settings('page'))

        # sorting
        self.assertFalse(grid.request_has_settings('sort'))
        with patch.object(self.request, 'GET', new={'sort1key': 'name'}):
            self.assertTrue(grid.request_has_settings('sort'))

    def test_get_setting(self):
        grid = self.make_grid(key='foo')
        settings = {}

        # default is null
        value = grid.get_setting(settings, 'pagesize')
        self.assertIsNone(value)

        # can read value from user session
        self.request.session['grid.foo.pagesize'] = 15
        value = grid.get_setting(settings, 'pagesize', src='session')
        self.assertEqual(value, 15)

        # string value not normalized
        self.request.session['grid.foo.pagesize'] = '15'
        value = grid.get_setting(settings, 'pagesize', src='session')
        self.assertEqual(value, '15')
        self.assertNotEqual(value, 15)

        # but can be normalized
        self.request.session['grid.foo.pagesize'] = '15'
        value = grid.get_setting(settings, 'pagesize', src='session', normalize=int)
        self.assertEqual(value, 15)

        # can read value from request
        self.request.GET = {'pagesize': '25'}
        value = grid.get_setting(settings, 'pagesize', src='request', normalize=int)
        self.assertEqual(value, 25)

        # null when normalization fails
        self.request.GET = {'pagesize': 'invalid'}
        value = grid.get_setting(settings, 'pagesize', src='request', normalize=int)
        self.assertIsNone(value)

        # reset
        del self.request.session['grid.foo.pagesize']
        self.request.GET = {}

        # value can come from provided settings
        settings['pagesize'] = '35'
        value = grid.get_setting(settings, 'pagesize', src='session', normalize=int)
        self.assertEqual(value, 35)

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

        # nothing happens if not sortable
        grid = self.make_grid(key='settings', model_class=model.Setting)
        settings = {'sorters.length': 0}
        self.request.session['grid.settings.sorters.length'] = 1
        self.request.session['grid.settings.sorters.1.key'] = 'name'
        self.request.session['grid.settings.sorters.1.dir'] = 'asc'
        grid.update_sort_settings(settings, src='session')
        self.assertEqual(settings['sorters.length'], 0)

        # nb. now use a sortable grid
        grid = self.make_grid(key='settings', model_class=model.Setting,
                              sortable=True, sort_on_backend=True)

        # settings are updated from session
        settings = {'sorters.length': 1, 'sorters.1.key': 'name', 'sorters.1.dir': 'asc'}
        self.request.session['grid.settings.sorters.length'] = 1
        self.request.session['grid.settings.sorters.1.key'] = 'name'
        self.request.session['grid.settings.sorters.1.dir'] = 'asc'
        grid.update_sort_settings(settings, src='session')
        self.assertEqual(settings['sorters.length'], 1)
        self.assertEqual(settings['sorters.1.key'], 'name')
        self.assertEqual(settings['sorters.1.dir'], 'asc')

        # settings are updated from request
        self.request.GET = {'sort1key': 'value', 'sort1dir': 'desc'}
        grid.update_sort_settings(settings, src='request')
        self.assertEqual(settings['sorters.length'], 1)
        self.assertEqual(settings['sorters.1.key'], 'value')
        self.assertEqual(settings['sorters.1.dir'], 'desc')

    def test_update_page_settings(self):

        # nothing happens if not paginated
        grid = self.make_grid(key='foo')
        settings = {'pagesize': 20, 'page': 1}
        self.request.session['grid.foo.pagesize'] = 10
        self.request.session['grid.foo.page'] = 2
        grid.update_page_settings(settings)
        self.assertEqual(settings['pagesize'], 20)
        self.assertEqual(settings['page'], 1)

        # nb. now use a paginated grid
        grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)

        # settings are updated from session
        settings = {'pagesize': 20, 'page': 1}
        self.request.session['grid.foo.pagesize'] = 10
        self.request.session['grid.foo.page'] = 2
        grid.update_page_settings(settings)
        self.assertEqual(settings['pagesize'], 10)
        self.assertEqual(settings['page'], 2)

        # settings are updated from request
        self.request.GET = {'pagesize': '15', 'page': '4'}
        grid.update_page_settings(settings)
        self.assertEqual(settings['pagesize'], 15)
        self.assertEqual(settings['page'], 4)

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

        # nb. start out with paginated-only grid
        grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)

        # invalid dest
        self.assertRaises(ValueError, grid.persist_settings, {}, dest='doesnotexist')

        # nb. no error if empty settings, but it saves null values
        grid.persist_settings({}, dest='session')
        self.assertIsNone(self.request.session['grid.foo.page'])

        # provided values are saved
        grid.persist_settings({'pagesize': 15, 'page': 3}, dest='session')
        self.assertEqual(self.request.session['grid.foo.page'], 3)

        # nb. now switch to sortable-only grid
        grid = self.make_grid(key='settings', model_class=model.Setting,
                              sortable=True, sort_on_backend=True)

        # no error if empty settings; does not save values
        grid.persist_settings({}, dest='session')
        self.assertNotIn('grid.settings.sorters.length', self.request.session)

        # provided values are saved
        grid.persist_settings({'sorters.length': 2,
                               'sorters.1.key': 'name',
                               'sorters.1.dir': 'desc',
                               'sorters.2.key': 'value',
                               'sorters.2.dir': 'asc'},
                              dest='session')
        self.assertEqual(self.request.session['grid.settings.sorters.length'], 2)
        self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
        self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
        self.assertEqual(self.request.session['grid.settings.sorters.2.key'], 'value')
        self.assertEqual(self.request.session['grid.settings.sorters.2.dir'], 'asc')

        # old values removed when new are saved
        grid.persist_settings({'sorters.length': 1,
                               'sorters.1.key': 'name',
                               'sorters.1.dir': 'desc'},
                              dest='session')
        self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
        self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
        self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
        self.assertNotIn('grid.settings.sorters.2.key', self.request.session)
        self.assertNotIn('grid.settings.sorters.2.dir', self.request.session)

    ##############################
    # sorting methods
    ##############################

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

        # default is empty
        grid = self.make_grid()
        sorters = grid.make_backend_sorters()
        self.assertEqual(sorters, {})

        # makes sorters if model class
        grid = self.make_grid(model_class=model.Setting)
        sorters = grid.make_backend_sorters()
        self.assertEqual(len(sorters), 2)
        self.assertIn('name', sorters)
        self.assertIn('value', sorters)

        # does not replace supplied sorters
        grid = self.make_grid(model_class=model.Setting)
        mysorters = {'value': 42}
        sorters = grid.make_backend_sorters(mysorters)
        self.assertEqual(len(sorters), 2)
        self.assertIn('name', sorters)
        self.assertIn('value', sorters)
        self.assertEqual(sorters['value'], 42)
        self.assertEqual(mysorters['value'], 42)

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

        # plain data
        grid = self.make_grid(columns=['name', 'value'])
        sorter = grid.make_sorter('name')
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})

        # model class, but still plain data
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter('name')
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})

        # repeat previous test, w/ model property
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter(model.Setting.name)
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})

        # sqlalchemy query
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter('name')
        sorted_query = sorter(sample_query, 'desc')
        sorted_data = sorted_query.all()
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo9', 'value': 'nine'})
        sorted_query = sorter(sample_query, 'asc')
        sorted_data = sorted_query.all()
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})

        # repeat previous test, w/ model property
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter(model.Setting.name)
        sorted_query = sorter(sample_query, 'desc')
        sorted_data = sorted_query.all()
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo9', 'value': 'nine'})
        sorted_query = sorter(sample_query, 'asc')
        sorted_data = sorted_query.all()
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})

        # sortfunc for "invalid" column will fail when called; however
        # it can work for manual sort w/ custom keyfunc
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter('doesnotexist')
        self.assertRaises(TypeError, sorter, sample_query, 'desc')
        self.assertRaises(KeyError, sorter, sample_data, 'desc')
        sorter = grid.make_sorter('doesnotexist', keyfunc=lambda obj: obj['name'])
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(len(sorted_data), 9)
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(len(sorted_data), 9)

        # case folding is on by default
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter('value')
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})

        # results are different with case folding off
        grid = self.make_grid(model_class=model.Setting)
        sorter = grid.make_sorter('value', foldcase=False)
        sorted_data = sorter(sample_data, 'desc')
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
        sorted_data = sorter(sample_data, 'asc')
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})

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

        # explicit sortfunc
        grid = self.make_grid()
        self.assertEqual(grid.sorters, {})
        sortfunc = lambda data, direction: data
        grid.set_sorter('foo', sortfunc)
        self.assertIs(grid.sorters['foo'], sortfunc)

        # auto from model property
        grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
        self.assertEqual(grid.sorters, {})
        grid.set_sorter('name', model.Setting.name)
        self.assertTrue(callable(grid.sorters['name']))

        # auto from column name
        grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
        self.assertEqual(grid.sorters, {})
        grid.set_sorter('name', 'name')
        self.assertTrue(callable(grid.sorters['name']))

        # auto from key
        grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
        self.assertEqual(grid.sorters, {})
        grid.set_sorter('name')
        self.assertTrue(callable(grid.sorters['name']))

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

        # basics
        grid = self.make_grid(model_class=model.Setting, sortable=True)
        self.assertEqual(len(grid.sorters), 2)
        self.assertIn('name', grid.sorters)
        self.assertIn('value', grid.sorters)
        grid.remove_sorter('value')
        self.assertNotIn('value', grid.sorters)

    def test_set_sort_defaults(self):
        model = self.app.model
        grid = self.make_grid(model_class=model.Setting, sortable=True)
        self.assertEqual(grid.sort_defaults, [])

        # can set just sortkey
        grid.set_sort_defaults('name')
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])

        # can set sortkey, sortdir
        grid.set_sort_defaults('name', 'desc')
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])

        # can set sortkey, sortdir as tuple
        grid.set_sort_defaults(('value', 'asc'))
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')])

        # can set as list
        grid.sort_multiple = True
        grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')])
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc'),
                                              mod.SortInfo('name', 'desc')])

        # list is pruned if multi-sort disabled
        grid.sort_multiple = False
        grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')])
        self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')])

        # error if any other single arg
        self.assertRaises(ValueError, grid.set_sort_defaults, 42)

        # error if more than 2 args
        self.assertRaises(ValueError, grid.set_sort_defaults, 'name', 'asc', 'value', 'desc')

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

        # basics, frontend sorting
        grid = self.make_grid(model_class=model.Setting, sortable=True, sort_on_backend=False)
        self.assertTrue(grid.is_sortable('name'))
        self.assertTrue(grid.is_sortable('value'))
        grid.remove_sorter('value')
        # nb. columns are always sortable for frontend, despite remove_sorter()
        self.assertTrue(grid.is_sortable('value'))
        # nb. when grid is not sortable, no column is either
        grid.sortable = False
        self.assertFalse(grid.is_sortable('name'))

        # same test but with backend sorting
        grid = self.make_grid(model_class=model.Setting, sortable=True, sort_on_backend=True)
        self.assertTrue(grid.is_sortable('name'))
        self.assertTrue(grid.is_sortable('value'))
        grid.remove_sorter('value')
        self.assertFalse(grid.is_sortable('value'))
        # nb. when grid is not sortable, no column is either
        grid.sortable = False
        self.assertFalse(grid.is_sortable('name'))

    ##############################
    # data methods
    ##############################

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

        # data is sorted and paginated
        grid = self.make_grid(model_class=model.Setting,
                              data=sample_query,
                              sortable=True, sort_on_backend=True,
                              sort_defaults=('name', 'desc'),
                              paginated=True, paginate_on_backend=True,
                              pagesize=4, page=2)
        grid.load_settings()
        visible = grid.get_visible_data()
        self.assertEqual([s.name for s in visible], ['foo5', 'foo4', 'foo3', 'foo2'])

    def test_sort_data(self):
        model = self.app.model
        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 sample_data:
            self.app.save_setting(self.session, setting['name'], setting['value'])
        self.session.commit()
        sample_query = self.session.query(model.Setting)

        grid = self.make_grid(model_class=model.Setting,
                              sortable=True, sort_on_backend=True,
                              sort_defaults=('name', 'desc'))
        grid.load_settings()

        # can sort a simple list of data
        sorted_data = grid.sort_data(sample_data)
        self.assertIsInstance(sorted_data, list)
        self.assertEqual(len(sorted_data), 9)
        self.assertEqual(sorted_data[0]['name'], 'foo9')
        self.assertEqual(sorted_data[-1]['name'], 'foo1')

        # can also sort a data query
        sorted_query = grid.sort_data(sample_query)
        self.assertIsInstance(sorted_query, orm.Query)
        sorted_data = sorted_query.all()
        self.assertEqual(len(sorted_data), 9)
        self.assertEqual(sorted_data[0]['name'], 'foo9')
        self.assertEqual(sorted_data[-1]['name'], 'foo1')

        # cannot sort data if sorter missing in overrides
        sorted_data = grid.sort_data(sample_data, sorters=[])
        # nb. sorted data is in same order as original sample (not sorted)
        self.assertEqual(sorted_data[0]['name'], 'foo1')
        self.assertEqual(sorted_data[-1]['name'], 'foo9')

        # multi-column sorting for list data
        sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
                                                           {'key': 'name', 'dir': 'asc'}])
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
        self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
        self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
        self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})

        # multi-column sorting for query
        sorted_query = grid.sort_data(sample_query, sorters=[{'key': 'value', 'dir': 'asc'},
                                                             {'key': 'name', 'dir': 'asc'}])
        self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
        self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
        self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
        self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})

        # cannot sort data if sortfunc is missing for column
        grid.remove_sorter('name')
        sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
                                                           {'key': 'name', 'dir': 'asc'}])
        # nb. sorted data is in same order as original sample (not sorted)
        self.assertEqual(sorted_data[0]['name'], 'foo1')
        self.assertEqual(sorted_data[-1]['name'], 'foo9')

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

        # basic list pager
        grid = self.make_grid(paginated=True, paginate_on_backend=True)
        pager = grid.paginate_data(sample_data)
        self.assertIsInstance(pager, Page)

        # basic query pager
        grid = self.make_grid(paginated=True, paginate_on_backend=True)
        pager = grid.paginate_data(sample_query)
        self.assertIsInstance(pager, SqlalchemyOrmPage)

        # page is reset to 1 for empty data
        self.request.session['grid.foo.page'] = 2
        grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
        grid.load_settings()
        self.assertEqual(grid.page, 2)
        self.assertEqual(self.request.session['grid.foo.page'], 2)
        pager = grid.paginate_data(sample_data)
        self.assertEqual(pager.page, 1)
        self.assertEqual(grid.page, 1)
        self.assertEqual(self.request.session['grid.foo.page'], 1)

    ##############################
    # rendering methods
    ##############################

    def test_render_vue_tag(self):
        grid = self.make_grid(columns=['foo', 'bar'])
        html = grid.render_vue_tag()
        self.assertEqual(html, '<wutta-grid></wutta-grid>')

    def test_render_vue_template(self):
        self.pyramid_config.include('pyramid_mako')
        self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
                                           'pyramid.events.BeforeRender')

        grid = self.make_grid(columns=['foo', 'bar'])
        html = grid.render_vue_template()
        self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)

    def test_get_vue_columns(self):

        # error if no columns are set
        grid = self.make_grid()
        self.assertRaises(ValueError, grid.get_vue_columns)

        # otherwise get back field/label dicts
        grid = self.make_grid(columns=['foo', 'bar'])
        columns = grid.get_vue_columns()
        first = columns[0]
        self.assertEqual(first['field'], 'foo')
        self.assertEqual(first['label'], 'Foo')

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

        # empty
        grid = self.make_grid(key='foo', sortable=True, sort_on_backend=True)
        grid.load_settings()
        sorters = grid.get_vue_active_sorters()
        self.assertEqual(sorters, [])

        # format is different
        grid = self.make_grid(key='settings', model_class=model.Setting,
                              sortable=True, sort_on_backend=True,
                              sort_defaults='name')
        grid.load_settings()
        self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
        sorters = grid.get_vue_active_sorters()
        self.assertEqual(sorters, [{'field': 'name', 'order': 'asc'}])

    def test_get_vue_data(self):

        # empty if no columns defined
        grid = self.make_grid()
        data = grid.get_vue_data()
        self.assertEqual(data, [])

        # typical data is a list
        mydata = [
            {'foo': 'bar'},
        ]
        grid = self.make_grid(columns=['foo'], data=mydata)
        data = grid.get_vue_data()
        self.assertEqual(data, [{'foo': 'bar'}])

        # if grid has actions, that list may be supplemented
        grid.actions.append(mod.GridAction(self.request, 'view', url='/blarg'))
        data = grid.get_vue_data()
        self.assertIsNot(data, mydata)
        self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])

        # also can override value rendering
        grid.set_renderer('foo', lambda record, key, value: "blah blah")
        data = grid.get_vue_data()
        self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])

    def test_get_vue_pager_stats(self):
        data = [
            {'foo': 1, 'bar': 1},
            {'foo': 2, 'bar': 2},
            {'foo': 3, 'bar': 3},
            {'foo': 4, 'bar': 4},
            {'foo': 5, 'bar': 5},
            {'foo': 6, 'bar': 6},
            {'foo': 7, 'bar': 7},
            {'foo': 8, 'bar': 8},
            {'foo': 9, 'bar': 9},
        ]

        grid = self.make_grid(columns=['foo', 'bar'], pagesize=4, page=2)
        grid.pager = grid.paginate_data(data)
        stats = grid.get_vue_pager_stats()
        self.assertEqual(stats['item_count'], 9)
        self.assertEqual(stats['items_per_page'], 4)
        self.assertEqual(stats['page'], 2)
        self.assertEqual(stats['first_item'], 5)
        self.assertEqual(stats['last_item'], 8)


class TestGridAction(TestCase):

    def setUp(self):
        self.config = WuttaConfig()
        self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)

    def make_action(self, key, **kwargs):
        return mod.GridAction(self.request, key, **kwargs)

    def test_render_icon(self):

        # icon is derived from key by default
        action = self.make_action('blarg')
        html = action.render_icon()
        self.assertIn('<i class="fas fa-blarg">', html)

        # oruga has different output
        self.request.use_oruga = True
        html = action.render_icon()
        self.assertIn('<o-icon icon="blarg">', html)

    def test_render_label(self):

        # label is derived from key by default
        action = self.make_action('blarg')
        label = action.render_label()
        self.assertEqual(label, "Blarg")

        # otherwise use what caller provides
        action = self.make_action('foo', label="Bar")
        label = action.render_label()
        self.assertEqual(label, "Bar")

    def test_render_icon_and_label(self):
        action = self.make_action('blarg')
        with patch.multiple(action,
                            render_icon=lambda: 'ICON',
                            render_label=lambda: 'LABEL'):
            html = action.render_icon_and_label()
            self.assertEqual('ICON LABEL', html)

    def test_get_url(self):
        obj = {'foo': 'bar'}

        # null by default
        action = self.make_action('blarg')
        url = action.get_url(obj)
        self.assertIsNone(url)

        # or can be "static"
        action = self.make_action('blarg', url='/foo')
        url = action.get_url(obj)
        self.assertEqual(url, '/foo')

        # or can be "dynamic"
        action = self.make_action('blarg', url=lambda o, i: '/yeehaw')
        url = action.get_url(obj)
        self.assertEqual(url, '/yeehaw')
