import itertools
import ldap.schema

object_classes = [
	"( 0 NAME 'top' DESC 'top' ABSTRACT MUST objectClass )",
	"( 1 NAME 'foo' DESC 'foo' STRUCTURAL MUST ( foo1 $ foo2 $ foo3 ) MAY ( foo4 $ foo5 $ foo6 ) )",
	"( 2 NAME 'bar' DESC 'bar' AUXILIARY MUST  ( abc1 $ bar1 $ bar2 $ bar3 ) MAY ( bar4 $ bar5 $ bar6 ) )",
	"( 3 NAME 'baz' DESC 'baz' AUXILIARY MUST  ( baz1 $ baz2 $ baz3 ) MAY ( baz4 $ baz5 $ baz6 ) )",
	"( 4 NAME 'abc' DESC 'abc' AUXILIARY MUST  ( abc1 $ abc2 $ abc3 ) MAY ( abc4 $ abc5 $ abc6 ) )",
	"( 5 NAME 'def' DESC 'def' AUXILIARY MUST  ( def1 $ def2 $ def3 ) MAY ( def4 $ def5 $ def6 ) )",
	"( 6 NAME 'ghi' DESC 'ghi' AUXILIARY MUST  ( ghi1 $ ghi2 $ ghi3 ) MAY ( ghi4 $ ghi5 $ ghi6 $ abc2 ) )",
]

i = 0
def foo():
	global i
	i += 1
	return i
attribute_types = [
	"( %d.%d NAME '%s%d' DESC 'nothing' EQUALITY objectIdentifierMatch SYNTAX 1000 )" % (y, foo(), name, y)
	for (name, y) in itertools.product(['foo', 'bar', 'baz', 'abc', 'def', 'ghi'], range(1, 7))
] + ["( 0.0 NAME 'objectClass' DESC 'OCS' )"]

schema = ldap.schema.SubSchema({'objectClasses': object_classes, 'attributeTypes': attribute_types})

# we want to remove 'abc' and 'def'
# so the object must not have {abc,def}{1,2,3} but it is allowed to have abc2 (due to ghi) and it must have abc1 (due to bar)
valid = [{
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
	'abc2': ['a'],  # MAY in OCS 'ghi'
}, {
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
}]

invalid = [{
	# doesn't contain 'abc1' MUST of 'bar'
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc2': ['a'],  # MAY in OCS 'ghi'
}, {
	# doesn't contain 'abc1' MUST of 'bar'
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
}, {
	# doesn't contain 'foo1' MUST of 'foo'
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
}, {
	# contains abc3 MAY only by 'abc'
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
	'abc3': ['3'],
}, {
	# doesn't contain structural OCS 'foo'
	'objectClass': ['top', 'bar', 'baz', 'ghi'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
}, {
	# contains 'def1' MUST only by 'def'
	'objectClass': ['top', 'foo', 'bar', 'baz', 'ghi'],
	'foo1': ['1'],
	'foo2': ['2'],
	'foo3': ['3'],
	'bar1': ['1'],
	'bar2': ['2'],
	'bar3': ['3'],
	'baz1': ['1'],
	'baz2': ['2'],
	'baz3': ['3'],
	'ghi1': ['1'],
	'ghi2': ['2'],
	'ghi3': ['3'],
	'abc1': ['a'],  # MUST in OCS 'bar'
	'def1': ['d'],
}]

class Invalid(Exception):
	pass
class Failed(Exception):
	pass

def test_valid(obj):
	removed_object_classes = set(['top', 'foo', 'bar', 'baz', 'abc', 'def', 'ghi']) - set(obj['objectClass'])
	object_classes_afterwards = obj['objectClass']
	object_classes_previous = set(removed_object_classes) | set(object_classes_afterwards)
	if not schema.get_structural_oc(object_classes_afterwards):
		raise Invalid('no structural object class')
	must, may = schema.attribute_types(object_classes_afterwards)
	must = list(itertools.chain.from_iterable(x.names for x in must.values()))
	may = list(itertools.chain.from_iterable(x.names for x in may.values()))
	for x in must:
		if not obj.get(x):
			raise Invalid('attribute %r required' % (x,))
	for attr, val in obj.items():
		if not val:
			continue
		if attr not in must and attr not in may:
			raise Invalid('no object class for attribute %r' % (attr,))

for obj in valid:
	test_valid(obj)
for obj in invalid:
	try:
		test_valid(obj)
	except Invalid as exc:
		print 'detected invalid entry:', exc
		continue
	print Failed('failed', obj)
