"""
Tests for base.Structure-s.

This is for sort-of generic functionality of structs.

Tests for specific types of attributes should go to attrdeftest.
"""

#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 io import StringIO
import re
import unittest

from gavo.helpers import testhelpers

from gavo import base
from gavo.base import structure
from gavo.base import xmlstruct

from structscaffold import (Color, Foo, Foos, Bla,
	Palette, Image, Root)


class SimpleStructureTest(unittest.TestCase):
	"""tests for very basic structures.
	"""
	def _getStruct(self):
		class Bla(structure.ParseableStructure):
			_att1 = base.UnicodeAttribute("att1", default="default")
		return Bla

	def testSimpleAttSetting(self):
		class Bla(structure.ParseableStructure):
			_att1 = base.UnicodeAttribute("att1")
		s1 = Bla(None)
		self.assertEqual(s1.att1, None)
		s1._att1.feed(None, s1, "value")
		self.assertEqual(s1.att1, "value")
	
	def testDefaultAtt(self):
		Bla = self._getStruct()
		s1 = Bla(None)
		self.assertEqual(s1.att1, "default")
	
	def testNoInterference(self):
		Bla = self._getStruct()
		s1, s2 = Bla(None), Bla(None)
		s2._att1.feed(None, s2, "Foo")
		self.assertEqual(s1.att1, "default")
		self.assertEqual(s2.att1, "Foo")

	def testDefault(self):
		Bla = self._getStruct()
		s1 = Bla(None, att1="no default")
		self.assertEqual(s1.att1, "no default")


class AtomListStructureTest(unittest.TestCase):
	"""tests for structures with lists.
	"""
	def testBasic(self):
		s1 = Bla(None)
		self.assertEqual(s1.items, [])
		s1._att1.feed(None, s1, "2")
		self.assertEqual(s1.items, [2])
		s1._att1.feedObject(s1, 1)
		self.assertEqual(s1.items, [2, 1])
	
	def testNoInterference(self):
		s1, s2 = Bla(None), Bla(None)
		s1._att1.feedObject(s1, 3)
		s2._att1.feedObject(s2, 1)
		self.assertEqual(s1.items, [3])
		self.assertEqual(s2.items, [1])
	
	def testRaises(self):
		s1 = Bla(None)
		try:
			s1._att1.feed(None, s1, "x4")
		except base.LiteralParseError as msg:
			self.assertEqual(msg.attName, "item")
			self.assertEqual(msg.literal, "x4")
		else:
			self.fail("Bad integer literal doesn't raise correct exc. in list")


def _feedInto(baseStruct, eventList):
	ep = xmlstruct.EventProcessor(baseStruct, base.ParseContext())
	for ev in eventList:
		ep.feed(*ev)
	return ep.result


class AtomicFeedTest(testhelpers.VerboseTest):
	"""tests for feeding into atomic structures.
	"""
	def _getPlainStructure(self):
		class Bla(structure.ParseableStructure):
			name_ = "bla"
			_foo = base.UnicodeAttribute("foo", default="default")
			_bar = base.IntAttribute("bar", 0)
		return Bla

	def testSomeAtoms(self):
		Bla = self._getPlainStructure()
		s1 = _feedInto(Bla, [
			("start", "bla"),
			("start", "foo"),
			("value", "content_", "no default"),
			("end", "foo"),
			("value", "bar", 3),])
		self.assertEqual(s1.foo, "no default")
		self.assertEqual(s1.bar, 3)

	def testRaising(self):
		Bla = self._getPlainStructure()
		self.assertRaisesWithMsg(base.StructureError,
			"bla elements have no xxx attributes or children.", _feedInto,
			(Bla, [("start", "bla"), ("start", "xxx")]))


class XMLParseTest(testhelpers.VerboseTest):
	"""tests for building structures out of XML documents.
	"""
	def testSimpleParse(self):
		f = xmlstruct.parseFromStream(Foo, StringIO("""<foo name="red">
			<color g="0" b="0"/></foo>"""))
		self.assertEqual(f.color.r, 255)
		self.assertEqual(f.color.g, 0)
		self.assertEqual(f.color.b, 0)
		self.assertEqual(f.name, "red")

	def testIdSet(self):
		f = xmlstruct.parseFromStream(Foos, StringIO("""<foos>
			<foo name="red" id="x"><color g="0" b="0"/></foo>
			<foo name="blue"><color g="0" r="0" id="y"/></foo></foos>"""))
		self.assertEqual(f.plups[0].id, "x")
		self.assertEqual(f.plups[1].color.id, "y")

	def testWrongRootElement(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<foo name="red"> <color g="0" b="0"/></foo>\', (1, 0):'
			" Expected root element color, found foo",
			xmlstruct.parseFromStream, (Color, StringIO("""<foo name="red">
			<color g="0" b="0"/></foo>""")))

	def testMsgBadAtt(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<color noAtt="30"/>\', (1, 0):'
			" color elements have no noAtt attributes or children.",
			xmlstruct.parseFromString, (Color, '<color noAtt="30"/>'))

	def testMsgBadChild(self):
		self.assertRaisesWithMsg(base.StructureError,
			"At IO:'<color><noAtt>30</noAtt></color>', (1, 7):"
			" color elements have no noAtt attributes or children.",
			xmlstruct.parseFromString, (Color, '<color><noAtt>30</noAtt></color>'))

	def testAtomListParse(self):
		f = xmlstruct.parseFromStream(Bla, StringIO('<bla item="0"><item>1</item>'
			'<item>2</item></bla>'))
		self.assertEqual(f.items, [0, 1, 2])

	def testStructContent(self):
		f = xmlstruct.parseFromString(Foo, '<foo name="xy">Some content</foo>')
		self.assertEqual(f.content_, "Some content")
		self.assertRaisesWithMsg(base.StructureError,
			"At IO:'<color>Some content</color>', (1, 19):"
			" color elements must not have character data content.",
			xmlstruct.parseFromString, (Color, '<color>Some content</color>'))

	def testNoAttrsOnAttrs(self):
		self.assertRaisesWithMsg(
			base.StructureError,
			"At IO:'<foo><name attr=\"out\">xy</name></foo>', (1, 5):"
			" name is atomic and thus has no attribute attr.",
			xmlstruct.parseFromString,
			(Foo, '<foo><name attr="out">xy</name></foo>'))

	def testNoChildrenOnAttrs(self):
		self.assertRaisesWithMsg(
			base.StructureError,
			"At IO:'<foo><name><child>out</child>xy</name></foo>', (1, 11):"
			" name elements cannot have child children",
			xmlstruct.parseFromString,
			(Foo, '<foo><name><child>out</child>xy</name></foo>'))


class CopyTest(testhelpers.VerboseTest):
	"""tests for copying structures.
	"""
	def testCopyStruct(self):
		f = xmlstruct.parseFromStream(Foo, StringIO("""<foo name="red">
			<color g="0" b="0"/></foo>"""))
		f2 = f.copy(None)
		f.color.g = 27
		self.assertEqual(f2.color.g, 0)
	
	def testCopyAtomList(self):
		f = xmlstruct.parseFromStream(Bla, StringIO('<bla item="0"><item>1</item>'
			'<item>2</item></bla>'))
		f2 = f.copy(None)
		f2.items.append(3)
		self.assertEqual(f.items, [0, 1, 2])
		self.assertEqual(f2.items, [0, 1, 2, 3])

	def testCopyStructList(self):
		f = xmlstruct.parseFromStream(Palette, StringIO('<pal><color/>'
			'<color g="0"/></pal>'))
		f2 = f.copy(None)
		f2.colors.append(Color(f2, r=0))
		self.assertEqual(len(f.colors), 2)
		self.assertEqual(len(f2.colors), 3)
	
	def testOriginal(self):
		f = xmlstruct.parseFromStream(Palette, StringIO('<pal><color id="x">'
			'<r>7</r></color><color g="0" original="x"/></pal>'))
		self.assertEqual(f.colors[0].g, 255)
		self.assertEqual(f.colors[1].g, 0)
		f.colors[1].r = 8
		self.assertEqual(f.colors[1].r, 8)
		self.assertEqual(f.colors[0].r, 7)

	def testTypecheck(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<pal><foo name="x" id="xy"/><color original="xy"/></pal>\', (1, 28):'
			" Reference to 'xy' yielded object of type Foo, but expected a(n) Color"
			" instance.",
			xmlstruct.parseFromString, (Palette, '<pal><foo name="x" id="xy"/>'
			'<color original="xy"/></pal>'))

	def testElementRefusal(self):
		self.assertRaisesWithMsg(base.StructureError,
			'At IO:\'<pal><color id="xy"/><color r="2"><original>xy</origi...\', (1, 46):'
			" Original must be applied before modifying the destination structure.",
			xmlstruct.parseFromString, (Palette, '<pal><color id="xy"/>'
			'<color r="2"><original>xy</original></color></pal>'))

	def testBadId(self):
		self.assertRaisesWithMsg(base.NotFoundError,
			"Element with id 'xy' could not be located in parse context",
			xmlstruct.parseFromString, (Palette, '<pal><color original="xy"/>'
			'</pal>'))


class FeedFromTest(testhelpers.VerboseTest):
	"""tests for working of feedFrom hackery.
	"""
	def testBasic(self):
		f = xmlstruct.parseFromString(Palette,
			'<pal id="pal1"><foo name="x"><color r="0"/>content</foo></pal>')
		f2 = xmlstruct.parseFromString(Palette,
			'<pal id="pal2"/>')
		f2.feedFrom(f)
		self.assertEqual(f2.foos[0].content_, "content",
			"Copyable attribute was not copied")
		self.assertEqual(f2.id, "pal2", "Non-copyable attribute was copied")


class BeforeTest(testhelpers.VerboseTest):
	"""tests for sorting of children.
	"""
	def testWorkingSort(self):
		class Foo(base.Structure):
			_att1 = base.UnicodeAttribute("b", before="a")
			_att2 = base.UnicodeAttribute("a")
		self.assertEqual([a.name_ for a in Foo.attrSeq], ["b", "a", "id"])
	
	def testBailOnCycle(self):
		def defineBadStruct():
			class Foo(base.Structure):
				_att1 = base.UnicodeAttribute("b", before="a")
				_att2 = base.UnicodeAttribute("a", before="c")
				_att3 = base.UnicodeAttribute("c", before="b")
		self.assertRaises(ValueError, defineBadStruct)


class FeedingTest(testhelpers.VerboseTest):
	def testMixedParse(self):
		f = _feedInto(Foo, [
			("start", "foo"),
			("start", "color"),
			("value", "r", "10"),
			("start", "g"),
			("value", "content_", "20"),
			("end", "g"),
			("end", "color"),
			("start", "name"),
			("value", "content_", "blue"),
			("end", "name"),])
		self.assertEqual(f.color.r, 10)
		self.assertEqual(f.color.g, 20)
		self.assertEqual(f.color.b, 255)
		self.assertEqual(f.name, "blue")


class IterEventsTest(testhelpers.VerboseTest):
	def testPlainAttribute(self):
		s = base.parseFromString(Color, '<color r="5" g="7"/>')
		self.assertEqual(list(s.iterEvents()),
			[('value', 'g', '7'), ('value', 'r', '5')])

	def testNamedRef(self):
		# see testImmediateRef for the counterpart where there's not
		# actually a reference to be filled
		stuff = base.parseFromString(Root, '<root>'
			'<pal id="pal1"><color r="20"/></pal>'
			'<image pal="pal1"/></root>')
		self.assertTrue(('value', 'pal', 'pal1') in list(stuff.iterEvents()))

# See commented-out parsecontext.ReferenceAttribute.iterEvents
#	def testImmediateRef(self):
#		stuff = base.parseFromString(Root, '<root>'
#			'<image><pal id="pal1"><color r="20"/></pal></image></root>')
#		# make sure the pal element is present (rather than a bogus reference)
#		self.failUnless(('start', 'pal', None) in
#			list(stuff.iterEvents()))
	
	def testNamedRefCopied(self):
		stuff = base.parseFromString(Root, '<root>'
			'<pal id="pal1"><color r="20"/></pal>'
			'<image id="im1" pal="pal1"/><image original="im1"/></root>')
		self.assertEqual(
			list(stuff.iterEvents()).count(('value', 'pal', 'pal1')),
			2)

	def testAtomList(self):
		stuff = base.parseFromString(Image,
			'<image><calib>abc</calib><calib>def</calib></image>')
		evs = list(stuff.iterEvents())
		self.assertEqual(len(evs), 6)
		self.assertEqual(evs[1], ('value', 'content_', 'abc'))
		self.assertEqual(evs[2], ('end', 'calib', None))


class AttachedPositionsTest(testhelpers.VerboseTest):
	sampleXML = ('<root>\n'
			'<pal id="pal1">\n<color r="20"/></pal>\n'
			'<pal id="pal2">\n<color r="10"/></pal>\n'
			'</root>')

	def testPositionAdded(self):
		root = base.parseFromString(Root, self.sampleXML)
		self.assertEqual(root.getSourcePosition(),
			'IO:\'<root> <pal id="pal1"> <color r="20"/></pal> <pal id=...\', line 1')
		self.assertEqual(root.pals[0].getSourcePosition(),
			'IO:\'<root> <pal id="pal1"> <color r="20"/></pal> <pal id=...\', line 2')
		self.assertEqual(root.pals[1].getSourcePosition(),
			'IO:\'<root> <pal id="pal1"> <color r="20"/></pal> <pal id=...\', line 4')

	def testFilePositions(self):
		def clean(s):
			return re.sub(".*/", "", s)

		with testhelpers.testFile("bla.xml", self.sampleXML) as p:
			with open(p) as f:
				root = base.parseFromStream(Root, f)
		self.assertEqual(
			clean(root.getSourcePosition()),
			'bla.xml, line 1')
		self.assertEqual(
			clean(root.pals[0].getSourcePosition()),
			'bla.xml, line 2')
		self.assertEqual(
			clean(root.pals[1].getSourcePosition()),
			'bla.xml, line 4')


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