"""
Helpers for trial-based tests, in particular retrieving pages.
"""

#c Copyright 2008-2021, 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 functools
import os

from twisted.web import resource
from twisted.web import server
from twisted.web import util as twu
from twisted.trial.unittest import TestCase as TrialTest
from twisted.python import failure  #noflake: exported name

from gavo.helpers.testhelpers import ( #noflake: exported names
	FakeRequest, 
	getTestRD, DataServer, getXMLTree, tempConfig, testFile)
from gavo.helpers import testtricks

from gavo import base
from gavo import rsc
from gavo import utils

base.setConfig("web", "enabletests", "True")
from gavo.web import common as webcommon
from gavo.web import root


def debug(arg):
	import pdb; pdb.set_trace()
	return arg


def raiseException(failure):
	failure.raiseException()


def _doRender(page, request):
	result = page.render(request)

	if isinstance(result, int) and result==server.NOT_DONE_YET:
		# the thing is set up in a way that eventually some deferred
		# will fire and complete
		return request.deferred

	elif isinstance(result, bytes):
		request.write(result)
		request.finish()
		return request.deferred

	elif isinstance(result, twu.Redirect):
		request.code = 302
		request.headers["Location"] = str(result)
		request.finish()
		return request.deferred

	else:
		raise Exception("Unsupported render result: %s"%repr(result))


def _buildRequest(
		method, 
		path, 
		rawArgs, 
		moreHeaders=None, 
		requestClass=None):
	if requestClass is None:
		requestClass = FakeRequest

	args = {}
	for k, v in rawArgs.items():
		if isinstance(v, list):
			args[k] = v
		else:
			args[k] = [v]
	req = requestClass(path, args=args)

	req.headers = {}
	if moreHeaders:
		for k, v in moreHeaders.items():
			req.requestHeaders.setRawHeaders(k, [v])

	req.method = utils.bytify(method)
	return req


def runQuery(page, 
		method, 
		path, 
		args, 
		moreHeaders=None, 
		requestMogrifier=None,
		requestClass=None):
	"""runs a query on a page.

	The query should look like it's coming from localhost.

	The thing returns a deferred firing a pair of the result (a string)
	and the request (from which you can glean headers and such).
	"""
	req = _buildRequest(
		method, path, args, moreHeaders=moreHeaders, requestClass=requestClass)
	if requestMogrifier is not None:
		requestMogrifier(req)

	try:
		rsc = resource.getChildForRequest(page, req)
		return _doRender(rsc, req)
	except Exception as ex:
		webcommon.produceErrorDocument(failure.Failure(ex), req)
		return req.deferred


class RenderTest(TrialTest):
	"""a base class for tests of twisted web resources.
	"""
	renderer = None # Override with the resource to be tested.

	def assertStringsIn(self, result, strings, inverse=False, 
			customTest=None):
		content = result[0]
		try:
			for s in strings:
				if inverse:
					self.assertFalse(utils.bytify(s) in content, 
						"'%s' in remote.data"%s)
				else:
					self.assertFalse(utils.bytify(s) not in content, 
						"'%s' not in remote.data"%s)

			if customTest is not None:
				customTest(content)
		except AssertionError:
			with open("remote.data", "wb") as f:
				f.write(content)
			raise
		return result
	
	def assertResultHasStrings(self, method, path, args, strings, 
			rm=None, inverse=False, customTest=None):
		return runQuery(self.renderer, method, path, args, requestMogrifier=rm,
			).addCallback(self.assertStringsIn, strings, inverse=inverse,
			customTest=customTest)

	def assertGETHasStrings(self, path, args, strings, rm=None,
			customTest=None):
		return self.assertResultHasStrings("GET", 
			path, args, strings, rm=rm, customTest=customTest)

	def assertGETLacksStrings(self, path, args, strings, rm=None):
		return self.assertResultHasStrings("GET", 
			path, args, strings, rm=rm, inverse=True)

	def assertPOSTHasStrings(self, path, args, strings, rm=None):
		return self.assertResultHasStrings("POST", path, args, strings,
			rm=rm)

	def assertStatus(self, path, status, args={}, rm=None):
		def check(res):
			self.assertEqual(res[1].code, status)
			return res
		return runQuery(self.renderer, "GET", path, args, requestMogrifier=rm
			).addCallback(check)

	def assertGETRaises(self, path, args, exc, alsoCheck=None):
		def cb(res):
			raise AssertionError("%s not raised (returned %s instead)"%(
				exc, res))
		def eb(flr):
			flr.trap(exc)
			if alsoCheck is not None:
				alsoCheck(flr)

		return runQuery(self.renderer, "GET", path, args
			).addCallback(cb
			).addErrback(eb)

	def assertResponseIsValid(self, res):
		errs = testtricks.getXSDErrors(res[0], True)
		if errs:
			with open("remote.data", "w") as f:
				f.write(res[0])
			raise AssertionError(errs)

	def assertGETIsValid(self, path, args={}):
		return runQuery(self.renderer, "GET", path, args
			).addCallback(self.assertResponseIsValid)


class ArchiveTest(RenderTest):
	renderer = root.ArchiveService()


@functools.lru_cache(1)
def getImportConnection():
	# we cannot use the connection pools here since they may create threads.
	return base.getDBConnection("admin")


def provideRDData(rdName, ddId, _imported=set()):
	"""makes ddId from rdName and returns a cleanup function.

	This is for creating temporary data for tests; it's supposed to be used
	as in::

		atexit.register(provideRDData("test", "import_fitsprod"))
	
	This keeps track of (rdName, ddId) pairs it's already loaded and
	doesn't import them again.
	"""
	if (rdName, ddId) in _imported:
		return lambda: None

	dd = getTestRD(rdName).getById(ddId)
	conn = getImportConnection()
	dataCreated = rsc.makeData(dd, connection=conn)
	conn.commit()
	_imported.add((rdName, ddId))

	# rsc may already be gone in atexit
	nvArg = rsc.parseNonValidating

	def cleanup():
		dataCreated.dropTables(nvArg)
	
	return cleanup


if os.environ.get("GAVO_LOG")!="no":
	from gavo.user import logui
	logui.LoggingUI(base.ui)
