# coding=utf-8
"""
"""
from __future__ import absolute_import, division, print_function, \
    unicode_literals

import datetime
from itertools import count

import sqlalchemy as sa
from six import python_2_unicode_compatible, text_type
from sqlalchemy import Column, Date, ForeignKey, Integer, Text, Unicode, \
    UnicodeText
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm.attributes import NEVER_SET

from abilian.core.entities import Entity
from abilian.core.extensions import db
from abilian.core.models.base import AUDITABLE_HIDDEN, SEARCHABLE
from abilian.testing import BaseTestCase

from . import CREATION, DELETION, UPDATE, AuditEntry, audit_service


@python_2_unicode_compatible
class IntegerCollection(db.Model):
    __tablename__ = 'integer_collection'
    id = Column(Integer, primary_key=True)

    def __str__(self):
        return text_type(self.id)


class DummyAccount(Entity):
    name = Column(UnicodeText, default="", info=SEARCHABLE)
    password = Column(Unicode, default='*', info=AUDITABLE_HIDDEN)
    website = Column(Text, default="")
    office_phone = Column(UnicodeText, default="")
    birthday = Column(Date)

    @declared_attr
    def integers(cls):
        secondary_tbl = sa.Table(
            'account_integers',
            db.Model.metadata,
            Column('integer_id', ForeignKey(IntegerCollection.id)),
            Column('account_id', ForeignKey(cls.id)),
            sa.schema.UniqueConstraint('account_id', 'integer_id'),
        )

        return sa.orm.relationship(IntegerCollection, secondary=secondary_tbl)


class AccountRelated(db.Model):
    __tablename__ = 'account_related'
    __auditable_entity__ = ('account', 'data', ('id',))
    id = Column(Integer, primary_key=True)

    account_id = Column(Integer, ForeignKey(DummyAccount.id), nullable=False)
    account = relationship(
        DummyAccount,
        backref=backref(
            'data',
            order_by='AccountRelated.id',
            cascade='all, delete-orphan',
        ),
    )

    text = Column(UnicodeText, default="")


class CommentRelated(db.Model):
    __tablename__ = 'account_related_comment'
    __auditable_entity__ = (
        'related.account',
        'data.comments',
        ('related.id', 'id'),
    )
    id = Column(Integer, primary_key=True)

    related_id = Column(Integer, ForeignKey(AccountRelated.id), nullable=False)
    related = relationship(
        AccountRelated,
        backref=backref(
            'comments',
            order_by='CommentRelated.id',
            cascade='all, delete-orphan',
        ),
    )
    text = Column(UnicodeText, default="")


class TestAudit(BaseTestCase):

    def setUp(self):
        audit_service.start()
        BaseTestCase.setUp(self)

    def tearDown(self):
        BaseTestCase.tearDown(self)
        if audit_service.running:
            audit_service.stop()

    def test_audit(self):
        # Creation of system user(0) should have created one entry.
        # We clear it for this test.
        AuditEntry.query.delete()
        db.session.flush()
        assert len(AuditEntry.query.all()) == 0

        account = DummyAccount(name="John SARL")
        db.session.add(account)
        db.session.commit()
        assert len(AuditEntry.query.all()) == 1

        entry = AuditEntry.query.one()
        assert entry.type == CREATION
        assert entry.entity_id == account.id
        assert entry.entity == account

        account.website = "http://www.john.com/"
        db.session.commit()
        assert len(AuditEntry.query.all()) == 2

        entry = AuditEntry.query.order_by(AuditEntry.happened_at).all()[1]
        assert entry.type == UPDATE
        assert entry.entity_id == account.id
        assert entry.entity == account
        assert entry.changes.columns == {
            'website': ('', 'http://www.john.com/'),
        }

        account.birthday = datetime.date(2012, 12, 25)
        db.session.commit()
        assert len(AuditEntry.query.all()) == 3

        entry = AuditEntry.query.order_by(AuditEntry.happened_at).all()[2]
        assert entry.type == UPDATE
        assert entry.entity_id == account.id
        assert entry.entity == account
        assert entry.changes.columns == {
            'birthday': (None, datetime.date(2012, 12, 25)),
        }

        # content hiding
        account.password = 'new super secret password'
        assert account.__changes__.columns == {'password': ('******', '******')}
        db.session.commit()

        entry = AuditEntry.query.order_by(AuditEntry.happened_at).all()[3]
        assert entry.type == UPDATE
        assert entry.entity_id == account.id
        assert entry.entity == account
        assert entry.changes.columns == {'password': ('******', '******')}

        # deletion
        db.session.delete(account)
        db.session.commit()
        assert len(AuditEntry.query.all()) == 5

        entry = AuditEntry.query.order_by(AuditEntry.happened_at).all()[4]
        assert entry.type == DELETION
        assert entry.entity_id == account.id
        assert entry.entity is None

        # check all entries are still present (but have lost reference to entity)
        entries = AuditEntry.query.all()
        assert len(entries) == 5
        assert all(e.entity_id == account.id for e in entries)
        assert all(e.entity is None for e in entries)

    def test_audit_related(self):
        AuditEntry.query.delete()
        db.session.flush()
        assert len(AuditEntry.query.all()) == 0

        #  helper
        audit_idx = count()
        audit_query = AuditEntry.query.order_by(AuditEntry.happened_at)

        def next_entry():
            return audit_query.all()[next(audit_idx)]

        account = DummyAccount(name="John SARL")
        db.session.add(account)
        db.session.commit()
        assert len(AuditEntry.query.all()) == 1
        next(audit_idx)

        data = AccountRelated(account=account, text='text 1')
        db.session.add(data)
        db.session.commit()

        entry = next_entry()
        assert entry.op == CREATION
        assert entry.related
        assert entry.entity_type == account.entity_type
        assert entry.entity_id == account.id
        assert entry.entity == account

        changes = entry.changes.columns
        assert len(changes) == 1
        assert 'data 1' in changes
        changes = changes['data 1']
        assert changes.columns == {
            'text': (NEVER_SET, 'text 1'),
            'account_id': (NEVER_SET, 1),
            'id': (NEVER_SET, 1),
        }

        comment = CommentRelated(related=data, text='comment')
        db.session.add(comment)
        db.session.commit()
        entry = next_entry()
        assert entry.op == CREATION
        assert entry.related
        assert entry.entity_type == account.entity_type
        assert entry.entity_id == account.id

        changes = entry.changes.columns
        assert len(changes) == 1
        assert 'data.comments 1 1' in changes
        changes = changes['data.comments 1 1']
        assert changes.columns == {
            'text': (NEVER_SET, 'comment'),
            'related_id': (NEVER_SET, 1),
            'id': (NEVER_SET, 1),
        }

        comment = CommentRelated(related=data, text='comment 2')
        db.session.add(comment)
        db.session.commit()
        entry = next_entry()
        assert entry.op == CREATION
        assert entry.related
        assert entry.entity_type == account.entity_type
        assert entry.entity_id == account.id

        changes = entry.changes.columns
        assert len(changes) == 1
        assert 'data.comments 1 2' in changes

        changes = changes['data.comments 1 2']
        assert changes.columns == {
            'text': (NEVER_SET, 'comment 2'),
            'related_id': (NEVER_SET, 1),
            'id': (NEVER_SET, 2),
        }

        # deletion
        db.session.delete(comment)
        db.session.commit()

        entry = next_entry()
        assert entry.op == DELETION
        assert entry.related
        assert entry.entity_id == account.id
        # entity not deleted: audit should still have reference to it
        assert entry.entity == account

    def test_audit_collections(self):
        I1 = IntegerCollection(id=1)
        I2 = IntegerCollection(id=2)
        self.session.add(I1)
        self.session.add(I2)
        self.session.flush()

        account = DummyAccount(name='John')
        account.integers.append(I1)
        self.session.add(account)
        self.session.flush()

        entry = AuditEntry.query.one()
        changes = entry.changes
        assert changes.collections == {'integers': (['1'], [])}
