"""
Tests for sepecific AttributeDef derivations.

These are far too few so far because most of this is really old code.
Let's fill them back as we go.
"""

#c Copyright 2008-2025, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


from gavo.helpers import testhelpers

from gavo import base
from gavo import utils
from gavo.user import docgen
from gavo.base.structure import Structure

from structscaffold import (Color, Child, Child2, Image, Root)


MS = base.makeStruct


class MiscAttributeTest(testhelpers.VerboseTest):
	"""tests for various predefined attribute types.
	"""
	def testEnumUnicode(self):
		class En(Structure):
			_att1 = base.EnumeratedUnicodeAttribute("att1", default="left",
				validValues=["left", "right"])
		e = En(None)
		self.assertEqual(e.att1, "left")
		e._att1.feed(None, e, "right")
		self.assertEqual(e.att1, "right")
		self.assertRaises(base.LiteralParseError, e._att1.feed, None, e, "center")
		self.assertEqual(e._att1.typeDesc_, "One of: left, right")

	def testBadFloat(self):
		self.assertRaisesWithMsg(base.LiteralParseError,
			"At IO:'<color b='knall'/>', (1, 0):"
			" 'knall' is not a valid value for b",
		base.parseFromString,
		(Color, "<color b='knall'/>"))

	def testBadInt(self):
		self.assertRaisesWithMsg(base.LiteralParseError,
			"At IO:'<color r='knall'/>', (1, 0):"
			" 'knall' is not a valid value for r",
		base.parseFromString,
		(Color, "<color r='knall'/>"))

	def testBadBoolean(self):
		class q(Structure):
			name_ = "q"
			_att1 = base.BooleanAttribute("att1", default=None)
		self.assertRaisesWithMsg(base.LiteralParseError,
			'At IO:\'<q att1="banana"/>\', (1, 0):'
			' \'banana\' is not a valid value for att1',
		base.parseFromString,
		(q, '<q att1="banana"/>'))
	

class ReferenceAttTest(testhelpers.VerboseTest):
	def testRealReference(self):
		res = base.parseFromString(Root, '<root>'
			'<pal id="pal1"><color r="20"/></pal>'
			'<image pal="pal1"/></root>')
		self.assertEqual(res.ims[0].pal.colors[0].r, 20)
		self.assertEqual(res.ims[0].pal.parent, res)

	def testElementReference(self):
		res = base.parseFromString(Root, '<root>'
			'<pal id="pal1"><color r="20"/></pal>'
			'<image><pal>pal1</pal></image></root>')
		self.assertEqual(res.ims[0].pal.colors[0].r, 20)
		self.assertEqual(res.ims[0].pal.parent, res)

	def testImmediateReference(self):
		res = base.parseFromString(Root, '<root>'
			'<image><pal id="pal1"><color r="20"/></pal>'
			'</image></root>')
		self.assertEqual(res.ims[0].pal.colors[0].r, 20)
		self.assertEqual(res.ims[0].pal.parent, res.ims[0])

	def testAtomicFirstChild(self):
		res = base.parseFromString(Root,
			'<root><image><pal><map>a, b</map><flavor>yellow</flavor></pal>'
			'</image></root>')
		self.assertEqual(res.ims[0].pal.map,  ['a', 'b'])
		self.assertEqual(res.ims[0].pal.flavor,  "yellow")


class DictAttrTest(testhelpers.VerboseTest):
	def testBasic(self):
		i = base.parseFromString(Image, """<image><map key="a">arthur</map>
			<map from="b">berta</map></image>""")
		self.assertEqual(i.maps, {"a": "arthur", "b": "berta"})

	def testNoKey(self):
		self.assertRaisesWithMsg(base.StructureError,
			"At IO:'<image><map>arthur</map>', (1, 18):"
			" Content 'arthur' has no from attribute",
			base.parseFromString,
			(Image, """<image><map>arthur</map>"""))

	def testNoExtra(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<image><map key="a" extra="no">arthur</map>\', (1, 7):'
			' No "extra" attributes on mappings',
			base.parseFromString,
			(Image, """<image><map key="a" extra="no">arthur</map>"""))

	def testCumulate(self):
		i = base.parseFromString(Image, """<image><map key="a">arthur</map>
			<map from="a" cumulate="True"> berta</map></image>""")
		self.assertEqual(i.maps, {"a": "arthur berta"})

	def testInverted(self):
		i = base.parseFromString(Image, """<image><rev key="a">arthur</rev>
			<rev to="b" cumulate="True">berta</rev></image>""")
		self.assertEqual(i.rev, {"arthur": "a", "berta": "b"})

	def testMapEmpty(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<image><map key="a"/></image>\', (1, 21):'
			' Empty map values are not allowed',
			base.parseFromString,
			(Image, """<image><map key="a"/></image>"""))

	def testNonString(self):
		class WithInts(Structure):
			name_ = "withints"
			_nums = base.DictAttribute("ints",
				itemAttD=base.IntAttribute("int"),
				keyName="german")

		res = base.parseFromString(WithInts,
			'<withints><int german="eins">1</int><int german="zwei">2'
			'</int></withints>')
		self.assertEqual(res.ints, {"eins": 1, "zwei": 2})
	
	def testEvents(self):
		i = base.parseFromString(Image, """<image><map key="a">arthur</map>
			<map from="b">berta</map></image>""")
		self.assertEqual(list(i._maps.iterEvents(i)),
			[('start', 'map', None), ('value', 'key', 'a'), ('value', 'content_', 'arthur'), ('end', 'map', None),
			('start', 'map', None), ('value', 'key', 'b'), ('value', 'content_', 'berta'), ('end', 'map', None)])


class PropertyAttributeTest(testhelpers.VerboseTest):
	class WithProp(Structure):
		name_ = "with"
		_prop = base.PropertyAttribute(copyable=True)
		def __repr__(self): return "<WithProp>"

	def testBasic(self):
		res = base.parseFromString(self.WithProp,
			'<with><property name="propA">Value</property>'
			'<property name="propB">other</property></with>')
		self.assertEqual(res.getProperty("propA"), "Value")
		self.assertEqual(res.getProperty("propB"), "other")
		self.assertEqual(res.getProperty("propC", "x"), "x")
		self.assertRaisesWithMsg(base.NotFoundError,
			"property 'propC' could not be located in <WithProp>",
			res.getProperty,
			("propC",))

	def testNoEmpties(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<with><property name="propA"/></with>\', (1, 30): Empty map values are not allowed',
			base.parseFromString,
			(self.WithProp,
			'<with><property name="propA"/></with>'))

	def testOverwrite(self):
		res = base.parseFromString(self.WithProp,
			'<with><property name="propA">Value</property>'
			'<property name="propA">overwritten</property></with>')
		self.assertEqual(res.getProperty("propA"), "overwritten")

	def testCumulate(self):
		res = base.parseFromString(self.WithProp,
			'<with><property name="propA">Value</property>'
			'<property name="propA" cumulate="True"> amended</property></with>')
		self.assertEqual(res.getProperty("propA"), "Value amended")

	def testEvents(self):
		res = base.parseFromString(self.WithProp,
			'<with><property name="propA">Value</property>'
			'<property name="propB">other</property></with>')
		self.assertEqual(
			list(res.iterEvents()),
			[('start', 'property', None), ('value', 'key', 'propA'), ('value', 'content_', 'Value'), ('end', 'property', None),
			('start', 'property', None), ('value', 'key', 'propB'), ('value', 'content_', 'other'), ('end', 'property', None)])


class SimpleStructAttributeTest(testhelpers.VerboseTest):
	class StructBearer(Structure):
		name_ = "bearer"
		_simple = base.StructAttribute("simple", childFactory=Child)

	def testConstruction(self):
		res = MS(self.StructBearer, simple=MS(Child, fab="one"))
		self.assertEqual(res.simple.fab, "one")

	def testParsing(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><simple fab="one"/></bearer>')
		self.assertEqual(res.simple.fab, "one")

	def testOverwriting(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><simple fab="one"/><simple fab="two"/></bearer>')
		self.assertEqual(res.simple.fab, "two")

	def testDefaulting(self):
		class DefaultBearer(Structure):
			name_ = "bearer"
			_simple = base.StructAttribute("simple", childFactory=Child,
				default=MS(Child, fab="fallback"))
		res = base.parseFromString(DefaultBearer,
			'<bearer/>')
		self.assertEqual(res.simple.fab, "fallback")
		res = base.parseFromString(DefaultBearer,
			'<bearer><simple fab="one"/></bearer>')
		self.assertEqual(res.simple.fab, "one")

	def testAliases(self):
		class DefaultBearer(Structure):
			name_ = "bearer"
			_simple = base.StructAttribute("simple", childFactory=Child,
				aliases=["foo", "bar"])

		for elName in ["simple", "foo", "bar"]:
			res = base.parseFromString(DefaultBearer,
				f'<bearer><{elName} fab="one"/></bearer>')
			self.assertEqual(res.simple.fab, "one")

	def testChanging(self):
		orig = MS(self.StructBearer, simple=MS(Child, fab="one"))
		res = orig.change(simple=MS(Child, fab="new"))
		self.assertEqual(res.simple.fab, "new")

	def testMissing(self):
		self.assertRaisesWithMsg(
			base.StructureError,
			"You must set simple on bearer elements",
			MS, (self.StructBearer,))
			
	def testNotMissing(self):
		class DefaultBearer(Structure):
			name_ = "bearer"
			_simple = base.StructAttribute(
				"simple", childFactory=Child, default=None)
		self.assertEqual(MS(DefaultBearer).simple, None)

	def testDoc(self):
		self.assertEqual(
			self.StructBearer._simple.makeUserDoc(),
			"**simple** (contains `Element child`_) -- Undocumented")


class MultiStructAttributeTest(testhelpers.VerboseTest):
	class StructBearer(Structure):
		name_ = "bearer"
		
		@staticmethod
		def getChild(name):
			return {
				"child": Child,
				"other": Child2}[name]

		_simple = base.MultiStructAttribute("poly",
			childFactory=getChild, childNames=["child", "other"],
			default=utils.Undefined)

	def testConstruction(self):
		res = MS(self.StructBearer, poly=MS(Child, fab="one"))
		self.assertEqual(res.poly.fab, "one")
		res = MS(self.StructBearer, poly=MS(Child2, romp="other"))
		self.assertEqual(res.poly.romp, "other")

	def testParsing(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><child fab="one"/></bearer>')
		self.assertEqual(res.poly.fab, "one")
		res = base.parseFromString(self.StructBearer,
			'<bearer><other romp="one"/></bearer>')
		self.assertEqual(res.poly.romp, "one")

	def testOverwriting(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><child fab="one"/><other romp="two"/></bearer>')
		self.assertEqual(res.poly.romp, "two")
		self.assertRaises(AttributeError,
			lambda: res.poly.fab)

	def testDefaulting(self):
		class DefaultBearer(self.StructBearer):
			name_ = "bearer"
			_simple = base.MultiStructAttribute("poly",
				childFactory=self.StructBearer.getChild, default=MS(Child2, romp="x"),
				childNames=["child", "other"])
		res = base.parseFromString(DefaultBearer, '<bearer/>')
		self.assertEqual(res.poly.romp, "x")
		res = base.parseFromString(DefaultBearer,
			'<bearer><child fab="one"/></bearer>')
		self.assertEqual(res.poly.fab, "one")

	def testNoAliases(self):
		try:
			class Wontwork(self.StructBearer):
				name_ = "bearer"
				_poly = base.MultiStructAttribute("poly",
					childFactory=self.StructBearer.getChild,
					childNames=["child", "other"],
					aliases=["foo", "bar"])
		except TypeError as msg:
			self.assertEqual(
				str(msg),
				"aliases are not supported with MultiStructAttribute")
		else:
			raise AssertionError("MultiStructAttribute does not reject aliases?")

	def testChanging(self):
		orig = MS(self.StructBearer, poly=MS(Child, fab="one"))
		res = orig.change(poly=MS(Child2, romp="new"))
		self.assertEqual(res.poly.romp, "new")

	def testMissing(self):
		self.assertRaisesWithMsg(
			base.StructureError,
			"You must set poly on bearer elements",
			MS, (self.StructBearer,))
			
	def testNotMissing(self):
		class DefaultBearer(self.StructBearer):
			name_ = "bearer"
			_simple = base.MultiStructAttribute("poly", default=None,
				childFactory=self.StructBearer.getChild, childNames=["child", "other"])

		self.assertEqual(MS(DefaultBearer).poly, None)

	def testBadChild(self):
		self.assertRaisesWithMsg(utils.StructureError,
			"At IO:'<bearer><invalidChild/></bearer>', (1, 8): bearer elements have no invalidChild attributes or children.",
			base.parseFromString,
			(self.StructBearer, '<bearer><invalidChild/></bearer>'))

	def testDoc(self):
		self.assertEqual(
			self.StructBearer._simple.makeUserDoc(),
			"**poly** (contains one of child, an `Element child`_;"
			" other, an `Element child2`_) -- Undocumented")


class StructListAttributeTest(testhelpers.VerboseTest):
	class StructBearer(Structure):
		name_ = "bearer"
		
		_simple = base.StructListAttribute("simple", childFactory=Child,
			copyable=True)
	
	def testConstruction(self):
		res = MS(self.StructBearer, simple=[])
		self.assertEqual(res.simple, [])

		res = MS(self.StructBearer, simple=[MS(Child)])
		self.assertEqual(res.simple[0].fab, "empty")

		res = MS(self.StructBearer, simple=[MS(Child), MS(Child, fab="ooh")])
		self.assertEqual(res.simple[1].fab, "ooh")

	def testConstructionAutoBuildsList(self):
		res = MS(self.StructBearer, simple=MS(Child))
		self.assertEqual(res.simple[0].fab, "empty")

	def testParsing(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><simple fab="one"/><simple/></bearer>')
		self.assertEqual(res.simple[0].fab, "one")
		self.assertEqual(res.simple[1].fab, "empty")

	def testDefaulting(self):
		self.assertRaisesWithMsg(TypeError,
			"StructListAttributes always default to an empty list",
			base.StructListAttribute,
			("simple", Child),
			default=MS(Child, fab="fallback"))

	def testAliases(self):
		class DefaultBearer(Structure):
			name_ = "bearer"
			_simple = base.StructListAttribute("simple", childFactory=Child,
				aliases=["foo", "bar"])

		res = base.parseFromString(DefaultBearer,
			'<bearer><simple fab="1"/><foo fab="2"/><bar fab="3"/></bearer>')
		self.assertEqual(res.simple[2].fab, "3")

	def testEvents(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><simple fab="one"/><simple fab="two"/></bearer>')
		self.assertEqual(
			list(res._simple.iterEvents(res)), [
				('start', 'child', None), ('value', 'fab', 'one'), ('end', 'child', None),
				('start', 'child', None), ('value', 'fab', 'two'), ('end', 'child', None)])

	def testCopying(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><simple fab="one"/><simple fab="two"/></bearer>')
		newOb = MS(self.StructBearer)
		newOb.feedObject("simple", res._simple.getCopy(res, newOb, None))
		self.assertEqual(res.simple[1].fab, "two")
		self.assertEqual(newOb.simple[1].fab, "two")
		self.assertTrue(res.simple[0] is not newOb.simple[0])

	def testCanUseChildName(self):
		res = base.parseFromString(self.StructBearer,
			'<bearer><child fab="one"/><child fab="two"/></bearer>')
		self.assertEqual(res.simple[0].fab, "one")
		self.assertEqual(res.simple[1].fab, "two")

	def testDoc(self):
		self.assertEqual(
			self.StructBearer._simple.makeUserDoc(),
			"**simple** (contains `Element child`_ and may be repeated zero or more times) -- Undocumented")


class TypingAttributesTest(testhelpers.VerboseTest):
	def testDefaults(self):
		class Silly(Structure):
			_klotz = base.AttributeDef("klotz")

		self.assertEqual(docgen._getAnnotationsForStructure(Silly), [
			('managedAttrs', 'Dict[str, Any]'),
			('attrSeq', 'List[]'),
			('id', 'str'),
			('klotz', 'Any')])

	def testParentMethods(self):
		class MethAttr(base.AttributeDef):
			typingType_ = "IO"
			def iterParentMethods(self):
				def getSomething(self, par1, par2):
					return par1+par2
				def getOther(self, foo: str, bar: int) -> str:
					return foo*bar
				yield ("getSomething", getSomething)
				yield ("getOther", getOther)

		class Foo(Structure):
			_platt = MethAttr("platt")

		self.assertEqual(docgen._getAnnotationsForStructure(Foo), [
			('managedAttrs', 'Dict[str, Any]'),
			('attrSeq', 'List[]'),
			('id', 'str'),
			('platt', 'IO'),
			('getSomething', 'Callable'),
			('getOther', 'Callable[..., "str"]')])


if __name__=="__main__":
	testhelpers.main(SimpleStructAttributeTest)
