# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import tempfile
import time
import unittest

import mock
import version


def _ReplaceArgs(args, *replacements):
    new_args = args[:]
    for flag, val in replacements:
        flag_index = args.index(flag)
        new_args[flag_index + 1] = val
    return new_args


class _VersionTest(unittest.TestCase):
    """Unittests for the version module."""

    _CHROME_VERSION_FILE = os.path.join(
        os.path.dirname(__file__), os.pardir, os.pardir, 'chrome', 'VERSION'
    )

    _SCRIPT = os.path.join(os.path.dirname(__file__), 'version.py')

    _EXAMPLE_VERSION = {
        'MAJOR': '74',
        'MINOR': '0',
        'BUILD': '3720',
        'PATCH': '0',
    }

    _EXAMPLE_TEMPLATE = (
        'full = "@MAJOR@.@MINOR@.@BUILD@.@PATCH@" '
        'major = "@MAJOR@" minor = "@MINOR@" '
        'build = "@BUILD@" patch = "@PATCH@" version_id = @VERSION_ID@ '
    )

    _ANDROID_CHROME_VARS = [
        'chrome_version_code',
        'monochrome_version_code',
        'trichrome_version_code',
        'webview_stable_version_code',
        'webview_beta_version_code',
        'webview_dev_version_code',
    ]

    _EXAMPLE_ANDROID_TEMPLATE = _EXAMPLE_TEMPLATE + ''.join(
        ['%s = "@%s@" ' % (el, el.upper()) for el in _ANDROID_CHROME_VARS]
    )

    _EXAMPLE_ARGS = [
        '-f',
        _CHROME_VERSION_FILE,
        '-t',
        _EXAMPLE_TEMPLATE,
    ]

    _EXAMPLE_ANDROID_ARGS = _ReplaceArgs(
        _EXAMPLE_ARGS, ['-t', _EXAMPLE_ANDROID_TEMPLATE]
    ) + [
        '-a',
        'arm',
        '--os',
        'android',
    ]

    @staticmethod
    def _RunBuildOutput(
        new_version_values={}, get_new_args=lambda old_args: old_args
    ):
        """Parameterized helper method for running the main testable method in
        version.py.

        Keyword arguments:
        new_version_values -- dict used to update _EXAMPLE_VERSION
        get_new_args -- lambda for updating _EXAMPLE_ANDROID_ARGS
        """

        with mock.patch(
            'version.FetchValuesFromFile'
        ) as fetch_values_from_file_mock:
            fetch_values_from_file_mock.side_effect = lambda values, file: (
                values.update(
                    dict(_VersionTest._EXAMPLE_VERSION, **new_version_values)
                )
            )

            new_args = get_new_args(_VersionTest._EXAMPLE_ARGS)
            return version.BuildOutput(new_args)

    def testFetchValuesFromFile(self):
        """It returns a dict in correct format - { <str>: <str> }, to verify
        assumption of other tests that mock this function
        """
        result = {}
        version.FetchValuesFromFile(result, self._CHROME_VERSION_FILE)

        for key, val in result.items():
            self.assertIsInstance(key, str)
            self.assertIsInstance(val, str)

    def testBuildOutputAndroid(self):
        """Assert it gives includes assignments of expected variables"""
        output = self._RunBuildOutput(
            get_new_args=lambda args: self._EXAMPLE_ANDROID_ARGS
        )
        contents = output['contents']

        self.assertRegex(contents, r'\bchrome_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bmonochrome_version_code = "\d+"\s')
        self.assertRegex(contents, r'\btrichrome_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bwebview_stable_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bwebview_beta_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bwebview_dev_version_code = "\d+"\s')

    def testBuildOutputAndroidArchVariantsArm64(self):
        """Assert 64-bit-specific version codes"""
        new_template = (
            self._EXAMPLE_ANDROID_TEMPLATE
            + "monochrome_64_32_version_code = \"@MONOCHROME_64_32_VERSION_CODE@\" "
            "monochrome_64_version_code = \"@MONOCHROME_64_VERSION_CODE@\" "
            "trichrome_64_32_version_code = \"@TRICHROME_64_32_VERSION_CODE@\" "
            "trichrome_64_version_code = \"@TRICHROME_64_VERSION_CODE@\" "
        )
        args_with_template = _ReplaceArgs(
            self._EXAMPLE_ANDROID_ARGS, ['-t', new_template]
        )
        new_args = _ReplaceArgs(args_with_template, ['-a', 'arm64'])
        output = self._RunBuildOutput(get_new_args=lambda args: new_args)
        contents = output['contents']

        self.assertRegex(contents, r'\bmonochrome_64_32_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bmonochrome_64_version_code = "\d+"\s')
        self.assertRegex(contents, r'\btrichrome_64_32_version_code = "\d+"\s')
        self.assertRegex(contents, r'\btrichrome_64_version_code = "\d+"\s')

    def testBuildOutputAndroidArchVariantsX64(self):
        """Assert 64-bit-specific version codes"""
        new_template = (
            self._EXAMPLE_ANDROID_TEMPLATE
            + "monochrome_64_32_version_code = \"@MONOCHROME_64_32_VERSION_CODE@\" "
            "monochrome_64_version_code = \"@MONOCHROME_64_VERSION_CODE@\" "
            "trichrome_64_32_version_code = \"@TRICHROME_64_32_VERSION_CODE@\" "
            "trichrome_64_version_code = \"@TRICHROME_64_VERSION_CODE@\" "
        )
        args_with_template = _ReplaceArgs(
            self._EXAMPLE_ANDROID_ARGS, ['-t', new_template]
        )
        new_args = _ReplaceArgs(args_with_template, ['-a', 'x64'])
        output = self._RunBuildOutput(get_new_args=lambda args: new_args)
        contents = output['contents']

        self.assertRegex(contents, r'\bmonochrome_64_32_version_code = "\d+"\s')
        self.assertRegex(contents, r'\bmonochrome_64_version_code = "\d+"\s')
        self.assertRegex(contents, r'\btrichrome_64_32_version_code = "\d+"\s')
        self.assertRegex(contents, r'\btrichrome_64_version_code = "\d+"\s')

    def testBuildOutputAndroidChromeArchInput(self):
        """Assert it raises an exception when using an invalid architecture input"""
        new_args = _ReplaceArgs(self._EXAMPLE_ANDROID_ARGS, ['-a', 'foobar'])
        # Mock sys.stderr because argparse will print to stderr when we pass
        # the invalid '-a' value.
        with self.assertRaises(SystemExit) as cm, mock.patch('sys.stderr'):
            self._RunBuildOutput(get_new_args=lambda args: new_args)

        self.assertEqual(cm.exception.code, 2)

    def testSetExecutable(self):
        """Assert that -x sets executable on POSIX and is harmless on Windows."""
        with tempfile.TemporaryDirectory() as tmpdir:
            in_file = os.path.join(tmpdir, "in")
            out_file = os.path.join(tmpdir, "out")
            with open(in_file, "w") as f:
                f.write("")
            self.assertEqual(
                version.main(['-i', in_file, '-o', out_file, '-x']), 0
            )

            # Whether lstat(out_file).st_mode has the executable bits set is
            # platform-specific. Therefore, test that out_file has the same
            # permissions that in_file would have after chmod(in_file, 0o755).
            # On Windows: both files will have 0o666.
            # On POSIX: both files will have 0o755.
            os.chmod(in_file, 0o755)  # On Windows, this sets in_file to 0o666.
            self.assertEqual(
                os.lstat(in_file).st_mode, os.lstat(out_file).st_mode
            )

    def testWriteIfChangedUpdateWhenContentChanged(self):
        """Assert it updates mtime of file when content is changed."""
        with tempfile.TemporaryDirectory() as tmpdir:
            file_name = os.path.join(tmpdir, "version.h")
            old_contents = "old contents"
            with open(file_name, "w") as f:
                f.write(old_contents)
            os.chmod(file_name, 0o644)
            mtime = os.lstat(file_name).st_mtime
            time.sleep(0.1)
            contents = "new contents"
            version.WriteIfChanged(file_name, contents, 0o644)
            with open(file_name) as f:
                self.assertEqual(contents, f.read())
            self.assertNotEqual(mtime, os.lstat(file_name).st_mtime)

    def testWriteIfChangedUpdateWhenModeChanged(self):
        """Assert it updates mtime of file when mode is changed."""
        with tempfile.TemporaryDirectory() as tmpdir:
            file_name = os.path.join(tmpdir, "version.h")
            contents = "old contents"
            with open(file_name, "w") as f:
                f.write(contents)
            os.chmod(file_name, 0o644)
            mtime = os.lstat(file_name).st_mtime
            time.sleep(0.1)
            version.WriteIfChanged(file_name, contents, 0o755)
            with open(file_name) as f:
                self.assertEqual(contents, f.read())
            self.assertNotEqual(mtime, os.lstat(file_name).st_mtime)

    def testWriteIfChangedNoUpdate(self):
        """Assert it does not update mtime of file when nothing is changed."""
        with tempfile.TemporaryDirectory() as tmpdir:
            file_name = os.path.join(tmpdir, "version.h")
            contents = "old contents"
            with open(file_name, "w") as f:
                f.write(contents)
            os.chmod(file_name, 0o644)
            mtime = os.lstat(file_name).st_mtime
            time.sleep(0.1)
            version.WriteIfChanged(file_name, contents, 0o644)
            with open(file_name) as f:
                self.assertEqual(contents, f.read())
            self.assertEqual(mtime, os.lstat(file_name).st_mtime)


if __name__ == '__main__':
    unittest.main()
