# -*- coding: latin-1 -*-

# Copyright 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
try:
    unicode
except NameError:
    unicode = str
    unichr = chr

import curses
import inspect
import os
import sys
import tempfile
import unittest

import app.ci_program
import app.curses_util

#from app.curses_util import *


def debug_print_stack(*args):
    stack = inspect.stack()[1:]
    stack.reverse()
    lines = []
    for i, frame in enumerate(stack):
        lines.append(u"stack %2d %14s %4s %s" % (i, os.path.split(frame[1])[1],
                                                 frame[2], frame[3]))
    print(u"\n".join(lines))


class FakeCursesTestCase(unittest.TestCase):

    def setUp(self):
        self.cursesScreen = curses.StandardScreen()
        self.prg = app.ci_program.CiProgram()
        self.prg.setUpCurses(self.cursesScreen)
        # For testing, use the internal clipboard. Using the system clipboard
        # can create races between tests running in parallel.
        self.prg.clipboard.setOsHandlers(None, None)

    def findTextAndClick(self, timeStamp, screenText, bState):
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def createEvent(display, cmdIndex):
            row, col = self.findText(screenText)
            if row < 0:
                output = u"%s at index %d, did not find %r" % (
                    callerText, cmdIndex, screenText)
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            # Note that the mouse info is x,y (col, row).
            info = (timeStamp, col, row, 0, bState)
            curses.addMouseEvent(info)
            return curses.KEY_MOUSE

        return createEvent

    def mouseEvent(self, timeStamp, mouseRow, mouseCol, bState):
        """
        bState may be a logical or of:
          curses.BUTTON1_PRESSED;
          curses.BUTTON1_RELEASED;
          ...
          curses.BUTTON_SHIFT
          curses.BUTTON_CTRL
          curses.BUTTON_ALT
        """
        assert isinstance(timeStamp, int)
        assert isinstance(mouseRow, int)
        assert isinstance(mouseCol, int)
        assert isinstance(bState, int)
        # Note that the mouse info is x,y (col, row).
        info = (timeStamp, mouseCol, mouseRow, 0, bState)

        def createEvent(display, cmdIndex):
            curses.addMouseEvent(info)
            return curses.KEY_MOUSE

        return createEvent

    def addMouseInfo(self, timeStamp, mouseRow, mouseCol, bState):
        """
        bState may be a logical or of:
          curses.BUTTON1_PRESSED;
          curses.BUTTON1_RELEASED;
          ...
          curses.BUTTON_SHIFT
          curses.BUTTON_CTRL
          curses.BUTTON_ALT
        """
        assert isinstance(timeStamp, int)
        assert isinstance(mouseRow, int)
        assert isinstance(mouseCol, int)
        assert isinstance(bState, int)
        # Note that the mouse info is x,y (col, row).
        info = (timeStamp, mouseCol, mouseRow, 0, bState)

        def createEvent(display, cmdIndex):
            curses.addMouseEvent(info)
            return None

        return createEvent

    def call(self, *args):
        """Call arbitrary function as a 'fake input'."""
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])
        def caller(display, cmdIndex):
            try:
                args[0](*args[1:])
            except Exception as e:
                output = callerText + u' at index ' + str(cmdIndex)
                print(output)
                self.fail(e)
            return None

        return caller

    def displayCheck(self, *args):
        assert isinstance(args[0], int)
        assert isinstance(args[1], int)
        assert isinstance(args[2], list)
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def displayChecker(display, cmdIndex):
            result = display.checkText(*args)
            if result is not None:
                output = callerText + u' at index ' + str(cmdIndex) + result
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            return None

        return displayChecker

    def displayFindCheck(self, *args):
        """
        Args:
            find_string (unicode): locate this string.
            check_string (unicode): verify this follows |find_string|.
        """
        assert len(args) == 2
        assert isinstance(args[0], unicode)
        assert isinstance(args[1], unicode)
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def displayFindChecker(display, cmdIndex):
            find_string, check_string = args
            row, col = display.findText(find_string)
            result = display.checkText(row, col + len(find_string),
                                       [check_string])
            if result is not None:
                output = callerText + u' at index ' + str(cmdIndex) + result
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            return None

        return displayFindChecker

    def displayCheckNot(self, *args):
        """
        Verify that the display does not match.
        """
        assert isinstance(args[0], int)
        caller = inspect.stack()[1]
        callerText = "\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def displayCheckerNot(display, cmdIndex):
            result = display.checkText(*args)
            if result is None:
                output = callerText + u' at index ' + str(cmdIndex)
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            return None

        return displayCheckerNot

    def displayCheckStyle(self, *args):
        """*args are (row, col, height, width, colorPair)."""
        (row, col, height, width, colorPair) = args
        assert height != 0
        assert width != 0
        assert colorPair is not None
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def displayStyleChecker(display, cmdIndex):
            result = display.checkStyle(*args)
            if result is not None:
                output = callerText + u' at index ' + str(cmdIndex) + result
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            return None

        return displayStyleChecker

    def findText(self, screenText):
        """Locate |screenText| on the display, returning row, col.
        """
        return self.cursesScreen.test_find_text(screenText)

    def cursorCheck(self, expectedRow, expectedCol):
        assert isinstance(expectedRow, int)
        assert isinstance(expectedCol, int)
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def cursorChecker(display, cmdIndex):
            if self.cursesScreen.movie:
                return None
            win = self.prg.programWindow.focusedWindow
            tb = win.textBuffer
            screenRow, screenCol = self.cursesScreen.getyx()
            self.assertEqual((win.top + tb.penRow - win.scrollRow,
                              win.left + tb.penCol - win.scrollCol),
                             (screenRow, screenCol),
                             callerText + u"internal mismatch")
            self.assertEqual((expectedRow, expectedCol), (screenRow, screenCol),
                             callerText)
            return None

        return cursorChecker

    def pathToSample(self, relPath):
        path = os.path.dirname(os.path.dirname(__file__))
        return os.path.join(path, u"sample", relPath)

    def prefCheck(self, *args):
        assert isinstance(args[0], unicode)
        assert isinstance(args[1], unicode)
        assert isinstance(args[2], (int, bool))
        caller = inspect.stack()[1]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def prefChecker(display, cmdIndex):
            result = self.prg.prefs.category(args[0])[args[1]]
            if result != args[2]:
                output = u"%s at index %s, expected %r, found %r" % (
                    callerText, unicode(cmdIndex), args[2], result)
                if self.cursesScreen.movie:
                    print(output)
                else:
                    self.fail(output)
            return None

        return prefChecker

    def printParserState(self):
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def redoChain(display, cmdIndex):
            print("Parser state", callerText)
            tb = self.prg.programWindow.focusedWindow.textBuffer
            tb.parser.debugLog(print, tb.parser.data)
            return None

        return redoChain

    def printRedoState(self):
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def redoState(display, cmdIndex):
            print("Redo state", callerText)
            tb = self.prg.programWindow.focusedWindow.textBuffer
            tb.printRedoState(print)
            return None

        return redoState

    def resizeScreen(self, rows, cols):
        assert isinstance(rows, int)
        assert isinstance(cols, int)

        def setScreenSize(display, cmdIndex):
            self.cursesScreen.fakeDisplay.setScreenSize(rows, cols)
            return curses.KEY_RESIZE

        return setScreenSize

    def setClipboard(self, text):
        assert isinstance(text, str)
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def copyToClipboard(display, cmdIndex):
            self.assertTrue(self.prg.clipboard.copy, callerText)
            self.prg.clipboard.copy(text)
            return None

        return copyToClipboard

    def setMovieMode(self, enabled):
        self.cursesScreen.movie = enabled
        self.cursesScreen.fakeInput.isVerbose = enabled

    def writeText(self, text):
        assert isinstance(text, unicode), type(text)
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def copyToClipboard(display, cmdIndex):
            self.assertTrue(self.prg.clipboard.copy, callerText)
            self.prg.clipboard.copy(text)
            return app.curses_util.CTRL_V
        return copyToClipboard

    def checkNotReached(self, depth=1):
        """Check that this step doesn't occur. E.g. verify the app exited.

        Args:
          depth (int): how many stack frames up to report as the error location.
        """
        caller = inspect.stack()[depth]
        callerText = u"\n  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                            caller[2], caller[3])

        def checkEndOfInputs(display, cmdIndex):
            self.fail(callerText +
                    "\n  Unexpectedly ran out of fake inputs. Consider adding"
                    " CTRL_Q (and 'n' if necessary).")
            return None

        return checkEndOfInputs

    def runWithFakeInputs(self, fakeInputs, argv=None):
        assert hasattr(fakeInputs, "__getitem__") or hasattr(
            fakeInputs, "__iter__")
        if argv is None:
            argv = ["no_argv"]
        sys.argv = argv
        self.cursesScreen.setFakeInputs(fakeInputs + [
            self.checkNotReached(2),
        ])
        self.assertTrue(self.prg)
        self.assertFalse(self.prg.exiting)
        self.prg.run()
        #curses.printFakeDisplay()
        if app.ci_program.userConsoleMessage:
            message = app.ci_program.userConsoleMessage
            app.ci_program.userConsoleMessage = None
            self.fail(message)
        # Check that the application is closed down (don't leave it running
        # across tests).
        self.assertTrue(self.prg.exiting)
        self.assertEqual(self.cursesScreen.fakeInput.inputsIndex,
                         len(fakeInputs) - 1)
        # Handy for debugging.
        if 0:
            caller = inspect.stack()[1]
            callerText = u"  %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                              caller[2], caller[3])
            print(u'\n-------- finished', callerText)

    def runWithTestFile(self, kTestFile, fakeInputs):
        if os.path.isfile(kTestFile):
            os.unlink(kTestFile)
        self.assertFalse(os.path.isfile(kTestFile))
        self.runWithFakeInputs(fakeInputs, ["ci_test_program", kTestFile])

    def selectionDocumentCheck(self, expectedPenRow, expectedPenCol,
                               expectedMarkerRow, expectedMarkerCol,
                               expectedMode):
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def checker(display, cmdIndex):
            selection = self.prg.getDocumentSelection()
            self.assertEqual((expectedPenRow, expectedPenCol, expectedMarkerRow,
                              expectedMarkerCol, expectedMode), selection,
                             callerText)

        return checker

    def selectionCheck(self, expectedPenRow, expectedPenCol, expectedMarkerRow,
                       expectedMarkerCol, expectedMode):
        caller = inspect.stack()[1]
        callerText = u"in %s:%s:%s(): " % (os.path.split(caller[1])[1],
                                           caller[2], caller[3])

        def checker(display, cmdIndex):
            selection = self.prg.getSelection()
            self.assertEqual((expectedPenRow, expectedPenCol, expectedMarkerRow,
                              expectedMarkerCol, expectedMode), selection,
                             callerText)

        return checker

    def tearDown(self):
        # Disable mouse tracking in xterm.
        sys.stdout.write(u"\033[?1002l")
        # Disable Bracketed Paste Mode.
        sys.stdout.write(u"\033[?2004l")