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

from unittest.mock import patch

from sqlalchemy import orm

import colander

from wuttaweb.grids import Grid
from wuttaweb.views import users as mod
from wuttaweb.testing import WebTestCase


class TestUserView(WebTestCase):

    def make_view(self):
        return mod.UserView(self.request)

    def test_includeme(self):
        self.pyramid_config.include('wuttaweb.views.users')

    def test_get_query(self):
        view = self.make_view()
        query = view.get_query(session=self.session)
        self.assertIsInstance(query, orm.Query)

    def test_configure_grid(self):
        model = self.app.model
        view = self.make_view()
        grid = view.make_grid(model_class=model.User)
        self.assertFalse(grid.is_linked('person'))
        view.configure_grid(grid)
        self.assertTrue(grid.is_linked('person'))

    def test_grid_row_class(self):
        model = self.app.model
        user = model.User(username='barney', active=True)
        data = dict(user)
        view = self.make_view()

        self.assertIsNone(view.grid_row_class(user, data, 1))

        user.active = False
        self.assertEqual(view.grid_row_class(user, data, 1), 'has-background-warning')

    def test_is_editable(self):
        model = self.app.model
        view = self.make_view()

        # active user is editable
        user = model.User(username='barney', active=True)
        self.assertTrue(view.is_editable(user))

        # inactive also editable
        user = model.User(username='barney', active=False)
        self.assertTrue(view.is_editable(user))

        # but not if prevent_edit flag is set
        user = model.User(username='barney', prevent_edit=True)
        self.assertFalse(view.is_editable(user))

        # unless request user is root
        self.request.is_root = True
        self.assertTrue(view.is_editable(user))

    def test_configure_form(self):
        model = self.app.model
        person = model.Person(first_name='Barney', last_name='Rubble', full_name='Barney Rubble')
        barney = model.User(username='barney', person=person)
        self.session.add(barney)
        self.session.commit()
        view = self.make_view()

        # person replaced with first/last name when creating or editing
        with patch.object(view, 'viewing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('person', form)
            self.assertNotIn('first_name', form)
            self.assertNotIn('last_name', form)
            view.configure_form(form)
            self.assertIn('person', form)
            self.assertNotIn('first_name', form)
            self.assertNotIn('last_name', form)
        with patch.object(view, 'creating', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('person', form)
            self.assertNotIn('first_name', form)
            self.assertNotIn('last_name', form)
            view.configure_form(form)
            self.assertNotIn('person', form)
            self.assertIn('first_name', form)
            self.assertIn('last_name', form)
        with patch.object(view, 'editing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('person', form)
            self.assertNotIn('first_name', form)
            self.assertNotIn('last_name', form)
            view.configure_form(form)
            self.assertNotIn('person', form)
            self.assertIn('first_name', form)
            self.assertIn('last_name', form)

        # first/last name have default values when editing
        with patch.object(view, 'editing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertNotIn('first_name', form.defaults)
            self.assertNotIn('last_name', form.defaults)
            view.configure_form(form)
            self.assertIn('first_name', form.defaults)
            self.assertEqual(form.defaults['first_name'], 'Barney')
            self.assertIn('last_name', form.defaults)
            self.assertEqual(form.defaults['last_name'], 'Rubble')

        # password removed (always, for now)
        with patch.object(view, 'viewing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('password', form)
            view.configure_form(form)
            self.assertNotIn('password', form)
        with patch.object(view, 'editing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('password', form)
            view.configure_form(form)
            self.assertNotIn('password', form)

        # api tokens grid shown only if current user has perm
        with patch.object(view, 'viewing', new=True):
            form = view.make_form(model_instance=barney)
            self.assertIn('api_tokens', form)
            view.configure_form(form)
            self.assertNotIn('api_tokens', form)
            with patch.object(self.request, 'is_root', new=True):
                form = view.make_form(model_instance=barney)
                self.assertIn('api_tokens', form)
                view.configure_form(form)
                self.assertIn('api_tokens', form)

    def test_unique_username(self):
        model = self.app.model
        view = self.make_view()

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

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

            # invalid if same username in data
            node = colander.SchemaNode(colander.String(), name='username')
            self.assertRaises(colander.Invalid, view.unique_username, node, 'foo')

            # but not if username belongs to current user
            view.editing = True
            self.request.matchdict = {'uuid': user.uuid}
            node = colander.SchemaNode(colander.String(), name='username')
            self.assertIsNone(view.unique_username(node, 'foo'))

    def test_objectify(self):
        model = self.app.model
        auth = self.app.get_auth_handler()
        view = self.make_view()

        blokes = model.Role(name="Blokes")
        self.session.add(blokes)
        others = model.Role(name="Others")
        self.session.add(others)
        barney = model.User(username='barney')
        auth.set_user_password(barney, 'testpass')
        barney.roles.append(blokes)
        self.session.add(barney)
        self.session.commit()

        with patch.object(self.request, 'matchdict', new={'uuid': barney.uuid}):
            with patch.object(view, 'editing', new=True):

                # sanity check, user is just in 'blokes' role
                self.session.refresh(barney)
                self.assertEqual(len(barney.roles), 1)
                self.assertEqual(barney.roles[0].name, "Blokes")

                # form can update user password
                self.assertTrue(auth.check_user_password(barney, 'testpass'))
                form = view.make_model_form(model_instance=barney)
                form.validated = {'username': 'barney', 'set_password': 'testpass2'}
                with patch.object(view, 'Session', return_value=self.session):
                    user = view.objectify(form)
                self.assertIs(user, barney)
                self.assertTrue(auth.check_user_password(barney, 'testpass2'))

                # form can update user roles
                form = view.make_model_form(model_instance=barney)
                form.validated = {'username': 'barney', 'roles': {others.uuid}}
                with patch.object(view, 'Session', return_value=self.session):
                    user = view.objectify(form)
                self.assertIs(user, barney)
                self.assertEqual(len(user.roles), 1)
                self.assertEqual(user.roles[0].name, "Others")

                # person is auto-created
                self.assertIsNone(barney.person)
                form = view.make_model_form(model_instance=barney)
                form.validated = {'username': 'barney', 'first_name': 'Barney', 'last_name': 'Rubble'}
                with patch.object(view, 'Session', return_value=self.session):
                    user = view.objectify(form)
                self.assertIsNotNone(barney.person)
                self.assertEqual(barney.person.first_name, 'Barney')
                self.assertEqual(barney.person.last_name, 'Rubble')
                self.assertEqual(barney.person.full_name, 'Barney Rubble')

                # person is auto-removed
                self.assertIsNotNone(barney.person)
                form = view.make_model_form(model_instance=barney)
                form.validated = {'username': 'barney', 'first_name': '', 'last_name': ''}
                with patch.object(view, 'Session', return_value=self.session):
                    user = view.objectify(form)
                self.assertIsNone(barney.person)

                # nb. re-attach the person
                barney.person = self.session.query(model.Person).one()

                # person name is updated
                self.assertEqual(barney.person.first_name, 'Barney')
                self.assertEqual(barney.person.last_name, 'Rubble')
                self.assertEqual(barney.person.full_name, 'Barney Rubble')
                form = view.make_model_form(model_instance=barney)
                form.validated = {'username': 'barney', 'first_name': 'Fred', 'last_name': 'Flintstone'}
                with patch.object(view, 'Session', return_value=self.session):
                    user = view.objectify(form)
                self.assertIsNotNone(barney.person)
                self.assertEqual(barney.person.first_name, 'Fred')
                self.assertEqual(barney.person.last_name, 'Flintstone')
                self.assertEqual(barney.person.full_name, 'Fred Flintstone')

        with patch.object(view, 'creating', new=True):

            # person is auto-created when making new user
            form = view.make_model_form()
            form.validated = {'username': 'betty', 'first_name': 'Betty', 'last_name': 'Boop'}
            with patch.object(view, 'Session', return_value=self.session):
                user = view.objectify(form)
            self.assertIsNotNone(user.person)
            self.assertEqual(user.person.first_name, 'Betty')
            self.assertEqual(user.person.last_name, 'Boop')
            self.assertEqual(user.person.full_name, 'Betty Boop')

            # nb. keep ref to last user
            last_user = user

            # person is *not* auto-created if no name provided
            form = view.make_model_form()
            form.validated = {'username': 'betty', 'first_name': '', 'last_name': ''}
            with patch.object(view, 'Session', return_value=self.session):
                user = view.objectify(form)
            self.assertIsNone(user.person)
            self.assertIsNot(user, last_user)

    def test_update_roles(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)
        others = model.Role(name="Others")
        self.session.add(others)
        barney = model.User(username='barney')
        barney.roles.append(blokes)
        self.session.add(barney)
        self.session.commit()
        view = self.make_view()
        view.editing = True
        self.request.matchdict = {'uuid': barney.uuid}

        # no error if data is missing roles
        form = view.make_model_form(model_instance=barney)
        form.validated = {'username': 'barneyx'}
        user = view.objectify(form)
        self.assertIs(user, barney)
        self.assertEqual(barney.username, 'barneyx')

        # sanity check, user is just in 'blokes' role
        self.session.refresh(barney)
        self.assertEqual(len(barney.roles), 1)
        self.assertEqual(barney.roles[0].name, "Blokes")

        # let's test a bunch at once to ensure:
        # - user roles are updated
        # - authed / anon roles are not added
        # - admin role not added if current user is not root
        form = view.make_model_form(model_instance=barney)
        form.validated = {'username': 'barney',
                          'roles': {admin.uuid, authed.uuid, anon.uuid, others.uuid}}
        with patch.object(view, 'Session', return_value=self.session):
            user = view.objectify(form)
        self.assertIs(user, barney)
        self.assertEqual(len(user.roles), 1)
        self.assertEqual(user.roles[0].name, "Others")

        # let's test a bunch at once to ensure:
        # - user roles are updated
        # - admin role is added if current user is root
        self.request.is_root = True
        form = view.make_model_form(model_instance=barney)
        form.validated = {'username': 'barney',
                          'roles': {admin.uuid, blokes.uuid, others.uuid}}
        with patch.object(view, 'Session', return_value=self.session):
            user = view.objectify(form)
        self.assertIs(user, barney)
        self.assertEqual(len(user.roles), 3)
        role_uuids = set([role.uuid for role in user.roles])
        self.assertEqual(role_uuids, {admin.uuid, blokes.uuid, others.uuid})

        # admin role not removed if current user is not root
        self.request.is_root = False
        form = view.make_model_form(model_instance=barney)
        form.validated = {'username': 'barney',
                          'roles': {blokes.uuid, others.uuid}}
        with patch.object(view, 'Session', return_value=self.session):
            user = view.objectify(form)
        self.assertIs(user, barney)
        self.assertEqual(len(user.roles), 3)

        # admin role is removed if current user is root
        self.request.is_root = True
        form = view.make_model_form(model_instance=barney)
        form.validated = {'username': 'barney',
                          'roles': {blokes.uuid, others.uuid}}
        with patch.object(view, 'Session', return_value=self.session):
            user = view.objectify(form)
        self.assertIs(user, barney)
        self.assertEqual(len(user.roles), 2)

    def test_normalize_api_token(self):
        model = self.app.model
        auth = self.app.get_auth_handler()
        view = self.make_view()

        user = model.User(username='foo')
        self.session.add(user)
        token = auth.add_api_token(user, 'test token')
        self.session.commit()

        normal = view.normalize_api_token(token)
        self.assertIn('uuid', normal)
        self.assertEqual(normal['uuid'], token.uuid.hex)
        self.assertIn('description', normal)
        self.assertEqual(normal['description'], 'test token')
        self.assertIn('created', normal)

    def test_make_api_tokens_grid(self):
        model = self.app.model
        auth = self.app.get_auth_handler()
        view = self.make_view()

        user = model.User(username='foo')
        self.session.add(user)
        token1 = auth.add_api_token(user, 'test1')
        token2 = auth.add_api_token(user, 'test2')
        self.session.commit()

        # grid should have 2 records but no tools/actions
        grid = view.make_api_tokens_grid(user)
        self.assertIsInstance(grid, Grid)
        self.assertEqual(len(grid.data), 2)
        self.assertEqual(len(grid.tools), 0)
        self.assertEqual(len(grid.actions), 0)

        # create + delete allowed
        with patch.object(self.request, 'is_root', new=True):
            grid = view.make_api_tokens_grid(user)
            self.assertEqual(len(grid.tools), 1)
            self.assertIn('create', grid.tools)
            self.assertEqual(len(grid.actions), 1)
            self.assertEqual(grid.actions[0].key, 'delete')

    def test_add_api_token(self):
        model = self.app.model
        view = self.make_view()

        user = model.User(username='foo')
        self.session.add(user)
        self.session.commit()
        self.session.refresh(user)
        self.assertEqual(len(user.api_tokens), 0)

        with patch.object(view, 'Session', return_value=self.session):
            with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):
                with patch.object(self.request, 'json_body', create=True,
                                  new={'description': 'testing'}):
                    result = view.add_api_token()
                    self.assertEqual(len(user.api_tokens), 1)
                    token = user.api_tokens[0]
                    self.assertEqual(result['token_string'], token.token_string)
                    self.assertEqual(result['description'], 'testing')

    def test_delete_api_token(self):
        model = self.app.model
        auth = self.app.get_auth_handler()
        view = self.make_view()

        user = model.User(username='foo')
        self.session.add(user)
        token1 = auth.add_api_token(user, 'test1')
        token2 = auth.add_api_token(user, 'test2')
        self.session.commit()
        self.session.refresh(user)
        self.assertEqual(len(user.api_tokens), 2)

        with patch.object(view, 'Session', return_value=self.session):
            with patch.object(self.request, 'matchdict', new={'uuid': user.uuid}):

                # normal behavior
                with patch.object(self.request, 'json_body', create=True,
                                  new={'uuid': token1.uuid.hex}):
                    result = view.delete_api_token()
                    self.assertEqual(result, {})
                    self.session.refresh(user)
                    self.assertEqual(len(user.api_tokens), 1)
                    token = user.api_tokens[0]
                    self.assertIs(token, token2)

                # token for wrong user
                user2 = model.User(username='bar')
                self.session.add(user2)
                token3 = auth.add_api_token(user2, 'test3')
                self.session.commit()
                with patch.object(self.request, 'json_body', create=True,
                                  new={'uuid': token3.uuid.hex}):
                    result = view.delete_api_token()
                    self.assertEqual(result, {'error': "API token not found"})

                # token not found
                with patch.object(self.request, 'json_body', create=True,
                                  new={'uuid': self.app.make_true_uuid().hex}):
                    result = view.delete_api_token()
                    self.assertEqual(result, {'error': "API token not found"})
