#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# This file is part of input-remapper.
#
# input-remapper is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with input-remapper.  If not, see <https://www.gnu.org/licenses/>.


import unittest

from evdev.ecodes import (
    EV_KEY,
    EV_ABS,
    EV_REL,
    BTN_C,
    BTN_B,
    BTN_A,
    BTN_LEFT,
    BTN_MIDDLE,
    BTN_RIGHT,
    BTN_SIDE,
    BTN_EXTRA,
    BTN_FORWARD,
    BTN_BACK,
    BTN_TASK,
    REL_X,
    REL_Y,
    REL_WHEEL,
    REL_HWHEEL,
    ABS_RY,
    ABS_X,
    ABS_HAT0Y,
    ABS_HAT0X,
    KEY_A,
    KEY_LEFTSHIFT,
    KEY_RIGHTALT,
    KEY_LEFTCTRL,
)

from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.test_setup import test_setup


@test_setup
class TestInputConfig(unittest.TestCase):
    def test_input_config(self):
        test_cases = [
            # basic test, nothing fancy here
            {
                "input": {
                    "type": EV_KEY,
                    "code": KEY_A,
                    "origin_hash": "foo",
                },
                "properties": {
                    "type": EV_KEY,
                    "code": KEY_A,
                    "origin_hash": "foo",
                    "input_match_hash": (EV_KEY, KEY_A, "foo"),
                    "defines_analog_input": False,
                    "type_and_code": (EV_KEY, KEY_A),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "a",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_KEY, KEY_A, "foo", None)),
                    },
                ],
            },
            # removes analog_threshold
            {
                "input": {
                    "type": EV_KEY,
                    "code": KEY_A,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                },
                "properties": {
                    "type": EV_KEY,
                    "code": KEY_A,
                    "origin_hash": "foo",
                    "analog_threshold": None,
                    "input_match_hash": (EV_KEY, KEY_A, "foo"),
                    "defines_analog_input": False,
                    "type_and_code": (EV_KEY, KEY_A),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "a",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_KEY, KEY_A, "foo", None)),
                    },
                ],
            },
            # abs to btn
            {
                "input": {
                    "type": EV_ABS,
                    "code": ABS_X,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                },
                "properties": {
                    "type": EV_ABS,
                    "code": ABS_X,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                    "input_match_hash": (EV_ABS, ABS_X, "foo"),
                    "defines_analog_input": False,
                    "type_and_code": (EV_ABS, ABS_X),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "Joystick-X Right 10%",
                    },
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {"exclude_threshold": True},
                        "return": "Joystick-X Right",
                    },
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {
                            "exclude_threshold": True,
                            "exclude_direction": True,
                        },
                        "return": "Joystick-X",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_ABS, ABS_X, "foo", 10)),
                    },
                ],
            },
            # abs to btn with d-pad
            {
                "input": {
                    "type": EV_ABS,
                    "code": ABS_HAT0Y,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                },
                "properties": {
                    "type": EV_ABS,
                    "code": ABS_HAT0Y,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                    "input_match_hash": (EV_ABS, ABS_HAT0Y, "foo"),
                    "defines_analog_input": False,
                    "type_and_code": (EV_ABS, ABS_HAT0Y),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "DPad-Y Down 10%",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_ABS, ABS_HAT0Y, "foo", 10)),
                    },
                ],
            },
            # rel to btn
            {
                "input": {
                    "type": EV_REL,
                    "code": REL_Y,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                },
                "properties": {
                    "type": EV_REL,
                    "code": REL_Y,
                    "origin_hash": "foo",
                    "analog_threshold": 10,
                    "input_match_hash": (EV_REL, REL_Y, "foo"),
                    "defines_analog_input": False,
                    "type_and_code": (EV_REL, REL_Y),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "Y Down 10",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_REL, REL_Y, "foo", 10)),
                    },
                ],
            },
            # abs as axis
            {
                "input": {
                    "type": EV_ABS,
                    "code": ABS_X,
                    "origin_hash": "foo",
                    "analog_threshold": 0,
                },
                "properties": {
                    "type": EV_ABS,
                    "code": ABS_X,
                    "origin_hash": "foo",
                    "analog_threshold": None,
                    "input_match_hash": (EV_ABS, ABS_X, "foo"),
                    "defines_analog_input": True,
                    "type_and_code": (EV_ABS, ABS_X),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "Joystick-X",
                    },
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {
                            "exclude_threshold": True,
                            "exclude_direction": True,
                        },
                        "return": "Joystick-X",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_ABS, ABS_X, "foo", None)),
                    },
                ],
            },
            # rel as axis
            {
                "input": {
                    "type": EV_REL,
                    "code": REL_WHEEL,
                    "origin_hash": "foo",
                },
                "properties": {
                    "type": EV_REL,
                    "code": REL_WHEEL,
                    "origin_hash": "foo",
                    "analog_threshold": None,
                    "input_match_hash": (EV_REL, REL_WHEEL, "foo"),
                    "defines_analog_input": True,
                    "type_and_code": (EV_REL, REL_WHEEL),
                },
                "methods": [
                    {
                        "name": "description",
                        "args": (),
                        "kwargs": {},
                        "return": "Wheel",
                    },
                    {
                        "name": "__hash__",
                        "args": (),
                        "kwargs": {},
                        "return": hash((EV_REL, REL_WHEEL, "foo", None)),
                    },
                ],
            },
        ]
        for test_case in test_cases:
            input_config = InputConfig(**test_case["input"])
            for property_, value in test_case["properties"].items():
                self.assertEqual(
                    value,
                    getattr(input_config, property_),
                    f"property mismatch for input: {test_case['input']} "
                    f"property: {property_} expected value: {value}",
                )
            for method in test_case["methods"]:
                self.assertEqual(
                    method["return"],
                    getattr(input_config, method["name"])(
                        *method["args"], **method["kwargs"]
                    ),
                    f"wrong method return for input: {test_case['input']} "
                    f"method: {method}",
                )

    def test_is_immutable(self):
        input_config = InputConfig(type=1, code=2)
        with self.assertRaises(TypeError):
            input_config.origin_hash = "foo"


@test_setup
class TestInputCombination(unittest.TestCase):
    def test_eq(self):
        a = InputCombination(
            [
                InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
                InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
            ]
        )
        b = InputCombination(
            [
                InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
                InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
            ]
        )
        self.assertEqual(a, b)

    def test_not_eq(self):
        a = InputCombination(
            [
                InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="2345"),
                InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="bcde"),
            ]
        )
        b = InputCombination(
            [
                InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
                InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
            ]
        )
        self.assertNotEqual(a, b)

    def test_can_be_used_as_dict_key(self):
        dict_ = {
            InputCombination(
                [
                    InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
                    InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
                ]
            ): "foo"
        }
        key = InputCombination(
            [
                InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
                InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
            ]
        )
        self.assertEqual(dict_.get(key), "foo")

    def test_get_permutations(self):
        key_1 = InputCombination(InputCombination.from_tuples((1, 3, 1)))
        self.assertEqual(len(key_1.get_permutations()), 1)
        self.assertEqual(key_1.get_permutations()[0], key_1)

        key_2 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1)))
        self.assertEqual(len(key_2.get_permutations()), 1)
        self.assertEqual(key_2.get_permutations()[0], key_2)

        key_3 = InputCombination(
            InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1))
        )
        self.assertEqual(len(key_3.get_permutations()), 2)
        self.assertEqual(
            key_3.get_permutations()[0],
            InputCombination(
                InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1))
            ),
        )
        self.assertEqual(
            key_3.get_permutations()[1],
            InputCombination(
                InputCombination.from_tuples((1, 5, 1), (1, 3, 1), (1, 7, 1))
            ),
        )

    def test_is_problematic(self):
        key_1 = InputCombination(
            InputCombination.from_tuples((1, KEY_LEFTSHIFT, 1), (1, 5, 1))
        )
        self.assertTrue(key_1.is_problematic())

        key_2 = InputCombination(
            InputCombination.from_tuples((1, KEY_RIGHTALT, 1), (1, 5, 1))
        )
        self.assertTrue(key_2.is_problematic())

        key_3 = InputCombination(
            InputCombination.from_tuples((1, 3, 1), (1, KEY_LEFTCTRL, 1))
        )
        self.assertTrue(key_3.is_problematic())

        key_4 = InputCombination(InputCombination.from_tuples((1, 3, 1)))
        self.assertFalse(key_4.is_problematic())

        key_5 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1)))
        self.assertFalse(key_5.is_problematic())

    def test_init(self):
        self.assertRaises(TypeError, lambda: InputCombination(1))
        self.assertRaises(TypeError, lambda: InputCombination(None))
        self.assertRaises(TypeError, lambda: InputCombination([1]))
        self.assertRaises(TypeError, lambda: InputCombination((1,)))
        self.assertRaises(TypeError, lambda: InputCombination((1, 2)))
        self.assertRaises(TypeError, lambda: InputCombination("1"))
        self.assertRaises(TypeError, lambda: InputCombination("(1,2,3)"))
        self.assertRaises(
            TypeError,
            lambda: InputCombination(((1, 2, 3), (1, 2, 3), None)),
        )

        # those don't raise errors
        InputCombination(({"type": 1, "code": 2}, {"type": 1, "code": 1}))
        InputCombination(({"type": 1, "code": 2},))
        InputCombination(({"type": "1", "code": "2"},))
        InputCombination([InputConfig(type=1, code=2, analog_threshold=3)])
        InputCombination(
            (
                {"type": 1, "code": 2},
                {"type": "1", "code": "2"},
                InputConfig(type=1, code=2),
            )
        )

    def test_to_config(self):
        c1 = InputCombination([InputConfig(type=1, code=2, analog_threshold=3)])
        c2 = InputCombination(
            (
                InputConfig(type=1, code=2, analog_threshold=3),
                InputConfig(type=4, code=5, analog_threshold=6),
            )
        )
        # analog_threshold is removed for key events
        self.assertEqual(c1.to_config(), ({"type": 1, "code": 2},))
        self.assertEqual(
            c2.to_config(),
            ({"type": 1, "code": 2}, {"type": 4, "code": 5, "analog_threshold": 6}),
        )

    def test_beautify(self):
        # not an integration test, but I have all the selection_label tests here already
        self.assert_beautify_single(EV_KEY, KEY_A, 1, "a")
        self.assert_beautify_single(EV_KEY, KEY_A, 1, "a")
        self.assert_beautify_single(EV_ABS, ABS_HAT0Y, -1, "DPad-Y Up")
        self.assert_beautify_single(EV_KEY, BTN_A, 1, "Button A")
        self.assert_beautify_single(EV_KEY, 1234, 1, "unknown (1, 1234)")
        self.assert_beautify_single(EV_ABS, ABS_HAT0X, -1, "DPad-X Left")
        self.assert_beautify_single(EV_ABS, ABS_HAT0Y, -1, "DPad-Y Up")
        self.assert_beautify_single(EV_KEY, BTN_A, 1, "Button A")
        self.assert_beautify_single(EV_ABS, ABS_X, 1, "Joystick-X Right")
        self.assert_beautify_single(EV_ABS, ABS_RY, 1, "Joystick-RY Down")
        self.assert_beautify_single(EV_REL, REL_HWHEEL, 1, "Wheel Right")
        self.assert_beautify_single(EV_REL, REL_WHEEL, -1, "Wheel Down")

        # region "mouse buttons"
        self.assert_beautify_single(EV_KEY, BTN_LEFT, 1, "Mouse Button LEFT")
        self.assert_beautify_single(EV_KEY, BTN_MIDDLE, 1, "Mouse Button MIDDLE")
        self.assert_beautify_single(EV_KEY, BTN_RIGHT, 1, "Mouse Button RIGHT")
        self.assert_beautify_single(EV_KEY, BTN_SIDE, 1, "Mouse Button 4")
        self.assert_beautify_single(EV_KEY, BTN_EXTRA, 1, "Mouse Button 5")
        self.assert_beautify_single(EV_KEY, BTN_FORWARD, 1, "Mouse Button 6")
        self.assert_beautify_single(EV_KEY, BTN_BACK, 1, "Mouse Button 7")
        self.assert_beautify_single(EV_KEY, BTN_TASK, 1, "Mouse Button 8")

        # Mouse buttons 9+ do not have an evdev name, and so must be expressed
        # as an offset from BTN_LEFT AKA "Button 1". Subtract 1 for base index
        # so that `btn_mouse_base + 1` would be "Button 1".
        btn_mouse_base: int = BTN_LEFT - 1

        self.assert_beautify_single(EV_KEY, btn_mouse_base + 9, 1, "Mouse Button 9")
        self.assert_beautify_single(EV_KEY, btn_mouse_base + 10, 1, "Mouse Button 10")
        # endregion "mouse buttons"

        # combinations
        self.assertEqual(
            InputCombination(
                InputCombination.from_tuples(
                    (EV_KEY, BTN_A, 1),
                    (EV_KEY, BTN_B, 1),
                    (EV_KEY, BTN_C, 1),
                ),
            ).beautify(),
            "Button A + Button B + Button C",
        )

    def test_find_analog_input_config(self):
        analog_input = InputConfig(type=EV_REL, code=REL_X)

        combination = InputCombination(
            (
                InputConfig(type=EV_KEY, code=BTN_MIDDLE),
                InputConfig(type=EV_REL, code=REL_Y, analog_threshold=1),
                analog_input,
            )
        )
        self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
        self.assertEqual(
            combination.find_analog_input_config(type_=EV_REL), analog_input
        )
        self.assertEqual(combination.find_analog_input_config(), analog_input)

        combination = InputCombination(
            (
                InputConfig(type=EV_REL, code=REL_X, analog_threshold=1),
                InputConfig(type=EV_KEY, code=BTN_MIDDLE),
            )
        )
        self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
        self.assertIsNone(combination.find_analog_input_config(type_=EV_REL))
        self.assertIsNone(combination.find_analog_input_config())

    # region helper methods
    def assert_beautify_single(self, type_, code, direction, expected_beautified_name):
        """
        Assert that the beautified name of the event tuple `(type_, code,
        direction)` matches the `expected_beautified_name`.
        """
        self.assertEqual(
            InputCombination(
                InputCombination.from_tuples((type_, code, direction))
            ).beautify(),
            expected_beautified_name,
        )

    # endregion


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