#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

from __future__ import (division as _py3_division,
                        print_function as _py3_print,
                        absolute_import as _py3_abs_import)

from itertools import product
import unittest
import logging

from hypothesis import strategies as s, given
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle
from hypothesis.stateful import run_state_machine_as_test

from xoeuf.osv import expression as expr
from xoeuf.osv import ql
from xoeuf.odoo.osv import expression as odoo_expr
from xoeuf.osv.expression import Domain, DomainTree

from xoeuf.odoo.tests.common import TransactionCase

names = s.text(alphabet='abdefgh', min_size=1, max_size=5)
operators = s.sampled_from(['=', '!=', '<', '>', '<>'])
all_operators = s.sampled_from(
    ['=', '!=', '<', '>', '<>', 'like', 'ilike', 'not like', 'not ilike']
)
ages = s.integers(min_value=0, max_value=120)

# Logical connectors with the amount of terms it connects.  Notice that ''
# takes two arguments because it's the same as '&'.
connectors = s.sampled_from([('', 2), ('&', 2), ('!', 1), ('|', 2)])

logger = logging.getLogger(__name__)


@s.composite
def terms(draw, fields=None, values=s.integers(min_value=-10, max_value=10)):
    f = fields or names
    name = str(draw(f))
    operator = draw(operators)
    value = draw(values)
    return (name, operator, value)


@s.composite
def domains(draw, fields=None, min_size=1, max_size=10):
    leaves = draw(s.lists(
        terms(fields),
        min_size=min_size,
        max_size=max_size,
    ))
    result = []
    while leaves:
        connector, many = draw(connectors)
        if many <= len(leaves):
            if connector:
                result.append(connector)
            result.extend(leaves[:many])
            leaves = leaves[many:]
        elif len(leaves) == 1:
            result.append(leaves.pop(0))
    return Domain(result)


class TestDomain(unittest.TestCase):
    @given(domains())
    def test_first_normal_form_idempotency(self, domain):
        self.assertEqual(
            domain.first_normal_form,
            domain.first_normal_form.first_normal_form
        )

    @given(domains())
    def test_second_normal_form_idempotency(self, domain):
        self.assertEqual(
            domain.second_normal_form,
            domain.second_normal_form.second_normal_form
        )

    @given(domains())
    def test_versions_equivalency(self, domain):
        # Any domain must be equivalent in all its versions.
        all_versions = [
            domain,
            domain.simplified,
            domain.first_normal_form,
            domain.second_normal_form
        ]
        for version1, version2 in product(all_versions, all_versions):
            self.assertEqual(
                version1,
                version2,
                msg="%r != %r" % (version1, version2)
            )

    @given(domains())
    def test_simplified(self, domain):
        # A simplified domain never start with '&' operator.
        self.assertNotEqual(
            domain.simplified[0],
            expr.AND_OPERATOR,
            msg="%r start with &" % domain.simplified
        )
        # The simplified domain must be the shortest.
        all_versions = [
            domain,
            domain.first_normal_form,
            domain.second_normal_form
        ]
        for version in all_versions:
            self.assertLessEqual(
                len(domain.simplified),
                len(version),
                msg="len(%r) > len(%r)" % (domain.simplified, version)
            )
        self.assertTrue(
            all(odoo_expr.is_leaf(term) or odoo_expr.is_operator(term)
                for term in domain.simplified)
        )

    @given(domains())
    def test_simplified_idempotency(self, domain):
        self.assertEqual(
            domain.simplified,
            domain.simplified.simplified
        )

    @given(s.lists(domains(), min_size=2, max_size=10))
    def test_expr_replacement(self, domains):
        expr.AND(domains)
        odoo_expr.AND(domains)

    def test_eq(self):
        A = Domain([
            ('field_x', '=', 1)
        ])
        # A == A
        self.assertTrue(A == A)

        # ~A == ~A
        self.assertTrue(~A == ~A)

        # (x = 1)  == ! (x <> 1)
        A1 = Domain([
            '!',
            ('field_x', '<>', 1)
        ])
        self.assertTrue(A == A1)

    def test_implies(self):
        # Basic implications.
        # -------------------

        A = Domain([
            ('field_x', '=', 1)
        ])
        B = Domain([
            ('field_y', '!=', False)
        ])

        # A => A
        self.assertTrue(A.implies(A))

        # A => A | B
        self.assertTrue(A.implies(A | B))

        # A & B => A
        self.assertTrue((A & B).implies(A))

        # A & B => A | B
        self.assertTrue((A & B).implies(A | B))

        self.assertEqual(Domain.AND(list(A), B), (A & B))
        self.assertEqual(expr.AND([list(A), B]), (A & B))

        # Inter terms implications.
        # -------------------------

        # (x = 1)  => (x = 1)
        A = Domain([
            ('field_x', '=', 1)
        ])
        A1 = Domain([
            ('field_x', '=', 1)
        ])
        self.assertTrue(A.implies(A1))

        # (x != 1)  => (x <> 1)
        A = Domain([
            ('field_x', '!=', 1)
        ])
        A1 = Domain([
            ('field_x', '<>', 1)
        ])
        self.assertTrue(A.implies(A1))

        # (x > 1) => (x > 0)
        A = Domain([
            ('field_x', '>', 10)
        ])
        A1 = Domain([
            ('field_x', '>', 1)
        ])
        self.assertTrue(A.implies(A1))

        # (x in (1,2,3)) => (x in (1,2,3,4))
        A = Domain([
            ('field_x', 'in', (1, 2, 3))
        ])
        A1 = Domain([
            ('field_x', 'in', (1, 2, 3, 4))
        ])
        self.assertTrue(A.implies(A1))
        #

        # Compound implications.
        # ----------------------
        # A => B & C iff (A => B) & (A => C)
        A = Domain([
            ('field_x', 'in', (1, 4))
        ])
        B = Domain([
            ('field_x', 'in', (1, 2, 4))
        ])
        C = Domain([
            ('field_x', 'in', (1, 3, 4))
        ])
        self.assertTrue(A.implies(B))
        self.assertTrue(A.implies(C))
        self.assertTrue(A.implies(B & C))

        # More complex implications
        # -------------------------

        x = Domain([
            '|',
            ('field_y', '!=', False),
            ('field_x', '=', 'value'),
            ('field_z', 'in', (1,))
        ])
        y = Domain([
            '|',
            ('field_y', '!=', False),
            ('field_x', '=', 'value'),
            ('field_z', 'in', (1, 2, 3)),
            '|',
            ('field_x', '=', 'value'),
            ('field_y', '!=', False)
        ])
        assert x.implies(y)

        x = Domain([
            ('field_y', '!=', False),
            ('field_z', 'in', (1,)),
            ('field_z', 'in', (1, 2)),
            '|',
            ('field_y', '!=', False),
            '|',
            ('field_z', 'in', (1,)),
            ('field_z', 'in', (2,))
        ])
        y = Domain([
            ('field_y', '!=', False),
            ('field_z', 'in', (1, 2, 3))
        ])
        assert x.implies(y)

    def test_walk(self):
        y = Domain([
            '|',
            ('a', '!=', False),
            ('b', '=', 'value'),
            ('c', 'in', (1, 2, 3)),
            '|', '|', '!',
            ('d', '=', 'value'),
            ('f', '!=', False),
            ('g', '=', 'h')
        ])
        DomainTree(y.second_normal_form)
        # expected = [
        #     ('TERM', ('c', 'in', (1, 2, 3))),
        #     ('TERM', ('g', '=', 'h')),
        #     ('TERM', ('d', '!=', 'value')),
        #     ('TERM', ('f', '!=', False)),
        #     ('OPERATOR', '|'),
        #     ('OPERATOR', '|'),
        #     ('TERM', ('a', '!=', False)),
        #     ('TERM', ('b', '=', 'value')),
        #     ('OPERATOR', '|'),
        #     ('OPERATOR', '&'),
        #     ('OPERATOR', '&')
        # ]
        # self.assertEqual(expected, list(tree.walk()))

    def test_get_filter_ast_simple_one_term_with_in(self):
        self.assertEqual(
            DomainTree(Domain([('state', 'in', [1, 2])]))._get_filter_ast(),
            ql.Expression(ql.Lambda(
                ql.make_arguments('this'),
                expr._constructor_in('this', 'state', [1, 2])
            ))
        )
        Domain([('state', 'in', [1, 2])]).asfilter()

    def test_empty_domain(self):
        self.assertTrue(Domain([]).asfilter()(0))


def get_model_domain_machine(this):
    Model = this.env['test_domain.model']

    class ModelDomainMachine(RuleBasedStateMachine):
        objects = Bundle('objects')

        @rule(target=objects, name=names, age=ages)
        def create_object(self, name, age):
            logger.info("Creating object name: %s, age: %s", name, age)
            return Model.create({'name': name, 'age': age})

        @rule(target=objects, name=names, age=ages, parent=objects)
        def create_child_object(self, name, age, parent):
            return Model.create({
                'name': name,
                'age': age,
                'parent_id': parent.id
            })

        @rule(age=ages, op=operators)
        def find_by_age(self, age, op):
            query = Domain([('age', op, age)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(age=ages, op=operators,
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_parent_age(self, age, op, path):
            attr = '.'.join(path) + '.age'
            query = Domain([(attr, op, age)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(ages=s.lists(ages))
        def find_by_ages(self, ages):
            query = Domain([('age', 'in', ages)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(ages=s.lists(ages),
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_parent_ages(self, ages, path):
            attr = '.'.join(path) + '.age'
            query = Domain([(attr, 'in', ages)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(ages=s.lists(ages))
        def find_by_not_ages(self, ages):
            query = Domain([('age', 'not in', ages)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(ages=s.lists(ages),
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_path_not_ages(self, ages, path):
            attr = '.'.join(path) + '.age'
            query = Domain([(attr, 'not in', ages)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(domain=domains(fields=s.just('age')))
        def find_by_arbitrary_domain(self, domain):
            res = Model.search(domain)
            logger.info("Check filter/domain: %s; count: %s", domain, len(res))
            this.assertEqualRecordset(res.filtered(domain.asfilter()), res)

        @rule(name=names, op=all_operators)
        def find_by_name(self, name, op):
            query = Domain([('name', op, name)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(name=names, op=all_operators,
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_path_name(self, name, op, path):
            attr = '.'.join(path) + '.name'
            query = Domain([(attr, op, name)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(names=s.lists(names))
        def find_by_names(self, names):
            query = Domain([('name', 'in', names)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(names=s.lists(names),
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_path_names(self, names, path):
            attr = '.'.join(path) + '.name'
            query = Domain([(attr, 'in', names)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(names=s.lists(names))
        def find_by_not_names(self, names):
            query = Domain([('name', 'not in', names)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

        @rule(names=s.lists(names),
              path=s.lists(s.sampled_from(['parent_id', 'children_ids']),
                           min_size=1, max_size=4))
        def find_by_path_not_names(self, names, path):
            attr = '.'.join(path) + '.name'
            query = Domain([(attr, 'not in', names)])
            res = Model.search(query)
            logger.info("Check filter/domain: %s; count: %s", query, len(res))
            this.assertEqualRecordset(res.filtered(query.asfilter()), res)

    return ModelDomainMachine


class TestConsistencyOfFilters(TransactionCase):
    def assertEqualRecordset(self, rs1, rs2):
        ours = rs1 - rs2
        theirs = rs2 - rs1
        self.assertEqual(
            rs1,
            rs2,
            msg="ours: {0!r}; theirs: {1!r}".format(ours, theirs)
        )

    def test_consistency_of_domains(self):
        run_state_machine_as_test(get_model_domain_machine(self))
