"""
Tests for SODA functionality.

There's a lot of tests like these in productstest, but for the old,
atomic parameter-style dlget stuff.  This should go ca. 2017.
"""

#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 io
import gc
import os
import re

from gavo.helpers import testhelpers

from gavo import api
from gavo import rscdef
from gavo import svcs
from gavo import utils
from gavo import votable
from gavo.helpers import trialhelpers
from gavo.rscdef import regtest
from gavo.protocols import datalink
from gavo.protocols import products #noflake: for mapper registration
from gavo.protocols import soda
from gavo.utils import fitstools
from gavo.utils import pgsphere
from gavo.utils import pyfits

import tresc

class SODAElementTest(testhelpers.VerboseTest):
	resources = [("prodtestTable", tresc.prodtestTable)]
	parent = None

	def testMetaMakerSyntaxError(self):
		mm = api.parseFromString(datalink.MetaMaker,
			'<metaMaker>\n'
			' <code>\n'
			'	return return\n'
			' </code>\n'
			'</metaMaker>\n')
		self.assertRaisesWithMsg(
			api.BadCode,
			regtest.EqualingRE(r"Bad source code in function \(invalid syntax"
				r" \(<generated code \d+>, line 4\)\)"),
			mm.compile,
			(self,))

	def testStandardDescGenWorks(self):
		ivoid = rscdef.getStandardPubDID(
			os.path.join(api.getConfig("inputsDir"),
				"data/a.imp"))
		dge = api.parseFromString(datalink.DescriptorGenerator,
			'<descriptorGenerator procDef="//soda#fromStandardPubDID">'
			'<bind name="contentQualifier">"http://g-vo.org/#ha"</bind>'
			'</descriptorGenerator>'
			)
		dg = dge.compile(dge)
		res = dg(ivoid, {})
		self.assertEqual(res.accref, "data/a.imp")
		self.assertEqual(res.owner, "X_test")
		self.assertEqual(res.mime, "text/plain")
		self.assertEqual(res.accessPath, "data/a.imp")
		self.assertEqual(res.contentQualifier, "http://g-vo.org/#ha")

	def testProductsGenerator(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlget">
			<datalinkCore>
				<dataFunction procDef="//soda#generateProduct"/>
				<metaMaker><code>yield MS(InputKey, name="ignored")</code></metaMaker>
				</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "form", {"ID": rscdef.getStandardPubDID(
			"data/b.imp"), "ignored": 0.4})
		self.assertEqual(b"".join(res.iterData()), b'alpha: 03 34 33.45'
			b'\ndelta: 42 34 59.7\nobject: michael\nembargo: 2003-12-31\n')

	def testProductsGeneratorMimecheck(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlget">
			<datalinkCore>
				<dataFunction procDef="//soda#generateProduct">
					<bind name="requireMimes">["application/fits"]</bind></dataFunction>
					<metaMaker><code>yield MS(InputKey, name="ignored")</code></metaMaker>
				</datalinkCore>
			</service>""")
		self.assertRaisesWithMsg(api.ValidationError,
			"Field PUBDID: Document type not supported: text/plain",
			trialhelpers.runSvcWith,
			(svc, "form", {"ID": rscdef.getStandardPubDID("data/b.imp"),
				"ignored": 0.5}))

	def testAccrefPrefixDeny(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fromStandardPubDID">
					<bind name="accrefPrefix">'nodata'</bind>
				</descriptorGenerator>
			</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "dlmeta",
			{"ID": rscdef.getStandardPubDID("data/b.imp")})
		row = list(next(votable.parseBytes(res[1])))[0]
		self.assertEqual(row[3],
			'AuthenticationFault: This Datalink'
			' service not available with this pubDID')
		self.assertEqual(row[5], "#this")

	def testAccrefPrefixAccept(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fromStandardPubDID">
					<bind name="accrefPrefix">'data/b'</bind>
					<bind name="contentQualifier">'#junk'</bind>
				</descriptorGenerator>
			</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "dlmeta",
			{"ID": rscdef.getStandardPubDID("data/b.imp")})
		row = list(next(votable.parseBytes(res[1])))[0]
		self.assertTrue(row[1].endswith("/getproduct/data/b.imp"))
		self.assertEqual(row[5], "#this")
		self.assertEqual(row[9], "#junk")

	def testPlainAccrefWorks(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fromStandardPubDID">
					<bind name="accrefPrefix">'data/b'</bind>
				</descriptorGenerator>
			</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "dlmeta", {"ID": "data/b.imp"})
		row = list(next(votable.parseBytes(res[1])))[0]
		self.assertTrue(row[1].endswith("/getproduct/data/b.imp"))
		self.assertEqual(row[5], "#this")

	def testProductsGeneratorFailure(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlget">
			<datalinkCore>
				<dataFunction procDef="//soda#generateProduct">
					<code>descriptor.data = None
					</code></dataFunction>
					<metaMaker><code>yield MS(InputKey, name="ignored")
					</code></metaMaker>
				</datalinkCore>
			</service>""")
		self.assertRaisesWithMsg(api.ReportableError,
			"Internal Error: a first data function did not create data.",
			trialhelpers.runSvcWith,
			(svc, "form", {"ID": rscdef.getStandardPubDID("data/b.imp"),
				"ignored": 0.4}))

	def testProductsMogrifier(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo">
			<datalinkCore>
				<dataFunction procDef="//soda#generateProduct"/>
				<inputKey name="addto" type="integer" multiplicity="single"/>
				<dataFunction>
					<setup>
						<code>
							from gavo.protocols import products
							class MogrifiedProduct(products.ProductBase):
								def __init__(self, input, offset):
									self.input, self.offset = input, offset
									products.ProductBase.__init__(self, input.rAccref)

								def iterData(self):
									for chunk in self.input.iterData():
										yield bytes(c+self.offset
											for c in chunk)
						</code>
					</setup>
					<code>
						descriptor.data = MogrifiedProduct(descriptor.data,
							args["addto"])
					</code>
				</dataFunction></datalinkCore>
			</service>""")
		res = b"".join(trialhelpers.runSvcWith(svc, "form", {
			"ID": [rscdef.getStandardPubDID("data/b.imp")],
			"addto": ["4"]}).iterData())
		self.assertEqual(res,
			b"eptle>$47$78$77289\x0ehipxe>$86$78$9=2;\x0e"
			b"sfnigx>$qmgleip\x0eiqfevks>$6447156175\x0e")

	def testAccrefFilter(self):
		svc = api.parseFromString(svcs.Service, """<service id="uh">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="accrefPrefix">"test"</bind>
				</descriptorGenerator>
			</datalinkCore></service>""")

		self.assertRaisesWithMsg(svcs.ForbiddenURI,
			"This Datalink service not available with this pubDID"
			" (pubDID: ivo://x-testing/~?goo/boo)",
			trialhelpers.runSvcWith,
			(svc, "dlget", {"ID": [rscdef.getStandardPubDID("goo/boo")]}))

		self.assertRaisesWithMsg(api.NotFoundError,
			"The authority in the dataset identifier"
				" 'ivo://great.scott/goo/boo' could not be located"
				" in the authorities managed here",
			trialhelpers.runSvcWith,
			(svc, "dlget", {"ID": ["ivo://great.scott/goo/boo"]}))

	def testDescGenNone(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta">
			<datalinkCore>
				<descriptorGenerator>
					<code>
						return None
					</code>
				</descriptorGenerator>
			</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "dlmeta",
			{"ID": "ivo://ivoa.net/data/b.imp"})
		row = list(next(votable.parseBytes(res[1])))[0]
		self.assertEqual(row[0], "ivo://ivoa.net/data/b.imp")
		self.assertEqual(row[3], "NotFoundFault: dataset 'ivo://ivoa.net/data/"
			"b.imp' could not be located in this site's data holdings")

	def testDescGenRaised(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta">
			<datalinkCore>
				<descriptorGenerator>
					<code>
						from gavo import svcs
						raise svcs.UnknownURI("This does not exist")
					</code>
				</descriptorGenerator>
			</datalinkCore>
			</service>""")
		res = trialhelpers.runSvcWith(svc, "dlmeta",
			{"ID": "ivo://ivoa.net/data/b.imp"})
		row = list(next(votable.parseBytes(res[1])))[0]
		self.assertEqual(row[0], "ivo://ivoa.net/data/b.imp")
		self.assertEqual(row[3], "NotFoundFault: This does not exist")


def _dissectSODAFile(sodaFile):
	"""returns mime and content for a soda-returned File.

	It also calls cleanup(), if it's there -- basically, that's stuff
	twisted does for us in actual action.
	"""
	content = sodaFile.getContent()
	sodaFile.remove()
	if hasattr(sodaFile, "cleanup"):
		sodaFile.cleanup(None)
	return sodaFile.type, content


############### Start FITS tests

class SODAFITSTest(testhelpers.VerboseTest):
	resources = [("fitsTable", tresc.fitsTable)]

	def testNotFound(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta, dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="accrefPrefix">"data/"</bind>
				</descriptorGenerator>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()

		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": ["ivo://junky.ivorn/made/up"]})
		self.assertEqual("application/x-votable+xml;content=datalink", mime)
		self.assertTrue(b"<TR><TD>ivo://junky.ivorn/made/up</TD><TD></TD>"
			b"<TD></TD><TD>NotFoundFault: The authority in the dataset identifier 'ivo://junky.ivorn/made/up' could not be located in the authorities managed here</TD>" in data)

	def testMakeDescriptor(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="accrefPrefix">"data/"</bind>
				</descriptorGenerator>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<metaMaker><code>
					assert descriptor.hdr["EQUINOX"]==2000.
					assert ([int(n) for n in
							descriptor.skyWCS.wcs_world2pix([(166, 20)], 0)[0]]
						==[7261, 7984])
					yield descriptor.makeLink("http://dc.g-vo.org",
						semantics="#sentinel",
						contentType="text/english")
				</code></metaMaker>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()

		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits")]})
		tree = testhelpers.getXMLTree(data)
		self.assertEqual(tree.xpath("//PARAM[@name='RA']")[0].get("unit"),
			"deg")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("xtype"),
			"interval")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("datatype"),
			"double")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("arraysize"),
			"2")
		self.assertEqual(tree.xpath("//PARAM[@name='RA']/VALUES/MIN"
			)[0].get("value")[:7], "168.243")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']/VALUES/MAX"
			)[0].get("value")[:10], "22.2192872")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']/DESCRIPTION"
			)[0].text, "The latitude coordinate")
		# If the next test fails with "list index out of range",
		# there's been a problem in the meta maker above -- perhaps
		# a failed assertion?  See your logs.
		self.assertEqual(tree.xpath("//TR[TD='#sentinel']/TD"
			)[1].text, "http://dc.g-vo.org")

	def testScaledDataCutout(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
					allowed="dlmeta,dlget">
				<datalinkCore>
					<descriptorGenerator procDef="//soda#fits_genDesc"/>
     			<FEED source="//soda#fits_standardDLFuncs"/>
     		</datalinkCore>
			</service>""")
		svc.parent = testhelpers.getTestRD()

		product = trialhelpers.runSvcWith(svc, "dlget", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits")],
			"PIXEL_1": ["2 3"],
			"PIXEL_2": ["1 3"]})
		bytes = product.getContent()
		product._cleanup(None)
		self.assertTrue(b"NAXIS1  =                    2" in bytes)
		self.assertTrue(b"NAXIS2  =                    3" in bytes)
		self.assertTrue(b"BZERO   =                32768" in bytes,
			"Scaling info lost in cutout")
   	
	def testMakeCubeDescriptor(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams">
					<bind key="axisMetaOverrides">{
						"RA": {"ucd": "pos.eq.ra;meta.special"},
						3:    {"name": "ANGSTROMS", "unit": "0.1nm"}}
					</bind>
				</metaMaker>
				<FEED source="//soda#fits_standardBANDCutout"
					spectralAxis="3"
					wavelengthOverride="0.1nm"/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()

		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": [rscdef.getStandardPubDID("data/excube.fits")]})
		tree = testhelpers.getXMLTree(data, debug=False)
		self.assertEqual(tree.xpath("//PARAM[@name='RA']")[0].get("ucd"),
			"pos.eq.ra;meta.special")
		self.assertEqual(tree.xpath("//PARAM[@name='RA']/VALUES/MIN"
			)[0].get("value")[:11], "359.3580942")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']/VALUES/MAX"
			)[0].get("value")[:11], "30.98484850")
		self.assertEqual(tree.xpath("//PARAM[@name='ANGSTROMS']/VALUES/MIN"
			)[0].get("value"), "3749.0")
		self.assertEqual(tree.xpath("//PARAM[@name='ANGSTROMS']/VALUES/MAX"
			)[0].get("value"), "3755.0")
		lmaxPar = tree.xpath("//PARAM[@name='BAND']")[0]
		self.assertEqual(lmaxPar.get("ucd"), "em.wl")
		self.assertEqual(lmaxPar.get("unit"), "m")
		self.assertEqual(lmaxPar.xpath("VALUES/MAX")[0].get("value"),
			"3.7550000000000005e-07")

	def testCutoutNoSpatialCube(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams">
					<bind name="axisMetaOverrides">{3:{}}</bind>
				</metaMaker>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<dataFormatter procDef="//soda#fits_formatHDUs"/>
			</datalinkCore></service>""")

		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"COO_3": ["3753 3755"],
				}))

		self.assertEqual(mime, "application/fits")
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 11)
		self.assertEqual(hdr["NAXIS2"], 7)
		self.assertEqual(hdr["NAXIS3"], 2)

	def testCutoutBANDCube(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_standardBANDCutout"
					spectralAxis="3"
					wavelengthOverride="0.1nm"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<dataFormatter procDef="//soda#fits_formatHDUs"/>
			</datalinkCore></service>""")

		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"BAND": ["3.755e-7 +Inf"],
				"RA": ["359.359 +Inf"],
				"DEC": ["30.39845 +Inf"],
				}))

		self.assertEqual(mime, "application/fits")
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 8)
		self.assertEqual(hdr["NAXIS2"], 7)
		self.assertEqual(hdr["NAXIS3"], 2)

	def testCutoutCube(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams">
					<bind name="axisMetaOverrides">{3:{}}</bind>
				</metaMaker>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<dataFormatter procDef="//soda#fits_formatHDUs"/>
			</datalinkCore></service>""")

		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"RA": ["359.36 359.359"],
				"DEC": ["30.9845 30.985"],
				"COO_3": ["3753 3755"],
				}))

		self.assertEqual(mime, "application/fits")
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 4)
		self.assertEqual(hdr["NAXIS2"], 2)
		self.assertEqual(hdr["NAXIS3"], 2)

	_geoDLServiceLiteral = """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_Geometries"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<dataFormatter procDef="//soda#fits_formatHDUs"/>
			</datalinkCore></service>"""

	def testCutoutPOLYGON(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"POLYGON": ["359.36 30.9845 359.359 30.9845"
					" 359.359 30.985"]
				}))

		self.assertEqual(mime, "application/fits")
		with io.BytesIO(data) as f:
			hdr = fitstools.readPrimaryHeaderQuick(f)
		self.assertEqual(hdr["NAXIS1"], 4)
		self.assertEqual(hdr["NAXIS2"], 2)
		self.assertEqual(hdr["NAXIS3"], 4)

	def testPOLYGONClashesRA(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		self.assertRaisesWithMsg(api.ValidationError,
			"Field RA: Attempt to cut out along axis 1 that has been"
				" modified before.",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"RA": ["359.36 359.359"],
				"POLYGON": ["359.36 30.9845 359.359 30.9845"
					" 359.359 30.985"]
				}))

	def testEmptyCutout(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		self.assertRaisesWithMsg(soda.EmptyData,
			"",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"CIRCLE": ["36 48 0.0004"],
				}))

	def testCutoutCIRCLE(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"CIRCLE": ["359.36 30.984 0.0004"]
				}))

		self.assertEqual(mime, "application/fits")
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
# TODO: I'd really expect NAXIS1==NAXIS2, but I cannot find a mistake
# in my reasoning up to here.  Hm.
		self.assertEqual(hdr["NAXIS1"], 4)
		self.assertEqual(hdr["NAXIS2"], 3)
		self.assertEqual(hdr["NAXIS3"], 4)
	
	def testCIRCLEClashesPOLYGON(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		self.assertRaisesWithMsg(api.ValidationError,
			"Field CIRCLE: Attempt to cut out along axis 1 that has been"
				" modified before.",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"CIRCLE": ["359.36 30.9845 0.0004"],
				"POLYGON": ["359.36 30.9845 359.359 30.9845"
					" 359.359 30.985"]
				}))

	def testKindPar(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_genKindPar"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
			"ID": [rscdef.getStandardPubDID("data/excube.fits")],
			"KIND": ["HEADER"],})
		self.assertEqual(mime, "application/fits-header")
		self.assertEqual(len(data), 2880)

	def testCutoutHeader(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker procDef="//soda#fits_makeWCSParams">
					<bind name="axisMetaOverrides">{3:{}}</bind>
				</metaMaker>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<FEED source="//soda#fits_genKindPar"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
			"ID": rscdef.getStandardPubDID("data/excube.fits"),
				"COO_3": ["3753 400000"],
			"KIND": "HEADER",})
		self.assertEqual(mime, "application/fits-header")
		self.assertTrue(b"NAXIS3  =                    2" in data)

	def testFITSNoSTC(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<FEED source="//soda#fits_standardDLFuncs"
					accrefPrefix="" stcs=""/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": rscdef.getStandardPubDID("data/excube.fits")})
		self.assertTrue(b"<DATA><TABLEDATA>" in data)

	def testPixelMeta(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<FEED source="//soda#fits_genPixelPar"/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": rscdef.getStandardPubDID("data/excube.fits")})
		tree = testhelpers.getXMLTree(data, debug=False)
		self.assertEqual(tree.xpath("//PARAM[@name='PIXEL_3']/VALUES/MAX"
			)[0].get("value"), "4")
		self.assertEqual(tree.xpath("//PARAM[@name='PIXEL_1']"
			)[0].get("datatype"), "int")

	def testPixelCutout(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<FEED source="//soda#fits_standardDLFuncs"
					stcs="" accrefPrefix=""/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
			"ID": rscdef.getStandardPubDID("data/excube.fits"),
				"PIXEL_1": ["4 4"],
				"PIXEL_3": ["2 2"]}))
		self.assertEqual(mime, "application/fits")
		self.assertTrue(b"NAXIS1  =                    1" in data)
		self.assertTrue(b"NAXIS2  =                    7" in data)
		self.assertTrue(b"NAXIS3  =                    1" in data)


class FITSAdaptationTests(testhelpers.VerboseTest):
	resources = [("fitsTable", tresc.fitsTable)]

	def testNoScaleWhen3D(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<FEED source="//soda#fits_standardDLFuncs"
					spectralAxis="1"/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		self.assertRaisesWithMsg(api.ValidationError,
			"Field scale: The following parameter(s) are not accepted by this service: scale",
			trialhelpers.runSvcWith,
			(svc, "dlget", {"SCALE": ["2"],
				"ID": rscdef.getStandardPubDID("data/excube.fits"),}))

	def testNoBandByDefault(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<FEED source="//soda#fits_standardDLFuncs"/>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		self.assertRaisesWithMsg(api.ValidationError,
			"Field band: The following parameter(s) are not accepted by this service: band",
			trialhelpers.runSvcWith,
			(svc, "dlget", {"BAND": ["2 4"],
				"ID": rscdef.getStandardPubDID("data/excube.fits"),}))


class SODAArtificialTests(testhelpers.VerboseTest):
# these are tests on totally artificial FITS headers as per
# data/cores#make-fits-desc

	def testNonLambdaCutout(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="data/cores#make-fits-desc"/>
				<FEED source="//soda#fits_standardBANDCutout"
					spectralAxis="3"/>
				<dataFormatter procDef="data/cores#debug-formatter"/>
			</datalinkCore></service>""")

		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
				"ID": ["NAXIS3=300&CRVAL3=400&CRPIX3=1&CDELT3=0.5&CUNIT3=MHz"],
				"BAND": ["0.7425 0.7444"],
				})
		self.assertEqual(re.sub(r"\.[0-9]*", "", data), "(3, 6, 9)")

	_velCubeId = (
		"NAXIS=3&NAXIS3=400&CUNIT3=km%2fs&CTYPE3=VRAD&CRVAL3=4710.6"
			"&CRPIX3=30&CDELT3=-3")

	def testVelocityCutout(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="data/cores#make-fits-desc"/>
				<FEED source="//soda#fits_standardDLFuncs" velocityAxis="3">
					<PRUNE name="makeHDUList"/>
					<PRUNE name="doWCSCutout"/>
					<PRUNE name="formatHDUs"/>
				</FEED>
				<dataFormatter procDef="data/cores#debug-formatter"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [self._velCubeId],
				"VELOCITY": ["4710.6e3 4790000"],
				})
		self.assertEqual(data, '(3, 3, 30)')

		# second test: Make sure no automatic 3rd axis param is present
		# (see testAutomatic3rdAxis)
		self.assertRaisesWithMsg(api.ValidationError,
			"Field vrad_3: The following parameter(s) are not accepted by this service: vrad_3",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [self._velCubeId],
				"VRAD_3": ["4710 4790"],
				}))

	def testAutomatic3rdAxis(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="data/cores#make-fits-desc"/>
				<FEED source="//soda#fits_standardDLFuncs" velocityAxis="3"
						axisMetaOverrides="{3:{}}">
					<PRUNE name="makeHDUList"/>
					<PRUNE name="formatHDUs"/>
				</FEED>
				<dataFormatter procDef="data/cores#debug-formatter"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [self._velCubeId],
				"VRAD_3": ["4710 4790"],
				})
		self.assertEqual(data, '(3, 3, 31)')


class DatalinkSTCTest(testhelpers.VerboseTest):
	resources = [("fitsTable", tresc.fitsTable)]

	def testSTCDefsPresent(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker>
					<setup>
						<code>
							parSTC = stc.parseQSTCS("PositionInterval ICRS BARYCENTER"
								' "RA_MIN" "DEC_MIN" "RA_MAX" "DEC_MAX"')
						</code>
					</setup>
					<code>
						for name in ["RA", "DEC"]:
							for ik in genLimitKeys(MS(InputKey, name=name, stc=parSTC)):
								yield ik
					</code>
				</metaMaker>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits")]})
		tree = testhelpers.getXMLTree(data, debug=False)
		self.assertEqual(len(tree.xpath(
			"//GROUP[@utype='stc:CatalogEntryLocation']/PARAM")), 4)
		self.assertEqual(len(tree.xpath(
			"//GROUP[@utype='stc:CatalogEntryLocation']/PARAMref")), 4)
		self.assertEqual(tree.xpath(
			"//PARAM[@utype='stc:AstroCoordSystem.SpaceFrame.ReferencePosition']"
			)[0].get("value"), "BARYCENTER")

		# follow a reference
		id = tree.xpath("//PARAMref[@utype='stc:AstroCoordArea"
			".Position2VecInterval.HiLimit2Vec.C2']")[0].get("ref")
		self.assertEqual(tree.xpath("//PARAM[@ID='%s']"%id)[0].get("name"),
			"DEC_MAX")

	def testTwoSystems(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta, dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<metaMaker>
					<setup>
						<code>
							parSTC = stc.parseQSTCS("PositionInterval ICRS BARYCENTER"
								' "RA_MIN" "DEC_MIN" "RA_MAX" "DEC_MAX"')
						</code>
					</setup>
					<code>
						for name in ["RA", "DEC"]:
							for ik in genLimitKeys(MS(InputKey, name=name, stc=parSTC)):
								yield ik
					</code>
				</metaMaker>
				<metaMaker>
					<setup>
						<code>
							parSTC = stc.parseQSTCS("PositionInterval GALACTIC"
								' "LAMB_MIN" "BET_MIN" "LAMB_MAX" "BET_MAX"')
						</code>
					</setup>
					<code>
						for name in ["LAMB", "BET"]:
							for ik in genLimitKeys(MS(InputKey, name=name, stc=parSTC)):
								yield ik
					</code>
				</metaMaker>

			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()
		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits")]})

		tree = testhelpers.getXMLTree(data, debug=False)
		self.assertEqual(len(tree.xpath(
			"//GROUP[@utype='stc:CatalogEntryLocation']")), 2)
		ids = [el.get("ref") for el in
				tree.xpath("//PARAMref[@utype='stc:AstroCoordArea"
				".Position2VecInterval.HiLimit2Vec.C2']")]
		names = set(tree.xpath("//PARAM[@ID='%s']"%id)[0].get("name")
			for id in ids)
		self.assertEqual(names, set(["DEC_MAX", "BET_MAX"]))


def _iterWCSSamples():
	for center in [
			(0, 0),
			(10, 45),
			(120, -30),
			(240, 87),
			(0, -90),
			(180, 90)]:
		yield  center, testhelpers.computeWCSKeys(center, (2, 3), cutCrap=True)
	

def _getFakeDescriptorForWCS(wcsDict):
	"""returns something good enough to stand in for a FITSDescriptor
	in SODA code from stuff generated by computeWCSKeys.
	"""
	res = testhelpers.StandIn(hdr=wcsDict,
		changingAxis=lambda *args: None,
		slices=[])
	soda.ensureSkyWCS(res)
	return res


class _MetaProcBase(testhelpers.TestResource):
	procId = None

	def make(self, deps):
		proc = api.makeStruct(datalink.MetaMaker,
				procDef=api.resolveCrossId(self.procId)
			).compile(parent=testhelpers.getTestRD())
		return lambda arg: proc(None, arg)


class _POLYGONMetaProc(_MetaProcBase):
	procId = "//soda#fits_makePOLYGONMeta"


class SODA_POLYGONMetaTest(testhelpers.VerboseTest, metaclass=testhelpers.SamplesBasedAutoTest):
	resources = [('proc', _POLYGONMetaProc())]

	# generated through maketruth below
	truth = {
		(0, 0): '359.0021009128 -1.4964326084 359.0021009128 1.4994301037 0.9998984794 1.4994291918 0.9998984794 -1.4964316983',
		(10, 45): '8.62481612 43.4950994178 8.5509805405 46.4905074605 11.4519220644 46.490470762 11.3779386981 43.4950663641',
		(120, -30): '118.8301216734 -31.4913388619 118.8649164101 -28.4956276477 121.1373577096 -28.4956087361 121.1722221187 -31.4913175209',
		(240, 87): '227.4781529243 85.3942399052 206.3749183808 88.1983560305 273.6779932638 88.1972488451 252.546147226 85.3938074824',
		(0, -90): '213.690067526 -88.2014209215 326.3628534376 -88.1989264651 33.690067526 -88.1978189291 146.2569707456 -88.2003118463'  ,
		(180, 90): '146.309932474 88.2014209215 33.6371465624 88.1989264651 326.309932474 88.1978189291 213.7430292544 88.2003118463',
		}
	
	def _produceKeys(self, sample):
		pos, header = sample
		desc = _getFakeDescriptorForWCS(header)
		return list(self.proc(desc))

	def _runTest(self, sample):
		keys = self._produceKeys(sample)
		key = testhelpers.pickSingle(keys)
		self.assertEqual(key.type, "double precision(*)")
		self.assertEqual(key.values.max,
			self.truth[sample[0]])

	def getTruthFor(self, sample):
		key = self._produceKeys(sample)[0]
		points = [float(v) for v in key.values.max.split()]
		return "%r: %r"%(sample[0], key.values.max), [
			('center', sample[0][0], sample[0][1])]+[
			('vertex',)+p for p in utils.grouped(2, points)]

	samples = list(_iterWCSSamples())


class _CIRCLEMetaProc(_MetaProcBase):
	procId = "//soda#fits_makeCIRCLEMeta"


class SODA_CIRCLEMetaTest(testhelpers.VerboseTest, metaclass=testhelpers.SamplesBasedAutoTest):
	resources = [('proc', _CIRCLEMetaProc())]

	# generated through maketruth below
	truth = {
		 (0, 0): '0. 0. 1.8021810709',
  	(10, 45): '10. 45. 1.8021810709',
  	(120, -30): '120. -30. 1.8021810709',
  	(240, 87): '240. 87. 1.8021810709',
  	(0, -90): '180. -90. 1.8021810709',
  	(180, 90): '180. 90. 1.8021810709',
	}
	
	def _produceKeys(self, sample):
		pos, header = sample
		desc = _getFakeDescriptorForWCS(header)
		return list(self.proc(desc))

	def _runTest(self, sample):
		keys = self._produceKeys(sample)
		key = testhelpers.pickSingle(keys)
		self.assertEqual(key.type, "double precision(3)")
		self.assertEqual(key.values.max,
			self.truth[sample[0]])

	def getTruthFor(self, sample):
		from gavo.utils import pgsphere

		key = self._produceKeys(sample)[0]
		circ = pgsphere.SCircle.fromDALI(key.values.max)
		points = [float(v) for v in circ.asPoly().asSODA().split()]
		return "%r: %r"%(sample[0], key.values.max), [
			('center', sample[0][0], sample[0][1])]+[
			('vertex',)+p for p in utils.grouped(2, points)]

	samples = list(_iterWCSSamples())


class _CIRCLESliceProc(_MetaProcBase):
	procId = "//soda#fits_makeCIRCLESlice"

	def make(self, deps):
		return api.makeStruct(datalink.DataFunction,
				procDef=api.resolveCrossId(self.procId)
			).compile(parent=testhelpers.getTestRD())


class SODA_CIRCLECutoutIntervalTest(testhelpers.VerboseTest, metaclass=testhelpers.SamplesBasedAutoTest):
	resources = [('proc', _CIRCLESliceProc())]

	def _runTest(self, sample):
		pos, header = sample
		desc = _getFakeDescriptorForWCS(header)
		soda.ensureSkyWCS(desc)
		args = {"CIRCLE": pgsphere.SCircle.fromDALI(pos+(0.5,))}
		_ = self.proc(desc, args)
		self.assertEqual(
			desc.slices,
			[(1, 250, 750), (2, 333, 667)])

	samples = list(_iterWCSSamples())


class SODAPOSTest(testhelpers.VerboseTest):
	resources = [("fitsTable", tresc.fitsTable)]

	def _getSvc(self):
		return api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc"/>
				<FEED source="//soda#fits_standardDLFuncs"/>
			</datalinkCore></service>""")

	def testPOSError(self):
		self.assertRaisesWithMsg(api.ValidationError,
			"Field POS: Invalid SIAPv2 geometry: 'Circle 30 40'"
			" (expected a SIAPv2 shape name)",
			trialhelpers.runSvcWith,
			(self._getSvc(),
				"dlget", {
				"ID": [rscdef.getStandardPubDID("data/excube.fits")],
				"POS": ["Circle 30 40"],
				}))
	
	def testPOSRange(self):
		mime, data = _dissectSODAFile(
			trialhelpers.runSvcWith(self._getSvc(),
				"dlget", {
					"ID": [rscdef.getStandardPubDID("data/excube.fits")],
					"POS": ["RANGE 359.359 359.36 30.9845 30.985"],
					}))

		self.assertEqual(mime, "application/fits")
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 4)
		self.assertEqual(hdr["NAXIS2"], 2)
		self.assertEqual(hdr["NAXIS3"], 4)

	def testPOSCircle(self):
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(self._getSvc(), "dlget", {
			"ID": [rscdef.getStandardPubDID("data/excube.fits")],
			"POS": ["CIRCLE 359.359 30.984 0.001"],
			}))
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 7)
		self.assertEqual(hdr["NAXIS2"], 7)

	def testPOSPolygon(self):
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(self._getSvc(), "dlget", {
			"ID": [rscdef.getStandardPubDID("data/excube.fits")],
			"POS": ["POLYGON 359.359 30.984 359.3592 30.985 359.3593 30.986"],
			}))
		hdr = fitstools.readPrimaryHeaderQuick(io.BytesIO(data))
		self.assertEqual(hdr["NAXIS1"], 2)
		self.assertEqual(hdr["NAXIS2"], 4)


################ let's run a few of them on compressed FITSes, too

class CFITSTable(tresc.RDDataResource):
	"""puts ex.fits.fz (a compressed FITS) input the products table.
	"""
	dataId = "import_compressed_fits"

_cfitsTable = CFITSTable()


class SODACFITSTest(testhelpers.VerboseTest):
	resources = [("cfitsTable", _cfitsTable)]

	_geoDLServiceLiteral = """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="qnd">False</bind>
				</descriptorGenerator>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_Geometries"/>
				<dataFunction procDef="//soda#fits_doWCSCutout"/>
				<dataFormatter procDef="//soda#fits_formatHDUs"/>
			</datalinkCore></service>"""

	def testMakeDescriptor(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="accrefPrefix">"data/"</bind>
					<bind key="qnd">False</bind>
				</descriptorGenerator>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<metaMaker><code>
					assert descriptor.hdr["EQUINOX"]==2000.
					assert ([int(n) for n in
							descriptor.skyWCS.wcs_world2pix([(166, 20)], 0)[0]]
						==[7261, 7984])
					yield descriptor.makeLink("http://dc.g-vo.org",
						semantics="#sentinel",
						contentType="text/english")
				</code></metaMaker>
			</datalinkCore></service>""")
		svc.parent = testhelpers.getTestRD()

		mime, data = trialhelpers.runSvcWith(svc, "dlmeta", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")]})
		tree = testhelpers.getXMLTree(data, debug=False)
		self.assertEqual(tree.xpath("//PARAM[@name='RA']")[0].get("unit"),
			"deg")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("xtype"),
			"interval")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("datatype"),
			"double")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']")[0].get("arraysize"),
			"2")
		self.assertEqual(tree.xpath("//PARAM[@name='RA']/VALUES/MIN"
			)[0].get("value")[:7], "168.243")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']/VALUES/MAX"
			)[0].get("value")[:8], "22.21928")
		self.assertEqual(tree.xpath("//PARAM[@name='DEC']/DESCRIPTION"
			)[0].text, "The latitude coordinate")
		# there's been a problem in the meta maker above -- perhaps
		# a failed assertion?  See your logs.
		self.assertEqual(tree.xpath("//TR[TD='#sentinel']/TD"
			)[1].text, "http://dc.g-vo.org")
		self.assertEqual(
			tree.xpath("//TR[TD[6]='#this']/TD[10]")[0].text,
			'#image')

	def testCutoutPOLYGON(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")],
				"POLYGON": ["168.2464 22.2147 168.2454 22.2147 168.2464 22.2149"]
				}))

		self.assertEqual(mime, "application/fits")
		with pyfits.open(io.BytesIO(data)) as hdus:
			hdr = hdus[1].header
			self.assertEqual(hdr["NAXIS1"], 4)
			self.assertEqual(hdr["NAXIS2"], 2)

	def testEmptyCutout(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		self.assertRaisesWithMsg(soda.EmptyData,
			"",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")],
				"CIRCLE": ["160 48 0.0004"],
				}))

	def testCutoutCIRCLE(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		mime, data = _dissectSODAFile(trialhelpers.runSvcWith(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")],
				"CIRCLE": ["168.2455 22.2161 0.0002"]
				}))

		self.assertEqual(mime, "application/fits")
		hdr = pyfits.open(io.BytesIO(data))[1].header
# TODO: I'd really expect NAXIS1==NAXIS2, but I cannot find a mistake
# in my reasoning up to here.  Hm.
		self.assertEqual(hdr["NAXIS1"], 2)
		self.assertEqual(hdr["NAXIS2"], 3)
	
	def testCIRCLEClashesPOLYGON(self):
		svc = api.parseFromString(svcs.Service, self._geoDLServiceLiteral)
		self.assertRaisesWithMsg(api.ValidationError,
			"Field CIRCLE: Attempt to cut out along axis 1 that has been"
				" modified before.",
			trialhelpers.runSvcWith,
			(svc, "dlget", {
				"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")],
				"CIRCLE": ["168.245 22.215 0.0004"],
				"POLYGON": ["168.244 22.210 168.244 22.213 168.246 22.213"]
				}))

	def testKindPar(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind key="qnd">False</bind>
				</descriptorGenerator>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_genKindPar"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
			"ID": [rscdef.getStandardPubDID("data/ex.fits.fz")],
			"KIND": ["HEADER"],})
		self.assertEqual(mime, "application/fits-header")
		self.assertEqual(len(data), 11520)

	def testCutoutHeader(self):
		svc = api.parseFromString(svcs.Service, """<service id="foo"
				allowed="dlmeta,dlget">
			<datalinkCore>
				<descriptorGenerator procDef="//soda#fits_genDesc">
					<bind name="qnd">False</bind>
				</descriptorGenerator>
				<metaMaker procDef="//soda#fits_makeWCSParams"/>
				<dataFunction procDef="//soda#fits_makeHDUList"/>
				<FEED source="//soda#fits_genPixelPar"/>
				<FEED source="//soda#fits_genKindPar"/>
			</datalinkCore></service>""")
		mime, data = trialhelpers.runSvcWith(svc, "dlget", {
			"ID": rscdef.getStandardPubDID("data/ex.fits.fz"),
				"PIXEL_2": ["1 2"],
			"KIND": "HEADER",})
		self.assertEqual(mime, "application/fits-header")
		self.assertTrue(b"NAXIS2  =                    2" in data)
		self.assertEqual(len(data), 11520)


################ Start SDM tests


class _SDMMetaTable(testhelpers.TestResource):
	resources = [("ssaTable", tresc.ssaTestTable)]

	def make(self, deps):
		mediaType, doc = trialhelpers.runSvcWith(
			api.resolveCrossId("data/ssatest#dl"), "dlmeta",
				{"ID": 'ivo://test.inv/test1', "RESPONSEFORMAT": "votable/td"})
		return testhelpers.getXMLTree(doc, debug=False)
	

class SDMMetaTest(testhelpers.VerboseTest):
	resources = [("tree", _SDMMetaTable())]

	def testDataOrigin(self):
		self.assertEqual(
			self.tree.xpath("//INFO[@name='citation']")[0].get("value"),
			"http://localhost:8080/data/ssatest/dl/howtocite")

	def testColumnsPresent(self):
		self.assertEqual(
			[f.get("name") for f in self.tree.xpath("//FIELD")],
			['ID', 'access_url', 'service_def', 'error_message',
				'description', 'semantics', 'content_type', 'content_length',
				'local_semantics', 'content_qualifier'])

	def testThisContentQualifier(self):
		# semantics is field 6, content_qualifier 10 at this time.
		self.assertEqual(
			self.tree.xpath("//TR[TD[6]='#this']/TD[10]")[0].text,
			'#spectrum')


class SDMDatalinkTest(testhelpers.VerboseTest):
	resources = [("ssaTable", tresc.ssaTestTable)]

	def runService(self, params, render="dlget"):
		return trialhelpers.runSvcWith(
			api.resolveCrossId("data/ssatest#dl"), render, params)

	def testRejectWithoutPUBDID(self):
		self.assertRaisesWithMsg(api.ValidationError,
			"Field ID: ID is mandatory with dlget",
			self.runService,
			({},))

	def testVOTDelivery(self):
		res = self.runService(
			{"ID": 'ivo://test.inv/test1', "FORMAT": "application/x-votable+xml"})
		mime, payload = res
		self.assertEqual(mime, "application/x-votable+xml")
		self.assertTrue(b'xmlns:spec="http://www.ivoa.net/xml/SpectrumModel/v1.01'
			in payload)
		self.assertTrue(b'utype="spec:Spectrum">' in payload)
		self.assertTrue(b'QJtoAAAAAABAm2g' in payload)

	def testTextDelivery(self):
		res = self.runService(
			{"ID": 'ivo://test.inv/test1',
				"FORMAT": "text/plain"})
		mime, payload = res
		self.assertTrue(isinstance(payload, bytes))
		self.assertTrue(b"1754.0\t1754.0\n1755.0\t1753.0\n"
			b"1756.0\t1752.0" in payload)

	def testCutoutFull(self):
		res = self.runService(
			{"ID": ['ivo://test.inv/test1'],
				"FORMAT": ["text/plain"],
				"BAND": ["1.762e-7 1.764e-7"]})
		mime, payload = res
		self.assertEqual(payload,
			b'1762.0\t1746.0\n1763.0\t1745.0\n')
		self.assertFalse(b'<TR>' in payload)

	def testCutoutHalfopen(self):
		res = self.runService(
			{"ID": ['ivo://test.inv/test1'],
				"FORMAT": ["application/x-votable+xml;serialization=tabledata"],
				"BAND": ["1.927e-7 +Inf"]})
		mime, payload = res
		self.assertTrue(b'xmlns:spec="http://www.ivoa.net/xml/SpectrumModel/v1.01'
			in payload)
		self.assertTrue(b'<TR><TD>1927.0</TD><TD>1581.0</TD>' in payload)
		self.assertFalse(b'<TR><TD>1756.0</TD>' in payload)
		tree = testhelpers.getXMLTree(payload, debug=False)
		self.assertEqual(tree.xpath("//PARAM[@utype="
			"'spec:Spectrum.Char.SpectralAxis.Coverage.Bounds.Start']"
			)[0].get("value"), "1.927e-07")
		self.assertAlmostEqual(float(tree.xpath("//PARAM[@utype="
			"'spec:Spectrum.Char.SpectralAxis.Coverage.Bounds.Extent']"
			)[0].get("value")), 1e-10)
	
	def testNoSwappingLimits(self):
		self.assertRaisesWithMsg(soda.EmptyData,
			"",
			self.runService,
			({"ID": 'ivo://test.inv/test1',
				"FORMAT": "application/x-votable+xml",
				"BAND": ["1.764e-7 1.762e-7"]},))

	def testEmptyCutoutFails(self):
		self.assertRaisesWithMsg(soda.EmptyData,
			"",
			self.runService,
			({"ID": 'ivo://test.inv/test1',
				"FORMAT": "application/x-votable+xml",
				"BAND": "-Inf 1.927e-8"},))

	def testBlankInIntervalIgnored(self):
		res = self.runService(
			{"ID": ['ivo://test.inv/test1'],
				"FORMAT": ["text/plain"],
				"BAND": " "})
		mime, payload = res
		self.assertEqual(len(payload), 2450)

	def testIncompleteArgumentRejected(self):
		self.assertRaisesWithMsg(
			api.ValidationError,
			"Field BAND: Bad parameter literal ['23 '] (Invalid literal for"
			" double[2] (field anonymous): '<1 token(s)>')",
			self.runService, ({
				"ID": ['ivo://test.inv/test1'],
				"responseFORMAT": ["text/plain"],
				"BAND": "23 "},))

	def testOriginalCalibOk(self):
		mime, payload = self.runService(
			{"ID": 'ivo://test.inv/test1',
				"FORMAT": "text/plain",
				"FLUXCALIB": "UNCALIBRATED"})
		self.assertTrue(payload.endswith(b"1928.0	1580.0\n"))

	def testNormalize(self):
		mime, payload = trialhelpers.runSvcWith(
			api.resolveCrossId("data/ssatest#dl"),
			"ssap.xml",
			{"ID": ['ivo://test.inv/test1'],
				"FORMAT": "application/x-votable+xml;serialization=tabledata",
				"BAND": ["1.9e-7 1.92e-7"],
				"FLUXCALIB": "RELATIVE"})
		self.assertTrue(b"<TD>1900.0</TD><TD>0.91676" in payload)
		tree = testhelpers.getXMLTree(payload, debug=False)
		self.assertEqual(tree.xpath(
			"//PARAM[@utype='spec:Spectrum.Char.FluxAxis.Calibration']")[0].get(
				"value"),
			"RELATIVE")

	def testBadCalib(self):
		self.assertRaisesWithMsg(api.ValidationError,
			"Field FLUXCALIB: 'ferpotschket' is not a valid value for FLUXCALIB",
			self.runService,
			({"ID": 'ivo://test.inv/test1',
				"RESPONSEFORMAT": "text/plain",
				"FLUXCALIB": ["ferpotschket"]},
			"dlget"))

	def testBadPubDID(self):
		self.assertRaisesWithMsg(svcs.UnknownURI,
			"No spectrum with this pubDID known here (pubDID: ivo://test.inv/bad)",
			self.runService,
				({"ID": 'ivo://test.inv/bad'},))

	def testRandomParamFails(self):
		self.assertRaisesWithMsg(api.ValidationError,
			"Field warp: The following parameter(s) are"
			" not accepted by this service: warp",
			self.runService,
			({"ID": 'ivo://test.inv/test1',
				"warp": "infinity"},))

	def testCoreForgetting(self):
		gns = testhelpers.getMemDiffer()
		self.runService(
			{"ID": ['ivo://test.inv/test1'],
				"FORMAT": ["text/plain"], "BAND": ["1.762e-7 1.764e-7"]})
		gc.collect()
		ns = gns()
		self.assertEqual(ns, [], "Spectrum cutout left garbage.")


class _RenderedSDMTable(testhelpers.TestResource):
	resources = [("ssaTable", tresc.ssaTestTable)]

	def make(self, ignored):
		doc = trialhelpers.runSvcWith(
			api.resolveCrossId("data/ssatest#dl"), "dlget",
			{"ID": 'ivo://test.inv/test1', "FORMAT": "application/x-votable+xml"})[1]
		return testhelpers.getXMLTree(doc, debug=False)


class SDMResultTest(testhelpers.VerboseTest):
	resources = [("tree", _RenderedSDMTable())]

	def testDataOrigin(self):
		self.assertEqual(
			self.tree.xpath("//INFO[@name='citation']")[0].get("value"),
			"http://localhost:8080/data/ssatest/dl/howtocite")

	def testParamValue(self):
		self.assertEqual(
			self.tree.xpath("//PARAM[@name='ssa_location']")[0].get("value"),
			"10.100000002783013 15.199999999820202")
	
	def testFieldUCD(self):
		self.assertEqual(
			self.tree.xpath("//FIELD[@ID='flux']")[0].get("ucd"),
			"phot.flux.density;em.wl")

	def testSpecGroup(self):
		self.assertEqual(
			self.tree.xpath("//GROUP[@utype='spec:spectrum.data']"
				"/FIELDref[@utype='spec:spectrum.data.spectralaxis.value']"
				)[0].get("ref"),
			"spectral")

	def testData(self):
		self.assertTrue("QJtoAAAA"
			in self.tree.xpath("//STREAM")[0].text)

class SDMAccrefDatalinkTest(testhelpers.VerboseTest):
	resources = [("ssaTable", tresc.ssaTestTable)]

	def runService(self, params):
		return trialhelpers.runSvcWith(api.resolveCrossId("data/ssatest#dlaccref"), "dlget", params)
	
	def testBasic(self):
		res = self.runService(
			{"ID": 'data/spec1.ssatest',
				"FORMAT": "text/plain"})
		mime, payload = res
		self.assertTrue(isinstance(payload, bytes))
		self.assertTrue(b"1754.0\t1754.0\n1755.0\t1753.0\n"
			b"1756.0\t1752.0" in payload)


def maketruth(testclass):
	"""writes a VOTable representative of the test class.

	This is because there's nothing like visualisation for getting
	an idea whether these spherical geometry things actually make sense.
	
	This writes 000truth.vot and 000truth.txt, the latter for the class' truth
	attribute.  Open truth.vot in topcat to see what's happening.

	testclass needs to have a gettruth method for this to work.  It has
	to return, for each test,
	"""
	from gavo.votable import V

	tests = testclass("test00")
	txt, rows  = [], []

	for sample in _iterWCSSamples():
		truthVal, truthPoints = tests.getTruthFor(sample)
		txt.append(truthVal)
		rows.extend(truthPoints)

	vot = V.VOTABLE[V.RESOURCE[
		votable.DelayedTable(
			V.TABLE[
				V.FIELD(name="ptype", datatype="char", arraysize="*"),
				V.FIELD(name="ra", datatype="double", unit="deg"),
				V.FIELD(name="dec", datatype="double", unit="deg")],
			rows, V.BINARY)]]
	with open("000truth.vot", "w") as f:
		votable.write(vot, f)

	with open("000truth.txt", "w") as f:
		f.write("  %s\n"%(",\n  ".join(txt)))


if __name__=="__main__":
#	maketruth(SODA_CIRCLEMetaTest)
	testhelpers.main(SDMDatalinkTest)
