import unittest
import os
import platform
import warnings
import pygame


DARWIN = "Darwin" in platform.platform()


class MouseTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # The display needs to be initialized for mouse functions.
        pygame.display.init()

    @classmethod
    def tearDownClass(cls):
        pygame.display.quit()


class MouseModuleInteractiveTest(MouseTests):
    __tags__ = ["interactive"]

    def test_set_pos(self):
        """Ensures set_pos works correctly.
        Requires tester to move the mouse to be on the window.
        """
        pygame.display.set_mode((500, 500))
        pygame.event.get()  # Pump event queue to make window get focus on macos.

        if not pygame.mouse.get_focused():
            # The window needs to be focused for the mouse.set_pos to work on macos.
            return
        clock = pygame.time.Clock()

        expected_pos = ((10, 0), (0, 0), (499, 0), (499, 499), (341, 143), (94, 49))

        for x, y in expected_pos:
            pygame.mouse.set_pos(x, y)
            pygame.event.get()
            found_pos = pygame.mouse.get_pos()

            clock.tick()
            time_passed = 0.0
            ready_to_test = False

            while not ready_to_test and time_passed <= 1000.0:  # Avoid endless loop
                time_passed += clock.tick()
                for event in pygame.event.get():
                    if event.type == pygame.MOUSEMOTION:
                        ready_to_test = True

            self.assertEqual(found_pos, (x, y))


class MouseModuleTest(MouseTests):
    @unittest.skipIf(
        os.environ.get("SDL_VIDEODRIVER", "") == "dummy",
        "Cursors not supported on headless test machines",
    )
    def test_get_cursor(self):
        """Ensures get_cursor works correctly."""

        # error should be raised when the display is uninitialized
        with self.assertRaises(pygame.error):
            pygame.display.quit()
            pygame.mouse.get_cursor()

        pygame.display.init()

        size = (8, 8)
        hotspot = (0, 0)
        xormask = (0, 96, 120, 126, 112, 96, 0, 0)
        andmask = (224, 240, 254, 255, 254, 240, 96, 0)

        expected_length = 4
        expected_cursor = pygame.cursors.Cursor(size, hotspot, xormask, andmask)
        pygame.mouse.set_cursor(expected_cursor)

        try:
            cursor = pygame.mouse.get_cursor()

            self.assertIsInstance(cursor, pygame.cursors.Cursor)
            self.assertEqual(len(cursor), expected_length)

            for info in cursor:
                self.assertIsInstance(info, tuple)

            pygame.mouse.set_cursor(size, hotspot, xormask, andmask)
            self.assertEqual(pygame.mouse.get_cursor(), expected_cursor)

        # SDLError should be raised when the mouse cursor is NULL
        except pygame.error:
            with self.assertRaises(pygame.error):
                pygame.mouse.get_cursor()

    @unittest.skipIf(
        os.environ.get("SDL_VIDEODRIVER", "") == "dummy",
        "mouse.set_system_cursor only available in SDL2",
    )
    def test_set_system_cursor(self):
        """Ensures set_system_cursor works correctly."""

        with warnings.catch_warnings(record=True) as w:
            """From Pygame 2.0.1, set_system_cursor() should raise a deprecation warning"""
            # Cause all warnings to always be triggered.
            warnings.simplefilter("always")

            # Error should be raised when the display is uninitialized
            with self.assertRaises(pygame.error):
                pygame.display.quit()
                pygame.mouse.set_system_cursor(pygame.SYSTEM_CURSOR_HAND)

            pygame.display.init()

            # TypeError raised when PyArg_ParseTuple fails to parse parameters
            with self.assertRaises(TypeError):
                pygame.mouse.set_system_cursor("b")
            with self.assertRaises(TypeError):
                pygame.mouse.set_system_cursor(None)
            with self.assertRaises(TypeError):
                pygame.mouse.set_system_cursor((8, 8), (0, 0))

            # Right type, invalid value
            with self.assertRaises(pygame.error):
                pygame.mouse.set_system_cursor(2000)

            # Working as intended
            self.assertEqual(
                pygame.mouse.set_system_cursor(pygame.SYSTEM_CURSOR_ARROW), None
            )

            # Making sure the warnings are working properly
            self.assertEqual(len(w), 6)
            self.assertTrue(
                all(issubclass(warn.category, DeprecationWarning) for warn in w)
            )

    @unittest.skipIf(
        os.environ.get("SDL_VIDEODRIVER", "") == "dummy",
        "Cursors not supported on headless test machines",
    )
    def test_set_cursor(self):
        """Ensures set_cursor works correctly."""

        # Bitmap cursor information
        size = (8, 8)
        hotspot = (0, 0)
        xormask = (0, 126, 64, 64, 32, 16, 0, 0)
        andmask = (254, 255, 254, 112, 56, 28, 12, 0)
        bitmap_cursor = pygame.cursors.Cursor(size, hotspot, xormask, andmask)

        # System cursor information
        constant = pygame.SYSTEM_CURSOR_ARROW
        system_cursor = pygame.cursors.Cursor(constant)

        # Color cursor information (also uses hotspot variable from Bitmap cursor info)
        surface = pygame.Surface((10, 10))
        color_cursor = pygame.cursors.Cursor(hotspot, surface)

        pygame.display.quit()

        # Bitmap: Error should be raised when the display is uninitialized
        with self.assertRaises(pygame.error):
            pygame.mouse.set_cursor(bitmap_cursor)

        # System: Error should be raised when the display is uninitialized
        with self.assertRaises(pygame.error):
            pygame.mouse.set_cursor(system_cursor)

        # Color: Error should be raised when the display is uninitialized
        with self.assertRaises(pygame.error):
            pygame.mouse.set_cursor(color_cursor)

        pygame.display.init()

        # Bitmap: TypeError raised when PyArg_ParseTuple fails to parse parameters
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(("w", "h"), hotspot, xormask, andmask)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, ("0", "0"), xormask, andmask)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, ("x", "y", "z"), xormask, andmask)

        # Bitmap: TypeError raised when either mask is not a sequence
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, hotspot, 12345678, andmask)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, hotspot, xormask, 12345678)

        # Bitmap: TypeError raised when element of mask is not an integer
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, hotspot, "00000000", andmask)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(size, hotspot, xormask, (2, [0], 4, 0, 0, 8, 0, 1))

        # Bitmap: ValueError raised when width not divisible by 8
        with self.assertRaises(ValueError):
            pygame.mouse.set_cursor((3, 8), hotspot, xormask, andmask)

        # Bitmap: ValueError raised when length of either mask != width * height / 8
        with self.assertRaises(ValueError):
            pygame.mouse.set_cursor((16, 2), hotspot, (128, 64, 32), andmask)
        with self.assertRaises(ValueError):
            pygame.mouse.set_cursor((16, 2), hotspot, xormask, (192, 96, 48, 0, 1))

        # Bitmap: Working as intended
        self.assertEqual(
            pygame.mouse.set_cursor((16, 1), hotspot, (8, 0), (0, 192)), None
        )
        pygame.mouse.set_cursor(size, hotspot, xormask, andmask)
        self.assertEqual(pygame.mouse.get_cursor(), bitmap_cursor)

        # Bitmap: Working as intended + lists + masks with no references
        pygame.mouse.set_cursor(size, hotspot, list(xormask), list(andmask))
        self.assertEqual(pygame.mouse.get_cursor(), bitmap_cursor)

        # System: TypeError raised when constant is invalid
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(-50021232)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor("yellow")

        # System: Working as intended
        self.assertEqual(pygame.mouse.set_cursor(constant), None)
        pygame.mouse.set_cursor(constant)
        self.assertEqual(pygame.mouse.get_cursor(), system_cursor)
        pygame.mouse.set_cursor(system_cursor)
        self.assertEqual(pygame.mouse.get_cursor(), system_cursor)

        # Color: TypeError raised with invalid parameters
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(("x", "y"), surface)
        with self.assertRaises(TypeError):
            pygame.mouse.set_cursor(hotspot, "not_a_surface")

        # Color: Working as intended
        self.assertEqual(pygame.mouse.set_cursor(hotspot, surface), None)
        pygame.mouse.set_cursor(hotspot, surface)
        self.assertEqual(pygame.mouse.get_cursor(), color_cursor)
        pygame.mouse.set_cursor(color_cursor)
        self.assertEqual(pygame.mouse.get_cursor(), color_cursor)

        # Color: Working as intended + Surface with no references is returned okay
        pygame.mouse.set_cursor((0, 0), pygame.Surface((20, 20)))
        cursor = pygame.mouse.get_cursor()
        self.assertEqual(cursor.type, "color")
        self.assertEqual(cursor.data[0], (0, 0))
        self.assertEqual(cursor.data[1].get_size(), (20, 20))

    def test_get_focused(self):
        """Ensures get_focused returns the correct type."""
        focused = pygame.mouse.get_focused()

        self.assertIsInstance(focused, int)

    def test_get_pressed(self):
        """Ensures get_pressed returns the correct types."""
        expected_length = 3
        buttons_pressed = pygame.mouse.get_pressed()
        self.assertIsInstance(buttons_pressed, tuple)
        self.assertEqual(len(buttons_pressed), expected_length)
        for value in buttons_pressed:
            self.assertIsInstance(value, bool)

        expected_length = 5
        buttons_pressed = pygame.mouse.get_pressed(num_buttons=5)
        self.assertIsInstance(buttons_pressed, tuple)
        self.assertEqual(len(buttons_pressed), expected_length)
        for value in buttons_pressed:
            self.assertIsInstance(value, bool)

        expected_length = 3
        buttons_pressed = pygame.mouse.get_pressed(3)
        self.assertIsInstance(buttons_pressed, tuple)
        self.assertEqual(len(buttons_pressed), expected_length)
        for value in buttons_pressed:
            self.assertIsInstance(value, bool)

        expected_length = 5
        buttons_pressed = pygame.mouse.get_pressed(5)
        self.assertIsInstance(buttons_pressed, tuple)
        self.assertEqual(len(buttons_pressed), expected_length)
        for value in buttons_pressed:
            self.assertIsInstance(value, bool)

        with self.assertRaises(ValueError):
            pygame.mouse.get_pressed(4)

    def test_get_pos(self):
        """Ensures get_pos returns the correct types."""
        expected_length = 2

        pos = pygame.mouse.get_pos()

        self.assertIsInstance(pos, tuple)
        self.assertEqual(len(pos), expected_length)
        for value in pos:
            self.assertIsInstance(value, int)

    def test_set_pos__invalid_pos(self):
        """Ensures set_pos handles invalid positions correctly."""
        for invalid_pos in ((1,), [1, 2, 3], 1, "1", (1, "1"), []):
            with self.assertRaises(TypeError):
                pygame.mouse.set_pos(invalid_pos)

    def test_get_rel(self):
        """Ensures get_rel returns the correct types."""
        expected_length = 2

        rel = pygame.mouse.get_rel()

        self.assertIsInstance(rel, tuple)
        self.assertEqual(len(rel), expected_length)
        for value in rel:
            self.assertIsInstance(value, int)

    def test_get_visible(self):
        """Ensures get_visible works correctly."""
        for expected_value in (False, True):
            pygame.mouse.set_visible(expected_value)

            visible = pygame.mouse.get_visible()

            self.assertEqual(visible, expected_value)

    def test_set_visible(self):
        """Ensures set_visible returns the correct values."""
        # Set to a known state.
        pygame.mouse.set_visible(True)

        for expected_visible in (False, True):
            prev_visible = pygame.mouse.set_visible(expected_visible)

            self.assertEqual(prev_visible, not expected_visible)

    def test_set_visible__invalid_value(self):
        """Ensures set_visible handles invalid positions correctly."""
        for invalid_value in ((1,), [1, 2, 3], 1.1, "1", (1, "1"), []):
            with self.assertRaises(TypeError):
                prev_visible = pygame.mouse.set_visible(invalid_value)


################################################################################

if __name__ == "__main__":
    unittest.main()
