import errno
import logging
import threading
import time

from unittest import TestCase

import exceptions
import mock

from abl.util import Bunch
from abl.vpath.base import URI
from abl.vpath.base.fs import CONNECTION_REGISTRY
from abl.webconnector import CleanupResidueFailed, DirectoryConnector, RPCConnector
from abl.webconnector.main import (
    CommanderInChief,
    main,
    run_async_with_sentinel,
    safe_to_install_auto_update,
    tags_for_config,
    wait_on_progress_or_live_termination,
)
from abl.webconnector.reporting import EventTypes as et

from .common import FakeUsageReporter, create_file


def verify_command_signatures(left, right):
    left_commands = set(name for name in dir(left) if name.startswith("command_"))
    right_commands = set(name for name in dir(right) if name.startswith("command_"))
    assert left_commands == right_commands, left_commands ^ right_commands


class FakeCommander(object):
    def __init__(self):
        self.commands = []

    def command_cleanup_event_logs(
        self, config, live_api_connector, usage_reporter, synchronize=False
    ):
        self.commands.append("command_cleanup_event_logs")

    def command_upload_event_logs(
        self, connector, config, live_api_connector, usage_reporter
    ):
        self.connector = connector
        self.commands.append("command_upload_event_logs")

    def command_check_for_auto_updates(
        self, connector, config, live_api_connector, usage_reporter
    ):
        self.connector = connector
        self.commands.append("command_check_for_auto_updates")

    def command_install_version(
        self, connector, config, live_api_connector, usage_reporter
    ):
        self.connector = connector
        self.commands.append("command_install_version")

    def command_skip_version(self, connector, config, live_api_connector, usage_reporter):
        self.connector = connector
        self.commands.append("command_skip_version")

    def command_upload_info(self, config, usage_reporter):
        self.commands.append("command_upload_info")


verify_command_signatures(FakeCommander, CommanderInChief)


class FakeApiConnector(object):
    def __init__(self, is_orphan=False, has_internet_connection=True):
        self.is_orphan = is_orphan
        self.has_internet_connection = has_internet_connection

    def IsCurrentProcessOrphan(self):
        if self.is_orphan is True:
            return True
        if self.is_orphan is False:
            return False

        if self.is_orphan:
            self.is_orphan -= 1
            return False
        return True

    def HasInternetConnection(self):
        return self.has_internet_connection


class FakeConnector(object):
    def __init__(self, info, tasks=[], fail_cleanup_residue=False, app_base_path=None):
        self.info = info
        self.tasks = tasks
        self.auto_updates_base_removed = False
        self.update_applied = False
        self.live_is_running = False
        self.checked_if_live_is_running = False
        self.fail_cleanup_residue = fail_cleanup_residue
        self.app_base_path = app_base_path
        self.live_ipc_channel = None

    def cleanup_auto_updates_base(self):
        self.auto_updates_base_removed = True

    def notify_error(self):
        pass

    def load_update_info(self):
        return self.info

    def apply_available_auto_update(self, usage_reporter):
        self.update_applied = True

    def save_update_info(self, info):
        self.info = info

    def check_and_process_auto_updates(self, *args, **kwargs):
        if self.fail_cleanup_residue:
            raise CleanupResidueFailed

        return self.tasks

    def pending_update_available(self):
        return True

    def check_live_is_running(self):
        self.checked_if_live_is_running = True
        return self.live_is_running


class MainTests(TestCase):
    def tearDown(self):
        URI("memory:///").remove(recursive=True)

    def test_mainloop(self):
        assert not wait_on_progress_or_live_termination(
            [None], FakeApiConnector(), FakeUsageReporter(), ""
        )
        assert not wait_on_progress_or_live_termination(
            [], FakeApiConnector(is_orphan=True), FakeUsageReporter(), ""
        )

        self.failUnlessRaises(
            CleanupResidueFailed,
            wait_on_progress_or_live_termination,
            [CleanupResidueFailed()],
            FakeApiConnector(),
            FakeUsageReporter(),
            "",
        )

        sentinel = []

        def set_sentinel():
            time.sleep(1.0)
            sentinel.append(None)

        threading.Thread(target=set_sentinel).start()
        assert wait_on_progress_or_live_termination(
            sentinel, FakeApiConnector(), FakeUsageReporter(), ""
        )

    def test_run_async(self):
        sentinel = []
        run_async_with_sentinel(lambda: None, sentinel, synchronize=True)
        assert sentinel

    def test_run_async_with_exception(self):
        def raise_something():
            raise Exception("Test exception; expected.")

        sentinel = []
        thread = run_async_with_sentinel(raise_something, sentinel, synchronize=True)
        assert sentinel
        assert isinstance(sentinel[0], Exception)

    def test_upload_event_logs_terminates_connector_when_aip_usage_data_dir_doesnt_exist(
        self
    ):
        CONNECTION_REGISTRY.cleanup(force=True)
        aip_usage_data_dir = "memory:///event_log_base1"

        connector = Bunch()
        config = Bunch(aip_usage_data_dir=aip_usage_data_dir)
        live_api_connector = FakeApiConnector()

        self.failUnlessRaises(
            SystemExit,
            CommanderInChief.command_upload_event_logs,
            connector,
            config,
            live_api_connector,
            FakeUsageReporter(),
        )

    def test_after_uploading_event_logs_connector_finishes(self):
        CONNECTION_REGISTRY.cleanup(force=True)
        aip_usage_data_dir = "memory:///event_log_base2"
        URI(aip_usage_data_dir).mkdir()

        connector = Bunch()
        config = Bunch(aip_usage_data_dir=aip_usage_data_dir)
        live_api_connector = FakeApiConnector()

        CommanderInChief.command_upload_event_logs(
            connector, config, live_api_connector, FakeUsageReporter(), synchronize=True
        )

    def test_failing_clean_usage_dir_doesnt_break_wc(self):
        CONNECTION_REGISTRY.cleanup(force=True)
        aip_usage_data_dir = "memory:///x/UsageData"
        aip_usage_data_path = URI(aip_usage_data_dir)
        aip_usage_data_path.makedirs()

        def simulate_exception_on_path(path, func):
            if func.__name__ == "remove":
                raise exceptions.OSError(2, "No such file or folder", str(path))
            pass

        for i in xrange(0, 200):
            p = aip_usage_data_path / ("event-%d.log" % i)
            create_file(p)
            if i in [20, 50, 100, 150]:
                p._manipulate(next_op_callback=simulate_exception_on_path)

        config = Bunch(aip_usage_data_dir=aip_usage_data_dir)
        live_api_connector = FakeApiConnector()

        CommanderInChief.command_cleanup_event_logs(
            config, live_api_connector, FakeUsageReporter(), synchronize=True
        )
        CONNECTION_REGISTRY.cleanup(force=True)

    def test_failing_upload_usage_dir_doesnt_break_wc(self):
        CONNECTION_REGISTRY.cleanup(force=True)
        aip_usage_data_dir = "memory:///x/UsageData"
        aip_usage_data_path = URI(aip_usage_data_dir)
        aip_usage_data_path.makedirs()

        connector = Bunch()

        def simulate_exception_on_path(path, func):
            if func.__name__ == "remove":
                raise exceptions.OSError(
                    errno.EACCES, "Lockfile access broken", str(path)
                )
            pass

        for i in xrange(0, 200):
            p = aip_usage_data_path / ("event-%d.log" % i)
            create_file(p)
            if i in [20, 50, 100, 150]:
                p = aip_usage_data_path / ("event-%d.log.lock" % i)
                p._manipulate(next_op_callback=simulate_exception_on_path)

        config = Bunch(aip_usage_data_dir=aip_usage_data_dir)
        live_api_connector = FakeApiConnector()

        CommanderInChief.command_upload_event_logs(
            connector, config, live_api_connector, FakeUsageReporter(), synchronize=True
        )
        CONNECTION_REGISTRY.cleanup(force=True)

    def test_tags_for_config(self):
        for config, expected in [
            (dict(platform="win", wordsize="64"), ["win64"]),
            (dict(platform="mac", architecture="universal"), ["mac-universal"]),
            (dict(platform="mac", architecture="x86_64"), ["mac-intel"]),
            (dict(platform="mac", architecture="whatever"), ["mac-whatever"]),
            (dict(platform="whatever"), ["whatever"]),
            (
                dict(platform="mac", architecture="x86_64", live_variant="Standard"),
                ["mac-intel", "standard"],
            ),
        ]:
            self.assertEqual(tags_for_config(Bunch(**config)), expected)

    def test_install_version(self):
        connector = FakeConnector({"prepared_version": ["from", "to"]})

        config = Bunch(version="from", commands=dict(install_version=dict(version="to")))
        live_api_connector = FakeApiConnector(1)  # first check for orphan fails

        CommanderInChief.command_install_version(
            connector, config, live_api_connector, FakeUsageReporter()
        )
        assert connector.auto_updates_base_removed
        assert connector.update_applied
        assert connector.checked_if_live_is_running

    def test_install_version_terminates_with_running_live(self):
        connector = FakeConnector({"prepared_version": ["from", "to"]})
        connector.live_is_running = True

        config = Bunch(version="from", commands=dict(install_version=dict(version="to")))
        live_api_connector = FakeApiConnector(1)  # first check for orphan fails

        CommanderInChief.command_install_version(
            connector, config, live_api_connector, FakeUsageReporter()
        )
        assert not connector.auto_updates_base_removed
        assert not connector.update_applied
        assert connector.checked_if_live_is_running

    def test_install_version_terminates_with_crash_detection_file(self):
        connector = FakeConnector({"prepared_version": ["from", "to"]})

        crash_detection_file = URI("memory:///preferences/live/CrashDetection.cfg")
        create_file(crash_detection_file)

        usage_reporter = FakeUsageReporter()

        config = Bunch(
            version="from",
            commands=dict(install_version=dict(version="to")),
            crash_detection_file=str(crash_detection_file),
        )
        live_api_connector = FakeApiConnector(1)  # first check for orphan fails

        CommanderInChief.command_install_version(
            connector, config, live_api_connector, usage_reporter
        )
        assert not connector.auto_updates_base_removed
        assert not connector.update_applied
        assert not connector.checked_if_live_is_running
        assert et.CRASH_DETECTION_FILE_FOUND in usage_reporter.events

    def test_skip_version(self):
        connector = FakeConnector({"versions": {"from": {"to": {}}}})

        config = Bunch(version="from", commands={"skip_version": {"version": "to"}})
        live_api_connector = FakeApiConnector(True)

        CommanderInChief.command_skip_version(
            connector, config, live_api_connector, FakeUsageReporter()
        )
        self.assertEqual(connector.info["versions"]["from"]["to"]["status"], "skipped")

    def test_check_for_auto_updates(self):
        class FakeTask(object):
            def perform(self, _usage_reporter):
                pass

        connector = FakeConnector(
            {"prepared_version": ["from", "to"]}, tasks=[FakeTask()]
        )

        config = Bunch(
            version="from",
            platform="win",
            wordsize="64",
            commands=dict(check_for_auto_updates=""),
        )
        live_api_connector = FakeApiConnector(True)

        CommanderInChief.command_check_for_auto_updates(
            connector, config, live_api_connector, FakeUsageReporter(), synchronize=True
        )

    @mock.patch("abl.webconnector.infoprocessing.upload_info")
    def test_info_upload(self, upload_info_mock):
        aip_usage_data_dir = "memory:///event_log_base1234"
        URI(aip_usage_data_dir).makedirs()

        url = "http://this/is/the/info/endpoint/"

        config = Bunch(aip_usage_data_dir=aip_usage_data_dir, info_endpoint=url)

        CommanderInChief.command_upload_info(config, FakeUsageReporter())
        self.assertEqual(1, len(upload_info_mock.call_args_list))
        (endpoint, basedir), _ = upload_info_mock.call_args_list[0]
        self.assertEqual(URI(aip_usage_data_dir), basedir)
        self.assertEqual(url, endpoint)

    @mock.patch("abl.webconnector.infoprocessing.upload_info")
    def test_info_upload_error_doesnt_crash(self, upload_info_mock):
        upload_info_mock.side_effect = Exception

        aip_usage_data_dir = "memory:///event_log_base1234"
        URI(aip_usage_data_dir).makedirs()

        config = Bunch(aip_usage_data_dir=aip_usage_data_dir)

        CommanderInChief.command_upload_info(config, FakeUsageReporter())
        self.assertTrue(upload_info_mock.call_args_list)

    def test_main(self):
        app_base_path = URI("memory:///app")
        app_base_path.mkdir()
        autoupdates_dir_base_path = URI("memory:///autoupdates_dir_base_path")
        autoupdates_dir_base_path.mkdir()
        config = {
            "app_base_path": str(app_base_path),
            "autoupdates_dir_base_path": str(autoupdates_dir_base_path),
            "services_endpoint": "http://foobar.baz",
            "version": "to",
            "session_id": "",
            "commands": {"check_for_auto_updates": "", "skip_version": ""},
        }

        commander = FakeCommander()

        live_api_connector = FakeApiConnector()
        main(
            config,
            live_api_connector,
            commander=commander,
            usage_reporter=FakeUsageReporter(),
        )
        assert isinstance(commander.connector, RPCConnector)
        # we automatically install after the check for
        # updates, so I ensure command_install_version
        # occurs
        self.assertTrue("command_install_version" in commander.commands)
        self.assertTrue("command_cleanup_event_logs" in commander.commands)
        self.assertTrue("command_upload_info" in commander.commands)

    def test_update_event_logs_command_runs_cleanup(self):
        app_base_path = URI("memory:///app")
        app_base_path.mkdir()
        autoupdates_dir_base_path = URI("memory:///autoupdates_dir_base_path")
        autoupdates_dir_base_path.mkdir()
        config = {
            "app_base_path": str(app_base_path),
            "autoupdates_dir_base_path": str(autoupdates_dir_base_path),
            "services_endpoint": "http://foobar.baz",
            "version": "to",
            "session_id": "",
            "commands": {"upload_event_logs": ""},
        }

        commander = FakeCommander()

        live_api_connector = FakeApiConnector()
        main(
            config,
            live_api_connector,
            commander=commander,
            usage_reporter=FakeUsageReporter(),
        )
        assert isinstance(commander.connector, RPCConnector)
        self.assertTrue("command_cleanup_event_logs" in commander.commands)

    def test_directory_connector_instantiated(self):
        dbase = URI("memory:///dbase")
        dbase.mkdir()
        app_base_path = URI("memory:///app")
        app_base_path.mkdir()
        autoupdates_dir_base_path = URI("memory:///autoupdates_dir_base_path")
        autoupdates_dir_base_path.mkdir()

        config = {
            "app_base_path": str(app_base_path),
            "autoupdates_dir_base_path": str(autoupdates_dir_base_path),
            "directory_connection": str(dbase),
            "version": "to",
            "session_id": "",
            "commands": {
                "upload_event_logs": "",
                "check_for_auto_updates": "",
                "install_version": "",
                "skip_version": "",
            },
        }

        commander = FakeCommander()

        live_api_connector = FakeApiConnector()
        main(
            config,
            live_api_connector,
            commander=commander,
            usage_reporter=FakeUsageReporter(),
        )
        assert isinstance(commander.connector, DirectoryConnector)

    def test_no_internet_connection(self):
        app_base_path = URI("memory:///app")
        app_base_path.mkdir()
        autoupdates_dir_base_path = URI("memory:///autoupdates_dir_base_path")
        autoupdates_dir_base_path.mkdir()
        config = {
            "app_base_path": str(app_base_path),
            "autoupdates_dir_base_path": str(autoupdates_dir_base_path),
            "services_endpoint": "http://foobar.baz",
            "version": "to",
            "session_id": "",
            "commands": {
                "upload_event_logs": "",
                "check_for_auto_updates": "",
                "install_version": "",
                "skip_version": "",
            },
        }

        commander = FakeCommander()
        live_api_connector = FakeApiConnector(has_internet_connection=False)
        self.failUnlessRaises(
            SystemExit, main, config, live_api_connector, commander=commander
        )

    def test_toplevel_exception_handling(self):
        app_base_path = URI("memory:///app")
        app_base_path.mkdir()
        config = {
            "app_base_path": str(app_base_path),
            "services_endpoint": "http://foobar.baz",
            "version": "to",
            "session_id": "",
            "commands": {
                "upload_event_logs": "",
                "check_for_auto_updates": "",
                "install_version": "",
                "skip_version": "",
            },
        }

        logger = logging.getLogger()

        records = []

        class NullHandler(logging.Handler):
            def emit(self, record):
                records.append(record)

        logger.addHandler(NullHandler())

        commander = FakeCommander()
        live_api_connector = object()  # this provokes a failure
        self.failUnlessRaises(
            AttributeError, main, config, live_api_connector, commander=commander
        )

        last_record = records[-1]
        assert isinstance(last_record.msg, AttributeError)

    def test_failing_residue_cleanup_terminates(self):
        class FakeTask(object):
            def perform(self, _usage_reporter):
                pass

        connector = FakeConnector({}, fail_cleanup_residue=True)

        config = Bunch(
            version="from",
            platform="win",
            wordsize="64",
            commands=dict(check_for_auto_updates=""),
        )

        live_api_connector = FakeApiConnector(True)

        self.failUnlessRaises(
            CleanupResidueFailed,
            CommanderInChief.command_check_for_auto_updates,
            connector,
            config,
            live_api_connector,
            FakeUsageReporter(),
            synchronize=True,
        )

    def test_safe_to_update(self):
        app_base_path = URI("memory:///programs/live")
        app_base_path.makedirs()
        connector = FakeConnector({}, app_base_path=app_base_path)
        usage_reporter = FakeUsageReporter()

        assert safe_to_install_auto_update(connector, usage_reporter)
        assert len(usage_reporter.events) == 0

    def test_safe_to_update_must_delete_old(self):
        app_base_path = URI("memory:///programs/live")
        app_base_path.makedirs()
        old_dir = URI("memory:///programs/live.Old")
        old_dir.mkdir()
        connector = FakeConnector({}, app_base_path=app_base_path)
        usage_reporter = FakeUsageReporter()

        assert safe_to_install_auto_update(connector, usage_reporter)
        assert len(usage_reporter.events) == 1
        assert usage_reporter.events[0] == et.OLD_INSTALLATION_FOUND
        assert not old_dir.exists()

    def test_safe_to_update_must_delete_old_undeletable(self):
        app_base_path = URI("memory:///programs/live")
        app_base_path.makedirs()
        old_dir = URI("memory:///programs/live.Old")
        old_dir.mkdir()

        # Artificially make the old_dir undeletable
        class CountdownToDestruction(object):
            def __init__(self, explode_on):
                self.explode_on = explode_on
                self.counter = 0

            def callback(self, path, func):
                should_explode = False
                if self.counter == self.explode_on:
                    should_explode = True
                self.counter = self.counter + 1
                if should_explode:
                    raise Exception("I'm a super-file! You can never delete me!!")

        old_dir._manipulate(next_op_callback=CountdownToDestruction(1).callback)

        connector = FakeConnector({}, app_base_path=app_base_path)
        usage_reporter = FakeUsageReporter()

        assert not safe_to_install_auto_update(connector, usage_reporter)
        assert len(usage_reporter.events) == 2
        assert usage_reporter.events[0] == et.OLD_INSTALLATION_FOUND
        assert usage_reporter.events[1] == et.FAILED_TO_DELETE_OLD_INSTALLATION
