# 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 datetime
import unittest

from trytond import backend
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
from trytond.tests.test_tryton import (
    TestCase, activate_module, with_transaction)
from trytond.transaction import Transaction


class HistoryTestCase(TestCase):
    'Test History'

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

    @with_transaction()
    def tearDown(self):
        pool = Pool()
        History = pool.get('test.history')
        HistoryLine = pool.get('test.history.line')
        transaction = Transaction()
        cursor = transaction.connection.cursor()
        for Model in [History, HistoryLine]:
            table = Model.__table__()
            history_table = Model.__table_history__()
            cursor.execute(*table.delete())
            cursor.execute(*history_table.delete())
        transaction.commit()

    @with_transaction()
    def test_read(self):
        'Test read history'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        # Create some history entry
        # It is needed to commit to have different timestamps
        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date

        transaction.commit()

        history = History(history_id)
        history.value = 2
        history.save()
        second = history.write_date

        transaction.commit()

        history = History(history_id)
        history.value = 3
        history.save()
        third = history.write_date

        transaction.commit()

        for timestamp, value in [
                (first, 1),
                (second, 2),
                (third, 3),
                (datetime.datetime.now(), 3),
                (datetime.datetime.max, 3),
                ]:
            with Transaction().set_context(_datetime=timestamp):
                history = History(history_id)
                self.assertEqual(history.value, value)

        with Transaction().set_context(_datetime=datetime.datetime.min):
            with self.assertRaises(AccessError):
                History.read([history_id], ['value'])

    @unittest.skipUnless(backend.name == 'postgresql',
        'CURRENT_TIMESTAMP as transaction_timestamp is specific to postgresql')
    @with_transaction()
    def test_read_same_timestamp(self):
        'Test read history with same timestamp'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date

        history.value = 2
        history.save()
        second = history.write_date

        self.assertEqual(first, second)

        transaction.commit()

        history = History(history_id)
        history.value = 3
        history.save()
        third = history.write_date

        transaction.commit()

        for timestamp, value in [
                (first, 2),
                (third, 3),
                ]:
            with Transaction().set_context(_datetime=timestamp):
                history = History(history_id)
                self.assertEqual(history.value, value)

    @with_transaction()
    def test_history_revisions(self):
        'Test history revisions'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date

        transaction.commit()

        history = History(history_id)
        history.value = 2
        history.save()
        second = history.write_date

        transaction.commit()

        history = History(history_id)
        history.value = 3
        history.save()
        third = history.write_date

        transaction.commit()

        revisions = History.history_revisions([history_id])
        self.assertEqual(revisions, [
                (third, history_id, 'Administrator'),
                (second, history_id, 'Administrator'),
                (first, history_id, 'Administrator'),
                ])

    @with_transaction()
    def test_restore_history(self):
        'Test restore history'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date

        transaction.commit()

        history = History(history_id)
        history.value = 2
        history.save()

        transaction.commit()

        History.restore_history([history_id], first)
        history = History(history_id)
        self.assertEqual(history.value, 1)

        transaction.rollback()

        History.restore_history([history_id], datetime.datetime.min)
        with self.assertRaises(AccessError):
            History.read([history_id], ['value'])

        transaction.rollback()

        History.delete([History(history_id)])

        transaction.commit()

        History.restore_history([history_id], datetime.datetime.max)
        with self.assertRaises(AccessError):
            History.read([history_id], ['value'])

    @with_transaction()
    def test_restore_history_after_delete(self):
        "Test restoring a record after deleting it"
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date

        transaction.commit()

        History.delete([History(history_id)])
        with self.assertRaises(AccessError):
            History.read([history_id], ['value'])

        transaction.commit()

        History.restore_history([history_id], first)
        history = History(history_id)
        self.assertEqual(history.value, 1)

    @with_transaction()
    def test_restore_history_dict(self):
        "Test restoring a record with a field.Dict"
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(dico={'a': 1})
        history.save()
        history_id = history.id
        first = history.create_date

        transaction.commit()

        history = History(history_id)
        history.dico = {'b': 2}
        history.save()

        transaction.commit()

        History.restore_history([history_id], first)
        history = History(history_id)
        self.assertEqual(history.dico, {'a': 1})

    @with_transaction()
    def test_restore_history_before(self):
        'Test restore history before'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id

        transaction.commit()

        history = History(history_id)
        history.value = 2
        history.save()
        second = history.write_date

        transaction.commit()

        history = History(history_id)
        history.value = 3
        history.save()

        transaction.commit()

        History.restore_history_before([history_id], second)
        history = History(history_id)
        self.assertEqual(history.value, 1)

    @unittest.skipUnless(backend.name == 'postgresql',
        'CURRENT_TIMESTAMP as transaction_timestamp is specific to postgresql')
    @with_transaction()
    def test_restore_history_same_timestamp(self):
        'Test restore history with same timestamp'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        first = history.create_date
        history.value = 2
        history.save()
        second = history.create_date

        self.assertEqual(first, second)

        transaction.commit()

        history = History(history_id)
        history.value = 3
        history.save()

        transaction.commit()

        History.restore_history([history_id], first)
        history = History(history_id)
        self.assertEqual(history.value, 2)

    @with_transaction()
    def test_search_historical_records(self):
        self._test_search_historical_records()

    def _test_search_historical_records(self):
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        first = History(value=1)
        first.save()
        first_stamp = first.create_date
        # Commit so that further call are at a different timestamp
        transaction.commit()

        second = History(value=1)
        first.value = 2
        History.save([first, second])
        second_stamp = second.create_date
        transaction.commit()

        third = History(value=1)
        second.value = 2
        History.delete([first])
        History.save([second, third])
        third_stamp = third.create_date
        transaction.commit()

        third.value = 2
        History.delete([second])
        third.save()
        transaction.commit()

        for test_name, timestamp, expected_1, expected_2 in [
                ('min', datetime.datetime.min, [], []),
                ('first', first_stamp, [first], []),
                ('second', second_stamp, [second], [first]),
                ('third', third_stamp, [third], [second]),
                ('max', datetime.datetime.max, [], [third]),
                ('no history', None, [], [third]),
                ]:
            with Transaction().set_context(_datetime=timestamp):
                with self.subTest(f"{test_name} ('value', '=', 1)"):
                    records = History.search([('value', '=', 1)])
                    self.assertEqual(records, expected_1)
                with self.subTest(f"{test_name} ('value', '=', 2)"):
                    records = History.search([('value', '=', 2)])
                    self.assertEqual(records, expected_2)

    @with_transaction()
    def test_ordered_search(self):
        'Test ordered search of history models'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()
        order = [('value', 'ASC')]

        history = History(value=1)
        history.save()
        first_id = history.id
        first_stamp = history.create_date
        transaction.commit()

        history = History(value=2)
        history.save()
        second_id = history.id
        second_stamp = history.create_date

        transaction.commit()

        first, second = History.search([], order=order)

        self.assertEqual(first.id, first_id)
        self.assertEqual(second.id, second_id)

        first.value = 3
        first.save()
        third_stamp = first.write_date
        transaction.commit()

        results = [
            (first_stamp, [first]),
            (second_stamp, [first, second]),
            (third_stamp, [second, first]),
            (datetime.datetime.now(), [second, first]),
            (datetime.datetime.max, [second, first]),
            ]
        for timestamp, instances in results:
            with Transaction().set_context(_datetime=timestamp):
                records = History.search([], order=order)
                self.assertEqual(records, instances)
            transaction.rollback()

        to_delete, _ = History.search([], order=order)

        self.assertEqual(to_delete.id, second.id)

        History.delete([to_delete])
        transaction.commit()

        results = [
            (first_stamp, [first]),
            (second_stamp, [first, second]),
            (third_stamp, [second, first]),
            (datetime.datetime.now(), [first]),
            (datetime.datetime.max, [first]),
            ]
        for timestamp, instances in results:
            with Transaction().set_context(_datetime=timestamp,
                    from_test=True):
                records = History.search([], order=order)
                self.assertEqual(records, instances)
            transaction.rollback()

    @unittest.skipUnless(backend.name == 'postgresql',
        'CURRENT_TIMESTAMP as transaction_timestamp is specific to postgresql')
    @with_transaction()
    def test_ordered_search_same_timestamp(self):
        'Test ordered search  with same timestamp'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()
        order = [('value', 'ASC')]

        history = History(value=1)
        history.save()
        first_stamp = history.create_date
        history.value = 4
        history.save()
        second_stamp = history.write_date

        self.assertEqual(first_stamp, second_stamp)
        transaction.commit()

        results = [
            (second_stamp, [history], [4]),
            (datetime.datetime.now(), [history], [4]),
            (datetime.datetime.max, [history], [4]),
            ]

        for timestamp, instances, values in results:
            with Transaction().set_context(_datetime=timestamp,
                    last_test=True):
                records = History.search([], order=order)
                self.assertEqual(records, instances)
                self.assertEqual([x.value for x in records], values)
            transaction.rollback()

    @with_transaction()
    def test_ordered_search_nested(self):
        "Test ordered search nested"
        pool = Pool()
        History = pool.get('test.history')
        HistoryLine = pool.get('test.history.line')
        transaction = Transaction()
        order = [('history.value', 'ASC')]

        history = History(value=1)
        history.save()
        history2 = History(value=2)
        history2.save()
        line = HistoryLine(history=history)
        line.save()
        line2 = HistoryLine(history=history2)
        line2.save()
        first_stamp = line2.create_date
        transaction.commit()

        history.value = 3
        history.save()
        second_stamp = history.write_date
        transaction.commit()

        results = [
            (first_stamp, [line, line2]),
            (second_stamp, [line2, line]),
            ]
        for timestamp, instances in results:
            with Transaction().set_context(_datetime=timestamp):
                records = HistoryLine.search([], order=order)
                self.assertListEqual(records, instances)

    @with_transaction()
    def test_browse(self):
        'Test browsing history'
        pool = Pool()
        History = pool.get('test.history')
        Line = pool.get('test.history.line')
        transaction = Transaction()

        history = History(value=1)
        history.save()
        history_id = history.id
        line_a = Line(name='a', history=history)
        line_a.save()
        line_a_id = line_a.id
        line_b = Line(name='b', history=history)
        line_b.save()
        line_b_id = line_b.id

        first_stamp = line_b.create_date

        history.stamp = first_stamp
        history.save()

        transaction.commit()

        history = History(history_id)
        history.value = 2
        history.save()

        Line.delete([Line(line_b_id)])

        line_a = Line(line_a_id)
        line_a.name = 'c'
        line_a.save()

        second_stamp = line_a.write_date

        transaction.commit()

        history = History(history_id)
        self.assertEqual(history.value, 2)
        self.assertEqual([l.name for l in history.lines], ['c'])
        self.assertEqual(history.stamp, first_stamp)
        self.assertEqual(
            [l.name for l in history.lines_at_stamp], ['a', 'b'])

        with Transaction().set_context(_datetime=first_stamp):
            history = History(history_id)
        self.assertEqual(history.value, 1)
        self.assertEqual([l.name for l in history.lines], ['a', 'b'])

        with Transaction().set_context(_datetime=second_stamp):
            history = History(history_id)
        self.assertEqual(history.value, 2)
        self.assertEqual([l.name for l in history.lines], ['c'])
        self.assertEqual(history.stamp, first_stamp)
        self.assertEqual(
            [l.name for l in history.lines_at_stamp], ['a', 'b'])

    @with_transaction()
    def test_search_large_number(self):
        'Test search with large number of history entries'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        history = History(value=-1)
        history.save()

        for history.value in range(1_000 + 1):
            history.save()

        with transaction.set_context(_datetime=datetime.datetime.max):
            record, = History.search([])

            self.assertEqual(record.value, 1_000)

    @with_transaction()
    def test_search_large_number_entries(self):
        'Test search for skipping first history entries of large number'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        for i in range(0, 2):
            history = History(value=-1)
            history.save()

            for history.value in range(1_000 + 1):
                history.save()

        with transaction.set_context(_datetime=datetime.datetime.max):
            records = History.search([])

            self.assertEqual({r.value for r in records}, {1_000})
            self.assertEqual(len(records), 2)

    @with_transaction()
    def test_search_large_number_histories(self):
        'Test search with large number of histories'
        pool = Pool()
        History = pool.get('test.history')
        transaction = Transaction()

        n = 1_000
        History.create([{'value': 1}] * n)

        with transaction.set_context(_datetime=datetime.datetime.max):
            records = History.search([])

            self.assertEqual({r.value for r in records}, {1})
            self.assertEqual(len(records), n)
