#!/usr/bin/python3
# vim: set expandtab shiftwidth=4:
# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
#
# Copyright © 2018 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

import argparse
import os
import sys
import unittest
import yaml
import re

from pkg_resources import parse_version


class TestYaml(unittest.TestCase):
    filename = ''

    @classmethod
    def setUpClass(cls):
        with open(cls.filename) as f:
            cls.yaml = yaml.safe_load(f)

    def dict_key_crosscheck(self, d, keys):
        '''Check that each key in d is in keys, and that each key is in d'''
        self.assertEqual(sorted(d.keys()), sorted(keys))

    def libinput_events(self, filter=None):
        '''Returns all libinput events in the recording, regardless of the
        device'''
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                try:
                    libinput = e['libinput']
                except KeyError:
                    continue

                for ev in libinput:
                    if (filter is None or ev['type'] == filter or
                        isinstance(filter, list) and ev['type'] in filter):
                        yield ev

    def test_sections_exist(self):
        sections = ['version', 'ndevices', 'libinput', 'system', 'devices']
        for section in sections:
            self.assertIn(section, self.yaml)

    def test_version(self):
        version = self.yaml['version']
        self.assertTrue(isinstance(version, int))
        self.assertEqual(version, 1)

    def test_ndevices(self):
        ndevices = self.yaml['ndevices']
        self.assertTrue(isinstance(ndevices, int))
        self.assertGreaterEqual(ndevices, 1)
        self.assertEqual(ndevices, len(self.yaml['devices']))

    def test_libinput(self):
        libinput = self.yaml['libinput']
        version = libinput['version']
        self.assertTrue(isinstance(version, str))
        self.assertGreaterEqual(parse_version(version),
                                parse_version('1.10.0'))
        git = libinput['git']
        self.assertTrue(isinstance(git, str))
        self.assertNotEqual(git, 'unknown')

    def test_system(self):
        system = self.yaml['system']
        kernel = system['kernel']
        self.assertTrue(isinstance(kernel, str))
        self.assertEqual(kernel, os.uname().release)

        dmi = system['dmi']
        self.assertTrue(isinstance(dmi, str))
        with open('/sys/class/dmi/id/modalias') as f:
            sys_dmi = f.read()[:-1]  # trailing newline
            self.assertEqual(dmi, sys_dmi)

    def test_devices_sections_exist(self):
        devices = self.yaml['devices']
        for d in devices:
            self.assertIn('node', d)
            self.assertIn('evdev', d)
            self.assertIn('udev', d)

    def test_evdev_sections_exist(self):
        sections = ['name', 'id', 'codes', 'properties']
        devices = self.yaml['devices']
        for d in devices:
            evdev = d['evdev']
            for s in sections:
                self.assertIn(s, evdev)

    def test_evdev_name(self):
        devices = self.yaml['devices']
        for d in devices:
            evdev = d['evdev']
            name = evdev['name']
            self.assertTrue(isinstance(name, str))
            self.assertGreaterEqual(len(name), 5)

    def test_evdev_id(self):
        devices = self.yaml['devices']
        for d in devices:
            evdev = d['evdev']
            id = evdev['id']
            self.assertTrue(isinstance(id, list))
            self.assertEqual(len(id), 4)
            self.assertGreater(id[0], 0)
            self.assertGreater(id[1], 0)

    def test_evdev_properties(self):
        devices = self.yaml['devices']
        for d in devices:
            evdev = d['evdev']
            properties = evdev['properties']
            self.assertTrue(isinstance(properties, list))

    def test_hid(self):
        devices = self.yaml['devices']
        for d in devices:
            hid = d['hid']
            self.assertTrue(isinstance(hid, list))
            for byte in hid:
                self.assertGreaterEqual(byte, 0)
                self.assertLessEqual(byte, 255)

    def test_udev_sections_exist(self):
        sections = ['properties']
        devices = self.yaml['devices']
        for d in devices:
            udev = d['udev']
            for s in sections:
                self.assertIn(s, udev)

    def test_udev_properties(self):
        devices = self.yaml['devices']
        for d in devices:
            udev = d['udev']
            properties = udev['properties']
            self.assertTrue(isinstance(properties, list))
            self.assertGreater(len(properties), 0)

            self.assertIn('ID_INPUT=1', properties)
            for p in properties:
                self.assertTrue(re.match('[A-Z0-9_]+=.+', p))

    def test_udev_id_inputs(self):
        devices = self.yaml['devices']
        for d in devices:
            udev = d['udev']
            properties = udev['properties']
            id_inputs = [p for p in properties if p.startswith('ID_INPUT')]
            # We expect ID_INPUT and ID_INPUT_something, but might get more
            # than one of the latter
            self.assertGreaterEqual(len(id_inputs), 2)

    def test_events_have_section(self):
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                self.assertTrue('evdev' in e or 'libinput' in e)

    def test_events_evdev(self):
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                try:
                    evdev = e['evdev']
                except KeyError:
                    continue

                for ev in evdev:
                    self.assertEqual(len(ev), 5)

                # Last event in each frame is SYN_REPORT
                ev_syn = evdev[-1]
                self.assertEqual(ev_syn[2], 0)
                self.assertEqual(ev_syn[3], 0)
                # SYN_REPORT value is 1 in case of some key repeats
                self.assertLessEqual(ev_syn[4], 1)

    def test_events_evdev_syn_report(self):
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                try:
                    evdev = e['evdev']
                except KeyError:
                    continue
                for ev in evdev[:-1]:
                    self.assertFalse(ev[2] == 0 and ev[3] == 0)

    def test_events_libinput(self):
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                try:
                    libinput = e['libinput']
                except KeyError:
                    continue

                self.assertTrue(isinstance(libinput, list))
                for ev in libinput:
                    self.assertTrue(isinstance(ev, dict))

    def test_events_libinput_type(self):
        types = ['POINTER_MOTION', 'POINTER_MOTION_ABSOLUTE', 'POINTER_AXIS',
                 'POINTER_BUTTON', 'DEVICE_ADDED', 'KEYBOARD_KEY',
                 'TOUCH_DOWN', 'TOUCH_MOTION', 'TOUCH_UP', 'TOUCH_FRAME',
                 'GESTURE_SWIPE_BEGIN', 'GESTURE_SWIPE_UPDATE',
                 'GESTURE_SWIPE_END', 'GESTURE_PINCH_BEGIN',
                 'GESTURE_PINCH_UPDATE', 'GESTURE_PINCH_END',
                 'TABLET_TOOL_AXIS', 'TABLET_TOOL_PROXIMITY',
                 'TABLET_TOOL_BUTTON', 'TABLET_TOOL_TIP',
                 'TABLET_PAD_STRIP', 'TABLET_PAD_RING',
                 'TABLET_PAD_BUTTON', 'SWITCH_TOGGLE',
                 ]
        for e in self.libinput_events():
            self.assertIn('type', e)
            self.assertIn(e['type'], types)

    def test_events_libinput_time(self):
        # DEVICE_ADDED has no time
        # first event may have 0.0 time if the first frame generates a
        # libinput event.
        try:
            for e in list(self.libinput_events())[2:]:
                self.assertIn('time', e)
                self.assertGreater(e['time'], 0.0)
                self.assertLess(e['time'], 60.0)
        except IndexError:
            pass

    def test_events_libinput_device_added(self):
        keys = ['type', 'seat', 'logical_seat']
        for e in self.libinput_events('DEVICE_ADDED'):
            self.dict_key_crosscheck(e, keys)
            self.assertEqual(e['seat'], 'seat0')
            self.assertEqual(e['logical_seat'], 'default')

    def test_events_libinput_pointer_motion(self):
        keys = ['type', 'time', 'delta', 'unaccel']
        for e in self.libinput_events('POINTER_MOTION'):
            self.dict_key_crosscheck(e, keys)
            delta = e['delta']
            self.assertTrue(isinstance(delta, list))
            self.assertEqual(len(delta), 2)
            for d in delta:
                self.assertTrue(isinstance(d, float))
            unaccel = e['unaccel']
            self.assertTrue(isinstance(unaccel, list))
            self.assertEqual(len(unaccel), 2)
            for d in unaccel:
                self.assertTrue(isinstance(d, float))

    def test_events_libinput_pointer_button(self):
        keys = ['type', 'time', 'button', 'state', 'seat_count']
        for e in self.libinput_events('POINTER_BUTTON'):
            self.dict_key_crosscheck(e, keys)
            button = e['button']
            self.assertGreater(button, 0x100)  # BTN_0
            self.assertLess(button, 0x160)  # KEY_OK
            state = e['state']
            self.assertIn(state, ['pressed', 'released'])
            scount = e['seat_count']
            self.assertGreaterEqual(scount, 0)

    def test_events_libinput_pointer_absolute(self):
        keys = ['type', 'time', 'point', 'transformed']
        for e in self.libinput_events('POINTER_MOTION_ABSOLUTE'):
            self.dict_key_crosscheck(e, keys)
            point = e['point']
            self.assertTrue(isinstance(point, list))
            self.assertEqual(len(point), 2)
            for p in point:
                self.assertTrue(isinstance(p, float))
                self.assertGreater(p, 0.0)
                self.assertLess(p, 300.0)

            transformed = e['transformed']
            self.assertTrue(isinstance(transformed, list))
            self.assertEqual(len(transformed), 2)
            for t in transformed:
                self.assertTrue(isinstance(t, float))
                self.assertGreater(t, 0.0)
                self.assertLess(t, 100.0)

    def test_events_libinput_touch(self):
        keys = ['type', 'time', 'slot', 'seat_slot']
        for e in self.libinput_events():
            if (not e['type'].startswith('TOUCH_') or
                    e['type'] == 'TOUCH_FRAME'):
                continue

            for k in keys:
                self.assertIn(k, e.keys())
            slot = e['slot']
            seat_slot = e['seat_slot']

            self.assertGreaterEqual(slot, 0)
            self.assertGreaterEqual(seat_slot, 0)

    def test_events_libinput_touch_down(self):
        keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
        for e in self.libinput_events('TOUCH_DOWN'):
            self.dict_key_crosscheck(e, keys)
            point = e['point']
            self.assertTrue(isinstance(point, list))
            self.assertEqual(len(point), 2)
            for p in point:
                self.assertTrue(isinstance(p, float))
                self.assertGreater(p, 0.0)
                self.assertLess(p, 300.0)

            transformed = e['transformed']
            self.assertTrue(isinstance(transformed, list))
            self.assertEqual(len(transformed), 2)
            for t in transformed:
                self.assertTrue(isinstance(t, float))
                self.assertGreater(t, 0.0)
                self.assertLess(t, 100.0)

    def test_events_libinput_touch_motion(self):
        keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
        for e in self.libinput_events('TOUCH_MOTION'):
            self.dict_key_crosscheck(e, keys)
            point = e['point']
            self.assertTrue(isinstance(point, list))
            self.assertEqual(len(point), 2)
            for p in point:
                self.assertTrue(isinstance(p, float))
                self.assertGreater(p, 0.0)
                self.assertLess(p, 300.0)

            transformed = e['transformed']
            self.assertTrue(isinstance(transformed, list))
            self.assertEqual(len(transformed), 2)
            for t in transformed:
                self.assertTrue(isinstance(t, float))
                self.assertGreater(t, 0.0)
                self.assertLess(t, 100.0)

    def test_events_libinput_touch_frame(self):
        devices = self.yaml['devices']
        for d in devices:
            events = d['events']
            if not events:
                raise unittest.SkipTest()
            for e in events:
                try:
                    evdev = e['libinput']
                except KeyError:
                    continue

                need_frame = False
                for ev in evdev:
                    t = ev['type']
                    if not t.startswith('TOUCH_'):
                        self.assertFalse(need_frame)
                        continue

                    if t == 'TOUCH_FRAME':
                        self.assertTrue(need_frame)
                        need_frame = False
                    else:
                        need_frame = True

                self.assertFalse(need_frame)

    def test_events_libinput_gesture_pinch(self):
        keys = ['type', 'time', 'nfingers', 'delta',
                'unaccel', 'angle_delta', 'scale']
        for e in self.libinput_events(['GESTURE_PINCH_BEGIN',
                                       'GESTURE_PINCH_UPDATE',
                                       'GESTURE_PINCH_END']):
            self.dict_key_crosscheck(e, keys)
            delta = e['delta']
            self.assertTrue(isinstance(delta, list))
            self.assertEqual(len(delta), 2)
            for d in delta:
                self.assertTrue(isinstance(d, float))
            unaccel = e['unaccel']
            self.assertTrue(isinstance(unaccel, list))
            self.assertEqual(len(unaccel), 2)
            for d in unaccel:
                self.assertTrue(isinstance(d, float))

            adelta = e['angle_delta']
            self.assertTrue(isinstance(adelta, list))
            self.assertEqual(len(adelta), 2)
            for d in adelta:
                self.assertTrue(isinstance(d, float))

            scale = e['scale']
            self.assertTrue(isinstance(scale, list))
            self.assertEqual(len(scale), 2)
            for d in scale:
                self.assertTrue(isinstance(d, float))

    def test_events_libinput_gesture_swipe(self):
        keys = ['type', 'time', 'nfingers', 'delta',
                'unaccel']
        for e in self.libinput_events(['GESTURE_SWIPE_BEGIN',
                                       'GESTURE_SWIPE_UPDATE',
                                       'GESTURE_SWIPE_END']):
            self.dict_key_crosscheck(e, keys)
            delta = e['delta']
            self.assertTrue(isinstance(delta, list))
            self.assertEqual(len(delta), 2)
            for d in delta:
                self.assertTrue(isinstance(d, float))
            unaccel = e['unaccel']
            self.assertTrue(isinstance(unaccel, list))
            self.assertEqual(len(unaccel), 2)
            for d in unaccel:
                self.assertTrue(isinstance(d, float))

    def test_events_libinput_tablet_pad_button(self):
        keys = ['type', 'time', 'button', 'state', 'mode', 'is-toggle']

        for e in self.libinput_events('TABLET_PAD_BUTTON'):
            self.dict_key_crosscheck(e, keys)

            b = e['button']
            self.assertTrue(isinstance(b, int))
            self.assertGreaterEqual(b, 0)
            self.assertLessEqual(b, 16)

            state = e['state']
            self.assertIn(state, ['pressed', 'released'])

            m = e['mode']
            self.assertTrue(isinstance(m, int))
            self.assertGreaterEqual(m, 0)
            self.assertLessEqual(m, 3)

            t = e['is-toggle']
            self.assertTrue(isinstance(t, bool))

    def test_events_libinput_tablet_pad_ring(self):
        keys = ['type', 'time', 'number', 'position', 'source', 'mode']

        for e in self.libinput_events('TABLET_PAD_RING'):
            self.dict_key_crosscheck(e, keys)

            n = e['number']
            self.assertTrue(isinstance(n, int))
            self.assertGreaterEqual(n, 0)
            self.assertLessEqual(n, 4)

            p = e['position']
            self.assertTrue(isinstance(p, float))
            if p != -1.0:  # special 'end' case
                self.assertGreaterEqual(p, 0.0)
                self.assertLess(p, 360.0)

            m = e['mode']
            self.assertTrue(isinstance(m, int))
            self.assertGreaterEqual(m, 0)
            self.assertLessEqual(m, 3)

            s = e['source']
            self.assertIn(s, ['finger', 'unknown'])

    def test_events_libinput_tablet_pad_strip(self):
        keys = ['type', 'time', 'number', 'position', 'source', 'mode']

        for e in self.libinput_events('TABLET_PAD_STRIP'):
            self.dict_key_crosscheck(e, keys)

            n = e['number']
            self.assertTrue(isinstance(n, int))
            self.assertGreaterEqual(n, 0)
            self.assertLessEqual(n, 4)

            p = e['position']
            self.assertTrue(isinstance(p, float))
            if p != -1.0:  # special 'end' case
                self.assertGreaterEqual(p, 0.0)
                self.assertLessEqual(p, 1.0)

            m = e['mode']
            self.assertTrue(isinstance(m, int))
            self.assertGreaterEqual(m, 0)
            self.assertLessEqual(m, 3)

            s = e['source']
            self.assertIn(s, ['finger', 'unknown'])

    def test_events_libinput_tablet_tool_proximity(self):
        keys = ['type', 'time', 'proximity', 'tool-type', 'serial', 'axes']

        for e in self.libinput_events('TABLET_TOOL_PROXIMITY'):
            for k in keys:
                self.assertIn(k, e)

            p = e['proximity']
            self.assertIn(p, ['in', 'out'])

            p = e['tool-type']
            self.assertIn(p, ['pen', 'eraser', 'brush', 'airbrush', 'mouse',
                              'lens', 'unknown'])

            s = e['serial']
            self.assertTrue(isinstance(s, int))
            self.assertGreaterEqual(s, 0)

            a = e['axes']
            for ax in e['axes']:
                self.assertIn(a, 'pdtrsw')

    def test_events_libinput_tablet_tool(self):
        keys = ['type', 'time', 'tip']

        for e in self.libinput_events(['TABLET_TOOL_AXIS',
                                       'TABLET_TOOL_TIP']):
            for k in keys:
                self.assertIn(k, e)

            t = e['tip']
            self.assertIn(t, ['down', 'up'])

    def test_events_libinput_tablet_tool_button(self):
        keys = ['type', 'time', 'button', 'state']

        for e in self.libinput_events('TABLET_TOOL_BUTTON'):
            self.dict_key_crosscheck(e, keys)

            b = e['button']
            # STYLUS, STYLUS2, STYLUS3
            self.assertIn(b, [0x14b, 0x14c, 0x139])

            s = e['state']
            self.assertIn(s, ['pressed', 'released'])

    def test_events_libinput_tablet_tool_axes(self):
        for e in self.libinput_events(['TABLET_TOOL_PROXIMITY',
                                       'TABLET_TOOL_AXIS',
                                       'TABLET_TOOL_TIP']):

            point = e['point']
            self.assertTrue(isinstance(point, list))
            self.assertEqual(len(point), 2)
            for p in point:
                self.assertTrue(isinstance(p, float))
                self.assertGreater(p, 0.0)

            try:
                tilt = e['tilt']
                self.assertTrue(isinstance(tilt, list))
                self.assertEqual(len(tilt), 2)
                for t in tilt:
                    self.assertTrue(isinstance(t, float))
            except KeyError:
                pass

            try:
                d = e['distance']
                self.assertTrue(isinstance(d, float))
                self.assertGreaterEqual(d, 0.0)
                self.assertNotIn('pressure', e)
            except KeyError:
                pass

            try:
                p = e['pressure']
                self.assertTrue(isinstance(p, float))
                self.assertGreaterEqual(p, 0.0)
                self.assertNotIn('distance', e)
            except KeyError:
                pass

            try:
                r = e['rotation']
                self.assertTrue(isinstance(r, float))
                self.assertGreaterEqual(r, 0.0)
            except KeyError:
                pass

            try:
                s = e['slider']
                self.assertTrue(isinstance(s, float))
                self.assertGreaterEqual(s, 0.0)
            except KeyError:
                pass

            try:
                w = e['wheel']
                self.assertTrue(isinstance(w, float))
                self.assertGreaterEqual(w, 0.0)
                self.assertIn('wheel-discrete', e)
                wd = e['wheel-discrete']
                self.assertTrue(isinstance(wd, 1))
                self.assertGreaterEqual(wd, 0.0)

                def sign(x): (1, -1)[x < 0]
                self.assertTrue(sign(w), sign(wd))
            except KeyError:
                pass

    def test_events_libinput_switch(self):
        keys = ['type', 'time', 'switch', 'state']

        for e in self.libinput_events('SWITCH_TOGGLE'):
            self.dict_key_crosscheck(e, keys)

            s = e['switch']
            self.assertTrue(isinstance(s, int))
            self.assertIn(s, [0x00, 0x01])

            # yaml converts on/off to true/false
            state = e['state']
            self.assertTrue(isinstance(state, bool))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Verify a YAML recording')
    parser.add_argument('recording', metavar='recorded-file.yaml',
                        type=str, help='Path to device recording')
    parser.add_argument('--verbose', action='store_true')
    args, remainder = parser.parse_known_args()
    TestYaml.filename = args.recording
    verbosity = 1
    if args.verbose:
        verbosity = 3

    argv = [sys.argv[0], *remainder]
    unittest.main(argv=argv, verbosity=verbosity)
