"""
Some tests for votable production.
"""

#c Copyright 2008-2024, 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.


import base64
import datetime
import io
import os
import unittest

from gavo.helpers import testhelpers

from gavo import base
from gavo import rsc
from gavo import rscdef
from gavo import rscdesc
from gavo import utils
from gavo import votable
from gavo.formats import votableread, votablewrite, getFormatted
from gavo.svcs import outputdef
from gavo.utils import ElementTree
from gavo.utils import pgsphere

import tresc


def getTDForVOTable(votCode):
	"""returns an rsc.TableDef instance for a votable defined by the
	fields, params, and groups within votCode.
	"""
	for votRes in votable.parseBytes(b"<VOTABLE><RESOURCE><TABLE>"
			b"%s<DATA/></TABLE></RESOURCE></VOTABLE>"%utils.bytify(votCode)):
		return votableread.makeTableDefForVOTable(
			"testing", votRes.tableDefinition)


class _TestVOTable(testhelpers.TestResource):
	"""Used in VOTableTest.
	"""

	testData = """some silly test data
-33  -3400abc
23829328.9xxy
     22
          nas
"""

	rdLiteral = """
		<resource resdir="%s" schema="test">
			<meta name="description">Some test data for VOTables.</meta>
			<meta name="_legal">Hands off this fascinating data</meta>
			<table id="foo">
				<meta name="utype">test:testTable</meta>
				<meta name="note" tag="1">Note 1</meta>
				<meta name="note" tag="2">Note 2</meta>
				<column name="anInt" type="integer"
					description="This is a first data field" note="1"
					xtype="test:junk">
					<values nullLiteral="-32923"/>
				</column>
				<column name="aFloat"
					description="This ain't &amp;alpha; for sure." note="1"/>
				<column name="bla" type="text" note="2"/>
				<column name="varArr" type="real[]"
					description="horrible array"/>
				<param name="somePar" type="double precision">3.500</param>
			</table>
			<data id="bar">
				<meta name="utype">test:testResource</meta>
				<meta name="_infolink">http://vo.org/x?a=b&amp;c=d</meta>
				<columnGrammar topIgnoredLines="1">
					<col key="anInt">1-5</col>
					<col key="aFloat">6-10</col>
					<col key="bla">11-13</col>
				</columnGrammar>
				<rowmaker id="_foo" idmaps="*">
					<apply>
						<code>
							if vars["anInt"]=='-33':
								vars["varArr"] = None
							else:
								vars["varArr"] = [1,2]
						</code>
					</apply>
				</rowmaker>
				<make table="foo" rowmaker="_foo"/>
			</data>
		</resource>"""%os.path.abspath("data")

	def _makeRD(self):
		"""returns a test resource descriptor.
		"""
		return base.parseFromString(rscdesc.RD, self.rdLiteral)

	def make(self, ignored):
		rd = self._makeRD()
		dataSet = rsc.makeData(rd.getById("bar"),
			forceSource=io.StringIO(self.testData))

		dataSet.tables["foo"].setMeta("name", "from-meta")

		# TODO: Port this (and the tests) to using getXMLTree and lxml xpath
		rawVOTable = votablewrite.getAsVOTable(dataSet, tablecoding="td")
		tree = ElementTree.fromstring(rawVOTable)
		return rawVOTable, tree

_testVOTable = _TestVOTable()


def _getVOTTreeForTable(tdXML):
	td = base.parseFromString(rscdef.TableDef, tdXML)
	table = rsc.TableForDef(td)
	rawVOTable = votablewrite.getAsVOTable(table, tablecoding="td",
		suppressNamespace=True)
	return ElementTree.fromstring(rawVOTable)


def _pprintEtree(root):
	import subprocess
	p = subprocess.Popen(["xmlstarlet", "fo"], stdin=subprocess.PIPE)
	ElementTree.ElementTree(root).write(p.stdin)
	p.stdin.close()


def _getElementByID(root, id):
	for el in root.iter():
		if el.attrib.get("ID")==id:
			return el


class VOTableTest(testhelpers.VerboseTest, testhelpers.XSDTestMixin):

	resources = [("testData", _testVOTable)]

	def testValidates(self):
		self.assertValidates(self.testData[0])

	def testNameFromMeta(self):
		self.assertEqual(
			self.testData[1].find(
				"{}/{}".format(
					votable.voTag("RESOURCE"),
					votable.voTag("TABLE"))).get("name"),
			"from-meta")

	def testFloatNullvalue(self):
		tree = self.testData[1]
		tbldata = tree.find(".//%s"%votable.voTag("TABLEDATA"))
		self.assertEqual(tbldata[3][1].text, 'NaN', "NULL isn't rendered as"
			" NaN")
	
	def testIntNullvalue(self):
		tree = self.testData[1]
		tbldata = tree.find(".//%s"%votable.voTag("TABLEDATA"))
		fields = tree.findall(".//%s"%votable.voTag("FIELD"))
		f0Null = fields[0].find(str(votable.voTag("VALUES"))).get("null")
		self.assertEqual(tbldata[2][0].text, f0Null)

	def testArrayNullvalue(self):
		tbldata = self.testData[1].find(".//%s"%votable.voTag("TABLEDATA"))
		self.assertEqual(tbldata[0][3].text, None)

	def testNotes(self):
		tree = self.testData[1]
		groups = tree.findall(".//%s"%votable.voTag("GROUP"))
		self.assertEqual(groups[0].get("name"), "note-1")
		self.assertEqual(groups[0][0].text, "Note 1")
		self.assertEqual(groups[0][1].get("ref"), "anInt")
		self.assertEqual(groups[0][2].get("ref"), "aFloat")
		self.assertEqual(groups[1][0].text, "Note 2")
		self.assertEqual(groups[1][1].get("ref"), "bla")

	def testXtype(self):
		tree = self.testData[1]
		intCol = tree.findall(".//%s"%votable.voTag("FIELD"))[0]
		self.assertEqual(intCol.get("xtype"), "test:junk")

	def testParamVal(self):
		tree = self.testData[1]
		table = tree.findall(".//%s"%votable.voTag("TABLE"))[0]
		params = table.findall(".//%s"%votable.voTag("PARAM"))
		self.assertEqual(params[0].get("value"), "3.500")
		self.assertEqual(params[0].get("name"), "somePar")
		self.assertEqual(params[0].get("datatype"), "double")

	def testTableUtype(self):
		table = self.testData[1].findall(".//%s"%votable.voTag("TABLE"))[0]
		self.assertEqual(table.get("utype"), "test:testTable")

	def testResourceUtype(self):
		votRes = self.testData[1].findall(".//%s"%votable.voTag("RESOURCE"))[0]
		self.assertEqual(votRes.get("utype"), "test:testResource")


class _ImportTestData(testhelpers.TestResource):
	def __init__(self, fName, nameMaker=None):
		self.fName, self.nameMaker = fName, nameMaker
		testhelpers.TestResource.__init__(self)

	def make(self, deps):
		# we need a connection of our own so the temp table gets torn down
		# immediately
		self.conn = base.getDBConnection("untrustedquery")
		with open(self.fName) as f:
			tableDef = votableread.uploadVOTable("votabletest", f,
				connection=self.conn, nameMaker=self.nameMaker
				).tableDef
			data = list(self.conn.query("select * from votabletest"))
		return tableDef, data
	
	def clean(self, ignored):
		self.conn.close()


class ImportTest(testhelpers.VerboseTest):
	"""tests for working VOTable ingestion.
	"""
	resources = [("testData", _ImportTestData("test_data/importtest.vot"))]

	def testValidData(self):
		td, data = self.testData
		row = data[0]
		self.assertAlmostEqual(row[0], 72.183030)
		self.assertEqual(row[3], 1)
		self.assertEqual(row[5], 'NGC 104')
		self.assertTrue(isinstance(row[6], str))
		self.assertAlmostEqual(row[7][0], 305.9, places=4)
		self.assertEqual(row[9], 34)

	def testNULLs(self):
		td, data = self.testData
		row = data[1]
		self.assertEqual(row, (None,)*len(row))

	def testNames(self):
		td, data = self.testData
		self.assertEqual([f.name for f in td],
			['_r', 'field', 'field_', 'class_', 'result__', 'Cluster',
				'RAJ2000', 'GLON', 'xFexHxz', 'n_xFexHxz', 'xFexHxc',
				'FileName', 'HR', 'n_VHB', 'apex', 'roi', 'dali_point',
				'dali_timestamp', 'cov'])

	def testTypes(self):
		td, data = self.testData
		self.assertEqual([f.type for f in td],
			['double precision', 'double precision', 'double precision',
				'integer', 'smallint', 'text', 'text', 'real[2]', 'real',
				'smallint', 'real', 'text', 'text', 'char', 'spoint', 'spoly',
				'spoint', 'timestamp', 'smoc'])

	def testParams(self):
		td, data = self.testData
		self.assertEqual(td.params[0].name, "qua1")
		self.assertEqual(td.params[1].name, "qua2")
		self.assertEqual(td.params[0].value, "first param")
		self.assertEqual(td.params[1].value, 2)

	def testColumnMeta(self):
		td, _ = self.testData
		col = td.getColumnByName("field")
		self.assertEqual(col.ucd, "POS_EQ_RA_MAIN")
		self.assertEqual(col.type, "double precision")
		self.assertEqual(col.unit, "deg")
		self.assertEqual(col.description,
			"Right ascension (FK5) Equinox=J2000. (computed by"
			" VizieR, not part of the original data)")

	def testPointVals(self):
		_, data = self.testData
		self.assertTrue(isinstance(data[0][14], pgsphere.SPoint))
		self.assertAlmostEqual(data[0][14].x, 42*utils.DEG)
		self.assertEqual(data[1][14], None)

	def testRegionVals(self):
		_, data = self.testData
		self.assertTrue(isinstance(data[0][15], pgsphere.SPoly))
		self.assertEqual(data[1][15], None)

	def testMOC(self):
		self.assertEqual(
			self.testData[1][0][18].asASCII(),
			"2/2 3/12 231 4/29-31 52 5/212 6/")
		self.assertEqual(self.testData[1][1][18], None)


class VizierImportTest(testhelpers.VerboseTest):
	"""tests for ingestion of a random vizier VOTable.
	"""
	resources = [("testData", _ImportTestData("test_data/vizier_votable.vot",
		nameMaker=votableread.AutoQuotedNameMaker()))]

	def testNames(self):
		td, data = self.testData
		self.assertEqual(len(data), 50)
		self.assertEqual(td.columns[4].name.name, "RA(ICRS)")
		self.assertEqual(td.columns[4].key, 'RA__ICRS__')
	
	def testData(self):
		td, data = self.testData
		for tuple in data:
			if tuple[4]=="04 26 20.741":
				break
		else:
			self.fail("04 26 20.741 not found in any row.")


class NastyImportTest(tresc.TestWithDBConnection):
	"""tests for working VOTable ingestion with ugly VOTables.
	"""
	def _assertAfterIngestion(self, fielddefs, literals, testCode,
			nameMaker):
		table = votableread.uploadVOTable("junk",
			io.BytesIO(
			b'<VOTABLE><RESOURCE><TABLE>'+
			utils.bytify(fielddefs)+
			b'<DATA><TABLEDATA>'+
			utils.bytify('\n'.join('<TR>%s</TR>'%''.join('<TD>%s</TD>'%l
				for l in row) for row in literals))+
			b'</TABLEDATA></DATA>'
			b'</TABLE></RESOURCE></VOTABLE>'),
			self.conn, nameMaker=nameMaker)
		testCode(table)

	def testDupesRejected(self):
		self.assertRaises(base.ValidationError,
			self._assertAfterIngestion,
			'<FIELD name="condition-x" datatype="boolean"/>'
			'<FIELD name="condition-x" datatype="int"/>',
			[['True', '0']], None, nameMaker=votableread.QuotedNameMaker())

	def testNastyName(self):
		def test(table):
			self.assertEqual(list(table), [{'condition-x': True}])
			self.assertEqual(table.tableDef.columns[0].name, "condition-x")

		self._assertAfterIngestion(
			'<FIELD name="condition-x" datatype="boolean"/>',
			[['True']], test, nameMaker=votableread.QuotedNameMaker())
	
	def testNastierName(self):
		def test(table):
			self.assertEqual(list(table),
				 [{'altogether "messy" shit': True}])
			self.assertEqual(table.tableDef.columns[0].name,
				'altogether "messy" shit')

		self._assertAfterIngestion(
			'<FIELD name=\'altogether "messy" shit\' datatype="boolean"/>',
			[['True']], test, nameMaker=votableread.QuotedNameMaker())

	def testNoIdentifiers(self):
		def test(table):
			self.assertTrue(isinstance(table.tableDef.columns[0].name,
				utils.QuotedName))
			self.assertTrue(isinstance(table.tableDef.columns[1].name,
				str))

		self._assertAfterIngestion(
			'<FIELD name="SELECT" datatype="boolean"/>'
			'<FIELD name="SELECT_" datatype="boolean"/>',
			[['True', 'False']], test, nameMaker=votableread.AutoQuotedNameMaker())

	def testXtypes(self):
		def test(table):
			self.assertEqual(table.tableDef.columns[0].type, 'spoint')
			data = list(table)
			self.assertEqual(data[0]["p"], None)
			self.assertEqual(data[0]["d"], None)
			self.assertEqual(data[0]["j"], None)
			self.assertAlmostEqual(data[1]["p"].x, 2*utils.DEG)
			self.assertAlmostEqual(data[1]["p"].y, 3*utils.DEG)
			self.assertEqual(data[1]["d"], datetime.datetime(2005, 5, 6, 21, 10, 19))
			self.assertEqual(data[1]["u"], '2005-05-06T21:10:19')
			self.assertEqual(data[1]["j"], {'Tätigkeit': 'klönen'})

		self._assertAfterIngestion(
			'<FIELD name="p" datatype="char" arraysize="*" xtype="adql:POINT"/>'
			'<FIELD name="u" datatype="char" arraysize="*" xtype="adql:FANTASY"/>'
			'<FIELD name="x" datatype="char" arraysize="*" xtype="adql:TIMESTAMP"/>'
			'<FIELD name="d" datatype="char" arraysize="*" xtype="timestamp"/>'
			'<FIELD name="j" datatype="char" arraysize="*" xtype="json"/>',
			[['', '', '', "", ""],
				[
					'Position ICRS 2 3',
					'2005-05-06T21:10:19',
					'2005-05-06T21:10:19',
					'2005-05-06T21:10:19',
					'{"Tätigkeit": "klönen"}']],
			test, nameMaker=votableread.AutoQuotedNameMaker())


class MetaTest(testhelpers.VerboseTest):
	"""tests for inclusion of some meta items.
	"""
	def _getTestData(self):
		table = rsc.TableForDef(
			testhelpers.getTestRD().getById("typesTable").change(onDisk=False,
				id="fud"),
			rows=[{"anint": 10, "afloat": 0.1, "adouble": 0.2,
				"atext": "a", "adate": datetime.date(2004, 0o1, 0o1),
				"somejson": '"abc"'}])
		return rsc.wrapTable(table)

	def _assertVOTableContains(self, setupFunc, expectedStrings):
		data = self._getTestData()
		setupFunc(data)
		vot = votablewrite.getAsVOTable(data)
		try:
			for s in expectedStrings:
				self.assertTrue(utils.bytify(s) in vot, "%r not in VOTable"%s)
		except AssertionError:
			open("lastbad.xml", "wb").write(vot)
			raise

	def testWarning(self):
		def setupData(data):
			data.getPrimaryTable().addMeta("_warning",
				"Last warning: Do not use ' or \".")
			data.getPrimaryTable().addMeta("_warning",
				"Now, this *really* is the last warning")
		self._assertVOTableContains(setupData, [
			'<INFO name="warning" value="In table fud: Last warning:'
				' Do not use \' or &quot;."',
			'<INFO name="warning" value="In table fud: Now, this *really*',
		])

	def testLegal(self):
		def setupData(data):
			data.dd.rd.addMeta("copyright", "Please reference someone else")
		self._assertVOTableContains(setupData, [
			'<INFO name="legal" value="Please reference someone else"'])

	def testMetaExpanded(self):
		def setupData(data):
			data.dd.rd.addMeta("copyright", "\\RSTccby{world}")
		self._assertVOTableContains(setupData, [
			'<INFO name="legal" value="world is licensed under the `Creative'])

	def testSource(self):
		def setupData(data):
			data.getPrimaryTable().addMeta("source", "1543droc.book.....C")
		self._assertVOTableContains(setupData, [
			'<INFO name="publication_id" ucd="meta.bib.bibcode"'
				' value="1543droc.book.....C">'])

	def testSourceNoBibcode(self):
		def setupData(data):
			data.getPrimaryTable().addMeta("source", "Demleitner 2017, in prep.")
		self._assertVOTableContains(setupData, [
			'<INFO name="publication_id" ucd="meta.bib" value="Demleitner 2017, in prep."'])


class VOTableRenderTest(testhelpers.VerboseTest):
	def _getTable(self, colDef, rows=[{'x': None}]):
		return rsc.TableForDef(base.parseFromString(rscdef.TableDef,
				'<table>%s</table>'%colDef), rows=rows)

	def _getAsVOTable(self, colDef, **contextArgs):
		rows = contextArgs.pop("rows", [{"x": None}])
		contextArgs["tablecoding"] = contextArgs.get("tablecoding", "td")
		return votablewrite.getAsVOTable(
			self._getTable(colDef, rows),
			votablewrite.VOTableContext(**contextArgs))

	def _assertVOTContains(self, colDef, literals, **contextArgs):
		res = self._getAsVOTable(colDef, **contextArgs)
		for lit in literals:
			try:
				self.assertTrue(utils.bytify(lit) in res)
			except AssertionError:
				print(res)
				raise

	def _getAsETree(self, colDef, **contextArgs):
		vot = self._getAsVOTable(colDef, **contextArgs)
		return testhelpers.getXMLTree(vot, debug=False)


class ParamNullValueTest(VOTableRenderTest):
	def _getParamsFor(self, colDef):
		tree = self._getAsETree(colDef)
		return tree.xpath("//PARAM")

	def _getParamFor(self, colDef):
		pars = self._getParamsFor(colDef)
		return testhelpers.pickSingle(pars)

	def _assertDeclaredNull(self, colDef, nullLiteral):
		par = self._getParamFor(colDef)
		self.assertEqual(par.get("value"), nullLiteral)
		self.assertEqual(par[0].tag, votable.voTag("VALUES"))
		self.assertEqual(par[0].get("null"), nullLiteral)

	def _assertParsesAsNULL(self, colDef):
		par = self._getParamFor(colDef)
		self.assertEqual(par.get("value"), "")

	def testEmptyString(self):
		par = self._getParamFor('<param name="x" type="text"/>')
		self.assertEqual(par.get("value"), "")

	def testEmptyIntIsNull(self):
		self._assertParsesAsNULL(
			'<param name="x" type="integer"/>')

	def testStringNullDefault(self):
		self._assertParsesAsNULL(
			'<param name="x" type="text">__NULL__</param>')

	def testNonDefaultNULL(self):
		par = self._getParamFor(
			'<param name="x" type="integer"><values nullLiteral="-1"/>-1</param>')
		self.assertEqual(par.get("value"), "")
		self.assertEqual(par[0].tag, "VALUES")
		self.assertEqual(par[0].get("null"), "-1")

	def testIntDefault(self):
		table = self._getTable('<param name="x" type="integer"/>')
		table.setParam("x", None)
		par = testhelpers.getXMLTree(votablewrite.getAsVOTable(table)
			).xpath("//PARAM")[0]
		self.assertEqual(par.get("value"), '')

	def testFloatNaN(self):
		par = self._getParamFor('<param name="x">NaN</param>')
		self.assertEqual(par.get("value"), "")

	def testFloatEmpty(self):
		par = self._getParamFor('<param name="x"/>')
		self.assertEqual(par.get("value"), "")

	def testNonNullNotDeclared(self):
		par = self._getParamsFor('<param name="z" type="text">abc</param>')[0]
		self.assertEqual(len(par.xpath("VALUES")), 0)


class TabledataNullValueTest(VOTableRenderTest):
	def testIntNullRaising(self):
		table = self._getTable('<column name="x" type="integer"/>')
		self.assertRaisesWithMsg(votable.BadVOTableData,
			"Field 'x', value None: None passed for field that has no NULL value",
			votablewrite.getAsVOTable,
			(table, votablewrite.VOTableContext(acquireSamples=False)))

	def testIntNullGiven(self):
		self._assertVOTContains('<column name="x" type="integer">'
			'<values nullLiteral="-99"/></column>', [
			'<VALUES null="-99">',
			'<TR><TD>-99</TD></TR>'])
	
	def testCharNullGiven(self):
		self._assertVOTContains('<column name="x" type="char">'
				'<values nullLiteral="x"/></column>', [
			'<VALUES null="x">',
			'<TR><TD>x</TD></TR>'])

	def testTextNullGiven(self):
		self._assertVOTContains('<column name="x" type="text">'
				'<values nullLiteral="&quot;not given&quot;"/></column>', [
			'<VALUES null="&quot;not given&quot;">',
			'<TR><TD></TD></TR>'])
	
	def testTextNullAuto(self):
		self._assertVOTContains('<column name="x" type="text"/>',[
			'<TR><TD></TD></TR>'])
	
	def testTextNullAutoNoSample(self):
		self._assertVOTContains('<column name="x" type="text"/>',[
			'<TR><TD></TD></TR>'], acquireSamples=False)

	def testRealNullIgnoreGiven(self):
		self._assertVOTContains('<column name="x">'
				'<values nullLiteral="-9999."/></column>', [
			'<VALUES null="-9999.">',
			'<TR><TD>NaN</TD></TR>'])


class BinaryNullValueTest(VOTableRenderTest):
	def testVarArrayNull(self):
		tree = self._getAsETree('<column name="x" type="real[]"/>',
			tablecoding="binary")
		self.assertEqual(
			base64.b64decode(tree.xpath("//STREAM")[0].text),
			b'\x00\x00\x00\x00')


class GeometryTest(VOTableRenderTest):
	def testMOC(self):
		tree = self._getAsETree('<column name="x" type="smoc"/>',
			rows=[
				{'x': pgsphere.SMoc.fromASCII("5/10,11,12,4 6/564-579,580,581")},
				{'x': None}])
		self.assertEqual(tree.xpath("RESOURCE/TABLE/FIELD[@name='x']")[
			0].get("xtype"), "moc")
		self.assertEqual(
			tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[0].text,
			"5/4 10-12 6/564-581")
		self.assertEqual(
			tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[1].text,
			None)

	def testPolygonXtype(self):
		tree = self._getAsETree('<column name="x" type="spoly"/>',
			rows=[{
				"x": pgsphere.SPoly.fromDALI([1, 1, 2, 1, 2, 2])},
				{"x": None}])
		fe = tree.xpath("//FIELD")[0]
		self.assertEqual(fe.get("xtype"), "polygon")
		self.assertEqual(fe.get("datatype"), "double")
		self.assertEqual(fe.get("arraysize"), "*")
		self.assertEqual(
			tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[0].text,
			"1.0 1.0 2.0 1.0 2.0 2.0")
		self.assertEqual(
			tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[1].text,
			None)

	def testCircleXtype(self):
		tree = self._getAsETree('<column name="x" type="scircle"/>',
			rows=[{
				"x": pgsphere.SCircle.fromDALI([10, 18, 0.5])},
				{"x": None}])
		fe = tree.xpath("//FIELD")[0]
		self.assertEqual(fe.get("xtype"), "circle")
		self.assertEqual(fe.get("datatype"), "double")
		self.assertEqual(fe.get("arraysize"), "3")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[0].text,
			"10.0 18.0 0.5")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[1].text,
			None)

	def testPointXtype(self):
		tree = self._getAsETree('<column name="x" type="spoint"/>',
			rows=[{
				"x": pgsphere.SPoint.fromDALI([10, 18])},
				{"x": None}])
		fe = tree.xpath("//FIELD")[0]
		self.assertEqual(fe.get("xtype"), "point")
		self.assertEqual(fe.get("datatype"), "double")
		self.assertEqual(fe.get("arraysize"), "2")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[0].text,
			"10.0 18.0")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[1].text,
			None)


class RangeTest(VOTableRenderTest):
	def testSimpleInterval(self):
		tree = self._getAsETree(
			'<column name="x" type="integer[2]" xtype="interval"/>',
			rows=[
				{"x": base.NumericRange(1, 2, '[)')},
				{"x": base.NumericRange(1, 2, '[]')},
				{"x": None}])
		fe = tree.xpath("//FIELD")[0]
		self.assertEqual(fe.get("xtype"), "interval")
		self.assertEqual(fe.get("datatype"), "int")
		self.assertEqual(fe.get("arraysize"), "2")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[0].text,
			"1 2")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[1].text,
			"1 3")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/DATA/TABLEDATA/TR/TD")[2].text,
			None)


class TypesSerializationTest(VOTableRenderTest):
	def testUnicode(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="unicode"/>',
				rows=[{"x": "f\u00fcr"}])))
		self.assertEqual(data[0][0], "f\u00fcr")

	def testUnicodeBin(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="unicode"/>',
				rows=[{"x": "f\u00fcr"}], tablecoding="binary")))
		self.assertEqual(data[0][0], "f\u00fcr")

	def testByteaBin(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="bytea"/>',
				rows=[{"x": "\0"}], tablecoding="binary")))
		self.assertEqual(data[0][0], 0)

	def testByteaBin2(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="bytea"/>',
				rows=[{"x": "u"}], tablecoding="binary2")))
		self.assertEqual(data[0][0], 117)

	def testByteaWithType(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="bytea"'
				' required="true"/>',
				rows=[{"x": "u"}], tablecoding="td")))
		self.assertEqual(metadata[0].datatype, "unsignedByte")
		self.assertEqual(data[0][0], 117)

	def testBytearr(self):
		data, metadata = votable.load(
			io.BytesIO(self._getAsVOTable('<column name="x" type="bytea"/>',
				rows=[{"x": "u"}], tablecoding="td")))
		self.assertEqual(data[0][0], 117)


class ValuesParsedTest(testhelpers.VerboseTest):
	def testNull(self):
		td = getTDForVOTable(
			'<FIELD name="foo" datatype="int"><VALUES null="-1"/></FIELD>')
		self.assertEqual(td.getColumnByName("foo").values.nullLiteral,
			"-1")
	
	def testMinMax(self):
		td = getTDForVOTable(
			'<FIELD name="foo" datatype="int"><VALUES>'
			'<MIN value="23"/><MAX value="42"/></VALUES></FIELD>')
		self.assertEqual(td.getColumnByName("foo").values.min, "23")
		self.assertEqual(td.getColumnByName("foo").values.max, "42")

	def testOptions(self):
		td = getTDForVOTable(
			'<FIELD name="foo" datatype="int"><VALUES>'
			'<OPTION value="23"/><OPTION value="42" name="yes"/></VALUES></FIELD>')
		opts = td.getColumnByName("foo").values.options
		self.assertEqual(opts[0].content_, 23)
		self.assertEqual(opts[0].title, "23")
		self.assertEqual(opts[1].content_, 42)
		self.assertEqual(opts[1].title, "yes")


class GroupWriteTest(testhelpers.VerboseTest):
	def testEmptyGroup(self):
		tree = _getVOTTreeForTable(
			'<table><group name="tg" ucd="empty.group" utype="testing:silly"'
			' description="A meaningless group"/>'
				'</table>')
		res = tree.findall("RESOURCE/TABLE/GROUP")
		grp = testhelpers.pickSingle(res)
		self.assertEqual(grp.attrib["ucd"], "empty.group")
		self.assertEqual(grp.attrib["utype"], "testing:silly")
		self.assertEqual(grp.attrib["name"], "tg")
		self.assertEqual(grp.find("DESCRIPTION").text,
			"A meaningless group")
	
	def testRefs(self):
		tree = _getVOTTreeForTable(
			'<table><group><columnRef dest="x"/><columnRef dest="y"/>'
			'<paramRef dest="z"/></group>'
			'<column name="x"/><column name="y"/>'
			'<param name="z" type="integer">4</param>'
			'</table>')
		table = tree.find("RESOURCE/TABLE")
		g = table.find("GROUP")

		refs = [el.attrib["ref"] for el in g.findall("FIELDref")]
		self.assertEqual(len(refs), 2)
		self.assertEqual(_getElementByID(table, refs[0]).attrib["name"], "x")
		self.assertEqual(_getElementByID(table, refs[1]).attrib["name"], "y")

		refs = [el.attrib["ref"] for el in g.findall("PARAMref")]
		self.assertEqual(
			_getElementByID(table, testhelpers.pickSingle(refs)).attrib["value"],
			"4")

	def testLocalParam(self):
		tree = _getVOTTreeForTable(
			'<table><group><param name="u" type="integer">5</param></group>'
				'</table>')
		pars = tree.findall("RESOURCE/TABLE/GROUP/PARAM")
		self.assertEqual(testhelpers.pickSingle(pars).attrib["value"], "5")

	def testRecursive(self):
		tree = _getVOTTreeForTable(
			"<table><group><group><columnRef dest='x'/><columnRef dest='y'/></group>"
			'<group><paramRef dest="z"/></group></group>'
				'<column name="x"/><column name="y"/>'
				'<param name="z" type="integer">4</param>'
				'</table>')
		groups = tree.findall("RESOURCE/TABLE/GROUP/GROUP")
		self.assertEqual(len(groups), 2)

		colRefs = [c.attrib["ref"] for c in groups[0].findall("FIELDref")]
		self.assertEqual(len(colRefs), 2)
		self.assertEqual(_getElementByID(tree, colRefs[1]).attrib["name"], "y")
		self.assertEqual(len(groups[0].findall("PARAMref")), 0)

		paramRefs = [c.attrib["ref"] for c in groups[1].findall("PARAMref")]
		self.assertEqual(_getElementByID(tree, paramRefs[0]).attrib["value"], "4")
		self.assertEqual(len(groups[1].findall("FIELDref")), 0)

	def testCopied(self):
		td = base.parseFromString(rscdef.TableDef,
			'<table><group><group><columnRef dest="x"/><columnRef dest="y"/>'
				'<param name="u" type="integer">5</param></group>'
				'<group><paramRef dest="z"/></group></group>'
				'<column name="x"/><column name="y"/>'
				'<param name="z" type="integer">4</param>'
				'</table>')
		td = td.copy(None)
		tree = ElementTree.fromstring(
			votablewrite.getAsVOTable(
				rsc.TableForDef(td), tablecoding="td", suppressNamespace=True))

		groups = tree.findall("RESOURCE/TABLE/GROUP/GROUP")
		self.assertEqual(len(groups), 2)

		colRefs = [c.attrib["ref"] for c in groups[0].findall("FIELDref")]
		self.assertEqual(_getElementByID(tree, colRefs[1]).attrib["name"], "y")

		paramRefs = [c.attrib["ref"] for c in groups[1].findall("PARAMref")]
		self.assertEqual(_getElementByID(tree, paramRefs[0]).attrib["value"], "4")

		self.assertEqual(tree.find("RESOURCE/TABLE/GROUP/GROUP/PARAM").
			attrib["value"], "5")


def _getTableWithSimpleSTC():
	td = base.parseFromString(rscdef.TableDef, """
		<table id="soot">
			<stc>Position ICRS BARYCENTER SPHER3 Epoch J2015.0
				"alpha" "delta" "dist"
				Velocity "pmra" "pmdec" "rv"</stc>
			<dm>(votable:Coords) { time: (votable:TimeCoordinate) {
					frame: (votable:TimeFrame) { timescale: TT refPosition: GEOCENTER
					} location: @tca}}</dm>

			<column name="alpha"/><column name="delta"/>
			<column name="pmra"/><column name="pmdec"/>
			<column name="dist"/><column name="rv"/>
			<column name="tca"/>
		</table>""")
	return rsc.TableForDef(td, rows=[
		{'alpha': 10, 'delta': -10, 'rv': -4, "dist": 45, "pmra": 0.001,
			"pmdec": -1e-4, "tca": 54783.875}])


class _SimpleSTCVOT(testhelpers.TestResource):
	"""A single table with a single coordinate system, serialised into a
	VOTable.
	"""
	def make(self, deps):
		source = votablewrite.getAsVOTable(_getTableWithSimpleSTC())
		return testhelpers.getXMLTree(source, debug=False)


class SingleSTCEmbedTest(testhelpers.VerboseTest):
	"""tests for proper inclusion of STC in VOTables.
	"""
	resources = [("tree", _SimpleSTCVOT())]

	def testSingleGroupPresent(self):
		res = self.tree.xpath("//TABLE/GROUP[@utype='stc:CatalogEntryLocation']")
		self.assertEqual(len(res), 1)

	def testFrameDefined(self):
		par = self.tree.xpath("//GROUP[@utype='stc:CatalogEntryLocation']"
			"/PARAM[@utype='stc:AstroCoordSystem.SpaceFrame.CoordRefFrame']")[0]
		self.assertEqual(par.get("value"), "ICRS")
	
	def testFieldIDed(self):
		self.assertEqual(len(self.tree.xpath("//FIELD[@ID='alpha']")), 1)
	
	def testAlphaReference(self):
		ref = self.tree.xpath("//GROUP[@utype='stc:CatalogEntryLocation']"
			"/FIELDref[@utype='stc:AstroCoords.Position3D.Value3.C1']")[0]
		self.assertEqual(ref.get("ref"), "alpha")

	def testCOOSYSEpochLiteral(self):
		self.assertEqual(self.tree.xpath(
			"RESOURCE/COOSYS")[0].get("epoch"), "J2015.0")

	def testCOOSYSFrame(self):
		self.assertEqual(self.tree.xpath(
			"RESOURCE/COOSYS")[0].get("system"), "ICRS")

	def testCOOSYSRefpos(self):
		self.assertEqual(self.tree.xpath(
			"RESOURCE/COOSYS")[0].get("refposition"), "BARYCENTER")

	def testCOOSYSExtraCoords(self):
		sysname = self.tree.xpath("//FIELD[@name='rv']")[0].get("ref")
		self.assertEqual(sysname, "system")

	def testRefsWork(self):
		sysname = self.tree.xpath("//FIELD[@name='alpha']")[0].get("ref")
		self.assertEqual(sysname, "system")
		self.assertEqual(
			self.tree.xpath("//*[@ID='%s']"%sysname)[0].get("system"),
			"ICRS")

	def testMultiTables(self):
		# twice the same table -- this is mainly for id mapping
		table = _getTableWithSimpleSTC()
		tdCopy = table.tableDef.copy(None)
		tdCopy.id = "copy"
		tableCopy = rsc.TableForDef(tdCopy)
		dd = base.makeStruct(rscdef.DataDescriptor, makes=[
			base.makeStruct(rscdef.Make, table=table.tableDef),
			base.makeStruct(rscdef.Make, table=tdCopy)],
			parent_=table.tableDef.rd)
		data = rsc.Data(dd, tables={table.tableDef.id: table,
			"copy": tableCopy})

		tree = testhelpers.getXMLTree(
			votablewrite.getAsVOTable(data), debug=False)
		for path in [
				"//FIELD[@ID='delta']",
				"//FIELD[@ID='delta-02']",
				"//FIELDref[@ref='delta' and @utype='stc:AstroCoords.Position3D.Value3.C2']",
				"//FIELDref[@ref='delta-02' and @utype='stc:AstroCoords.Position3D.Value3.C2']",
				]:
			self.assertTrue(len(tree.xpath(path))==1, "%s not found"%path)

	def testColumnCopy(self):
		table = _getTableWithSimpleSTC()
		td = table.tableDef.copy(None)
		# do another copy so the original copy gets un-referenced
		# and we catch if we still touch their columns.
		td = base.makeStruct(outputdef.OutputTableDef,
			columns=[
				td.getByName("alpha"),
				td.getByName("delta"),
				td.getByName("tca"),])
		table = rsc.TableForDef(td, rows=table.rows)

		tree = testhelpers.getXMLTree(
			votablewrite.getAsVOTable(table), debug=False)
		for path in [
				"RESOURCE/COOSYS/FIELDref[@utype='votable:LonLatPoint-lat']",
				"RESOURCE/COOSYS/FIELDref[@utype='votable:LonLatPoint-lon']",
				"//COOSYS[@epoch='J2015.0']"
				]:
			self.assertTrue(len(tree.xpath(path))==1, f"missing {path}")

	def testTIMESYSRendered(self):
		tsRef = self.tree.xpath("//FIELD[@name='tca']/@ref")[0]
		self.assertEqual("GEOCENTER",
			self.tree.xpath(f"RESOURCE/TIMESYS[@ID='{tsRef}']/@refposition")[0])

	def _assertUtypeResolves(self, utype, fieldname):
		ref = self.tree.xpath(f"RESOURCE/COOSYS/FIELDref[@utype='{utype}']"
			)[0].get("ref")
		self.assertEqual(fieldname,
			self.tree.xpath(f"//*[@ID='{ref}']")[0].get("name"))

	def testLong(self):
		self._assertUtypeResolves("votable:LonLatPoint-lon", "alpha")

	def testLat(self):
		self._assertUtypeResolves("votable:LonLatPoint-lat", "delta")

	def testDist(self):
		self._assertUtypeResolves("votable:LonLatPoint-dist", "dist")

	def testPmra(self):
		self._assertUtypeResolves("votable:ProperMotion-lon", "pmra")

	def testPmde(self):
		self._assertUtypeResolves("votable:ProperMotion-lat", "pmdec")

	def testRV(self):
		self._assertUtypeResolves("votable:ProperMotion-rv", "rv")

	def testCopying(self):
		# this is actually a test of OutputTableDef's adoption of its
		# column and params
		td = _getTableWithSimpleSTC().tableDef
		td = base.makeStruct(outputdef.OutputTableDef,
			columns=td.columns, params=td.params, id="copy")
		table = rsc.TableForDef(td)
		tree = testhelpers.getXMLTree(
			votablewrite.getAsVOTable(table), debug=False)

		ref = tree.xpath("RESOURCE/COOSYS/"
			"FIELDref[@utype='votable:ProperMotion-lon']")[0].get("ref")
		self.assertEqual("pmra",
			self.tree.xpath(f"//*[@ID='{ref}']")[0].get("name"))
	
	def testCoreference(self):
		td = base.parseFromString(rscdef.TableDef, """
			<table>
				<stc>Position ICRS "ra0" "dec0" Velocity "pmra_1" "pmde_1"</stc>
				<stc>Position ICRS "ra0" "dec0" Velocity "pmra_2" "pmde_2"</stc>
				<column name="ra0"/><column name="dec0"/>
				<column name="pmra_1"/><column name="pmde_1"/>
				<column name="pmra_2"/><column name="pmde_2"/>
			</table>""")

		tree = testhelpers.getXMLTree(
			votablewrite.getAsVOTable(
				rsc.TableForDef(td)),
			debug=False)

		self.assertEqual(tree.xpath("//FIELD[@name='ra0']")[0].get("ref"),
			"system")
		self.assertEqual(tree.xpath("//FIELD[@name='dec0']")[0].get("ref"),
			"system")
		self.assertEqual(tree.xpath("//FIELD[@name='pmra_1']")[0].get("ref"),
			"system")
		self.assertEqual(tree.xpath("//FIELD[@name='pmde_1']")[0].get("ref"),
			"system")
		self.assertEqual(tree.xpath("//FIELD[@name='pmra_2']")[0].get("ref"),
			"system-02")
		self.assertEqual(tree.xpath("//FIELD[@name='pmde_2']")[0].get("ref"),
			"system-02")

	def testLegacySTCSFrame(self):
		td = base.parseFromString(rscdef.TableDef, """
			<table>
				<stc>Position Galactic [pos]</stc>
				<column name="pos" type="spoint" xtype="adql:POINT"/>
			</table>""")
		tree = testhelpers.getXMLTree(
			votablewrite.getAsVOTable(
				rsc.TableForDef(td, rows=[
					{"pos": pgsphere.SPoint.fromDegrees(10, 45)}]),
				tablecoding="td"),
			debug=False)
		
		self.assertEqual(
			tree.xpath("//TD")[0].text,
			"Position GALACTIC_II 10. 45.")


class _TwoSystemSTCVOT(testhelpers.TestResource):
	def make(self, deps):
		td = base.parseFromString(rscdef.TableDef, """
			<table>
				<stc>Position ICRS Epoch "ep" "ra" "dec"</stc>
				<stc>Position GALACTIC Epoch J2000.3 "gal_l" "gal_b"</stc>
				<stc>Position ICRS [pos]</stc>
				<column name="ra"/><column name="dec"/>
				<column name="gal_l"/><column name="gal_b"/>
				<column name="pos" type="spoint"/>
				<column name="ep"/>
			</table>""")
		return testhelpers.getXMLTree(
			votablewrite.getAsVOTable(
				rsc.TableForDef(td, rows=[{
					"ra": 10.0, "dec": 12.0, "gal_l": 231.0, "gal_b": 33.0,
					"pos": pgsphere.SPoint.fromDegrees(10, 12), "ep": 2013.5}])),
			debug=False)


class TwinSTCEmbedTest(testhelpers.VerboseTest):
	resources = [("tree", _TwoSystemSTCVOT())]

	def testCOOSYSGalactic(self):
		systems = [c.get("system") for c in self.tree.xpath(
			"RESOURCE/COOSYS")]
		self.assertEqual(set(systems), set(["ICRS" , "galactic"]))

	def testCOOSYSEpochReference(self):
		destId = self.tree.xpath("RESOURCE/COOSYS[1]")[0].get("ID")
		self.assertEqual(self.tree.xpath(
			"//FIELD[@name='ep']")[0].get("ref"), destId)

	def testCOOSYSEpochColref(self):
		epref = self.tree.xpath(
			"RESOURCE/COOSYS/FIELDref[@utype='votable:CustomRefLocation-epoch']"
			)[0].get("ref")
		self.assertEqual("ep",
			self.tree.xpath(f"//*[@ID='{epref}']")[0].get("name"))

	def testPosRef(self):
		self.assertEqual(
			self.tree.xpath("//FIELDref[@ref='pos']")[0].get("utype"),
			"stc:AstroCoords.Position2D.Value2")

	def testPosXtype(self):
		posfield = self.tree.xpath("//FIELD[@name='pos']")[0]
		self.assertEqual(posfield.get("xtype"), "point")
		self.assertEqual(posfield.get("datatype"), "double")
		self.assertEqual(posfield.get("arraysize"), "2")

	def testRefsWithTwo(self):
		sysname = self.tree.xpath("//FIELD[@name='gal_l']")[0].get("ref")
		self.assertEqual(
			self.tree.xpath("//*[@ID='%s']"%sysname)[0].get("system"),
			"galactic")

		sysname = self.tree.xpath("//FIELD[@name='dec']")[0].get("ref")
		self.assertEqual(
			self.tree.xpath("//*[@ID='%s']"%sysname)[0].get("system"),
			"ICRS")

	def testNewStyleRefs(self):
		latRef = self.tree.xpath("RESOURCE/COOSYS[@system='ICRS']"
			"/FIELDref[@utype='votable:LonLatPoint-lat']")[0].get("ref")
		self.assertEqual("dec",
			self.tree.xpath(f"//*[@ID='{latRef}']")[0].get("name"))
		glonRef = self.tree.xpath("RESOURCE/COOSYS[@system='galactic']"
			"/FIELDref[@utype='votable:LonLatPoint-lon']")[0].get("ref")
		self.assertEqual("gal_l",
			self.tree.xpath(f"//*[@ID='{glonRef}']")[0].get("name"))

	def testEpochSyntax(self):
		self.assertEqual("J2000.3",
			self.tree.xpath("//COOSYS[@system='galactic']")[0].get("epoch"))


class STCParseTest(testhelpers.VerboseTest):
	"""tests for parsing of STC info from VOTables.
	"""
	def _doRoundtrip(self, table):
		serialized = votablewrite.getAsVOTable(table)
		vot = votable.readRaw(io.BytesIO(serialized))
		dddef = votableread.makeDDForVOTable("testTable", vot)
		return dddef.getPrimary()

	def _assertSTCEquivalent(self, td1, td2):
		for orig, deser in zip(td1, td2):
			self.assertEqual(orig.name, deser.name)
			self.assertEqual(orig.stcUtype, deser.stcUtype)
			self.assertEqual(orig.stc, deser.stc)

	def testSimpleRoundtrip(self):
		src = _getTableWithSimpleSTC()
		td = self._doRoundtrip(src)
		self._assertSTCEquivalent(src.tableDef, td)

	def testComplexRoundtrip(self):
		src = rsc.TableForDef(testhelpers.getTestRD().getById("stcfancy"))
		td = self._doRoundtrip(src)
		self._assertSTCEquivalent(src.tableDef, td)

	def testWhackyUtypesIgnored(self):
		vot = votable.readRaw(io.BytesIO(b"""
		<VOTABLE version="1.2"><RESOURCE><TABLE><GROUP ID="ll" utype="stc:CatalogEntryLocation"><PARAM arraysize="*" datatype="char" utype="stc:AstroCoordSystem.SpaceFrame.CoordRefFrame" value="ICRS" /><PARAM arraysize="*" datatype="char" utype="stc:AstroCoordSystem.SpaceFrame.Megablast" value="ENABLED" /><FIELDref ref="alpha" utype="stc:AstroCoords.Position2D.Value2.C1" /><FIELDref ref="delta" utype="stc:AstroCoords.BlasterLocation" /></GROUP><FIELD ID="alpha" datatype="float" name="alpha" unit="deg"/><FIELD ID="delta" datatype="float" name="delta" unit="deg"/></TABLE></RESOURCE></VOTABLE>"""))
		dddef = votableread.makeDDForVOTable("testTable", vot)
		td = dddef.getPrimary()
		self.assertEqual(
			td.getColumnByName("alpha").stc.sys.spaceFrame.refFrame, "ICRS")
		self.assertEqual(
			td.getColumnByName("alpha").stcUtype,
			"stc:AstroCoords.Position2D.Value2.C1")
		self.assertEqual(
			td.getColumnByName("delta").stcUtype,
			"stc:AstroCoords.BlasterLocation")


class SimpleAPIReadTest(testhelpers.VerboseTest):
	def testSimpleData(self):
		with open("test_data/importtest.vot", "rb") as f:
			data, metadata = votable.load(f)
			self.assertEqual(len(metadata), 19)
			self.assertEqual(metadata[0].name, "_r")
			self.assertEqual(data[0][3], 1)
			self.assertEqual(data[1][0], None)


class VOTReadTest(testhelpers.VerboseTest):
	def testNoQ3CIndexOnChar(self):
		rows = next(votable.parse(io.BytesIO(
			b"""<VOTABLE><RESOURCE><TABLE>
				<FIELD name="a" datatype="char" arraysize="*"
					ucd="pos.eq.ra;meta.main"/>
				<FIELD name="d" datatype="float" ucd="pos.eq.dec;meta.main"/>
				<DATA><TABLEDATA><TR><TD>1</TD><TD>2</TD></TR></TABLEDATA></DATA>
				</TABLE></RESOURCE></VOTABLE>""")))
		td = votableread.makeTableDefForVOTable("foo", rows.tableDefinition)
		self.assertEqual(len(td.indices), 0)

	def testNoIndex(self):
		# The thing shouldn't crash or do anything stupid with silly UCDs.
		rows = next(votable.parse(io.BytesIO(
			b"""<VOTABLE><RESOURCE><TABLE>
				<FIELD name="a" datatype="float" ucd="pos.eq.ra;meta.main"/>
				<FIELD name="d" datatype="float" ucd="pos.eq.ra;meta.main"/>
				<DATA><TABLEDATA><TR><TD>1</TD><TD>2</TD></TR></TABLEDATA></DATA>
				</TABLE></RESOURCE></VOTABLE>""")))
		td = votableread.makeTableDefForVOTable("foo", rows.tableDefinition)
		self.assertEqual(td.indices, [])

	def testBadPQNames(self):
		rows = next(votable.parse(io.BytesIO(
			b"""<VOTABLE><RESOURCE><TABLE>
				<FIELD name="oid" datatype="float"/>
				<FIELD name="tableoid" datatype="float"/>
				<FIELD name="xmin" datatype="float"/>
				<FIELD name="cmin" datatype="float"/>
				<FIELD name="xmax" datatype="float"/>
				<FIELD name="cmax" datatype="float"/>
				<FIELD name="ctid" datatype="float"/>
				<FIELD name="oid" datatype="float"/>
				<DATA><TABLEDATA><TR><TD>1</TD><TD>2</TD><TD>3</TD>
						<TD>1</TD><TD>2</TD><TD>3</TD>
						<TD>1</TD><TD>2</TD></TR></TABLEDATA></DATA>
				</TABLE></RESOURCE></VOTABLE>""")))
		td = votableread.makeTableDefForVOTable(
			"foo", rows.tableDefinition, votableread.AutoQuotedNameMaker())
		self.assertEqual(" ".join(c.name for c in td),
			"oid_ tableoid_ xmin_ cmin_ xmax_ cmax_ ctid_ oid__")


class OverflowTest(testhelpers.VerboseTest):
	resources = [("tab", tresc.randomDataTable)]

	def testWithoutOverflow(self):
		res = votablewrite.getAsVOTable(self.tab,
			votablewrite.VOTableContext(
				overflowElement=votable.OverflowElement(20,
					votable.V.GROUP(name="overflow"))))
		self.assertFalse(b'<GROUP name="overflow"' in res)

	def testWithOverflow(self):
		res = votablewrite.getAsVOTable(self.tab,
			votablewrite.VOTableContext(
				overflowElement=votable.OverflowElement(2,
					votable.V.GROUP(name="overflow"))))
		self.assertTrue(b'</TABLE><GROUP name="overflow"' in res)
		# since the overflow element is rendered in a context of its own,
		# it's non-trivial that no xml namespaces are declared on it.
		self.assertFalse(b'name="overflow" xmlns' in res)


class HackMetaTest(testhelpers.VerboseTest):
	"""tests for nasty hacks in data's meta stuff that lead so certain
	VOTable manipulations.
	"""
	def _getTestTable(self):
		td = base.parseFromString(rscdef.TableDef,
			'<table id="silly"><column name="u"/></table>')
		return rsc.TableForDef(td)

	def testRootAttributes(self):
		table = self._getTestTable()
		table.addMeta("_votableRootAttributes", "malformed mess")
		table.addMeta("_votableRootAttributes", "xmlns:crazy='http://forget.this'")
		res = votablewrite.getAsVOTable(table,
			votablewrite.VOTableContext(suppressNamespace=True))
		self.assertTrue(
			b'VOTABLE version=\"1.4\"'
			b" malformed mess xmlns:crazy='http://forget.this'"
			in res)

	def testInfoMeta(self):
		table = self._getTestTable()
		table.addMeta("info", "Info from meta",
			infoValue="bar", infoName="fromMeta", infoId="x_x")
		root = ElementTree.fromstring(votablewrite.getAsVOTable(table,
			votablewrite.VOTableContext(suppressNamespace=True)))
		mat = root.findall("RESOURCE/INFO")
		self.assertEqual(len(mat), 7)
		info = mat[0]
		self.assertEqual(info.attrib["ID"], "x_x")
		self.assertEqual(info.attrib["name"], "fromMeta")
		self.assertEqual(info.attrib["value"], "bar")
		self.assertEqual(info.text, "Info from meta")

	def testLinkMeta(self):
		table = rsc.TableForDef(base.parseFromString(rscdef.TableDef,
			"""<table id="silly">
				<column name="u">
					<property name="targetType">text/html</property>
				</column>
				<column name="v">
					<property name="targetType">image/png</property>
					<property name="targetTitle">Plot</property>
				</column>
			</table>"""))
		root = ElementTree.fromstring(votablewrite.getAsVOTable(table,
			votablewrite.VOTableContext(suppressNamespace=True)))

		mat = root.findall("RESOURCE/TABLE/FIELD/LINK")
		self.assertEqual(len(mat), 2)

		self.assertEqual(mat[0].attrib["content-type"], "text/html")
		self.assertEqual(mat[0].attrib["title"], "Link")
		self.assertEqual(mat[1].attrib["content-type"], "image/png")
		self.assertEqual(mat[1].attrib["title"], "Plot")


class LinkTest(testhelpers.VerboseTest):
	def _getAsTree(self, tableXML):
		if tableXML.startswith("<data"):
			rootType = rscdef.DataDescriptor
			makeThing = rsc.makeData
		else:
			rootType = rscdef.TableDef
			makeThing = rsc.TableForDef

		t = makeThing(base.parseFromString(rootType, tableXML))
		return testhelpers.getXMLTree(
			votablewrite.getAsVOTable(t,
				votablewrite.VOTableContext(suppressNamespace=True)), debug=False)

	def testColumnLink(self):
		tree = self._getAsTree("""<table id="u">
			<column name="z"><meta name="votlink" role="check">ivo://ranz/k
			</meta></column></table>""")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/FIELD/LINK/@href")[0],
			"ivo://ranz/k")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/FIELD/LINK/@content-role")[0],
			"check")

	def testTableLink(self):
		tree = self._getAsTree("""<table id="u">
			<meta name="votlink" contentType="inode/fifo">ivo://ranz/k</meta>
			<column name="z"></column></table>""")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@href")[0],
			"ivo://ranz/k")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@content-type")[0],
			"inode/fifo")

	def testDataLink(self):
		tree = self._getAsTree("""<data id="a">
			<meta name="votlink" linkname="onlink">http://server/on</meta>
			<meta name="votlink" linkname="offlink">http://server/off</meta>
			<table id="u">
			<column name="z"></column></table><make table="u"/></data>""")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@href")[0],
			"http://server/on")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@name")[0],
			"onlink")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@href")[1],
			"http://server/off")
		self.assertEqual(tree.xpath("RESOURCE/TABLE/LINK/@name")[1],
			"offlink")


class _VOTSTCTable(testhelpers.TestResource):
	def make(self, ignored):
		t = rsc.TableForDef(base.parseFromString(rscdef.TableDef,
			"""<table id="t">
				<dm>
					(votable:Coords) {
						time: (votable:TimeCoordinate) {
							frame:
								(votable:TimeFrame) {
									timescale: TCB
									refPosition: BARYCENTER
									time0: 2450000.5 }
							location: @epoch
						}
						space:
							(votable:SphericalCoordinate) {
								frame: (votable:SpaceFrame) {
									orientation: ICRS
									refPosition: TOPOCENTER
									epoch: "J2000.0" }
								longitude: @ra
								latitude: @dec
								distance: @dist
								pm_longitude: @pmra
								pm_latitude: @pmdec
								rv: @rv
							}
					}
				</dm>
				<column name="id" type="text"/>
				<param name="ra" unit="deg">23</param>
				<column name="err_ra" unit="mas"/>
				<column name="dec" unit="deg"/>
				<column name="err_dec" unit="mas"/>
				<column name="pmra" unit="rad/cy"/>
				<column name="pmdec" unit="rad/cy"/>
				<column name="dist" unit="pc"/>
				<column name="rv" unit="km/s"/>
				<column name="epoch" unit="yr"/>
			</table>"""))
		literal = votablewrite.getAsVOTable(t,
				votablewrite.VOTableContext())
		return literal, testhelpers.getXMLTree(literal, debug=False)


class VOTableSTCTest(testhelpers.VerboseTest, testhelpers.XSDTestMixin):
	resources = [("litAndTree", _VOTSTCTable())]

	def testTIMESYSMeta(self):
		ts = self.litAndTree[1].xpath("RESOURCE/TIMESYS")[0]
		self.assertEqual(ts.get("timescale"), "TCB")
		self.assertEqual(ts.get("refposition"), "BARYCENTER")
		self.assertEqual(ts.get("timeorigin"), "2450000.5")
	
	def testTIMESYSReference(self):
		tsID = self.litAndTree[1].xpath("//FIELD[@ID='epoch']")[0].get("ref")
		systems = self.litAndTree[1].xpath("//*[@ID='%s']"%tsID)
		sys = testhelpers.pickSingle(systems)
		self.assertEqual(sys.tag, "TIMESYS")
		self.assertEqual(sys.get("timeorigin"), "2450000.5")
	
	def testCOOSYSMeta(self):
		systems = self.litAndTree[1].xpath("RESOURCE/COOSYS")
		system = testhelpers.pickSingle(systems)
		self.assertEqual(system.get("system"), "ICRS")
		self.assertEqual(system.get("epoch"), "J2000.0")

	def testCOOSYSReference(self):
		sysId = self.litAndTree[1].xpath("RESOURCE/COOSYS")[0].get("ID")
		self.assertEqual(self.litAndTree[1].xpath(
			"//PARAM[@name='ra']")[0].get("ref"),
			sysId)
		self.assertEqual(self.litAndTree[1].xpath(
			"//FIELD[@name='dec']")[0].get("ref"),
			sysId)

	def testCOOSYSRefpos(self):
		self.assertEqual(self.litAndTree[1].xpath(
			"RESOURCE/COOSYS")[0].get("refposition"), "TOPOCENTER")

	def testValid(self):
		self.assertValidates(self.litAndTree[0])

	def testParamRef(self):
		paramRef = self.litAndTree[1].xpath(
			"RESOURCE/COOSYS/PARAMref[@utype='votable:LonLatPoint-lon']"
			)[0].get("ref")
		self.assertEqual(
			self.litAndTree[1].xpath(f"//PARAM[@ID='{paramRef}']")[0].get("name"),
			"ra")

	def testFieldRef(self):
		ref = self.litAndTree[1].xpath(
			"RESOURCE/COOSYS/FIELDref[@utype='votable:LonLatPoint-dist']"
			)[0].get("ref")
		self.assertEqual(
			self.litAndTree[1].xpath(f"//FIELD[@ID='{ref}']")[0].get("name"),
			"dist")
	
	def testAllUtypes(self):
		self.assertEqual(set(self.litAndTree[1].xpath("RESOURCE/COOSYS//@utype")),
			{'votable:LonLatPoint-lon', 'votable:LonLatPoint-lat',
			'votable:LonLatPoint-dist', 'votable:ProperMotion-lon',
			'votable:ProperMotion-lat', 'votable:ProperMotion-rv'})


class _ConstantSTCTable(testhelpers.TestResource):
	def make(self, ignored):
		base.ui.notifyError("Flabber")
		t = rsc.TableForDef(base.parseFromString(rscdef.TableDef,
			"""<table id="t">
				<stc>
					Time TT TOPOCENTER 2000-01-01T00:00:00
					Position ICRS Epoch J2000 "alpha" -14.2
				</stc>
				<column name="alpha"/>
				<column name="id" type="text"/>
			</table>"""))
		literal = votablewrite.getAsVOTable(t,
				votablewrite.VOTableContext())
		return testhelpers.getXMLTree(literal, debug=False)


class ConstantSTCTest(testhelpers.VerboseTest):
	resources = [("tree", _ConstantSTCTable())]

	def testTimeDeclared(self):
		ts = self.tree.xpath("RESOURCE/TIMESYS")[0]
		self.assertEqual(ts.get("refposition"), "TOPOCENTER")
		# TODO the literal 2000-01-01 should become a param

	@unittest.skip("need to make params from literals")
	def testCoordLiteral(self):
		coordParam = self.tree.xpath("RESOURCE/TABLE/PARAM[@utype='???']")[0]
		self.assertEqual(coordParam.get("value"), "-14.2")
		

class MetaWriteTest(testhelpers.VerboseTest):
	def testEmptyTableElided(self):
		dd = base.parseFromString(rscdef.DataDescriptor,
			"""<data><meta name="_type">meta</meta>
			<param name="foo">-1</param>
			<table id="boo"><column name="quxx"/></table>
			<make table="boo"/></data>""")
		data = rsc.makeData(dd)

		serialised = getFormatted("votable", data).decode("utf-8")
		self.assertTrue("<DATA" not in serialised)


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