# -*- coding: utf-8 -*-
# Copyright 2013 Google Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""Tests for the update command."""

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals

import os.path
import shutil
import subprocess
import sys
import tarfile

import boto
import gslib
from gslib.metrics import _UUID_FILE_PATH
import gslib.tests.testcase as testcase
from gslib.tests.util import ObjectToURI as suri
from gslib.tests.util import unittest
from gslib.utils import system_util
from gslib.utils.boto_util import CERTIFICATE_VALIDATION_ENABLED
from gslib.utils.constants import UTF8
from gslib.utils.update_util import DisallowUpdateIfDataInGsutilDir

TESTS_DIR = os.path.abspath(os.path.dirname(__file__))
GSUTIL_DIR = os.path.join(TESTS_DIR, '..', '..')


class UpdateTest(testcase.GsUtilIntegrationTestCase):
  """Update command test suite."""

  @unittest.skipUnless(CERTIFICATE_VALIDATION_ENABLED,
                       'Test requires https certificate validation enabled.')
  def test_update(self):
    """Tests that the update command works or raises proper exceptions."""
    if system_util.InvokedViaCloudSdk():
      stderr = self.RunGsUtil(['update'],
                              stdin='n',
                              return_stderr=True,
                              expected_status=1)
      self.assertIn('update command is disabled for Cloud SDK', stderr)
      return

    if gslib.IS_PACKAGE_INSTALL:
      # The update command is not present when installed via package manager.
      stderr = self.RunGsUtil(['update'], return_stderr=True, expected_status=1)
      self.assertIn('Invalid command', stderr)
      return

    # Create two temp directories, one of which we will run 'gsutil update' in
    # to pull the changes from the other.
    tmpdir_src = self.CreateTempDir()
    tmpdir_dst = self.CreateTempDir()

    # Copy gsutil to both source and destination directories.
    gsutil_src = os.path.join(tmpdir_src, 'gsutil')
    gsutil_dst = os.path.join(tmpdir_dst, 'gsutil')
    # Path when executing from tmpdir (Windows doesn't support in-place rename)
    gsutil_relative_dst = os.path.join('gsutil', 'gsutil')

    ignore_callable = shutil.ignore_patterns(
        '.git*',
        '*.pyc',
        '*.pyo',
        '__pycache__',
    )
    shutil.copytree(GSUTIL_DIR, gsutil_src, ignore=ignore_callable)
    # Copy specific files rather than all of GSUTIL_DIR so we don't pick up temp
    # working files left in top-level directory by gsutil developers (like tags,
    # .git*, .pyc files, etc.)
    os.makedirs(gsutil_dst)
    for comp in os.listdir(GSUTIL_DIR):
      if ('.git' not in comp and
          '__pycache__' not in comp and
           not comp.endswith('.pyc') and
           not comp.endswith('.pyo')):  # yapf: disable
        cp_src_path = os.path.join(GSUTIL_DIR, comp)
        cp_dst_path = os.path.join(gsutil_dst, comp)
        if os.path.isdir(cp_src_path):
          shutil.copytree(cp_src_path, cp_dst_path, ignore=ignore_callable)
        else:
          shutil.copyfile(cp_src_path, cp_dst_path)

    # Create a fake version number in the source so we can verify it in the
    # destination.
    expected_version = '17.25'
    src_version_file = os.path.join(gsutil_src, 'VERSION')
    self.assertTrue(os.path.exists(src_version_file))
    with open(src_version_file, 'w') as f:
      f.write(expected_version)

    # Create a tarball out of the source directory and copy it to a bucket.
    src_tarball = os.path.join(tmpdir_src, 'gsutil.test.tar.gz')

    normpath = os.path.normpath
    try:
      # We monkey patch os.path.normpath here because the tarfile module
      # normalizes the ./gsutil path, but the update command expects the tar
      # file to be prefixed with . This preserves the ./gsutil path.
      os.path.normpath = lambda fname: fname
      tar = tarfile.open(src_tarball, 'w:gz')
      tar.add(gsutil_src, arcname='./gsutil')
      tar.close()
    finally:
      os.path.normpath = normpath

    prefix = [sys.executable] if sys.executable else []

    # Run with an invalid gs:// URI.
    p = subprocess.Popen(prefix + ['gsutil', 'update', 'gs://pub'],
                         cwd=gsutil_dst,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    (_, stderr) = p.communicate()
    p.stdout.close()
    p.stderr.close()
    self.assertEqual(p.returncode, 1)
    self.assertIn(b'update command only works with tar.gz', stderr)

    # Run with non-existent gs:// URI.
    p = subprocess.Popen(prefix +
                         ['gsutil', 'update', 'gs://pub/Jdjh38)(;.tar.gz'],
                         cwd=gsutil_dst,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    (_, stderr) = p.communicate()
    p.stdout.close()
    p.stderr.close()
    self.assertEqual(p.returncode, 1)
    self.assertIn(b'NotFoundException', stderr)

    # Run with file:// URI wihout -f option.
    p = subprocess.Popen(
        prefix + ['gsutil', 'update', suri(src_tarball)],
        cwd=gsutil_dst,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE)
    (_, stderr) = p.communicate()
    p.stdout.close()
    p.stderr.close()
    self.assertEqual(p.returncode, 1)
    self.assertIn(b'command does not support', stderr)

    # Run with a file present that was not distributed with gsutil.
    with open(os.path.join(gsutil_dst, 'userdata.txt'), 'w') as fp:
      fp.write('important data\n')
    p = subprocess.Popen(
        prefix +
        ['gsutil', 'update', '-f', suri(src_tarball)],
        cwd=gsutil_dst,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stdin=subprocess.PIPE)
    (_, stderr) = p.communicate()
    p.stdout.close()
    p.stderr.close()
    # Clean up before next test, and before assertions so failure doesn't leave
    # this file around.
    os.unlink(os.path.join(gsutil_dst, 'userdata.txt'))
    self.assertEqual(p.returncode, 1)
    # Additional check for Windows since it has \r\n and string may have just \n
    os_ls = os.linesep.encode(UTF8)
    if os_ls in stderr:
      stderr = stderr.replace(os_ls, b' ')
    elif b'\n' in stderr:
      stderr = stderr.replace(b'\n', b' ')
    self.assertIn(
        b'The update command cannot run with user data in the gsutil directory',
        stderr)

    # Determine whether we'll need to decline the analytics prompt.
    analytics_prompt = not (os.path.exists(_UUID_FILE_PATH) or
                            boto.config.get_value('GSUtil',
                                                  'disable_analytics_prompt'))

    update_input = b'n\r\ny\r\n' if analytics_prompt else b'y\r\n'

    # Now do the real update, which should succeed.
    p = subprocess.Popen(
        prefix + [gsutil_relative_dst, 'update', '-f',
                  suri(src_tarball)],
        cwd=tmpdir_dst,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stdin=subprocess.PIPE)
    (_, stderr) = p.communicate(input=update_input)
    p.stdout.close()
    p.stderr.close()
    self.assertEqual(
        p.returncode,
        0,
        msg=('Non-zero return code (%d) from gsutil update. stderr = \n%s' %
             (p.returncode, stderr.decode(UTF8))))

    # Verify that version file was updated.
    dst_version_file = os.path.join(tmpdir_dst, 'gsutil', 'VERSION')
    with open(dst_version_file, 'r') as f:
      self.assertEqual(f.read(), expected_version)

    # If the analytics prompt was given, that means we disabled analytics. We
    # should reset to the default by deleting the UUID file.
    if analytics_prompt:
      os.unlink(_UUID_FILE_PATH)


class UpdateUnitTest(testcase.GsUtilUnitTestCase):
  """Tests the functionality of commands/update.py."""

  @unittest.skipUnless(
      not gslib.IS_PACKAGE_INSTALL,
      'Test is runnable only if gsutil dir is accessible, and update '
      'command is not valid for package installs.')
  def test_repo_matches_manifest(self):
    """Ensure that all files/folders match the manifest."""
    # Create a temp directory and copy specific files to it.
    tmpdir_src = self.CreateTempDir()
    gsutil_src = os.path.join(tmpdir_src, 'gsutil')
    os.makedirs(gsutil_src)
    copy_files = []
    for filename in os.listdir(GSUTIL_DIR):
      if (filename.endswith('.pyc') or filename.startswith('.git') or
          filename == '__pycache__' or filename == '.settings' or
          filename == '.project' or filename == '.pydevproject' or
          filename == '.style.yapf' or filename == '.yapfignore'):
        # Need to ignore any compiled code or Eclipse project folders.
        continue
      copy_files.append(filename)
    for comp in copy_files:
      if os.path.isdir(os.path.join(GSUTIL_DIR, comp)):
        func = shutil.copytree
      else:
        func = shutil.copyfile
      func(os.path.join(GSUTIL_DIR, comp), os.path.join(gsutil_src, comp))
    DisallowUpdateIfDataInGsutilDir(directory=gsutil_src)
