import json
import sys
import typing
from contextlib import contextmanager
from functools import cached_property
from random import choice
from threading import Thread
from time import time
from unittest.mock import patch
from urllib.parse import urlencode, urljoin

import requests
from filetype import guess_mime
from werkzeug.test import TestResponse

import jingrow
from jingrow.installer import update_site_config
from jingrow.tests.utils import JingrowTestCase, patch_hooks
from jingrow.utils import cint, get_site_url, get_test_client, get_url

try:
	_site = jingrow.local.site
except Exception:
	_site = None

authorization_token = None


@contextmanager
def suppress_stdout():
	"""Supress stdout for tests which expectedly make noise
	but that you don't need in tests"""
	sys.stdout = None
	try:
		yield
	finally:
		sys.stdout = sys.__stdout__


def make_request(
	target: str,
	args: tuple | None = None,
	kwargs: dict | None = None,
	site: str | None = None,
) -> TestResponse:
	t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs, site=site)
	t.start()
	t.join()
	return t._return


def patch_request_header(key, *args, **kwargs):
	if key == "Authorization":
		return f"token {authorization_token}"


class ThreadWithReturnValue(Thread):
	def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, site=None):
		if kwargs is None:
			kwargs = {}
		Thread.__init__(self, group, target, name, args, kwargs)
		self._return = None
		self.site = site or _site

	def run(self):
		if self._target is not None:
			with patch("jingrow.app.get_site_name", return_value=self.site):
				header_patch = patch("jingrow.get_request_header", new=patch_request_header)
				if authorization_token:
					header_patch.start()
				self._return = self._target(*self._args, **self._kwargs)
				if authorization_token:
					header_patch.stop()

	def join(self, *args):
		Thread.join(self, *args)
		return self._return


resource_key = {
	"": "resource",
	"v1": "resource",
	"v2": "document",
}


class JingrowAPITestCase(JingrowTestCase):
	version = ""  # Empty implies v1
	TEST_CLIENT = get_test_client()

	@property
	def site_url(self):
		return get_url()

	def resource(self, *parts):
		return self.get_path(resource_key[self.version], *parts)

	def method(self, *method):
		return self.get_path("method", *method)

	def pagetype_path(self, *method):
		return self.get_path("pagetype", *method)

	def get_path(self, *parts):
		return urljoin(self.site_url, "/".join(("api", self.version, *parts)))

	@cached_property
	def sid(self) -> str:
		from jingrow.auth import CookieManager, LoginManager
		from jingrow.utils import set_request

		set_request(path="/")
		jingrow.local.cookie_manager = CookieManager()
		jingrow.local.login_manager = LoginManager()
		jingrow.local.login_manager.login_as("Administrator")
		return jingrow.session.sid

	def get(self, path: str, params: dict | None = None, **kwargs) -> TestResponse:
		return make_request(target=self.TEST_CLIENT.get, args=(path,), kwargs={"json": params, **kwargs})

	def post(self, path, data, **kwargs) -> TestResponse:
		return make_request(target=self.TEST_CLIENT.post, args=(path,), kwargs={"json": data, **kwargs})

	def put(self, path, data, **kwargs) -> TestResponse:
		return make_request(target=self.TEST_CLIENT.put, args=(path,), kwargs={"json": data, **kwargs})

	def patch(self, path, data, **kwargs) -> TestResponse:
		return make_request(target=self.TEST_CLIENT.patch, args=(path,), kwargs={"json": data, **kwargs})

	def delete(self, path, **kwargs) -> TestResponse:
		return make_request(target=self.TEST_CLIENT.delete, args=(path,), kwargs=kwargs)


class TestResourceAPI(JingrowAPITestCase):
	PAGETYPE = "ToDo"
	GENERATED_DOCUMENTS: typing.ClassVar = []

	@classmethod
	def setUpClass(cls):
		super().setUpClass()
		for _ in range(20):
			pg = jingrow.get_pg({"pagetype": "ToDo", "description": jingrow.mock("paragraph")}).insert()
			cls.GENERATED_DOCUMENTS = []
			cls.GENERATED_DOCUMENTS.append(pg.name)
		jingrow.db.commit()

	@classmethod
	def tearDownClass(cls):
		for name in cls.GENERATED_DOCUMENTS:
			jingrow.delete_pg_if_exists(cls.PAGETYPE, name)
		jingrow.db.commit()

	def test_unauthorized_call(self):
		# test 1: fetch documents without auth
		response = requests.get(self.resource(self.PAGETYPE))
		self.assertEqual(response.status_code, 403)

	def test_get_list(self):
		# test 2: fetch documents without params
		response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid})
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(response.json, dict)
		self.assertIn("data", response.json)

	def test_get_list_limit(self):
		# test 3: fetch data with limit
		response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid, "limit": 2})
		self.assertEqual(response.status_code, 200)
		self.assertEqual(len(response.json["data"]), 2)

	def test_get_list_dict(self):
		# test 4: fetch response as (not) dict
		response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid, "as_dict": True})
		json = jingrow._dict(response.json)
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(json.data, list)
		self.assertIsInstance(json.data[0], dict)

		response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid, "as_dict": False})
		json = jingrow._dict(response.json)
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(json.data, list)
		self.assertIsInstance(json.data[0], list)

	def test_get_list_debug(self):
		# test 5: fetch response with debug
		with suppress_stdout():
			response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid, "debug": True})
		self.assertEqual(response.status_code, 200)
		self.assertIn("_debug_messages", response.json)
		self.assertIsInstance(response.json["_debug_messages"], str)
		self.assertIsInstance(json.loads(response.json["_debug_messages"]), list)

	def test_get_list_fields(self):
		# test 6: fetch response with fields
		response = self.get(self.resource(self.PAGETYPE), {"sid": self.sid, "fields": '["description"]'})
		self.assertEqual(response.status_code, 200)
		json = jingrow._dict(response.json)
		self.assertIn("description", json.data[0])

	def test_create_document(self):
		data = {"description": jingrow.mock("paragraph"), "sid": self.sid}
		response = self.post(self.resource(self.PAGETYPE), data)
		self.assertEqual(response.status_code, 200)
		pagename = response.json["data"]["name"]
		self.assertIsInstance(pagename, str)
		self.GENERATED_DOCUMENTS.append(pagename)

	def test_update_document(self):
		generated_desc = jingrow.mock("paragraph")
		data = {"description": generated_desc, "sid": self.sid}
		random_pg = choice(self.GENERATED_DOCUMENTS)

		response = self.put(self.resource(self.PAGETYPE, random_pg), data=data)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.json["data"]["description"], generated_desc)

		response = self.get(self.resource(self.PAGETYPE, random_pg))
		self.assertEqual(response.json["data"]["description"], generated_desc)

	def test_delete_document(self):
		pg_to_delete = choice(self.GENERATED_DOCUMENTS)
		response = self.delete(self.resource(self.PAGETYPE, pg_to_delete))
		self.assertEqual(response.status_code, 202)
		self.assertDictEqual(response.json, {"data": "ok"})

		response = self.get(self.resource(self.PAGETYPE, pg_to_delete))
		self.assertEqual(response.status_code, 404)
		self.GENERATED_DOCUMENTS.remove(pg_to_delete)

	def test_run_pg_method(self):
		# test 10: Run whitelisted method on pg via /api/data
		# status_code is 403 if no other tests are run before this - it's not logged in
		self.post(self.resource("Website Theme", "Standard"), {"run_method": "get_apps"})
		response = self.get(self.resource("Website Theme", "Standard"), {"run_method": "get_apps"})

		self.assertIn(response.status_code, (403, 200))

		if response.status_code == 403:
			self.assertTrue(set(response.json.keys()) == {"exc_type", "exception", "exc", "_server_messages"})
			self.assertEqual(response.json.get("exc_type"), "PermissionError")
			self.assertEqual(
				response.json.get("exception"), "jingrow.exceptions.PermissionError: Not permitted"
			)
			self.assertIsInstance(response.json.get("exc"), str)

		elif response.status_code == 200:
			data = response.json.get("data")
			self.assertIsInstance(data, list)
			self.assertIsInstance(data[0], dict)


class TestMethodAPI(JingrowAPITestCase):
	def test_ping(self):
		# test 2: test for /api/action/ping
		response = self.get(self.method("ping"))
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(response.json, dict)
		self.assertEqual(response.json["message"], "pong")

	def test_get_user_info(self):
		# test 3: test for /api/action/jingrow.realtime.get_user_info
		response = self.get(self.method("jingrow.realtime.get_user_info"))
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(response.json, dict)
		self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))

	def test_auth_cycle(self):
		# test 4: Pass authorization token in request
		global authorization_token
		generate_admin_keys()
		user = jingrow.get_pg("User", "Administrator")
		api_key, api_secret = user.api_key, user.get_password("api_secret")
		authorization_token = f"{api_key}:{api_secret}"
		response = self.get(self.method("jingrow.auth.get_logged_user"))

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.json["message"], "Administrator")

		authorization_token = f"{api_key}:INCORRECT"
		response = self.get(self.method("jingrow.auth.get_logged_user"))
		self.assertEqual(response.status_code, 401)

		authorization_token = "NonExistentKey:INCORRECT"
		response = self.get(self.method("jingrow.auth.get_logged_user"))
		self.assertEqual(response.status_code, 401)

		authorization_token = None

	def test_404s(self):
		response = self.get(self.get_path("rest"), {"sid": self.sid})
		self.assertEqual(response.status_code, 404)
		response = self.get(self.resource("User", "NonExistent@s.com"), {"sid": self.sid})
		self.assertEqual(response.status_code, 404)

	def test_logs(self):
		method = "jingrow.tests.test_api.test"

		def get_message(resp, msg_type):
			return jingrow.parse_json(jingrow.parse_json(jingrow.parse_json(resp.json)[msg_type])[0])

		expected_message = "Failed"
		response = self.get(self.method(method), {"sid": self.sid, "message": expected_message})
		self.assertEqual(get_message(response, "_server_messages").message, expected_message)

		# Cause handled failured
		with suppress_stdout():
			response = self.get(
				self.method(method), {"sid": self.sid, "message": expected_message, "fail": True}
			)
		self.assertEqual(get_message(response, "_server_messages").message, expected_message)
		self.assertEqual(response.json["exc_type"], "ValidationError")
		self.assertIn("Traceback", response.json["exc"])

		# Cause handled failured
		with suppress_stdout():
			response = self.get(
				self.method(method),
				{"sid": self.sid, "message": expected_message, "fail": True, "handled": False},
			)
		self.assertNotIn("_server_messages", response.json)
		self.assertIn("ZeroDivisionError", response.json["exception"])  # WHY?
		self.assertIn("Traceback", response.json["exc"])

	def test_array_response(self):
		method = "jingrow.tests.test_api.test_array"

		test_data = list(range(5))
		response = self.post(self.method(method), test_data)

		self.assertEqual(response.json["message"], test_data)


class TestReadOnlyMode(JingrowAPITestCase):
	"""During migration if read only mode can be enabled.
	Test if reads work well and writes are blocked"""

	@classmethod
	def setUpClass(cls):
		super().setUpClass()
		update_site_config("allow_reads_during_maintenance", 1)
		cls.addClassCleanup(update_site_config, "maintenance_mode", 0)
		# XXX: this has potential to crumble rest of the test suite.
		update_site_config("maintenance_mode", 1)

	def test_reads(self):
		response = self.get(self.resource("ToDo"), {"sid": self.sid})
		self.assertEqual(response.status_code, 200)
		self.assertIsInstance(response.json, dict)
		self.assertIsInstance(response.json["data"], list)

	def test_blocked_writes(self):
		with suppress_stdout():
			response = self.post(
				self.resource("ToDo"), {"description": jingrow.mock("paragraph"), "sid": self.sid}
			)
		self.assertEqual(response.status_code, 503)
		self.assertEqual(response.json["exc_type"], "InReadOnlyMode")


class TestWSGIApp(JingrowAPITestCase):
	def test_request_hooks(self):
		self.addCleanup(lambda: _test_REQ_HOOK.clear())

		with patch_hooks(
			{
				"before_request": ["jingrow.tests.test_api.before_request"],
				"after_request": ["jingrow.tests.test_api.after_request"],
			}
		):
			self.assertIsNone(_test_REQ_HOOK.get("before_request"))
			self.assertIsNone(_test_REQ_HOOK.get("after_request"))
			res = self.get("/api/action/ping")
			self.assertEqual(res.json, {"message": "pong"})
			self.assertLess(_test_REQ_HOOK.get("before_request"), _test_REQ_HOOK.get("after_request"))


_test_REQ_HOOK = {}


def before_request(*args, **kwargs):
	_test_REQ_HOOK["before_request"] = time()


def after_request(*args, **kwargs):
	_test_REQ_HOOK["after_request"] = time()


class TestResponse(JingrowAPITestCase):
	def test_generate_pdf(self):
		response = self.get(
			"/api/action/jingrow.utils.print_format.download_pdf",
			{"sid": self.sid, "pagetype": "User", "name": "Guest"},
		)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.headers["content-type"], "application/pdf")
		self.assertGreater(cint(response.headers["content-length"]), 0)

		self.assertEqual(guess_mime(response.data), "application/pdf")

	def test_binary_and_csv_response(self):
		def download_template(file_type):
			filters = json.dumps({})
			fields = json.dumps({"User": ["name"]})
			return self.post(
				"/api/action/jingrow.core.pagetype.data_import.data_import.download_template",
				{
					"sid": self.sid,
					"pagetype": "User",
					"export_fields": fields,
					"export_filters": filters,
					"file_type": file_type,
				},
			)

		response = download_template("Excel")
		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.headers["content-type"], "application/octet-stream")
		self.assertGreater(cint(response.headers["content-length"]), 0)
		self.assertEqual(
			guess_mime(response.data), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
		)

		response = download_template("CSV")
		self.assertEqual(response.status_code, 200)
		self.assertIn("text/csv", response.headers["content-type"])
		self.assertGreater(cint(response.headers["content-length"]), 0)

		from jingrow.desk.utils import provide_binary_file
		from jingrow.utils.response import build_response

		filename = "دفتر الأستاذ العام"
		encoded_filename = filename.encode("utf-8").decode("unicode-escape", "ignore") + ".xlsx"
		provide_binary_file(filename, "xlsx", "content")

		response = build_response("binary")
		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.headers["content-type"], "application/octet-stream")
		self.assertGreater(cint(response.headers["content-length"]), 0)
		self.assertEqual(response.headers["content-disposition"], f'filename="{encoded_filename}"')

	def test_download_private_file_with_unique_url(self):
		test_content = jingrow.generate_hash()
		file = jingrow.get_pg(
			{
				"pagetype": "File",
				"file_name": test_content,
				"content": test_content,
				"is_private": 1,
			}
		)
		file.insert()

		self.assertEqual(self.get(file.unique_url, {"sid": self.sid}).text, test_content)
		self.assertEqual(self.get(file.file_url, {"sid": self.sid}).text, test_content)

	def test_login_redirects(self):
		expected_redirects = {
			"/app/user": "/app/user",
			"/app/user?enabled=1": "/app/user?enabled=1",
			"http://example.com": "/app",  # No external redirect
			"https://google.com": "/app",
			"http://localhost:8000": "/app",
			"http://localhost/app": "http://localhost/app",
		}
		for redirect, expected_redirect in expected_redirects.items():
			response = self.get(f"/login?{urlencode({'redirect-to':redirect})}", {"sid": self.sid})
			self.assertEqual(response.location, expected_redirect)


def generate_admin_keys():
	from jingrow.core.pagetype.user.user import generate_keys

	generate_keys("Administrator")
	jingrow.db.commit()


@jingrow.whitelist()
def test(*, fail=False, handled=True, message="Failed"):
	if fail:
		if handled:
			jingrow.throw(message)
		else:
			1 / 0
	else:
		jingrow.msgprint(message)


@jingrow.whitelist(allow_guest=True)
def test_array(data):
	return data
