# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2016, Anaconda, Inc. All rights reserved.
#
# Licensed under the terms of the BSD 3-Clause License.
# The full license is in the file LICENSE.txt, distributed with this software.
# -----------------------------------------------------------------------------
from __future__ import absolute_import

import codecs
import os
import shutil
import zipfile

from anaconda_project.test.project_utils import project_no_dedicated_env
from anaconda_project.internal.test.tmpfile_utils import (with_directory_contents,
                                                          with_directory_contents_completing_project_file,
                                                          with_tmp_zipfile, complete_project_file_content)
from anaconda_project.test.environ_utils import minimal_environ
from anaconda_project.local_state_file import DEFAULT_LOCAL_STATE_FILENAME
from anaconda_project.local_state_file import LocalStateFile
from anaconda_project.requirements_registry.registry import RequirementsRegistry
from anaconda_project.requirements_registry.requirements.download import DownloadRequirement
from anaconda_project.prepare import (prepare_without_interaction, unprepare, prepare_in_stages)
from anaconda_project import provide
from anaconda_project.project_file import DEFAULT_PROJECT_FILENAME

from tornado import gen

DATAFILE_CONTENT = ("downloads:\n"
                    "    DATAFILE:\n"
                    "        url: http://localhost/data.csv\n"
                    "        md5: 12345abcdef\n"
                    "        filename: data.csv\n")

ZIPPED_DATAFILE_CONTENT = ("downloads:\n"
                           "    DATAFILE:\n"
                           "        url: http://localhost/data.zip\n"
                           "        filename: data\n")

ZIPPED_DATAFILE_CONTENT_CHECKSUM = (ZIPPED_DATAFILE_CONTENT + "        md5: 12345abcdef\n")

ZIPPED_DATAFILE_CONTENT_NO_UNZIP = (ZIPPED_DATAFILE_CONTENT + "        unzip: false\n")

# have to specify unzip:true manually here
ZIPPED_DATAFILE_CONTENT_NO_ZIP_SUFFIX = ("downloads:\n"
                                         "    DATAFILE:\n"
                                         "        url: http://localhost/data\n"
                                         "        unzip: true\n"
                                         "        filename: data\n")


def _download_requirement():
    return DownloadRequirement(registry=RequirementsRegistry(),
                               env_var="DATAFILE",
                               url='http://localhost/data.csv',
                               filename='data.csv')


def test_prepare_and_unprepare_download(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            with open(os.path.join(dirname, 'data.csv'), 'w') as out:
                out.write('data')
            self._hash = '12345abcdef'
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        filename = os.path.join(dirname, 'data.csv')
        assert os.path.exists(filename)

        project.frontend.reset()
        status = unprepare(project, result)
        assert project.frontend.logs == ["Removed downloaded file %s." % filename,
                                         ("Current environment is not in %s, no need to delete it." % dirname)]
        assert status.status_description == 'Success.'
        assert status
        assert not os.path.exists(filename)

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_prepare_download_mismatched_checksum_after_download(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            with open(os.path.join(dirname, 'data.csv'), 'w') as out:
                out.write('data')
            self._hash = 'mismatched'
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert not result
        assert ('Error downloading http://localhost/data.csv: mismatched hashes. '
                'Expected: 12345abcdef, calculated: mismatched') in result.errors

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_prepare_download_exception(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            raise Exception('error')

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert not result
        assert ('missing requirement to run this project: A downloaded file which is referenced by DATAFILE.'
                ) in result.errors

        project.frontend.reset()
        status = unprepare(project, result)
        filename = os.path.join(dirname, 'data.csv')
        assert project.frontend.logs == ["No need to remove %s which wasn't downloaded." % filename,
                                         ("Current environment is not in %s, no need to delete it." % dirname)]
        assert status.status_description == 'Success.'

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_unprepare_download_fails(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            with open(os.path.join(dirname, 'data.csv'), 'w') as out:
                out.write('data')
            self._hash = '12345abcdef'
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        filename = os.path.join(dirname, 'data.csv')
        assert os.path.exists(filename)

        def mock_remove(path):
            raise IOError("Not gonna remove this")

        monkeypatch.setattr("os.remove", mock_remove)

        project.frontend.reset()
        status = unprepare(project, result)
        assert project.frontend.logs == []
        assert status.status_description == ('Failed to remove %s: Not gonna remove this.' % filename)
        assert status.errors == []
        assert not status
        assert os.path.exists(filename)

        monkeypatch.undo()  # so os.remove isn't broken during directory cleanup

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_provide_minimal(monkeypatch):
    MIN_DATAFILE_CONTENT = ("downloads:\n" "    DATAFILE: http://localhost/data.csv\n")

    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            with open(os.path.join(dirname, 'data.csv'), 'w') as out:
                out.write('data')
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: MIN_DATAFILE_CONTENT}, provide_download)


def test_provide_no_download_in_check_mode(monkeypatch):
    MIN_DATAFILE_CONTENT = ("downloads:\n" "    DATAFILE: http://localhost/data.csv\n")

    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            raise Exception("should not have tried to download in check mode")

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project,
                                             environ=minimal_environ(PROJECT_DIR=dirname),
                                             mode=provide.PROVIDE_MODE_CHECK)
        assert not result

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: MIN_DATAFILE_CONTENT}, provide_download)


def test_provide_missing_url(monkeypatch):
    ERR_DATAFILE_CONTENT = ("downloads:\n" "    DATAFILE:\n" "       filename: data.csv\n")

    def provide_download(dirname):
        project = project_no_dedicated_env(dirname)
        prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert "Download item DATAFILE doesn't contain a 'url' field." in project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: ERR_DATAFILE_CONTENT}, provide_download)


def test_provide_empty_url(monkeypatch):
    ERR_DATAFILE_CONTENT = ("downloads:\n" "    DATAFILE:\n" "       url: \"\"\n")

    def provide_download(dirname):
        project = project_no_dedicated_env(dirname)
        prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert "Download item DATAFILE has an empty 'url' field." in project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: ERR_DATAFILE_CONTENT}, provide_download)


def test_provide_multiple_checksums(monkeypatch):
    ERR_DATAFILE_CONTENT = ("downloads:\n"
                            "    DATAFILE:\n"
                            "       url: http://localhost/\n"
                            "       md5: abcdefg\n"
                            "       sha1: abcdefg\n")

    def provide_download(dirname):
        project = project_no_dedicated_env(dirname)
        prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert "Multiple checksums for download DATAFILE: md5 and sha1." in project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: ERR_DATAFILE_CONTENT}, provide_download)


def test_provide_wrong_form(monkeypatch):
    ERR_DATAFILE_CONTENT = ("downloads:\n" "    - http://localhost/data.csv\n")

    def provide_download(dirname):
        project = project_no_dedicated_env(dirname)
        prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert ("%s: 'downloads:' section should be a dictionary, found ['http://localhost/data.csv']" %
                DEFAULT_PROJECT_FILENAME) in project.problems

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: ERR_DATAFILE_CONTENT}, provide_download)


def test_failed_download(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 400
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert not result
        assert ('missing requirement to run this project: A downloaded file which is referenced by DATAFILE.'
                ) in result.errors

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_failed_download_before_connect(monkeypatch):
    def provide_download(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            # if we don't even get an HTTP response, the errors are handled this way,
            # e.g. if the URL is bad.
            self._errors = ['This went horribly wrong']
            raise gen.Return(None)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)
        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert not result
        assert ('missing requirement to run this project: A downloaded file which is referenced by DATAFILE.'
                ) in result.errors

    with_directory_contents_completing_project_file({DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT}, provide_download)


def test_file_exists(monkeypatch):
    def provide_download(dirname):
        FILENAME = os.path.join(dirname, 'data.csv')
        requirement = _download_requirement()
        local_state_file = LocalStateFile.load_for_directory(dirname)
        local_state_file.set_service_run_state(requirement.env_var, {'filename': FILENAME})
        local_state_file.save()
        with open(FILENAME, 'w') as out:
            out.write('data')
        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ

    LOCAL_STATE = ("DATAFILE:\n" "  filename: data.csv")

    with_directory_contents_completing_project_file(
        {
            DEFAULT_PROJECT_FILENAME: DATAFILE_CONTENT,
            DEFAULT_LOCAL_STATE_FILENAME: LOCAL_STATE
        }, provide_download)


def test_prepare_download_of_zip_file(monkeypatch):
    def provide_download_of_zip(zipname, dirname):
        with codecs.open(os.path.join(dirname, DEFAULT_PROJECT_FILENAME), 'w', 'utf-8') as f:
            f.write(complete_project_file_content(ZIPPED_DATAFILE_CONTENT))

        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            assert self._url.endswith(".zip")
            assert self._filename.endswith(".zip")
            shutil.copyfile(zipname, self._filename)
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        assert os.path.isdir(os.path.join(dirname, 'data'))
        assert os.path.isfile(os.path.join(dirname, 'data', 'foo'))
        assert codecs.open(os.path.join(dirname, 'data', 'foo')).read() == 'hello\n'

    with_tmp_zipfile(dict(foo='hello\n'), provide_download_of_zip)


def test_prepare_download_of_zip_file_checksum(monkeypatch):
    def provide_download_of_zip(zipname, dirname):
        with codecs.open(os.path.join(dirname, DEFAULT_PROJECT_FILENAME), 'w', 'utf-8') as f:
            f.write(complete_project_file_content(ZIPPED_DATAFILE_CONTENT_CHECKSUM))

        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            assert self._url.endswith(".zip")
            assert self._filename.endswith(".zip")
            shutil.copyfile(zipname, self._filename)
            self._hash = '12345abcdef'
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)
        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        assert os.path.isdir(os.path.join(dirname, 'data'))
        assert os.path.isfile(os.path.join(dirname, 'data', 'foo'))
        assert codecs.open(os.path.join(dirname, 'data', 'foo')).read() == 'hello\n'

        project.frontend.reset()
        status = unprepare(project, result)
        filename = os.path.join(dirname, 'data')
        assert project.frontend.logs == ["Removed downloaded file %s." % filename,
                                         ("Current environment is not in %s, no need to delete it." % dirname)]
        assert status.status_description == "Success."

    with_tmp_zipfile(dict(foo='hello\n'), provide_download_of_zip)


def test_prepare_download_of_zip_file_no_unzip(monkeypatch):
    def provide_download_of_zip_no_unzip(zipname, dirname):
        with codecs.open(os.path.join(dirname, DEFAULT_PROJECT_FILENAME), 'w', 'utf-8') as f:
            f.write(complete_project_file_content(ZIPPED_DATAFILE_CONTENT_NO_UNZIP))

        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            assert self._url.endswith(".zip")
            # we aren't going to unzip so we should be downloading straignt to
            # the specified filename 'data' without the .zip on it
            assert not self._filename.endswith(".zip")
            shutil.copyfile(zipname, self._filename)
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        assert os.path.isfile(os.path.join(dirname, 'data'))
        with zipfile.ZipFile(os.path.join(dirname, 'data')) as zf:
            assert zf.namelist() == ['foo']

    with_tmp_zipfile(dict(foo='hello\n'), provide_download_of_zip_no_unzip)


def test_prepare_download_of_zip_file_no_zip_extension(monkeypatch):
    def provide_download_of_zip(zipname, dirname):
        with codecs.open(os.path.join(dirname, DEFAULT_PROJECT_FILENAME), 'w', 'utf-8') as f:
            f.write(complete_project_file_content(ZIPPED_DATAFILE_CONTENT_NO_ZIP_SUFFIX))

        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            # we add .zip to the download filename, even though it wasn't in the URL
            assert not self._url.endswith(".zip")
            assert self._filename.endswith(".zip")
            shutil.copyfile(zipname, self._filename)
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert hasattr(result, 'environ')
        assert 'DATAFILE' in result.environ
        assert os.path.isdir(os.path.join(dirname, 'data'))
        assert os.path.isfile(os.path.join(dirname, 'data', 'foo'))
        assert codecs.open(os.path.join(dirname, 'data', 'foo')).read() == 'hello\n'

    with_tmp_zipfile(dict(foo='hello\n'), provide_download_of_zip)


def test_prepare_download_of_broken_zip_file(monkeypatch):
    def provide_download_of_zip(dirname):
        with codecs.open(os.path.join(dirname, DEFAULT_PROJECT_FILENAME), 'w', 'utf-8') as f:
            f.write(complete_project_file_content(ZIPPED_DATAFILE_CONTENT))

        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            assert self._url.endswith(".zip")
            assert self._filename.endswith(".zip")
            with codecs.open(self._filename, 'w', 'utf-8') as f:
                f.write("This is not a zip file.")
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)

        result = prepare_without_interaction(project, environ=minimal_environ(PROJECT_DIR=dirname))
        assert not result
        assert [("Failed to unzip %s: File is not a zip file" % os.path.join(dirname, "data.zip")),
                "missing requirement to run this project: A downloaded file which is referenced by DATAFILE.",
                "  Environment variable DATAFILE is not set."] == result.errors

    with_directory_contents(dict(), provide_download_of_zip)


def _download_status(prepare_context):
    for status in prepare_context.statuses:
        if isinstance(status.requirement, DownloadRequirement):
            return status
    return None


def test_configure(monkeypatch):
    def check(dirname):
        @gen.coroutine
        def mock_downloader_run(self, loop):
            class Res:
                pass

            res = Res()
            res.code = 200
            with open(os.path.join(dirname, 'data.csv'), 'w') as out:
                out.write('data')
            self._hash = '12345abcdef'
            raise gen.Return(res)

        monkeypatch.setattr("anaconda_project.internal.http_client.FileDownloader.run", mock_downloader_run)

        project = project_no_dedicated_env(dirname)
        environ = minimal_environ(PROJECT_DIR=dirname)
        stage = prepare_in_stages(project, environ=environ)
        status = None
        while status is None and stage is not None:
            prepare_context = stage.configure()
            status = _download_status(prepare_context)
            if status is None:
                stage = stage.execute()

        assert status is not None

        req = status.requirement
        provider = status.provider

        # check initial config

        config = provider.read_config(req, prepare_context.environ, prepare_context.local_state_file,
                                      prepare_context.default_env_spec_name, prepare_context.overrides)

        assert dict(source='download') == config

        config['source'] = 'environ'
        config['value'] = 'abc.txt'

        provider.set_config_values_as_strings(req, prepare_context.environ, prepare_context.local_state_file,
                                              prepare_context.default_env_spec_name, prepare_context.overrides, config)

        config = provider.read_config(req, prepare_context.environ, prepare_context.local_state_file,
                                      prepare_context.default_env_spec_name, prepare_context.overrides)

        assert dict(source='download', value='abc.txt') == config

        config['source'] = 'variables'
        config['value'] = 'qrs.txt'
        provider.set_config_values_as_strings(req, prepare_context.environ, prepare_context.local_state_file,
                                              prepare_context.default_env_spec_name, prepare_context.overrides, config)

        config = provider.read_config(req, prepare_context.environ, prepare_context.local_state_file,
                                      prepare_context.default_env_spec_name, prepare_context.overrides)

        assert dict(source='variables', value='qrs.txt') == config

    with_directory_contents_completing_project_file(
        {DEFAULT_PROJECT_FILENAME: """
downloads:
  FOO: http://example.com/data.csv
    """}, check)
