"""
This module contains the TestResult class, which extends unittest.TextTestResult
to provide custom formatting and logging for test results in the Frappe framework.

Key components:
- TestResult: The main class for handling test results
- SLOW_TEST_THRESHOLD: A constant defining the threshold for slow tests

The TestResult class provides:
- Custom output formatting for different test outcomes (success, failure, error, skip)
- Timing information for each test, with highlighting for slow tests
- Logging of test results for debugging purposes
- Custom error reporting

Usage:
This TestResult class is typically used by the TestRunner to collect and display
test results during test execution in the Frappe framework.
"""

import io
import logging
import sys
import time
import unittest

import click

logger = logging.getLogger(__name__)


class TestResult(unittest.TextTestResult):
	def __init__(self, stream, descriptions, verbosity, failfast=False):
		super().__init__(stream, descriptions, verbosity)
		self.failfast = failfast
		self._old_stdout = []
		self._old_stderr = []

	def _setupStdout(self):
		pass

	def _restoreStdout(self):
		pass

	def startTestRun(self):
		if not sys.warnoptions:
			import warnings

			from frappe.deprecation_dumpster import FrappeDeprecationWarning

			warnings.simplefilter("ignore")
			warnings.filterwarnings("module", category=FrappeDeprecationWarning)

		# capture class & module setup & teardown in order to show it above the first test of the class
		if self.buffer:
			self._old_stderr.append(sys.stderr)
			self._old_stdout.append(sys.stdout)
			self._module_or_class_stdout_capture = io.StringIO()
			self._module_or_class_stderr_capture = io.StringIO()
			sys.stdout = self._module_or_class_stdout_capture
			sys.stderr = self._module_or_class_stderr_capture

	def stopTestRun(self):
		if self.buffer:
			sys.stdout = self._old_stdout.pop()
			sys.stderr = self._old_stderr.pop()

	def startTest(self, test):
		self.tb_locals = True
		self._started_at = time.monotonic()
		super(unittest.TextTestResult, self).startTest(test)
		test_class = unittest.util.strclass(test.__class__)
		if getattr(self, "current_test_class", None) != test_class:
			self.current_test_class = test_class
			self.stream.write(f"\n{test_class}\n")
			logger.info(f"{test_class}")

			if hasattr(self, "_module_or_class_stdout_capture"):
				for line in self._module_or_class_stdout_capture.getvalue().splitlines():
					self.stream.write(click.style(f"  ▹ {line}\n", fg="bright_black"))
					self.stream.flush()
				self._module_or_class_stdout_capture.seek(0)
				self._module_or_class_stdout_capture.truncate()

			if hasattr(self, "_module_or_class_stderr_capture"):
				for line in self._module_or_class_stderr_capture.getvalue().splitlines():
					# self.stream.write(f"  ▸ {line}\n")
					self.stream.write(click.style(f"  ▸ {line}\n", fg="bright_black"))
					self.stream.flush()
				self._module_or_class_stderr_capture.seek(0)
				self._module_or_class_stderr_capture.truncate()

			if new_doctypes := getattr(test.__class__, "_newly_created_test_records", None):
				records = [f"{name} ({qty})" for name, qty in reversed(new_doctypes)]
				hint = click.style(f"  Test Records created: {', '.join(records)}", fg="bright_black")
				self.stream.write(hint + "\n")
				logger.info(f"records created: {', '.join(records)}")
			self.stream.flush()

		if self.buffer:
			self._old_stderr.append(sys.stderr)
			self._old_stdout.append(sys.stdout)
			self._test_stdout_capture = io.StringIO()
			self._test_stderr_capture = io.StringIO()
			sys.stdout = self._test_stdout_capture
			sys.stderr = self._test_stderr_capture

	def stopTest(self, test):
		super().stopTest(test)
		if self.buffer:
			sys.stdout = self._old_stderr.pop()
			sys.stderr = self._old_stdout.pop()
			for line in self._test_stdout_capture.getvalue().splitlines():
				self.stream.write(f"       ▹  {line}\n")
				self.stream.flush()
			for line in self._test_stderr_capture.getvalue().splitlines():
				self.stream.write(f"       ▸  {line}\n")
				self.stream.flush()

	def getTestMethodName(self, test):
		return test._testMethodName if hasattr(test, "_testMethodName") else str(test)

	def addSuccess(self, test):
		super(unittest.TextTestResult, self).addSuccess(test)
		elapsed = time.monotonic() - self._started_at
		threshold_passed = elapsed >= SLOW_TEST_THRESHOLD
		long_elapsed = click.style(f" ({elapsed:.03}s)", fg="red") if threshold_passed else ""
		self._write_result(test, " ✔ ", "green", long_elapsed)
		logger.debug(f"{test!s:<200} {'[success]':>20} ⌛{elapsed}")

	def addError(self, test, err):
		super(unittest.TextTestResult, self).addError(test, err)
		self._write_result(test, " ✖ ", "red")
		logger.debug(f"{test!s:<200} {'[error]':>20}")

	def addFailure(self, test, err):
		super(unittest.TextTestResult, self).addFailure(test, err)
		self._write_result(test, " ✖ ", "red")
		logger.debug(f"{test!s:<200} {'[failure]':>20}")

	def addSkip(self, test, reason):
		super(unittest.TextTestResult, self).addSkip(test, reason)
		self._write_result(test, " = ", "white")
		logger.debug(f"{test!s:<200} {'[skipped]':>20}")

	def addExpectedFailure(self, test, err):
		super(unittest.TextTestResult, self).addExpectedFailure(test, err)
		self.stream.write("x")
		self._write_result(test, "✔ ", "green")
		logger.debug(f"{test!s:<200} {'[expected failure]':>20}")

	def addUnexpectedSuccess(self, test):
		super(unittest.TextTestResult, self).addUnexpectedSuccess(test)
		self.stream.write("u")
		self._write_result(test, "✖ ", "red")
		logger.debug(f"{test!s:<200} {'[unexpected success]':>20}")

	def printErrors(self):
		click.echo("\n")
		self.printErrorList(" ERROR ", self.errors, "red")
		self.printErrorList(" FAIL ", self.failures, "red")

	def printErrorList(self, flavour, errors, color):
		for test, err in errors:
			click.echo(self.separator1)
			click.echo(f"{click.style(flavour, bg=color)} {self.getDescription(test)}")
			click.echo(self.separator2)
			click.echo(err)

	def __str__(self):
		return f"Tests: {self.testsRun}, Failing: {len(self.failures)}, Errors: {len(self.errors)}"

	def _write_result(self, test, status, color, suffix=""):
		test_method = self.getTestMethodName(test)
		result = f"   {click.style(status, fg=color)} {test_method}"
		result += f" {suffix}" if suffix else ""
		result += "\n"
		self.stream.write(result)
		self.stream.flush()


SLOW_TEST_THRESHOLD = 2


class FrappeTestResult(unittest.TextTestResult):
	def __init__(self, stream, descriptions, verbosity):
		super().__init__(stream, descriptions, verbosity)

	def startTest(self, test):
		self.tb_locals = True
		self._started_at = time.monotonic()
		super(unittest.TextTestResult, self).startTest(test)
		test_class = unittest.util.strclass(test.__class__)
		if not hasattr(self, "current_test_class") or self.current_test_class != test_class:
			click.echo(f"\n{unittest.util.strclass(test.__class__)}")
			self.current_test_class = test_class

	def getTestMethodName(self, test):
		return test._testMethodName if hasattr(test, "_testMethodName") else str(test)

	def addSuccess(self, test):
		super(unittest.TextTestResult, self).addSuccess(test)
		elapsed = time.monotonic() - self._started_at
		threshold_passed = elapsed >= SLOW_TEST_THRESHOLD
		elapsed = click.style(f" ({elapsed:.03}s)", fg="red") if threshold_passed else ""
		click.echo(f"  {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}{elapsed}")

	def addError(self, test, err):
		super(unittest.TextTestResult, self).addError(test, err)
		click.echo(f"  {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")

	def addFailure(self, test, err):
		super(unittest.TextTestResult, self).addFailure(test, err)
		click.echo(f"  {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")

	def addSkip(self, test, reason):
		super(unittest.TextTestResult, self).addSkip(test, reason)
		click.echo(f"  {click.style(' = ', fg='white')} {self.getTestMethodName(test)}")

	def addExpectedFailure(self, test, err):
		super(unittest.TextTestResult, self).addExpectedFailure(test, err)
		click.echo(f"  {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")

	def addUnexpectedSuccess(self, test):
		super(unittest.TextTestResult, self).addUnexpectedSuccess(test)
		click.echo(f"  {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}")
