# -*- coding: utf-8 -*-
"""Test audit columns"""

import pytest

from pyrseas.testutils import AugmentToMapTestCase

CREATE_STMT = "CREATE TABLE t1 (c1 integer, c2 text)"
FUNC_SRC1 = """
BEGIN
  NEW.modified_by_user = SESSION_USER;
  NEW.modified_timestamp = CURRENT_TIMESTAMP;
  RETURN NEW;
END"""

FUNC_SRC2 = """
BEGIN
  NEW.updated = CURRENT_TIMESTAMP;
  RETURN NEW;
END"""


class AuditColumnsTestCase(AugmentToMapTestCase):
    """Test mapping of audit column augmentations"""

    def test_predef_column(self):
        "Add predefined audit column"
        augmap = {'schema public': {'table t1': {
            'audit_columns': 'created_date_only'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'created_date': {'type': 'date', 'not_null': True,
                              'default': "('now'::text)::date"}}]}
        assert expmap == dbmap['schema public']['table t1']

    def test_unknown_table(self):
        "Error on non-existent table"
        augmap = {'schema public': {'table t2': {
            'audit_columns': 'created_date_only'}}}
        with pytest.raises(KeyError):
            self.to_map([CREATE_STMT], augmap)

    def test_bad_audit_spec(self):
        "Error on bad audit column specification"
        augmap = {'schema public': {'table t1': {
            'audit_column': 'created_date_only'}}}
        with pytest.raises(KeyError):
            self.to_map([CREATE_STMT], augmap)

    def test_unknown_audit_spec(self):
        "Error on non-existent audit column specification"
        augmap = {'schema public': {'table t1': {
            'audit_columns': 'created_date'}}}
        with pytest.raises(KeyError):
            self.to_map([CREATE_STMT], augmap)

    def test_new_column(self):
        "Add new (non-predefined) audit column"
        augmap = {'augmenter': {'columns': {
            'modified_date': {'type': 'date', 'not_null': True,
                              'default': "('now'::text)::date"}},
            'audit_columns': {'modified_date_only': {
                'columns': ['modified_date']}}},
            'schema public': {'table t1': {
                'audit_columns': 'modified_date_only'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'modified_date': {'type': 'date', 'not_null': True,
                               'default': "('now'::text)::date"}}]}
        assert expmap == dbmap['schema public']['table t1']

    def test_rename_column(self):
        "Add predefined audit column but with new name"
        augmap = {'augmenter': {'columns': {
            'modified_timestamp': {'name': 'updated'}}},
            'schema public': {'table t1': {
                'audit_columns': 'modified_only'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        colmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'updated': {'type': 'timestamp with time zone',
                         'not_null': True}}],
            'triggers': {'t1_20_audit_modified_only': {
                'events': ['insert', 'update'], 'level': 'row',
                'procedure': 'audit_modified()', 'timing': 'before'}}}
        funcmap = {'language': 'plpgsql', 'returns': 'trigger',
                   'security_definer': True, 'description':
                   'Provides modified_timestamp values for audit columns.',
                   'source': FUNC_SRC2}
        assert dbmap['schema public']['table t1'] == colmap
        assert dbmap['schema public']['function audit_modified()'] == funcmap

    def test_change_column_type(self):
        "Add predefined audit column but with changed datatype"
        augmap = {'augmenter': {'columns': {'created_date': {'type': 'text'}}},
                  'schema public': {'table t1': {
                  'audit_columns': 'created_date_only'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'created_date': {'type': 'text', 'not_null': True,
                              'default': "('now'::text)::date"}}]}
        assert expmap == dbmap['schema public']['table t1']

    def test_columns_with_trigger(self):
        "Add predefined audit columns with trigger"
        augmap = {'schema public': {'table t1': {'audit_columns': 'default'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'modified_by_user': {'type': 'character varying(63)',
                                  'not_null': True}},
            {'modified_timestamp': {'type': 'timestamp with time zone',
                                    'not_null': True}}],
            'triggers': {'t1_20_audit_default': {
                'events': ['update'], 'level': 'row',
                'procedure': 'audit_default()', 'timing': 'before'}}}
        assert expmap == dbmap['schema public']['table t1']
        assert dbmap['schema public']['function audit_default()'][
            'returns'] == 'trigger'
        assert dbmap['schema public']['function audit_default()'][
            'source'] == FUNC_SRC1

    def test_nonpublic_schema_with_trigger(self):
        "Add predefined audit columns with trigger in a non-public schema"
        stmts = ["CREATE SCHEMA s1",
                 "CREATE TABLE s1.t1 (c1 integer, c2 text)"]
        augmap = {'schema s1': {'table t1': {'audit_columns': 'default'}}}
        dbmap = self.to_map(stmts, augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'modified_by_user': {'type': 'character varying(63)',
                                  'not_null': True}},
            {'modified_timestamp': {'type': 'timestamp with time zone',
                                    'not_null': True}}],
            'triggers': {'t1_20_audit_default': {
                'events': ['update'], 'level': 'row',
                'procedure': 's1.audit_default()', 'timing': 'before'}}}
        assert expmap == dbmap['schema s1']['table t1']
        assert dbmap['schema s1']['function audit_default()']['returns'] == \
            'trigger'
        assert dbmap['schema s1']['function audit_default()'][
            'source'] == FUNC_SRC1

    def test_skip_existing_columns(self):
        "Do not add already existing audit columns"
        stmts = [CREATE_STMT,
                 "ALTER TABLE t1 ADD modified_by_user varchar(63) NOT NULL",
                 "ALTER TABLE t1 ADD modified_timestamp "
                 "timestamp with time zone NOT NULL"]
        augmap = {'schema public': {'table t1': {
            'audit_columns': 'default'}}}
        dbmap = self.to_map(stmts, augmap)
        expmap = [{'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
                  {'modified_by_user': {'type': 'character varying(63)',
                                        'not_null': True}},
                  {'modified_timestamp': {'type': 'timestamp with time zone',
                                          'not_null': True}}]
        assert expmap == dbmap['schema public']['table t1']['columns']

    def test_change_existing_columns(self):
        "Change already existing audit columns"
        stmts = [CREATE_STMT, "ALTER TABLE t1 ADD modified_by_user text ",
                 "ALTER TABLE t1 ADD modified_timestamp "
                 "timestamp with time zone NOT NULL"]
        augmap = {'schema public': {'table t1': {'audit_columns': 'default'}}}
        dbmap = self.to_map(stmts, augmap)
        expmap = [{'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
                  {'modified_by_user': {'type': 'character varying(63)',
                                        'not_null': True}},
                  {'modified_timestamp': {'type': 'timestamp with time zone',
                                          'not_null': True}}]
        assert expmap == dbmap['schema public']['table t1']['columns']

    def test_custom_function_template(self):
        "Add new (non-predefined) audit trigger using a function template"
        template = """
        BEGIN
          NEW.{{modified_by_user}} = SESSION_USER;
          NEW.{{modified_timestamp}} = CURRENT_TIMESTAMP::timestamp(0);
          RETURN NEW;
        END"""
        source = """
        BEGIN
          NEW.modified_by_user = SESSION_USER;
          NEW.modified_timestamp = CURRENT_TIMESTAMP::timestamp(0);
          RETURN NEW;
        END"""
        augmap = {
            'augmenter': {
                'audit_columns': {'custom': {
                    'columns': ['modified_by_user', 'modified_timestamp'],
                    'triggers': ['custom_audit']}},
                'function_templates': {'custom_template': template},
                'functions': {'custom_audit()': {
                    'description': 'Maintain custom audit columns',
                    'language': 'plpgsql',
                    'returns': 'trigger',
                    'security_definer': True,
                    'source': '{{custom_template}}'}},
                'triggers': {'custom_audit': {
                    'events': ['insert', 'update'],
                    'level': 'row',
                    'name': '{{table_name}}_20_custom_audit',
                    'procedure': 'custom_audit()',
                    'timing': 'before'}}},
            'schema public': {'table t1': {
                'audit_columns': 'custom'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'modified_by_user': {'type': 'character varying(63)',
                                  'not_null': True}},
            {'modified_timestamp': {'type': 'timestamp with time zone',
                                    'not_null': True}}],
            'triggers': {'t1_20_custom_audit': {
                'events': ['insert', 'update'], 'level': 'row',
                'procedure': 'custom_audit()', 'timing': 'before'}}}
        assert expmap == dbmap['schema public']['table t1']
        assert dbmap['schema public']['function custom_audit()'][
            'returns'] == 'trigger'
        assert dbmap['schema public']['function custom_audit()'][
            'source'] == source

    def test_custom_function_inline_with_column_substitution(self):
        "Add new (non-predefined) audit trigger using an inline definition"
        template = """
        BEGIN
          NEW.{{modified_by_user}} = SESSION_USER;
          NEW.{{modified_timestamp}} = CURRENT_TIMESTAMP::timestamp(0);
          RETURN NEW;
        END"""
        source = """
        BEGIN
          NEW.modified_by_user = SESSION_USER;
          NEW.modified_timestamp = CURRENT_TIMESTAMP::timestamp(0);
          RETURN NEW;
        END"""
        augmap = {
            'augmenter': {
                'audit_columns': {'custom': {
                    'columns': ['modified_by_user', 'modified_timestamp'],
                    'triggers': ['custom_audit']}},
                'functions': {'custom_audit()': {
                    'description': 'Maintain custom audit columns',
                    'language': 'plpgsql',
                    'returns': 'trigger',
                    'security_definer': True,
                    'source': template}},
                'triggers': {'custom_audit': {
                    'events': ['insert', 'update'],
                    'level': 'row',
                    'name': '{{table_name}}_20_custom_audit',
                    'procedure': 'custom_audit()',
                    'timing': 'before'}}},
            'schema public': {'table t1': {
                'audit_columns': 'custom'}}}
        dbmap = self.to_map([CREATE_STMT], augmap)
        expmap = {'columns': [
            {'c1': {'type': 'integer'}}, {'c2': {'type': 'text'}},
            {'modified_by_user': {'type': 'character varying(63)',
                                  'not_null': True}},
            {'modified_timestamp': {'type': 'timestamp with time zone',
                                    'not_null': True}}],
            'triggers': {'t1_20_custom_audit': {
                'events': ['insert', 'update'], 'level': 'row',
                'procedure': 'custom_audit()', 'timing': 'before'}}}
        assert expmap == dbmap['schema public']['table t1']
        assert dbmap['schema public']['function custom_audit()'][
            'returns'] == 'trigger'
        assert dbmap['schema public']['function custom_audit()'][
            'source'] == source
