import datetime
import uuid
from copy import copy
from decimal import Decimal
from django.db.models.signals import post_save
from mock import patch
from django.test import SimpleTestCase, TestCase
from corehq.apps.commtrack.models import StockState, update_domain_mapping
from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.userreports.exceptions import BadSpecError
from corehq.apps.userreports.indicators import LedgerBalancesIndicator
from corehq.apps.userreports.indicators.factory import IndicatorFactory
from corehq.apps.userreports.specs import EvaluationContext
from corehq.apps.products.models import SQLProduct
from corehq.util.context_managers import drop_connected_signals


class SingleIndicatorTestBase(SimpleTestCase):

    def _check_result(self, indicator, document, value, context=None):
        [result] = indicator.get_values(document, context=context)
        self.assertEqual(value, result.value)


class BooleanIndicatorTest(SingleIndicatorTestBase):

    def setUp(self):
        self.indicator = IndicatorFactory.from_spec({
            'type': 'boolean',
            'column_id': 'col',
            'filter': {
                'type': 'property_match',
                'property_name': 'foo',
                'property_value': 'bar',
            }

        })
        self.assertEqual(1, len(self.indicator.get_columns()))

    def testNoColumnId(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'filter': {
                    'type': 'property_match',
                    'property_name': 'foo',
                    'property_value': 'bar',
                }
            })

    def testEmptyColumnId(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'column_id': '',
                'filter': {
                    'type': 'property_match',
                    'property_name': 'foo',
                    'property_value': 'bar',
                }
            })

    def testNoFilter(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'column_id': 'col',
            })

    def testEmptyFilter(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'column_id': 'col',
                'filter': None,
            })

    def testBadFilterType(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'column_id': 'col',
                'filter': 'wrong type',
            })

    def testInvalidFilter(self):
        with self.assertRaises(BadSpecError):
            IndicatorFactory.from_spec({
                'type': 'boolean',
                'column_id': 'col',
                'filter': {
                    'type': 'property_match',
                    'property_value': 'bar',
                }
            })

    def testIndicatorMatch(self):
        self._check_result(self.indicator, dict(foo='bar'), 1)

    def testIndicatorNoMatch(self):
        self._check_result(self.indicator, dict(foo='not bar'), 0)

    def testIndicatorMissing(self):
        self._check_result(self.indicator, dict(notfoo='bar'), 0)

    def testComplexStructure(self):
        # in slightly more compact format:
        # ((foo=bar) or (foo1=bar1 and foo2=bar2 and (foo3=bar3 or foo4=bar4)))
        indicator = IndicatorFactory.from_spec({
            "type": "boolean",
            "column_id": "col",
            "filter": {
                "type": "or",
                "filters": [
                    {
                        "type": "property_match",
                        "property_name": "foo",
                        "property_value": "bar"
                    },
                    {
                        "type": "and",
                        "filters": [
                            {
                                "type": "property_match",
                                "property_name": "foo1",
                                "property_value": "bar1"
                            },
                            {
                                "type": "property_match",
                                "property_name": "foo2",
                                "property_value": "bar2"
                            },
                            {
                                "type": "or",
                                "filters": [
                                    {
                                        "type": "property_match",
                                        "property_name": "foo3",
                                        "property_value": "bar3"
                                    },
                                    {
                                        "type": "property_match",
                                        "property_name": "foo4",
                                        "property_value": "bar4"
                                    }
                                ]
                            },
                        ]
                    },
                ]
            }
        })
        # first level or
        self._check_result(indicator, dict(foo='bar'), 1)
        # first level and with both or's
        self._check_result(indicator, dict(foo1='bar1', foo2='bar2', foo3='bar3'), 1)
        self._check_result(indicator, dict(foo1='bar1', foo2='bar2', foo4='bar4'), 1)

        # first and not right
        self._check_result(indicator, dict(foo1='not bar1', foo2='bar2', foo3='bar3'), 0)
        # second and not right
        self._check_result(indicator, dict(foo1='bar1', foo2='not bar2', foo3='bar3'), 0)
        # last and not right
        self._check_result(indicator, dict(foo1='bar1', foo2='bar2', foo3='not bar3', foo4='not bar4'), 0)

    def test_not_null_filter_root_doc(self):
        indicator = IndicatorFactory.from_spec(
            {
                "filter": {
                    "filter": {
                        "operator": "in",
                        "expression": {
                            "expression": {
                                "datatype": None,
                                "type": "property_name",
                                "property_name": "ccs_opened_date"
                            },
                            "type": "root_doc"
                        },
                        "type": "boolean_expression",
                        "property_value": [
                            "",
                            None
                        ]
                    },
                    "type": "not"
                },
                "type": "boolean",
                "display_name": None,
                "column_id": "prop1_not_null"
            }
        )
        self._check_result(indicator, {}, 1, EvaluationContext(root_doc=dict(ccs_opened_date='not_null')))
        self._check_result(indicator, {}, 1, EvaluationContext(root_doc=dict(ccs_opened_date=' ')))
        self._check_result(indicator, {}, 0, EvaluationContext(root_doc=dict(ccs_opened_date='')))
        self._check_result(indicator, {}, 0, EvaluationContext(root_doc=dict(ccs_opened_date=None)))
        self._check_result(indicator, {}, 0, EvaluationContext(root_doc=dict()))


class CountIndicatorTest(SingleIndicatorTestBase):

    def testCount(self):
        indicator = IndicatorFactory.from_spec({
            "type": "count",
            "column_id": "count",
            "display_name": "Count"
        })
        self._check_result(indicator, dict(), 1)


class RawIndicatorTest(SingleIndicatorTestBase):

    def testMetadataDefaults(self):
        indicator = IndicatorFactory.from_spec({
            "type": "raw",
            "column_id": "foo",
            "datatype": "integer",
            'property_name': 'foo',
            "display_name": "raw foos",
        })
        self.assertEqual(True, indicator.column.is_nullable)
        self.assertEqual(False, indicator.column.is_primary_key)

    def testMetadataOverrides(self):
        indicator = IndicatorFactory.from_spec({
            "type": "raw",
            "column_id": "foo",
            "datatype": "integer",
            'property_name': 'foo',
            "display_name": "raw foos",
            "is_nullable": False,
            "is_primary_key": True,
        })
        self.assertEqual(False, indicator.column.is_nullable)
        self.assertEqual(True, indicator.column.is_primary_key)

    def test_raw_ints(self):
        indicator = IndicatorFactory.from_spec({
            "type": "raw",
            "column_id": "foo",
            "datatype": "integer",
            'property_name': 'foo',
            "display_name": "raw foos",
        })
        self._check_result(indicator, dict(foo="bar"), None)
        self._check_result(indicator, dict(foo=1), 1)
        self._check_result(indicator, dict(foo=1.2), 1)
        self._check_result(indicator, dict(foo=None), None)
        self._check_result(indicator, dict(nofoo='foryou'), None)

    def test_raw_strings(self):
        indicator = IndicatorFactory.from_spec({
            "type": "raw",
            "column_id": "foo",
            "datatype": "string",
            'property_name': 'foo',
            "display_name": "raw foos",
        })
        self._check_result(indicator, dict(foo="bar"), 'bar')
        self._check_result(indicator, dict(foo=1), '1')
        self._check_result(indicator, dict(foo=1.2), '1.2')
        self._check_result(indicator, dict(foo=None), None)
        self._check_result(indicator, dict(nofoo='foryou'), None)

    def testNestedSinglePath(self):
        self._check_result(
            self._default_nested_indicator(['property']),
            {'property': 'the right value'},
            'the right value'
        )

    def testNestedDeepReference(self):
        test_doc = {
            'parent': {
                'child': {
                    'grandchild': 'the right value'
                }
            }
        }
        self._check_result(
            self._default_nested_indicator(["parent", "child", "grandchild"]),
            test_doc,
            'the right value'
        )

    def testNestedInvalidTopLevel(self):
        self._check_result(
            self._default_nested_indicator(['parent', 'child']),
            {'badparent': 'bad value'},
            None,
        )

    def testNestedInvalidMidLevel(self):
        test_doc = {
            'parent': {
                'badchild': {
                    'grandchild': 'the wrong value'
                }
            }
        }
        self._check_result(
            self._default_nested_indicator(["parent", "child", "grandchild"]),
            test_doc,
            None
        )

    def _default_nested_indicator(self, path):
        return IndicatorFactory.from_spec({
            "type": "raw",
            "column_id": "foo",
            "datatype": "string",
            "property_path": path,
            "display_name": "indexed",
        })


class ExpressionIndicatorTest(SingleIndicatorTestBase):

    @property
    def simple_indicator(self):
        return IndicatorFactory.from_spec({
            "type": "expression",
            "expression": {
                "type": "property_name",
                "property_name": "foo",
            },
            "column_id": "foo",
            "datatype": "string",
            "display_name": "expression foos",
        })

    @property
    def complex_indicator(self):
        # this expression is the equivalent to:
        #   doc.true_value if doc.test == 'match' else doc.false_value
        return IndicatorFactory.from_spec({
            "type": "expression",
            "expression": {
                'type': 'conditional',
                'test': {
                    'type': 'boolean_expression',
                    'expression': {
                        'type': 'property_name',
                        'property_name': 'test',
                    },
                    'operator': 'eq',
                    'property_value': 'match',
                },
                'expression_if_true': {
                    'type': 'property_name',
                    'property_name': 'true_value',
                },
                'expression_if_false': {
                    'type': 'property_name',
                    'property_name': 'false_value',
                },
            },
            "column_id": "foo",