#!/usr/bin/env python2

import unittest
from pyvap import Store, UnsolvableError
from pyvap.constraints import NumUnique
import pyvap.types

class TestBasics(unittest.TestCase):
	def setUp(self):
		self.store = Store()
		self.a = self.store.IntVar('a')
		self.b = self.store.IntVar('b')
		self.c = self.store.IntVar('c')
		self.constraint = NumUnique(self.a, self.b, self.c)
		self.out = self.constraint.result
		self.out.name = 'out'

	def test_zero_variables_raises(self):
		with self.assertRaises(TypeError):
			NumUnique()

	def test_type(self):
		self.assertIs(type(self.out), pyvap.types.IntVar)

	def test_attributes(self):
		self.assertEqual(len(self.constraint.operands), 3)
		self.assertIs(self.constraint.operands[0], self.a)
		self.assertIs(self.constraint.operands[1], self.b)
		self.assertIs(self.constraint.operands[2], self.c)

	def test_limits(self):
		default = self.store.IntVar()
		self.assertEqual(self.a.mini,   default.mini)
		self.assertEqual(self.a.maxi,   default.maxi)
		self.assertEqual(self.b.mini,   default.mini)
		self.assertEqual(self.b.maxi,   default.maxi)
		self.assertEqual(self.c.mini,   default.mini)
		self.assertEqual(self.c.maxi,   default.maxi)
		self.assertEqual(self.out.mini, 1)
		self.assertEqual(self.out.maxi, 3)

	def test_different_stores_raises(self):
		extra = Store().IntVar('extra')
		with self.assertRaisesRegexp(ValueError, 'Different stores'):
			NumUnique(self.a, extra, self.b)
		with self.assertRaisesRegexp(ValueError, 'Different stores'):
			NumUnique(extra, self.a, self.b)

	def test_exact(self):
		for aval in xrange(-2, 4):
			for bval in xrange(1, 3):
				for cval in xrange(3, 5):
					values = set((aval, bval, cval))

					# first one way...
					self.store._push()
					self.a.value = aval
					self.b.value = bval
					self.c.value = cval
					self.assertEqual(self.out.value, len(values))
					self.store._pop()

					# then another.
					self.store._push()
					self.c.value = cval
					self.a.value = aval
					self.b.value = bval
					self.assertEqual(self.out.value, len(values))
					self.store._pop()

	def test_raises(self):
		# an UnsolvableError must ALWAYS be raised when all values are
		# determined without fulfilling the constraint.
		for aval in xrange(3, 4):
			for bval in xrange(-2, 3):
				for cval in xrange(1, 3):
					values = set((aval, bval, cval))
					for oval in (x for x in xrange(0, 4) if x != len(values)):
						self.store._push()
						self.a.value = aval
						self.b.value = bval
						self.c.value = cval
						with self.assertRaises(UnsolvableError):
							self.out.value = oval
						self.store._pop()

						self.store._push()
						if 1 <= oval <= 3:
							self.out.value = oval
							with self.assertRaises(UnsolvableError):
								self.b.value = bval
								self.a.value = aval
								self.c.value = cval
							self.store._pop()

	def test_out_mini_determines(self):
		self.out.shrink(2, 5)
		self.b.value = 5
		self.c.value = 5
		self.assertNotIn(5, self.a)

	def test_out_maxi_excludes(self):
		self.out.shrink(0, 2)
		self.b.value = 5
		self.c.value = 10
		self.assertIn(5, self.a)
		self.assertIn(10, self.a)
		self.assertEqual(len(self.a), 2)

	def test_out_one(self):
		self.out.value = 1
		self.assertIsNone(self.a.value)
		self.assertIsNone(self.b.value)
		self.assertIsNone(self.c.value)
		self.a.value = 5
		self.assertEqual(self.b.value, 5)
		self.assertEqual(self.c.value, 5)

	def test_out_exact_coerce(self):
		self.out.value = 2
		self.c.value = 5
		self.a.value = 3
		self.assertIn(3, self.b)
		self.assertIn(5, self.b)
		self.assertEqual(len(self.b), 2)

	def test_out_exact_exclude(self):
		self.c.shrink(-10, 10)
		self.out.value = 3
		self.a.value = 5
		self.b.value = 2
		self.assertNotIn(2, self.c)
		self.assertNotIn(5, self.c)
		self.assertEqual(len(self.c), 19)

class TestAdvanced(unittest.TestCase):
	def setUp(self):
		self.store = Store()
		self.a = self.store.IntVar('a')
		self.b = self.store.IntVar('b')
		self.c = self.store.IntVar('c')
		self.d = self.store.IntVar('d')
		self.constraint = NumUnique(self.a, self.b, self.c, self.d)
		self.out = self.constraint.result
		self.out.name = 'out'

	def test_small_domains(self):
		self.a.shrink(1,2)
		self.b.shrink(1,2)
		self.c.value =  2
		self.d.value = 10
		self.assertEqual(self.out.mini, 2)
		self.assertLessEqual(self.out.maxi, 4)
		# TODO: would be nice if this worked:
		# self.assertEqual(self.out.maxi, 3)

	def test_tiny_domains(self):
		self.a.shrink(1)
		self.b.shrink(1)
		self.c.shrink(1,2)
		self.d.shrink(1)
		self.assertEqual(self.out.mini, 1)
		self.assertEqual(self.out.maxi, 2)

	def test_disjoint_domains(self):
		self.a.shrink( 1,  5)
		self.b.shrink( 6, 10)
		self.c.shrink(11, 15)
		self.d.shrink(16, 20)
		self.assertEqual(self.out.maxi, 4)
		self.assertGreaterEqual(self.out.mini, 1)
		# TODO: would be nice if this worked:
		# self.assertEqual(self.out.mini, 4)

if __name__ == '__main__':
	unittest.main()
