#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca

"""Utilities that can be used by tests."""

import difflib
import re
import sys

import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")

from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from macaroon.playback import *

testLogger = Gio.DBusProxy.new_for_bus_sync(
    Gio.BusType.SESSION,
    Gio.DBusProxyFlags.NONE,
    None,
    'org.gnome.Cthulhu',
    '/org/gnome/Cthulhu',
    'org.gnome.Cthulhu.Logger',
    None)

enable_assert = \
    environ.get('HARNESS_ASSERT', 'yes') in ('yes', 'true', 'y', '1', 1)
errFilename = environ.get('HARNESS_ERR', None)
outFilename = environ.get('HARNESS_OUT', None)

if errFilename and len(errFilename):
    myErr = open(errFilename, 'a', 0)
else:
    myErr = sys.stderr

if outFilename and len(outFilename):
    if outFilename == errFilename:
        myOut = myErr
    else:
        myOut = open(outFilename, 'a', 0)
else:
    myOut = sys.stdout

def getKeyCodeForName(name):
    keymap = Gdk.Keymap.get_default()
    success, entries = keymap.get_entries_for_keyval(Gdk.keyval_from_name(name))
    if success:
        return entries[-1].keycode

    return None

def setClipboardText(text):
    clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
    clipboard.set_text(text, -1)

class StartRecordingAction(AtomicAction):
    '''Tells Cthulhu to log speech and braille output to a string which we
    can later obtain and use in an assertion (see AssertPresentationAction)'''

    def __init__(self):
        if enable_assert:
            AtomicAction.__init__(self, 1000, self._startRecording)
        else:
            AtomicAction.__init__(self, 0, lambda: None)

    def _startRecording(self):
        testLogger.startRecording()

    def __str__(self):
        return 'Start Recording Action'

def assertListEquality(rawCthulhuResults, expectedList):
    '''Convert raw speech and braille output obtained from Cthulhu into a
    list by splitting it at newline boundaries.  Compare it to the
    list passed in and return the actual results if they differ.
    Otherwise, return None to indicate an equality.'''

    results = rawCthulhuResults.strip().split("\n")

    # Shoot for a string comparison first.
    #
    if results == expectedList:
        return None
    elif len(results) != len(expectedList):
        return results

    # If the string comparison failed, do a regex match item by item
    #
    for i in range(0, len(expectedList)):
        if results[i] == expectedList[i]:
            continue
        else:
            expectedResultRE = re.compile(expectedList[i])
            if expectedResultRE.match(results[i]):
                continue
            else:
                return results

    return None

class AssertPresentationAction(AtomicAction):
    '''Ask Cthulhu for the speech and braille logged since the last use
    of StartRecordingAction and apply an assertion predicate.'''

    totalCount = 0
    totalSucceed = 0
    totalFail = 0
    totalKnownIssues = 0

    def __init__(self, name, expectedResults,
                 assertionPredicate=assertListEquality):
        '''name:               the name of the test
           expectedResults:    the results we want (typically a list of strings
                               that can be treated as regular expressions)
           assertionPredicate: method to compare actual results to expected
                               results
        '''
        # [[[WDW: the pause is to wait for Cthulhu to process an event.
        # Probably should think of a better way to do this.]]]
        #
        if enable_assert:
            AtomicAction.__init__(self, 1000, self._stopRecording)
            self._name = sys.argv[0] + ":" + name
            self._expectedResults = expectedResults
            self._assertionPredicate = assertionPredicate
            AssertPresentationAction.totalCount += 1
            self._num = AssertPresentationAction.totalCount
        else:
            AtomicAction.__init__(self, 0, lambda: None)

    def printDiffs(self, results):
        """Compare the expected results with the actual results and print
        out a set of diffs.

        Arguments:
        - results: the actual results.

        Returns an indication of whether this test was expected to fail.
        """

        knownIssue = False
        print("DIFFERENCES FOUND:", file=myErr)
        if isinstance(self._expectedResults, [].__class__):
            for result in self._expectedResults:
                if result.startswith("KNOWN ISSUE") \
                   or result.startswith("BUG?"):
                    knownIssue = True
        else:
            if self._expectedResults.startswith("KNOWN ISSUE") \
               or self._expectedResults.startswith("BUG?"):
                knownIssue = True

        d = difflib.Differ()
        try:
            # This can stack trace for some odd reason (UTF-8 characters?),
            # so we need to capture it.  Otherwise, it can hang the tests.
            #
            diffs = list(d.compare(self._expectedResults, results))
            print('\n'.join(list(diffs)), file=myErr)
        except:
            print("(ERROR COMPUTING DIFFERENCES!!!)", file=myErr)
            for i in range(0, max(len(results), len(self._expectedResults))):
                try:
                    print("  EXPECTED: %s" \
                          % self._expectedResults[i].decode("UTF-8", "replace"), file=myErr)
                except:
                    pass
                try:
                    print("  ACTUAL:   %s" \
                          % results[i].decode("UTF-8", "replace"), file=myErr)
                except:
                    pass

        return knownIssue

    def _stopRecording(self):
        result = testLogger.stopRecording()
        results = self._assertionPredicate(result, self._expectedResults)
        if not results:
            AssertPresentationAction.totalSucceed += 1
            print("Test %d of %d SUCCEEDED: %s" \
                            % (self._num,
                               AssertPresentationAction.totalCount,
                               self._name), file=myOut)
        else:
            AssertPresentationAction.totalFail += 1
            print("Test %d of %d FAILED: %s" \
                            % (self._num,
                               AssertPresentationAction.totalCount,
                               self._name), file=myErr)

            knownIssue = self.printDiffs(results)
            if knownIssue:
                AssertPresentationAction.totalKnownIssues += 1
                print('[FAILURE WAS EXPECTED - ' \
                                'LOOK FOR KNOWN ISSUE OR BUG? ' \
                                'IN EXPECTED RESULTS]', file=myErr)
            else:
                print('[FAILURE WAS UNEXPECTED]', file=myErr)

    def __str__(self):
        return 'Assert Presentation Action: %s' % self._name

class AssertionSummaryAction(AtomicAction):
    '''Output the summary of successes and failures of
    AssertPresentationAction assertions.'''

    def __init__(self):
        AtomicAction.__init__(self, 0, self._printSummary)

    def _printSummary(self):
        print("SUMMARY: %d SUCCEEDED and %d FAILED (%d UNEXPECTED) of %d for %s"\
            % (AssertPresentationAction.totalSucceed,
               AssertPresentationAction.totalFail,
               (AssertPresentationAction.totalFail \
               - AssertPresentationAction.totalKnownIssues),
               AssertPresentationAction.totalCount,
               sys.argv[0]), file=myOut)

    def __str__(self):
        return 'Start Recording Action'