
# Copyright 2012-2022 Jaap Karssenberg <jaap.karssenberg@gmail.com>


import tests

from tests.config import EnvironmentConfigContext, ConfigManager

import sys
import io as StringIO
import threading
import time


from zim.main import *

import zim
import zim.main
import zim.main.ipc


from zim.notebook.info import NotebookInfo


class capture_stdout:

		def __enter__(self):
			self.real_stdout = sys.stdout
			sys.stdout = StringIO.StringIO()
			return sys.stdout

		def __exit__(self, type, value, traceback):
			sys.stdout = self.real_stdout


class TestParseCommand(tests.TestCase):

	def runTest(self):
		for command, klass in list(zim.main.commands.items()):
			obj = zim.main.build_command(['--%s' % command])
			self.assertIsInstance(obj, klass)

		obj = zim.main.build_command(['-v'])
		self.assertIsInstance(obj, VersionCommand)

		obj = zim.main.build_command(['-h'])
		self.assertIsInstance(obj, HelpCommand)

		obj = zim.main.build_command(['foo'])
		self.assertIsInstance(obj, GuiCommand)

		obj = zim.main.build_command(['--plugin', 'quicknote'])
		self.assertIsInstance(obj, Command)

		obj = zim.main.build_command(['--server', '--gui'])
		self.assertIsInstance(obj, ServerGuiCommand)


class TestVersion(tests.TestCase):

	def runTest(self):
		cmd = VersionCommand('version')
		with capture_stdout() as output:
			cmd.run()
		self.assertTrue(output.getvalue().startswith('zim'))


class TestHelp(tests.TestCase):

	def runTest(self):
		cmd = HelpCommand('help')
		with capture_stdout() as output:
			cmd.run()
		self.assertTrue(output.getvalue().startswith('usage:'))


class TestNotebookCommand(tests.TestCase):

	cmd_class = NotebookCommand

	def get_cmd(self, *args):
		cmd = self.cmd_class('gui')
		cmd.arguments = ('NOTEBOOK', '[PAGE]')
		cmd.parse_options(*args)
		return cmd

	def testPWDhandling(self):
		# check if PWD at "parse_options" is remembered after changing dir
		cmd = self.get_cmd('./Notes')

		pwd = os.getcwd()
		self.addCleanup(os.chdir, pwd)
		os.chdir('/')

		myinfo = NotebookInfo(pwd + '/Notes')
		notebookinfo, page = cmd.get_notebook_argument()
		self.assertEqual(notebookinfo, myinfo)

	#def testNotebookName(self):
	#	pass

	def testFile(self):
		folder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		file = folder.file('Foo.txt')
		file.touch()

		for arg in (file.path, file.uri):
			cmd = self.get_cmd(arg)
			myinfo = NotebookInfo(file.path)
			notebookinfo, page = cmd.get_notebook_argument()
			self.assertEqual(notebookinfo, myinfo)
			notebook, href = cmd.build_notebook()
			self.assertEqual(notebook.uri, folder.uri)
			self.assertEqual(href.names, 'Foo')

		# Test override by page argument
		cmd = self.get_cmd(file.path, 'Bar')
		notebook, href = cmd.build_notebook()
		self.assertEqual(notebook.uri, folder.uri)
		self.assertEqual(href.names, 'Bar')

	def testNotebookZimFile(self):
		folder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		config = folder.file('notebook.zim')
		config.touch()

		for arg in (config.path, config.uri):
			cmd = self.get_cmd(arg)
			myinfo = NotebookInfo(config.path)
			notebookinfo, page = cmd.get_notebook_argument()
			self.assertEqual(notebookinfo, myinfo)
			notebook, href = cmd.build_notebook()
			self.assertEqual(notebook.uri, folder.uri)
			self.assertIsNone(href)

	def testNestedNotebookZimFile(self):
		# Test specific case where parent folder also is a notebook - as in bug #2189
		parentfolder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		parentfolder.file('notebook.zim').touch()
		folder = parentfolder.folder('MyNotebook')
		config = folder.file('notebook.zim')
		config.touch()

		for arg in (config.path, config.uri):
			cmd = self.get_cmd(arg)
			myinfo = NotebookInfo(config.path)
			notebookinfo, page = cmd.get_notebook_argument()
			self.assertEqual(notebookinfo, myinfo)
			notebook, href = cmd.build_notebook()
			self.assertEqual(notebook.uri, folder.uri)
			self.assertIsNone(href)



class TestGuiCommand(TestNotebookCommand):

	cmd_class = GuiCommand


class TestGuiStart(tests.TestCase):

	## TODO: test default notebook logic when no argument

	def setUp(self):
		file = ConfigManager.get_config_file('notebooks.list')
		file.remove()

	def runTest(self):
		from zim.gui.mainwindow import MainWindow

		## Without argument should prompt
		def testAddNotebookDialog(dialog):
			self.assertIn(dialog.__class__.__name__,
				('AddNotebookDialog', 'NotebookDialog')
			)

		cmd = GuiCommand('gui')
		with tests.DialogContext(testAddNotebookDialog):
			cmd.run() # Exits without running due to no notebook given in dialog

		### Try again with argument
		folder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		folder.touch()

		cmd = GuiCommand('gui')
		cmd.parse_options(folder.path)
		with tests.WindowContext(MainWindow):
			with tests.LoggingFilter('zim', 'Exception while loading plugin:'):
				window = cmd.run()
				self.addCleanup(window.destroy)

		self.assertEqual(window.__class__.__name__, 'MainWindow')
		self.assertEqual(window.notebook.uri, folder.uri)
		self.assertGreaterEqual(len(ConfigManager.preferences['General']['plugins']), 3)
		self.assertGreaterEqual(len(window.pageview.__zim_extension_objects__), 3)

		with tests.WindowContext(MainWindow):
			window2 = cmd.run()
		self.assertIs(window2, window)
			# Ensure repeated calling gives unique window


class TestGuiListCommand(tests.TestCase):

	# NotebookDialog has it's own't test cases, which also cover the
	# AddNotebookDialog and prompt_notebook, so here we can mock it and
	# just test it is called properly from the command class

	def setUp(self):
		from zim.notebook import NotebookInfo

		self.folder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		self.folder.touch()

		import zim.gui.notebookdialog
		orig = zim.gui.notebookdialog.prompt_notebook
		def restore():
			zim.gui.notebookdialog.prompt_notebook = orig
		self.addCleanup(restore)

		def mock():
			return NotebookInfo(self.folder.uri, name='Test')

		zim.gui.notebookdialog.prompt_notebook = mock

	def runTest(self):
		from zim.gui.mainwindow import MainWindow

		cmd = GuiCommand('gui')
		cmd.parse_options('--list')

		with tests.WindowContext(MainWindow):
			with tests.LoggingFilter('zim', 'Exception while loading plugin:'):
				window = cmd.run()
				self.addCleanup(window.destroy)


class TestManual(tests.TestCase):

	def runTest(self):
		from zim.gui.mainwindow import MainWindow

		cmd = ManualCommand('manual')
		with tests.WindowContext(MainWindow):
			with tests.LoggingFilter('zim', 'Exception while loading plugin:'):
				window = cmd.run()
				self.addCleanup(window.destroy)

		self.assertEqual(window.__class__.__name__, 'MainWindow')


@tests.slowTest
class TestServer(tests.TestCase):

	def runTest(self):
		from urllib.request import urlopen
		from urllib.error import URLError

		dir = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		dir.touch()
		cmd = ServerCommand('server')
		cmd.parse_options(dir.path)
		t = threading.Thread(target=cmd.run)
		t.start()

		for i in range(30):
			try:
				re = urlopen('http://localhost:8080')
				self.assertEqual(re.getcode(), 200)
				break
			except URLError:
				time.sleep(1) # give more time to startup server
		else:
			assert False, 'Failed to start server within 10 seconds'

		cmd.server.shutdown()
		t.join()


class TestServerGui(tests.TestCase):

	def runTest(self):
		cmd = ServerGuiCommand('server')
		window = cmd.run()
		self.addCleanup(window.destroy)
		self.assertEqual(window.__class__.__name__, 'ServerWindow')




## ExportCommand() is tested in tests/export.py


class TestIPC(tests.TestCase):

	def runTest(self):
		if os.name == 'posix':
			# There is an upper limit to lenght of a socket name for AF_UNIX
			# (107 characters ?). On OS X the path to TMPDIR already consumes
			# 50 chars, and we use "zim-$USER" -- so can still give errors for
			# user names > 20 chars. But basename should be limitted.
			from zim.main.ipc import SERVER_ADDRESS
			self.assertLessEqual(
				len(os.path.basename(SERVER_ADDRESS)), 25,
				"name too long: %s" % os.path.basename(SERVER_ADDRESS)
			)

		inbox = [None]
		def handler(*args):
			inbox[0] = args

		zim.main.ipc.start_listening(handler)
		self.addCleanup(zim.main.ipc._close_listener)

		self.assertRaises(AssertionError, zim.main.ipc.dispatch, 'test', '123')
			# raises due to sanity check same process

		zim.main.ipc.set_in_main_process(False) # overrule sanity check
		t = threading.Thread(target=zim.main.ipc.dispatch, args=('test', '123'))
		t.start()
		while t.is_alive():
			tests.gtk_process_events()
		tests.gtk_process_events()
		self.assertEqual(inbox[0], ('test', '123'))


### TODO test various ways of calling ZimApplication ####

# Start main
# Handle incoming
# Toplevel life cycle
# Spawn new
# Spawn standalone

class TestZimApplication(tests.TestCase):

	def testSimple(self):
		app = ZimApplication()

		class MockCmd(object):

			def __init__(self):
				self.opts = {}
				self.commandline = ['mockcommand']
				self.hasrun = False

			def run(self):
				self.hasrun = True

		cmd = MockCmd()
		self.assertFalse(cmd.hasrun)
		app._run_cmd(cmd, ())
		self.assertTrue(cmd.hasrun)


	def testGtk(self):
		app = ZimApplication()

		class MockCmd(GtkCommand):

			def __init__(self):
				self.opts = {'standalone': True}
				self.hasrun = False

			def _quit(self, *a):
				from gi.repository import GObject
				from gi.repository import Gtk
				Gtk.main_quit()
				return False # stop timer

			def run(self):
				from gi.repository import GObject
				from gi.repository import Gtk
				self.hasrun = True
				GObject.timeout_add(500, self._quit)
				return Gtk.Window()

		cmd = MockCmd()
		self.assertFalse(cmd.hasrun)
		app._run_cmd(cmd, ())
		self.assertTrue(cmd.hasrun)

	def testPluginAPI(self):
		from zim.signals import SignalEmitter
		class MockWindow(SignalEmitter):

			__signals__ = {
				'destroy': (None, None, ())
			}

			def __init__(self, notebook):
				self.notebook = notebook

			def destroy(self):
				pass

		class MockNotebook(object):

			def __init__(self, uri):
				self.uri = uri

		n1 = MockNotebook('foo')
		n2 = MockNotebook('bar')
		w1 = MockWindow(n1)
		w2 = MockWindow(n2)

		app = ZimApplication()
		app.add_window(w1)
		app.add_window(w2)

		self.assertEqual(set(app.toplevels), {w1, w2})
		self.assertEqual(app.notebooks, {n1, n2})
		self.assertEqual(app.get_mainwindow(n1, _class=MockWindow), w1)
		self.assertEqual(app.get_mainwindow(MockNotebook('foo'), _class=MockWindow), w1)


import os

class TestZimScript(tests.TestCase):

	def testEnvironINI(self):
		# Ensure restoring old environment
		orig_environ = os.environ.copy()
		def restore_environ():
			os.environ.clear()
			os.environ.update(orig_environ)
		self.addCleanup(restore_environ)

		# Setup tmp file
		folder = self.setUpFolder(mock=tests.MOCK_ALWAYS_REAL)
		folder.touch()

		# Import the script - which is not a module ...
		globals = {}
		scriptfile = 'zim.py'
		with open(scriptfile) as f:
			code = compile(f.read(), scriptfile, 'exec')
			exec(code, globals)
		init_environment = globals['init_environment']

		# Test missing file is silent, and no chance to data dir
		os.environ['XDG_DATA_DIRS'] = 'TEST'
		init_environment(folder.path)
		self.assertEqual(os.environ['XDG_DATA_DIRS'], 'TEST')

		# Test with existing data dir
		data_dir = folder.folder('share')
		data_dir.touch()
		init_environment(folder.path)
		self.assertEqual(os.environ['XDG_DATA_DIRS'], 'TEST' + os.pathsep + os.path.normpath(data_dir.path))

		# Setup file
		file = folder.file('environ.ini')
		file.write(
			'[Environment]\n'
			'MYHOME=../home\n'
			'MYPATH=${PATH}' + os.pathsep + './bin\n'
		)
		# write tmp file
		# 	- abs path
		#   - rel path
		#   - os.pathsep

		# Test with file in place
		init_environment(folder.path)
		self.assertEqual(os.environ['MYHOME'], os.path.normpath(folder.parent().folder('home').path))
		self.assertEqual(os.environ['MYPATH'], os.environ['PATH'] + os.pathsep + os.path.normpath(folder.folder('bin').path))
