# -*- coding: utf-8 -*-
# This file is part of Tryton.  The COPYRIGHT file at the top level of this
# repository contains the full copyright notices and license terms.

import unittest
import time
from unittest.mock import patch, call

from trytond import backend
from trytond.exceptions import ConcurrencyException
from trytond.model.exceptions import (
    RequiredValidationError, SQLConstraintError)
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction


class ModelSQLTestCase(unittest.TestCase):
    'Test ModelSQL'

    @classmethod
    def setUpClass(cls):
        activate_module('tests')

    @with_transaction()
    def test_read(self):
        "Test simple read"
        pool = Pool()
        Model = pool.get('test.modelsql.read')

        foo, bar = Model.create([{'name': "Foo"}, {'name': "Bar"}])
        values = Model.read([foo.id, bar.id], ['name'])

        self.assertEqual(
            sorted(values, key=lambda v: v['id']),
            [{'id': foo.id, 'name': "Foo"}, {'id': bar.id, 'name': "Bar"}])

    @with_transaction()
    def test_read_related_2one(self):
        "Test read with related Many2One"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target, = Target.create([{'name': "Target"}])
        record, = Model.create([{'target': target.id}])
        values = Model.read([record.id], ['target.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'target.': {
                        'id': target.id,
                        'name': "Target",
                        },
                    }])

    @with_transaction()
    def test_read_related_2one_empty(self):
        "Test read with empty related Many2One"
        pool = Pool()
        Model = pool.get('test.modelsql.read')

        record, = Model.create([{'target': None}])
        values = Model.read([record.id], ['target.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'target.': None,
                    }])

    @with_transaction()
    def test_read_related_reference(self):
        "Test read with related Reference"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target, = Target.create([{'name': "Target"}])
        record, = Model.create([{'reference': str(target)}])
        values = Model.read([record.id], ['reference.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'reference.': {
                        'id': target.id,
                        'name': "Target",
                        },
                    }])

    @with_transaction()
    def test_read_related_reference_empty(self):
        "Test read with empty related Reference"
        pool = Pool()
        Model = pool.get('test.modelsql.read')

        record, = Model.create([{'name': "Foo", 'reference': None}])
        values = Model.read([record.id], ['reference.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'reference.': None,
                    }])

    @with_transaction()
    def test_read_related_2many(self):
        "Test read with related One2Many"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target, = Target.create([{'name': "Target"}])
        record, = Model.create(
            [{'targets': [('add', [target.id])]}])
        values = Model.read([record.id], ['targets.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'targets.': [{
                            'id': target.id,
                            'name': "Target",
                            }],
                    }])

    @with_transaction()
    def test_read_related_2many_empty(self):
        "Test read with empty related One2Many"
        pool = Pool()
        Model = pool.get('test.modelsql.read')

        record, = Model.create(
            [{'targets': None}])
        values = Model.read([record.id], ['targets.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'targets.': [],
                    }])

    @with_transaction()
    def test_read_related_2many_multiple(self):
        "Test read with multiple related One2Many"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target1, target2 = Target.create([
                {'name': "Target 1"},
                {'name': "Target 2"}])
        record, = Model.create(
            [{'targets': [('add', [target1.id, target2.id])]}])
        values = Model.read([record.id], ['targets.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'targets.': [{
                            'id': target1.id,
                            'name': "Target 1",
                            }, {
                            'id': target2.id,
                            'name': "Target 2",
                            }],
                    }])

    @with_transaction()
    def test_read_related_mixed(self):
        "Test read mixed related"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target1, target2, target3 = Target.create([
                {'name': "Target 1"},
                {'name': "Target 2"},
                {'name': "Target 3"}])
        record1, record2 = Model.create([{
                    'name': "Foo",
                    'target': target1.id,
                    'targets': [('add', [target1.id, target2.id])],
                    }, {
                    'name': "Bar",
                    'reference': str(target2),
                    'targets': [('add', [target3.id])],
                    }])
        values = Model.read(
            [record1.id, record2.id],
            ['name', 'target', 'target.name', 'targets', 'targets.name'])

        self.assertEqual(
            sorted(values, key=lambda v: v['id']), [{
                    'id': record1.id,
                    'name': "Foo",
                    'target': target1.id,
                    'target.': {
                        'id': target1.id,
                        'name': "Target 1",
                        },
                    'targets': (target1.id, target2.id),
                    'targets.': [{
                            'id': target1.id,
                            'name': "Target 1",
                            }, {
                            'id': target2.id,
                            'name': "Target 2",
                            }],
                    }, {
                    'id': record2.id,
                    'name': "Bar",
                    'target': None,
                    'target.': None,
                    'targets': (target3.id,),
                    'targets.': [{
                            'id': target3.id,
                            'name': "Target 3",
                            }],
                    }])

    @with_transaction()
    def test_read_related_nested(self):
        "Test read with nested related"
        pool = Pool()
        Model = pool.get('test.modelsql.read')
        Target = pool.get('test.modelsql.read.target')

        target, = Target.create([{'name': "Target"}])
        record, = Model.create(
            [{'name': "Record", 'targets': [('add', [target.id])]}])
        values = Model.read([record.id], ['targets.parent.name'])

        self.assertEqual(values, [{
                    'id': record.id,
                    'targets.': [{
                            'id': target.id,
                            'parent.': {
                                'id': record.id,
                                'name': "Record",
                                },
                            }],
                    }])

    @unittest.skipIf(backend.name() == 'sqlite',
        'SQLite not concerned because tryton don\'t set "NOT NULL"'
        'constraint: "ALTER TABLE" don\'t support NOT NULL constraint'
        'without default value')
    @with_transaction()
    def test_required_field_missing(self):
        'Test error message when a required field is missing'
        pool = Pool()
        Modelsql = pool.get('test.modelsql')
        transaction = Transaction()

        fields = {
            'desc': '',
            'integer': 0,
            }
        for key, value in fields.items():
            try:
                Modelsql.create([{key: value}])
            except RequiredValidationError as err:
                # message must not quote key
                msg = "'%s' not missing but quoted in error: '%s'" % (key,
                        err.message)
                self.assertTrue(key not in err.message, msg)
            else:
                self.fail('RequiredValidationError should be caught')
            transaction.rollback()

    @with_transaction()
    def test_check_timestamp(self):
        'Test check timestamp'
        pool = Pool()
        ModelsqlTimestamp = pool.get('test.modelsql.timestamp')
        transaction = Transaction()
        # transaction must be committed between each changes otherwise NOW()
        # returns always the same timestamp.
        record, = ModelsqlTimestamp.create([{}])
        transaction.commit()

        timestamp = ModelsqlTimestamp.read([record.id],
            ['_timestamp'])[0]['_timestamp']

        if backend.name() == 'sqlite':
            # timestamp precision of sqlite is the second
            time.sleep(1)

        transaction.timestamp[str(record)] = timestamp
        ModelsqlTimestamp.write([record], {})
        transaction.commit()

        transaction.timestamp[str(record)] = timestamp
        self.assertRaises(ConcurrencyException,
            ModelsqlTimestamp.write, [record], {})

        transaction.timestamp[str(record)] = timestamp
        self.assertRaises(ConcurrencyException,
            ModelsqlTimestamp.delete, [record])

        transaction.timestamp[str(record)] = None
        ModelsqlTimestamp.write([record], {})
        transaction.commit()

        transaction.timestamp.pop(str(record), None)
        ModelsqlTimestamp.write([record], {})
        transaction.commit()
        ModelsqlTimestamp.delete([record])
        transaction.commit()

    @with_transaction()
    def test_create_field_set(self):
        'Test field.set in create'
        pool = Pool()
        Model = pool.get('test.modelsql.field_set')

        with patch.object(Model, 'set_field') as setter:
            records = Model.create([{'field': 1}])
            setter.assert_called_with(records, 'field', 1)

        # Different values are not grouped
        with patch.object(Model, 'set_field') as setter:
            records = Model.create([{'field': 1}, {'field': 2}])
            setter.assert_has_calls([
                    call([records[0]], 'field', 1),
                    call([records[1]], 'field', 2),
                    ])

        # Same values are grouped in one call
        with patch.object(Model, 'set_field') as setter:
            records = Model.create([{'field': 1}, {'field': 1}])
            setter.assert_called_with(records, 'field', 1)

        # Mixed values are grouped per value
        with patch.object(Model, 'set_field') as setter:
            records = Model.create([{'field': 1}, {'field': 2}, {'field': 1}])
            setter.assert_has_calls([
                    call([records[0], records[2]], 'field', 1),
                    call([records[1]], 'field', 2),
                    ])

    @with_transaction()
    def test_integrity_error_with_created_record(self):
        "Test integrity error with created record"
        pool = Pool()
        ParentModel = pool.get('test.modelsql.one2many')
        TargetModel = pool.get('test.modelsql.one2many.target')

        # Create target record without required name
        # to ensure create_records is filled to prevent raising
        # foreign_model_missing
        record = ParentModel(name="test")
        record.targets = [TargetModel()]
        with self.assertRaises(RequiredValidationError) as cm:
            record.save()
        err = cm.exception
        self.assertIn(TargetModel.name.string, err.message)
        self.assertIn(TargetModel.__doc__, err.message)

    @with_transaction()
    def test_null_ordering(self):
        'Test NULL ordering'
        pool = Pool()
        NullOrder = pool.get('test.modelsql.null_order')

        NullOrder.create([{
                    'integer': 1,
                    }, {
                    'integer': 3,
                    }, {
                    'integer': None,
                    }])
        integers = NullOrder.search([], order=[('integer', 'ASC NULLS FIRST')])
        self.assertListEqual([i.integer for i in integers], [None, 1, 3])

        integers = NullOrder.search(
            [], order=[('integer', 'DESC NULLS FIRST')])
        self.assertListEqual([i.integer for i in integers], [None, 3, 1])

        integers = NullOrder.search([], order=[('integer', 'ASC NULLS LAST')])
        self.assertListEqual([i.integer for i in integers], [1, 3, None])

        integers = NullOrder.search([], order=[('integer', 'DESC NULLS LAST')])
        self.assertListEqual([i.integer for i in integers], [3, 1, None])

    @with_transaction()
    def test_delete_translations(self):
        "Test delete record trigger delete of translations"
        pool = Pool()
        Model = pool.get('test.modelsql.translation')
        Translation = pool.get('ir.translation')
        record, = Model.create([{'name': "Translation"}])

        with patch.object(Translation, 'delete_ids') as delete_ids:
            Model.delete([record])

        delete_ids.assert_called_with(
            'test.modelsql.translation', 'model', [record.id])

    @with_transaction()
    def test_constraint_check(self):
        "Test check constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.check')

        record, = Model.create([{'value': 50}])

        self.assertTrue(record.id)

    @with_transaction()
    def test_constraint_check_null(self):
        "Test check constraint with null"
        pool = Pool()
        Model = pool.get('test.modelsql.check')

        record, = Model.create([{'value': None}])

        self.assertTrue(record.id)

    @with_transaction()
    def test_constraint_check_invalid(self):
        "Test invalid check constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.check')

        with self.assertRaises(SQLConstraintError):
            Model.create([{'value': 10}])

    @with_transaction()
    def test_constraint_unique(self):
        "Test unique constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.unique')

        records = Model.create([{'value': 1}, {'value': 2}])

        self.assertEqual(len(records), 2)

    @with_transaction()
    def test_constraint_unique_null(self):
        "Test unique constraint with null"
        pool = Pool()
        Model = pool.get('test.modelsql.unique')

        records = Model.create([{'value': None}, {'value': None}])

        self.assertEqual(len(records), 2)

    @with_transaction()
    def test_constraint_unique_invalid(self):
        "Test invalid unique constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.unique')

        with self.assertRaises(SQLConstraintError):
            Model.create([{'value': 42}, {'value': 42}])

    @with_transaction()
    def test_constraint_exclude(self):
        "Test exclude constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.exclude')

        records = Model.create([{'value': 1}, {'value': 2}])

        self.assertEqual(len(records), 2)

    @with_transaction()
    def test_constraint_exclude_exclusion(self):
        "Test exclude constraint exclusion"
        pool = Pool()
        Model = pool.get('test.modelsql.exclude')

        records = Model.create([{'value': 1, 'condition': False}] * 2)

        self.assertEqual(len(records), 2)

    @with_transaction()
    def test_constraint_exclude_exclusion_mixed(self):
        "Test exclude constraint exclusion mixed"
        pool = Pool()
        Model = pool.get('test.modelsql.exclude')

        records = Model.create([
                {'value': 1, 'condition': False},
                {'value': 1, 'condition': True},
                ])

        self.assertEqual(len(records), 2)

    @with_transaction()
    def test_constraint_exclude_invalid(self):
        "Test invalid exclude constraint"
        pool = Pool()
        Model = pool.get('test.modelsql.exclude')

        with self.assertRaises(SQLConstraintError):
            Model.create([{'value': 42}, {'value': 42}])

    @unittest.skipIf(backend.name() == 'sqlite',
        'SQLite does not have lock at table level but on file')
    @with_transaction()
    def test_lock(self):
        "Test lock"
        pool = Pool()
        Model = pool.get('test.modelsql.lock')
        DatabaseOperationalError = backend.get('DatabaseOperationalError')
        transaction = Transaction()
        record_id = Model.create([{}])[0].id
        transaction.commit()

        with transaction.new_transaction():
            record = Model(record_id)
            record.lock()
            with transaction.new_transaction():
                record = Model(record_id)
                with self.assertRaises(DatabaseOperationalError):
                    record.lock()


def suite():
    return unittest.TestLoader().loadTestsFromTestCase(ModelSQLTestCase)
