#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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/>.


from tests.test import new_event, quick_cleanup

import unittest
import asyncio

import evdev
from evdev.ecodes import EV_KEY, EV_ABS, ABS_Y, EV_REL

from inputremapper.injection.consumers.keycode_mapper import active_macros
from inputremapper.configs.global_config import BUTTONS, MOUSE, WHEEL

from inputremapper.injection.context import Context
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.injection.consumer_control import ConsumerControl, consumer_classes
from inputremapper.injection.consumers.consumer import Consumer
from inputremapper.injection.consumers.keycode_mapper import KeycodeMapper
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.injection.global_uinputs import global_uinputs


class ExampleConsumer(Consumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def is_enabled(self):
        return True

    async def notify(self, event):
        pass

    def is_handled(self, event):
        pass

    async def run(self):
        pass


class TestConsumerControl(unittest.IsolatedAsyncioTestCase):
    def setUp(self):
        consumer_classes.append(ExampleConsumer)
        self.gamepad_source = evdev.InputDevice("/dev/input/event30")
        self.mapping = Preset()

    def tearDown(self):
        quick_cleanup()
        consumer_classes.remove(ExampleConsumer)

    def setup(self, source, mapping):
        """Set a a ConsumerControl up for the test and run it in the background."""
        forward_to = evdev.UInput()
        context = Context(mapping)
        context.uinput = evdev.UInput()
        consumer_control = ConsumerControl(context, source, forward_to)
        for consumer in consumer_control._consumers:
            consumer._abs_range = (-10, 10)
        asyncio.ensure_future(consumer_control.run())
        return context, consumer_control

    async def test_no_keycode_mapper_needed(self):
        self.mapping.change(EventCombination([EV_KEY, 1, 1]), "keyboard", "b")
        _, consumer_control = self.setup(self.gamepad_source, self.mapping)
        consumer_types = [type(consumer) for consumer in consumer_control._consumers]
        self.assertIn(KeycodeMapper, consumer_types)

        self.mapping.empty()
        _, consumer_control = self.setup(self.gamepad_source, self.mapping)
        consumer_types = [type(consumer) for consumer in consumer_control._consumers]
        self.assertNotIn(KeycodeMapper, consumer_types)

        self.mapping.change(EventCombination([EV_KEY, 1, 1]), "keyboard", "k(a)")
        _, consumer_control = self.setup(self.gamepad_source, self.mapping)
        consumer_types = [type(consumer) for consumer in consumer_control._consumers]
        self.assertIn(KeycodeMapper, consumer_types)

    async def test_if_single_joystick_then(self):
        # Integration test style for if_single.
        # won't care about the event, because the purpose is not set to BUTTON
        code_a = system_mapping.get("a")
        code_shift = system_mapping.get("KEY_LEFTSHIFT")
        trigger = 1
        self.mapping.change(
            EventCombination([EV_KEY, trigger, 1]),
            "keyboard",
            "if_single(k(a), k(KEY_LEFTSHIFT))",
        )
        self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b")

        self.mapping.set("gamepad.joystick.left_purpose", MOUSE)
        self.mapping.set("gamepad.joystick.right_purpose", WHEEL)
        context, _ = self.setup(self.gamepad_source, self.mapping)

        self.gamepad_source.push_events(
            [
                new_event(EV_KEY, trigger, 1),  # start the macro
                new_event(EV_ABS, ABS_Y, 10),  # ignored
                new_event(EV_KEY, 2, 2),  # ignored
                new_event(EV_KEY, 2, 0),  # ignored
                new_event(EV_REL, 1, 1),  # ignored
                new_event(
                    EV_KEY, trigger, 0
                ),  # stop it, the only way to trigger `then`
            ]
        )
        await asyncio.sleep(0.1)
        self.assertFalse(active_macros[(EV_KEY, 1)].running)
        history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]
        self.assertIn((EV_KEY, code_a, 1), history)
        self.assertIn((EV_KEY, code_a, 0), history)
        self.assertNotIn((EV_KEY, code_shift, 1), history)
        self.assertNotIn((EV_KEY, code_shift, 0), history)

    async def test_if_single_joystickelse_(self):
        """triggers else + delayed_handle_keycode"""
        # Integration test style for if_single.
        # If a joystick that is mapped to a button is moved, if_single stops
        code_b = system_mapping.get("b")
        code_shift = system_mapping.get("KEY_LEFTSHIFT")
        trigger = 1
        self.mapping.change(
            EventCombination([EV_KEY, trigger, 1]),
            "keyboard",
            "if_single(k(a), k(KEY_LEFTSHIFT))",
        )
        self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b")

        self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
        self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
        context, _ = self.setup(self.gamepad_source, self.mapping)

        self.gamepad_source.push_events(
            [
                new_event(EV_KEY, trigger, 1),  # start the macro
                new_event(EV_ABS, ABS_Y, 10),  # not ignored, stops it
            ]
        )
        await asyncio.sleep(0.1)
        self.assertFalse(active_macros[(EV_KEY, 1)].running)
        history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]

        # the key that triggered if_single should be injected after
        # if_single had a chance to inject keys (if the macro is fast enough),
        # so that if_single can inject a modifier to e.g. capitalize the
        # triggering key. This is important for the space cadet shift
        self.assertListEqual(
            history,
            [
                (EV_KEY, code_shift, 1),
                (EV_KEY, code_b, 1),  # would be capitalized now
                (EV_KEY, code_shift, 0),
            ],
        )

    async def test_if_single_joystick_under_threshold(self):
        """triggers then because the joystick events value is too low."""
        code_a = system_mapping.get("a")
        trigger = 1
        self.mapping.change(
            EventCombination([EV_KEY, trigger, 1]),
            "keyboard",
            "if_single(k(a), k(KEY_LEFTSHIFT))",
        )
        self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b")

        self.mapping.set("gamepad.joystick.left_purpose", BUTTONS)
        self.mapping.set("gamepad.joystick.right_purpose", BUTTONS)
        context, _ = self.setup(self.gamepad_source, self.mapping)

        self.gamepad_source.push_events(
            [
                new_event(EV_KEY, trigger, 1),  # start the macro
                new_event(EV_ABS, ABS_Y, 1),  # ignored because value too low
                new_event(EV_KEY, trigger, 0),  # stop, only way to trigger `then`
            ]
        )
        await asyncio.sleep(0.1)
        self.assertFalse(active_macros[(EV_KEY, 1)].running)
        history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]

        # the key that triggered if_single should be injected after
        # if_single had a chance to inject keys (if the macro is fast enough),
        # so that if_single can inject a modifier to e.g. capitalize the
        # triggering key. This is important for the space cadet shift
        self.assertListEqual(
            history,
            [
                (EV_KEY, code_a, 1),
                (EV_KEY, code_a, 0),
            ],
        )
