#!/usr/bin/env python
# -*- coding: utf-8 -*-
# cython: language_level=3, always_allow_keywords=True

## Copyright 2011-2026 by LivingLogic AG, Bayreuth/Germany
## Copyright 2011-2026 by Walter Dörwald
##
## All Rights Reserved
##
## See ll/xist/__init__.py for the license


import sys, os, json, datetime, math, tempfile, subprocess, re, textwrap, pathlib

import pytest

from ll import ul4on, ul4c, color, misc


home = os.environ["HOME"]

# For the Oracle tests to work the environment variable ``LL_ORASQL_TEST_CONNECT``
# must point to an Oracle schema where the packages ``UL4ON_PKG`` and
# ``UL4ONBUFFER_PKG`` from https://github.com/LivingLogic/LivingLogic.Oracle.ul4
# are installed

re_test_name = re.compile("(?P<file>[a-z0-9_/.]+)::(?P<test>[a-zA-Z0-9_]+)(?:\\[(?P<param>[a-z0-9_]+)\\])? \\((?P<phase>[a-z]+)\\)")


def _indent(text):
	return textwrap.indent(text, "\t\t")


def make_func_name():
	m = re_test_name.match(os.environ.get("PYTEST_CURRENT_TEST"))
	test = m.group('test')
	if test.startswith("test_oracle_"):
		test = test[12:]
	elif test.startswith("test_"):
		test = test[5:]
	funcname = f"test_ul4on_{test}"
	param = m.group('param')
	if param:
		if param.startswith("oracle_"):
			param = param[7:]
		if param == "ul4on":
			param = ""
		elif param == "ul4onbuffer":
			param = "buf"
		if param:
			funcname = f"{funcname}_{param}"
	return funcname


def oracle_ul4on(code):
	"""
	A test fixture that will execute the PL/SQL code passed in as a parameter.
	This PL/SQL code must output an UL4ON dump into the PL/SQL variable ``c_out``
	by using the ``UL4ON_PKG`` package. The package name is available as the
	function attribute ``pkg``.

	:func:`oracle_ul4on` returns the deserialized object dump as a Python object.

	For example::

		oracle('''
			{oracle.pkg}.begindict(c_out);
			{oracle.pkg}.enddict(c_out);
		''')

	should return a empty dictionary.
	"""
	connectstring = os.environ.get("LL_ORASQL_TEST_CONNECT")
	if connectstring:
		funcname = make_func_name()

		from ll import orasql
		db = orasql.connect(connectstring, readlobs=True)
		cursor = db.cursor()
		code = f"""
			create or replace function {funcname}
			return clob
			as
				c_out clob;
			begin
				{code}
				return c_out;
			end;
		"""
		print(code)
		cursor.execute(code)
		cursor.execute(f"select {funcname} as ul4ondump from dual")
		dump = cursor.fetchone().ul4ondump
		return ul4on.loads(dump)
	else:
		return None

oracle_ul4on.pkg = "ul4on_pkg"


def oracle_ul4onbuffer(code):
	"""
	A test fixture that will execute the PL/SQL code passed in as a parameter.
	This PL/SQL code must output an UL4ON dump into the PL/SQL variable ``c_out``
	by using the ``UL4ONBUFFER_PKG`` package. The package name is available as
	the function attribute ``pkg``.

	:func:`oracle_ul4onbuffer` returns the deserialized object dump as a Python
	object.

	For example::

		oracle('''
			{oracle.pkg}.begindict(c_out);
			{oracle.pkg}.enddict(c_out);
		''')

	should return a empty dictionary.

	Note that call to ``UL4ONBUFFER_PKG.INIT()`` and ``UL4ONBUFFER_PKG.FLUSH()``
	are not required in the code passed in (this makes it possible to call
	:func:`oracle_ul4on` and :func:`oracle_ul4onbuffer` with the code).
	"""
	connectstring = os.environ.get("LL_ORASQL_TEST_CONNECT")
	if connectstring:
		funcname = make_func_name()

		from ll import orasql
		db = orasql.connect(connectstring, readlobs=True)
		cursor = db.cursor()
		code = f"""
			create or replace function {funcname}
			return clob
			as
				c_out clob;
			begin
				ul4onbuffer_pkg.init(c_out);
				{code}
				ul4onbuffer_pkg.flush(c_out);
				return c_out;
			end;
		"""
		print(code)
		cursor.execute(code)
		cursor.execute(f"select {funcname} as ul4ondump from dual")
		dump = cursor.fetchone().ul4ondump
		return ul4on.loads(dump)
	else:
		return None

oracle_ul4onbuffer.pkg = "ul4onbuffer_pkg"


all_oracles = [
	("oracle_ul4on", oracle_ul4on),
	("oracle_ul4onbuffer", oracle_ul4onbuffer),
]


def _transport_python(obj, indent, registry):
	return ul4on.loads(ul4on.dumps(obj, indent=indent), registry=registry)


def transport_python(obj, registry=None):
	return _transport_python(obj, indent="", registry=registry)


def transport_python_pretty(obj, registry=None):
	return _transport_python(obj, indent="\t", registry=registry)


def _transport_js_v8(obj, indent):
	"""
	Generate Javascript source that loads the dump done by Python, dumps it
	again, and outputs this dump which is again loaded by Python.

	(this requires an installed ``d8`` shell from V8 (http://code.google.com/p/v8/))
	"""
	dump = ul4on.dumps(obj, indent=indent)
	js = f"obj = ul4.loads({ul4c._asjson(dump)});\nprint(ul4.dumps(obj, {ul4c._asjson(indent)}));\n"
	f = sys._getframe(1)
	print(f"Testing UL4ON via V8 ({f.f_code.co_filename}, line {f.f_lineno:,}):")
	print(js)
	with tempfile.NamedTemporaryFile(mode="wb", suffix=".js") as f:
		f.write(js.encode("utf-8"))
		f.flush()
		dir = os.path.expanduser("~/checkouts/LivingLogic.Javascript.ul4")
		cmd = f"d8 {dir}/dist/umd/ul4.js {f.name}"
		result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
	stdout = result.stdout.decode("utf-8")
	stderr = result.stderr.decode("utf-8")
	# Check if we have an exception
	if result.returncode:
		print(stdout, file=sys.stdout)
		print(stderr, file=sys.stderr)
		raise RuntimeError((stderr or stdout).splitlines()[0])
	output = stdout[:-1] # Drop the "\n"
	print(f"Got result {output!r}")
	return ul4on.loads(output)


def transport_js_v8(obj):
	return _transport_js_v8(obj, indent="")


def transport_js_v8_pretty(obj):
	return _transport_js_v8(obj, indent="\t")


def _transport_js_node(obj, indent):
	"""
	Generate Javascript source that loads the dump done by Python, dumps it
	again, and outputs this dump which is again loaded by Python.

	(this requires an installed ``node`` command from Node
	"""
	dump = ul4on.dumps(obj, indent=indent)
	js = f"""
		const ul4 = require('{home}/checkouts/LivingLogic.Javascript.ul4/dist/umd/ul4');
		var obj = ul4.loads({ul4c._asjson(dump)});
		console.log(JSON.stringify(ul4.dumps(obj, {ul4c._asjson(indent)})));
	"""
	f = sys._getframe(1)
	print(f"Testing UL4ON via Node ({f.f_code.co_filename}, line {f.f_lineno:,}):")
	print(js)
	with tempfile.NamedTemporaryFile(mode="wb", suffix=".js") as f:
		f.write(js.encode("utf-8"))
		f.flush()
		dir = os.path.expanduser("~/checkouts/LivingLogic.Javascript.ul4")
		cmd = f"node {f.name}"
		result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
	stdout = result.stdout.decode("utf-8")
	stderr = result.stderr.decode("utf-8")
	# Check if we have an exception
	if result.returncode:
		print(stdout, file=sys.stdout)
		print(stderr, file=sys.stderr)
		raise RuntimeError((stderr or stdout).splitlines()[0])
	print(f"Got result {stdout!r}")
	return ul4on.loads(json.loads(stdout))


def transport_js_node(obj):
	return _transport_js_node(obj, indent="")


def transport_js_node_pretty(obj):
	return _transport_js_node(obj, indent="\t")


def java_findexception(output):
	lines = output.splitlines()
	msg = None
	for line in lines:
		prefix1 = 'Exception in thread "main"'
		prefix2 = "Caused by:"
		if line.startswith(prefix1):
			msg = line[len(prefix1):].strip()
		elif line.startswith(prefix2):
			msg = line[len(prefix2):].strip()
	if msg is not None:
		print(output, file=sys.stderr)
		raise RuntimeError(msg)


def java_formatsource(string):
	"""
	Reindents the Java source.
	"""
	indent = 0
	newlines = []
	for line in string.strip().splitlines(False):
		line = line.strip()
		if line == "}" or line == "};":
			indent -= 1
		if line:
			newlines.append(indent*"\t" + line + "\n")
		if line == "{":
			indent += 1
	return "".join(newlines)


def run(data, indent=None):
	dump = ul4on.dumps(data, indent=indent)
	dump = ul4on.dumps({'indent': indent, 'dump': dump})
	print(f"\tInput data as UL4ON dump is:\n{_indent(dump)}")
	dump = dump.encode("utf-8")
	currentdir = pathlib.Path.cwd()
	try:
		os.chdir(pathlib.Path.home() / "checkouts/LivingLogic.Java.ul4")
		result = subprocess.run("./gradlew -q --console=plain execute_ul4on", input=dump, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
	finally:
		os.chdir(currentdir)
	print(f"\tReturn code is {result.returncode}")
	# Check if we have an exception
	stderr = result.stderr.decode("utf-8", "passbytes")
	print(f"\tOutput on stderr is:\n{_indent(stderr)}")
	if result.returncode != 0:
		self._find_exception(stderr)
		if stderr:
			# No exception found, but we still have error output,
			# so complain anyway with the original output
			raise ValueError(stderr)
	stdout = result.stdout.decode("utf-8", "passbytes")
	if stdout.startswith("hexdump:"):
		stdout = bytes.fromhex(stdout[8:]).decode("utf-8")
	else:
		raise ValueError(f"unrecognizable stdout: {stdout!r}")
	print(f"\tOutput on stdout is:\n{_indent(stdout)}")
	return stdout


def _transport_java(obj, indent):
	"""
	Generate Java source that loads the dump done by Python, dumps it again,
	and outputs this dump which is again loaded by Python.

	(this requires an installed Java compiler and the Java UL4 jar)
	"""

	f = sys._getframe(1)
	print(f"Testing UL4ON via Java ({f.f_code.co_filename}, line {f.f_lineno}):")

	output = run(obj, indent=indent)
	return ul4on.loads(output)


def transport_java(obj):
	return _transport_java(obj, indent="")


def transport_java_pretty(obj):
	return _transport_java(obj, indent="\t")


all_transports = [
	("python", transport_python),
	("python_pretty", transport_python_pretty),
	("js_v8", transport_js_v8),
	("js_v8_pretty", transport_js_v8_pretty),
	("js_node", transport_js_node),
	("js_node_pretty", transport_js_node_pretty),
	("java", transport_java),
	("java_pretty", transport_java_pretty),
]


def pytest_generate_tests(metafunc):
	if "t" in metafunc.fixturenames:
		metafunc.parametrize("t", [t for (id, t) in all_transports], ids=[id for (id, t) in all_transports])
	if "oracle" in metafunc.fixturenames:
		metafunc.parametrize("oracle", [oracle for (id, oracle) in all_oracles], ids=[id for (id, oracle) in all_oracles])


def test_none(t):
	assert None is t(None)


def test_bool(t):
	assert False is t(False)
	assert True is t(True)


def test_int(t):
	assert 42 == t(42)
	assert -42 == t(-42)
	assert 2147483647 == t(2147483647)
	assert -2147483648 == t(-2147483648)
	if t not in (transport_js_v8, transport_js_v8_pretty, transport_js_node, transport_js_node_pretty):
		assert 9223372036854775807 == t(9223372036854775807)
		assert -9223372036854775808 == t(-9223372036854775808)
		assert 10**30 == t(10**30)

def test_float(t):
	assert -42.5 == t(-42.5)
	if t not in (transport_js_v8, transport_js_v8_pretty, transport_js_node, transport_js_node_pretty):
		assert 1e42 == t(1e42)
	assert math.pi == t(math.pi)


def test_string(t):
	assert "gurk" == t("gurk")

	# This should make sure that all characters roundtrip properly
	# (except maybe those outside the BMP, as JS and Java don't support them properly)
	chars = "".join(chr(c) for c in range(0x1000))
	assert chars == t(chars)


def test_color(t):
	c = color.Color(0x66, 0x99, 0xcc, 0xff)
	assert c == t(c)


def test_date(t):
	expected = datetime.date(2012, 10, 29)
	got = t(expected)
	assert isinstance(got, datetime.date) and not isinstance(got, datetime.datetime)
	assert expected == got


def test_datetime(t):
	expected = datetime.datetime(2012, 10, 29)
	got = t(expected)
	assert isinstance(got, datetime.datetime)
	assert expected == got

	expected = datetime.datetime(2012, 10, 29, 16, 44, 55, 987000)
	got = t(expected)
	assert isinstance(got, datetime.datetime)
	assert expected == got


def test_timedelta(t):
	d = datetime.timedelta(1, 1, 1)
	assert d == t(d)


def test_monthdelta(t):
	d = misc.monthdelta(1)
	assert d == t(d)


def test_slice(t):
	d = slice(None, None)
	assert d == t(d)

	d = slice(1, None)
	assert d == t(d)

	d = slice(None, 3)
	assert d == t(d)

	d = slice(1, 3)
	assert d == t(d)


def test_list(t):
	assert [] == t([])
	assert [1, 2, 3] == t([1, 2, 3])


def test_dict(t):
	d = {}
	assert d == t(d)
	d = {"gurk": "hurz"}
	assert d == t(d)
	if t not in (transport_js_v8, transport_js_v8_pretty):
		d = {17: None, None: 23}
		assert d == t(d)


def test_ordereddict(t):
	if t not in (transport_js_v8, transport_js_v8_pretty):
		d = {}
		assert d == t(d)

		assert isinstance(t(d), dict)

		d = {"gurk": "hurz"}
		assert d == t(d)

		d1 = {}
		d1[1] = 'one'
		d1[2] = 'two'
		assert d1 == t(d1)
		assert list(t(d1)) == [1, 2]

		d2 = {}
		d2[2] = 'two'
		d2[1] = 'one'
		assert t(d1) == t(d2)


def test_set(t):
	if t not in (transport_js_v8, transport_js_v8_pretty):
		assert set() == t(set())
		assert {1, 2, 3} == t({1, 2, 3})


def test_template(t):
	template = ul4c.Template("<?for i in range(10)?>(<?print i?>)<?end for?>")
	assert template.renders() == t(template).renders()


def test_nested(t):
	d = {
		"nix": None,
		"int": 42,
		"float": math.pi,
		"foo": "bar",
		"baz": datetime.datetime(2012, 10, 29, 16, 44, 55, 987000),
		"td": datetime.timedelta(-1, 1, 1),
		"md": misc.monthdelta(-1),
		"gurk": ["hurz"],
	}
	assert d == t(d)


def test_template_from_source():
	t = ul4on.loads("o s'de.livinglogic.ul4.template' n s'test' s'namespace' s'<?print x + y?>' s'x, y=23' s'keep' )")

	assert t.name == "test"
	assert t.namespace == "namespace"
	assert t.source == "<?ul4 test(x, y=23)?><?print x + y?>"
	assert t.whitespace == "keep"
	assert t.renders(17) == "40"


def test_recursion(t):
	if t not in (transport_js_v8, transport_js_v8_pretty, transport_js_node, transport_js_node_pretty):
		l1 = []
		l1.append(l1)

		l2 = t(l1)
		assert len(l2) == 1
		assert l2[0] is l2


def test_custom_class(t):
	if t in (transport_python, transport_python_pretty):
		@ul4on.register("de.livinglogic.ul4.test.point")
		class Point:
			def __init__(self, x=None, y=None):
				self.x = x
				self.y = y

			def ul4ondump(self, encoder):
				encoder.dump(self.x)
				encoder.dump(self.y)

			def ul4onload(self, decoder):
				self.x = decoder.load()
				self.y = decoder.load()

		p = t(Point(17, 23))
		assert p.x == 17
		assert p.y == 23
		assert isinstance(p, Point)

		class Point2(Point):
			pass

		p = t(Point(17, 23), registry={"de.livinglogic.ul4.test.point": Point2})
		assert p.x == 17
		assert p.y == 23
		assert isinstance(p, Point2)

		@ul4on.register("de.livinglogic.ul4.test.pointcontent")
		class PointContent:
			def __init__(self, x=None, y=None):
				self.x = x
				self.y = y

			def ul4ondump(self, encoder):
				if self.x != 0:
					encoder.dump(self.x)
					if self.y != 0:
						encoder.dump(self.y)

			def ul4onload(self, decoder):
				i = -1
				for (i, item) in enumerate(decoder.loadcontent()):
					if i == 0:
						self.x = item
					elif i == 1:
						self.y = item
				if i < 1:
					self.y = 0
					if i < 0:
						self.x = 0

		p = t(PointContent(17, 23))
		assert p.x == 17
		assert p.y == 23
		assert isinstance(p, PointContent)

		p = t(PointContent(17, 0))
		assert p.x == 17
		assert p.y == 0
		assert isinstance(p, PointContent)

		p = t(PointContent(0, 0))
		assert p.x == 0
		assert p.y == 0
		assert isinstance(p, PointContent)


def test_chunked_encoder():
	encoder = ul4on.Encoder()
	s1 = "gurk"
	s2 = "hurz"
	assert "S'gurk'" == encoder.dumps(s1)
	assert "S'hurz'" == encoder.dumps(s2)
	assert "^0" == encoder.dumps(s1)
	assert "^1" == encoder.dumps(s2)


def test_chunked_decoder():
	decoder = ul4on.Decoder()
	assert "gurk" == decoder.loads("S'gurk'")
	assert "hurz" == decoder.loads("S'hurz'")
	assert "gurk" == decoder.loads("^0")
	assert "hurz" == decoder.loads("^1")


def test_incremental_without_id():
	class Point:
		ul4onname = "de.livinglogic.ul4.test.point"

		def __init__(self, id=None, x=None, y=None):
			self.ul4onid = id
			self.x = x
			self.y = y

		def ul4ondump(self, encoder):
			encoder.dump(self.x)
			encoder.dump(self.y)

		def ul4onload(self, decoder):
			self.x = decoder.load()
			self.y = decoder.load()

	registry = {Point.ul4onname: Point}

	p1 = Point(None, 17, 23)

	encoder = ul4on.Encoder()
	# Since ``ul4onid`` is ``None`` ``p1`` will not be treated as a persistent object
	dump = encoder.dumps(p1)

	decoder = ul4on.Decoder(registry)

	dump = dump.replace(" i23 ", " i24 ")
	p2 = decoder.loads(dump)

	assert p1 is not p2

	assert p1.ul4onid is None
	assert p1.x == 17
	assert p1.y == 23

	assert p2.ul4onid is None
	assert p2.x == 17
	assert p2.y == 24

	# Reset backreferences
	decoder.reset()

	# Decode a modified dump a second time
	dump = dump.replace(" i24 ", " i25 ")
	p3 = decoder.loads(dump)

	# Since ``p1`` wasn't treated as persistent, the decoder didn't remember ``p2``
	# So ``p3`` is different
	assert p2 is not p3

	assert p2.ul4onid is None
	assert p2.x == 17
	assert p2.y == 24

	assert p3.ul4onid is None
	assert p3.x == 17
	assert p3.y == 25


def test_incremental_with_id():
	class Point:
		ul4onname = "de.livinglogic.ul4.test.point"

		def __init__(self, id, x=None, y=None):
			self.ul4onid = id
			self.x = x
			self.y = y

		def ul4ondump(self, encoder):
			encoder.dump(self.x)
			encoder.dump(self.y)

		def ul4onload(self, decoder):
			self.x = decoder.load()
			self.y = decoder.load()

	registry = {Point.ul4onname: Point}

	p1 = Point("foo", 17, 23)

	encoder = ul4on.Encoder()
	dump = encoder.dumps(p1)

	decoder = ul4on.Decoder(registry)

	dump = dump.replace(" i23 ", " i24 ")
	p2 = decoder.loads(dump)

	# The decoder hasn't seen this object yet,
	# so the deserialized objects is not the original one,
	# but it has the correct attributes.
	# The attributes of the original object are unchanged.
	assert p1 is not p2

	assert p1.ul4onid == "foo"
	assert p1.x == 17
	assert p1.y == 23

	assert p2.ul4onid == "foo"
	assert p2.x == 17
	assert p2.y == 24

	# Reset backreferences
	decoder.reset()

	# Decode a modified dump a second time
	dump = dump.replace(" i24 ", " i25 ")
	p3 = decoder.loads(dump)

	# Now the :meth:`loads` call has updated the object ``p2`` that already exists
	# The attributes have been updated according to the info in the dump.
	assert p2 is p3

	assert p3.ul4onid == "foo"
	assert p3.x == 17
	assert p3.y == 25


@pytest.mark.db
def test_oracle_none(oracle):
	if oracle:
		assert None is oracle(f"{oracle.pkg}.none(c_out);")


@pytest.mark.db
def test_oracle_bool(oracle):
	if oracle:
		assert None is oracle(f"{oracle.pkg}.bool(c_out, null);")
		assert False is oracle(f"{oracle.pkg}.bool(c_out, 0);")
		assert True is oracle(f"{oracle.pkg}.bool(c_out, 1);")


@pytest.mark.db
def test_oracle_int(oracle):
	if oracle:
		assert None is oracle(f"{oracle.pkg}.int(c_out, null);")
		assert 42 == oracle(f"{oracle.pkg}.int(c_out, 42);")
		assert 0 == oracle(f"{oracle.pkg}.int(c_out, 0);")
		assert -42 == oracle(f"{oracle.pkg}.int(c_out, -42);")


@pytest.mark.db
def test_oracle_float(oracle):
	if oracle:
		assert None is oracle(f"{oracle.pkg}.float(c_out, null);")
		assert 42.5 == oracle(f"{oracle.pkg}.float(c_out, 42.5);")
		assert 0.0 == oracle(f"{oracle.pkg}.float(c_out, 0);")
		assert -42.5 == oracle(f"{oracle.pkg}.float(c_out, -42.5);")


@pytest.mark.db
def test_oracle_str(oracle):
	if oracle:
		assert "foo" == oracle(f"{oracle.pkg}.str(c_out, 'foo');")
		assert "\x00\a\b\t\n\r\x1b" == oracle(f"{oracle.pkg}.str(c_out, chr(0) || chr(7) || chr(8) || chr(9) || chr(10) || chr(13) || chr(27));")
		assert "\xa0äöüÄÖÜß€" == oracle(f"{oracle.pkg}.str(c_out, '\xa0äöüÄÖÜß\u20ac');")
		assert "foo" == oracle(f"{oracle.pkg}.str(c_out, to_clob('foo'));")

		# Check every BMP character (in blocks of 64)
		size = 0x40
		for offset in range(0, 0x1000, size):
			chars = "".join(chr(c) for c in range(offset, offset + size))

			oraclechars = "".join(f"\\{ord(c):04x}" for c in chars)
			oraclechars = f"unistr('{oraclechars}')"

			assert chars == oracle(f"{oracle.pkg}.str(c_out, {oraclechars});")


@pytest.mark.db
def test_oracle_color(oracle):
	if oracle:
		expected = color.Color(0x66, 0x99, 0xcc, 0xff)
		got = oracle(f"{oracle.pkg}.color(c_out, 102, 153, 204, 255);")
		assert expected == got


@pytest.mark.db
def test_oracle_date(oracle):
	if oracle:
		expected = datetime.date(2014, 11, 6)
		got = oracle(f"{oracle.pkg}.date_(c_out, to_date('2014-11-06', 'YYYY-MM-DD'));")
		assert expected == got


@pytest.mark.db
def test_oracle_datetime(oracle):
	if oracle:
		expected = datetime.datetime(2014, 11, 6, 12, 34, 56)
		got = oracle(f"{oracle.pkg}.datetime(c_out, to_date('2014-11-06 12:34:56', 'YYYY-MM-DD HH24:MI:SS'));")
		assert expected == got

		expected = datetime.datetime(2014, 11, 6, 12, 34, 56, 987654)
		got = oracle(f"{oracle.pkg}.datetime(c_out, to_timestamp('2014-11-06 12:34:56,987654', 'YYYY-MM-DD HH24:MI:SS,FF6'));")
		assert expected == got


@pytest.mark.db
def test_oracle_timedelta(oracle):
	if oracle:
		expected = datetime.timedelta(days=42)
		got = oracle(f"{oracle.pkg}.timedelta(c_out, 42);")
		assert expected == got

		expected = datetime.timedelta(days=1, seconds=2, microseconds=3)
		got = oracle(f"{oracle.pkg}.timedelta(c_out, 1, 2, 3);")
		assert expected == got


@pytest.mark.db
def test_oracle_monthdelta(oracle):
	if oracle:
		expected = misc.monthdelta(42)
		got = oracle(f"{oracle.pkg}.monthdelta(c_out, 42);")
		assert expected == got

		expected = misc.monthdelta(-42)
		got = oracle(f"{oracle.pkg}.monthdelta(c_out, -42);")
		assert expected == got


@pytest.mark.db
def test_oracle_slice(oracle):
	if oracle:
		expected = slice(None, None)
		got = oracle(f"{oracle.pkg}.slice(c_out, null, null);")
		assert expected == got

		expected = slice(1, None)
		got = oracle(f"{oracle.pkg}.slice(c_out, 1, null);")
		assert expected == got

		expected = slice(None, 3)
		got = oracle(f"{oracle.pkg}.slice(c_out, null, 3);")
		assert expected == got

		expected = slice(1, 3)
		got = oracle(f"{oracle.pkg}.slice(c_out, 1, 3);")
		assert expected == got


@pytest.mark.db
def test_oracle_list(oracle):
	if oracle:
		expected = []
		got = oracle(f"""
			{oracle.pkg}.beginlist(c_out);
			{oracle.pkg}.endlist(c_out);
		""")
		assert expected == got

		expected = [None, 42, "foo"]
		got = oracle(f"""
			{oracle.pkg}.beginlist(c_out);
				{oracle.pkg}.none(c_out);
				{oracle.pkg}.int(c_out, 42);
				{oracle.pkg}.str(c_out, 'foo');
			{oracle.pkg}.endlist(c_out);
		""")
		assert expected == got


@pytest.mark.db
def test_oracle_set(oracle):
	if oracle:
		expected = set()
		got = oracle(f"""
			{oracle.pkg}.beginset(c_out);
			{oracle.pkg}.endset(c_out);
		""")
		assert expected == got

		expected = {None, 42, "foo"}
		got = oracle(f"""
			{oracle.pkg}.beginset(c_out);
				{oracle.pkg}.none(c_out);
				{oracle.pkg}.int(c_out, 42);
				{oracle.pkg}.str(c_out, 'foo');
			{oracle.pkg}.endset(c_out);
		""")
		assert expected == got


@pytest.mark.db
def test_oracle_dict(oracle):
	if oracle:
		expected = {}
		got = oracle(f"""
			{oracle.pkg}.begindict(c_out);
			{oracle.pkg}.enddict(c_out);
		""")
		assert expected == got

		expected = {"foo": None, "bar": 42, 42: [1, 2, 3]}
		got = oracle(f"""
			{oracle.pkg}.begindict(c_out);
				{oracle.pkg}.keynone(c_out, 'foo');
				{oracle.pkg}.keyint(c_out, 'bar', 42);
				{oracle.pkg}.int(c_out, 42);
				{oracle.pkg}.beginlist(c_out);
					{oracle.pkg}.int(c_out, 1);
					{oracle.pkg}.int(c_out, 2);
					{oracle.pkg}.int(c_out, 3);
				{oracle.pkg}.endlist(c_out);
			{oracle.pkg}.enddict(c_out);
		""")
		assert expected == got


@pytest.mark.db
def test_oracle_object(oracle):
	if oracle:
		@ul4on.register("de.livinglogic.xist.ul4on.test.person")
		class Person:
			def __init__(self, firstname=None, lastname=None):
				self.firstname = firstname
				self.lastname = lastname

			def ul4ondump(self, encoder):
				encoder.dump(self.firstname)
				encoder.dump(self.lastname)

			def ul4onload(self, decoder):
				self.firstname = decoder.load()
				self.lastname = decoder.load()

		p = oracle(f"""
			{oracle.pkg}.beginobject(c_out, 'de.livinglogic.xist.ul4on.test.person');
				{oracle.pkg}.str(c_out, 'Otto');
				{oracle.pkg}.str(c_out, 'Normalverbraucher');
			{oracle.pkg}.endobject(c_out);
		""")

		assert isinstance(p, Person)
		assert p.firstname == "Otto"
		assert p.lastname == "Normalverbraucher"
