# coding: utf-8
from __future__ import unicode_literals, division, absolute_import, print_function

import sys
import threading
import unittest
from os import path
import time
import re
import shutil
import os

import sublime

import shellenv
import package_events

if sys.version_info < (3,):
    from Queue import Queue
else:
    from queue import Queue

from .mocks import GolangBuildMock


TEST_GOPATH = path.join(path.dirname(__file__), 'go_projects')
TEST_GOPATH2 = path.join(path.dirname(__file__), 'go_projects2')
VIEW_SETTINGS = {
    'GOPATH': TEST_GOPATH,
    'GOOS': None,
    'GOARCH': None,
    'GOARM': None,
    'GO386': None,
    'GORACE': None
}

CROSS_COMPILE_OS = 'darwin' if sys.platform != 'darwin' else 'linux'


class GolangBuildTests(unittest.TestCase):

    def setUp(self):
        skip_entries = {}
        skip_entries[TEST_GOPATH] = set(['.git-keep', 'good', 'bad', 'runnable'])
        skip_entries[TEST_GOPATH2] = set(['.git-keep', 'runnable2'])

        for gopath in (TEST_GOPATH, TEST_GOPATH2):
            for subdir in ('pkg', 'bin', 'src'):
                full_path = path.join(gopath, subdir)
                for entry in os.listdir(full_path):
                    if entry in skip_entries[gopath]:
                        continue
                    entry_path = path.join(full_path, entry)
                    if path.isdir(entry_path):
                        shutil.rmtree(entry_path)
                    else:
                        os.remove(entry_path)

    def test_build(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build')

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go build" succeed?'))

    def test_build_flags(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'flags': ['-v', '-x']})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go build" succeed and print all commands?'))

    def test_build_flags_from_settings(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        with GolangBuildMock(sublime_settings={'build:flags': ['-v', '-x']}):
            def _run_build(view, result_queue):
                view.window().run_command('golang_build')

            result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
            result = wait_build(result_queue)
            self.assertEqual('success', result)
            self.assertTrue(confirm_user('Did "go build" succeed and print all commands?'))

    def test_install_flags_from_view_settings(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'install'})

        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['install:flags'] = ['-v', '-x']

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go install" succeed and print all commands?'))

    def test_clean(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'clean'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go clean" succeed?'))

    def test_test(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'test'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go test" succeed?'))

    def test_run(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'runnable', 'main.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'run'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go run" succeed?'))

    def test_run_with_file_path_flag_absolute(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'run'})

        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['run:flags'] = [os.path.join(TEST_GOPATH, 'src', 'runnable', 'main.go')]

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go run" succeed for runnable/main.go?'))

    def test_run_with_file_path_flag_relative(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'run'})

        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['run:flags'] = ['runnable/main.go']

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go run" succeed for runnable/main.go?'))

    def test_run_with_file_path_flag_relative_multiple_gopath(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'run'})

        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['GOPATH'] = os.pathsep.join([TEST_GOPATH, TEST_GOPATH2])
        custom_view_settings['run:flags'] = ['runnable2/main.go']

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go run" succeed for runnable2/main.go?'))

    def test_install(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build', {'task': 'install'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go install" succeed?'))

    def test_cross_compile(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
        begin_event = threading.Event()

        def _run_build(view, result_queue):
            notify_user('Select %s/amd64 from quick panel' % CROSS_COMPILE_OS)
            begin_event.set()
            view.window().run_command('golang_build', {'task': 'cross_compile'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        begin_event.wait()
        result = wait_build(result_queue, timeout=15)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did the cross-compile succeed?'))

    def test_get(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
        begin_event = threading.Event()

        def _run_build(view, result_queue):
            sublime.set_clipboard('github.com/golang/example/hello')
            notify_user('Paste from the clipboard into the input panel')
            begin_event.set()
            view.window().run_command('golang_build_get')

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        begin_event.wait()
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go get" succeed?'))

    def test_get_flags(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
        begin_event = threading.Event()

        def _run_build(view, result_queue):
            sublime.set_clipboard('github.com/golang/example/hello')
            notify_user('Paste from the clipboard into the input panel')
            begin_event.set()
            view.window().run_command('golang_build_get', {'flags': ['-v', '-d']})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        begin_event.wait()
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go get" download but not install?'))

    def test_get_flags_from_settings(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        with GolangBuildMock(sublime_settings={'get:flags': ['-v', '-d']}):
            def _run_build(view, result_queue):
                view.window().run_command('golang_build_get', {'url': 'github.com/golang/example/hello'})

            result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
            result = wait_build(result_queue)
            self.assertEqual('success', result)
            self.assertTrue(confirm_user('Did "go get" download but not install?'))

    def test_get_url(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build_get', {'url': 'github.com/golang/example/hello'})

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)
        self.assertTrue(confirm_user('Did "go get" succeed for "github.com/golang/example/hello"?'))

    def test_terminal(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build_terminal')

        open_file(file_path, VIEW_SETTINGS, _run_build)
        self.assertTrue(confirm_user('Did a terminal open to Packages/Golang Build/dev/go_projects/src/good/?'))

    def test_build_bad(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'bad', 'hello.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build')

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('error', result)
        self.assertTrue(confirm_user('Did "go build" fail?'))

    def test_build_cancel(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build')

            def _cancel_build():
                view.window().run_command('golang_build_cancel')

            sublime.set_timeout(_cancel_build, 50)

        # We perform a cross-compile so the user has time to interrupt the build
        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['GOOS'] = CROSS_COMPILE_OS
        custom_view_settings['GOARCH'] = 'amd64'

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('cancelled', result)
        self.assertTrue(confirm_user('Was "go build" successfully cancelled?'))

    def test_build_reopen(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

        def _run_build(view, result_queue):
            view.window().run_command('golang_build')

        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
        result = wait_build(result_queue)
        self.assertEqual('success', result)

        time.sleep(0.4)

        def _hide_panel():
            sublime.active_window().run_command('hide_panel')
        sublime.set_timeout(_hide_panel, 1)

        time.sleep(0.4)
        self.assertTrue(confirm_user('Was the build output hidden?'))

        def _reopen_panel():
            sublime.active_window().run_command('golang_build_reopen')
        sublime.set_timeout(_reopen_panel, 1)

        time.sleep(0.4)
        self.assertTrue(confirm_user('Was the build output reopened?'))

    def test_build_interrupt(self):
        ensure_not_ui_thread()

        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
        begin_event = threading.Event()
        second_begin_event = threading.Event()

        def _run_build(view, result_queue):
            notify_user('Press the "Stop Running Build" button when prompted')

            begin_event.set()
            view.window().run_command('golang_build')

            def _new_build():
                view.window().run_command('golang_build')
                second_begin_event.set()

            sublime.set_timeout(_new_build, 50)

        # We perform a cross-compile so the user has time to interrupt the build
        custom_view_settings = VIEW_SETTINGS.copy()
        custom_view_settings['GOOS'] = CROSS_COMPILE_OS
        custom_view_settings['GOARCH'] = 'amd64'

        result_queue = open_file(file_path, custom_view_settings, _run_build)
        begin_event.wait()
        result1 = wait_build(result_queue)
        self.assertEqual('cancelled', result1)
        second_begin_event.wait()
        result2 = wait_build(result_queue)
        self.assertEqual('success', result2)
        self.assertTrue(confirm_user('Was the first build cancelled and the second successful?'))

    def test_build_go_missing(self):
        ensure_not_ui_thread()

        shell, _ = shellenv.get_env()
        search_path = path.expanduser('~')
        with GolangBuildMock(shell=shell, env={'PATH': search_path}):

            file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

            def _run_build(view, result_queue):
                notify_user('Press the "Open Documentation" button when prompted about go not being found in the PATH')

                view.window().run_command('golang_build')

            open_file(file_path, VIEW_SETTINGS, _run_build)
            time.sleep(0.5)
            self.assertTrue(confirm_user('Were you prompted that go could not be found in the PATH?'))
            self.assertTrue(confirm_user('When you pressed "Open Documentation", was it opened in your browser?'))

    def test_build_no_gopath(self):
        ensure_not_ui_thread()

        shell, env = shellenv.get_env()
        if 'GOPATH' in env:
            del env['GOPATH']
        with GolangBuildMock(shell=shell, env=env):

            file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')

            def _run_build(view, result_queue):
                notify_user('Press the "Open Documentation" button when prompted about GOPATH not being set')

                view.window().run_command('golang_build')

            custom_view_settings = VIEW_SETTINGS.copy()
            del custom_view_settings['GOPATH']
            open_file(file_path, custom_view_settings, _run_build)
            time.sleep(0.5)
            self.assertTrue(confirm_user('Were you prompted that GOPATH was not set?'))
            self.assertTrue(confirm_user('When you pressed "Open Documentation", was it opened in your browser?'))


def ensure_not_ui_thread():
    """
    The tests won't function properly if they are run in the UI thread, so
    this functions throws an exception if that is attempted
    """

    if isinstance(threading.current_thread(), threading._MainThread):
        raise RuntimeError('Tests can not be run in the UI thread')


def open_file(file_path, view_settings, callback):
    """
    Open a file in Sublime Text, sets settings on the view and then executes
    the callback once the file is opened

    :param file_path:
        A unicode string of the path to the file to open

    :param view_settings:
        A dict of settings to set the "golang" key of the view's settings to

    :param callback:
        The callback to execute in the UI thread once the file is opened
    """

    result_queue = Queue()
    file_param = file_path
    if sys.platform == 'win32':
        file_param = re.sub('^([a-zA-Z]):', '/\\1', file_param)
        file_param = file_param.replace('\\', '/')

    def open_file_callback():
        window = sublime.active_window()

        window.run_command(
            'open_file',
            {
                'file': file_param
            }
        )

        when_file_opened(window, file_path, view_settings, callback, result_queue)
    sublime.set_timeout(open_file_callback, 50)
    return result_queue


def when_file_opened(window, file_path, view_settings, callback, result_queue):
    """
    Periodic polling callback used by open_file() to find the newly-opened file

    :param window:
        The sublime.Window to look for the view in

    :param file_path:
        The file path of the file that was opened

    :param view_settings:
        A dict of settings to set to the view's "golang" setting key

    :param callback:
        The callback to execute when the file is opened

    :param result_queue:
        A Queue() object the callback can use to communicate with the test
    """

    view = window.active_view()
    if view and view.file_name() == file_path:
        view.settings().set('golang', view_settings)
        callback(view, result_queue)
        return
    # If the view was not ready, retry a short while later
    sublime.set_timeout(lambda: when_file_opened(window, file_path, view_settings, callback, result_queue), 50)


def wait_build(result_queue, timeout=5):
    """
    Uses the result queue to wait for a result from the open_file() callback

    :param result_queue:
        The Queue() to get the result from

    :param timeout:
        How long to wait before considering the test a failure

    :return:
        The value from the queue
    """

    def _send_result(package_name, event_name, payload):
        result_queue.put(payload.result)

    try:
        package_events.listen('Golang Build', _send_result)
        return result_queue.get(timeout=timeout)
    finally:
        package_events.unlisten('Golang Build', _send_result)


def confirm_user(message):
    """
    Prompts the user to via a dialog to confirm a question

    :param message:
        A unicode string of the message to present to the user

    :return:
        A boolean - if the user clicked "Yes"
    """

    queue = Queue()

    def _show_ok_cancel():
        response = sublime.ok_cancel_dialog('Test Suite for Golang Build\n\n' + message, 'Yes')
        queue.put(response)

    sublime.set_timeout(_show_ok_cancel, 1)
    return queue.get()


def notify_user(message):
    """
    Open a dialog for the user to inform them of a user interaction that is
    part of the test suite

    :param message:
        A unicode string of the message to present to the user
    """

    sublime.ok_cancel_dialog('Test Suite for Golang Build\n\n' + message, 'Ok')
