#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Download image unittest."""


import os
import re
import unittest
import unittest.mock as mock

from cros_utils import command_executer
from cros_utils import logger
import download_images
import test_flag


MOCK_LOGGER = logger.GetLogger(log_dir="", mock=True)


class RegexMatcher:
    """A regex matcher, for passing to mocks."""

    def __init__(self, regex):
        self._regex = re.compile(regex)

    def __eq__(self, string):
        return self._regex.search(string) is not None


class ImageDownloaderTestcast(unittest.TestCase):
    """The image downloader test class."""

    def __init__(self, *args, **kwargs):
        super(ImageDownloaderTestcast, self).__init__(*args, **kwargs)
        self.called_download_image = False
        self.called_uncompress_image = False
        self.called_get_build_id = False
        self.called_download_autotest_files = False
        self.called_download_debug_file = False

    @mock.patch.object(os, "makedirs")
    @mock.patch.object(os.path, "exists")
    def test_download_image(self, mock_path_exists, mock_mkdirs):
        # Set mock and test values.
        mock_cmd_exec = mock.Mock(spec=command_executer.CommandExecuter)
        test_chroot = "/usr/local/home/chromeos"
        test_build_id = "lumpy-release/R36-5814.0.0"
        image_path = (
            "gs://chromeos-image-archive/%s/chromiumos_test_image.tar.xz"
            % test_build_id
        )

        downloader = download_images.ImageDownloader(
            logger_to_use=MOCK_LOGGER, cmd_exec=mock_cmd_exec
        )

        # Set os.path.exists to always return False and run downloader
        mock_path_exists.return_value = False
        test_flag.SetTestMode(True)
        self.assertRaises(
            download_images.MissingImage,
            downloader.DownloadImage,
            test_chroot,
            test_build_id,
            image_path,
        )

        # Verify os.path.exists was called thrice, with proper arguments.
        self.assertEqual(mock_path_exists.call_count, 3)
        mock_path_exists.assert_any_call(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/"
                "R36-5814.0.0/chromiumos_test_image.bin"
            )
        )
        mock_path_exists.assert_any_call(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0"
            )
        )
        mock_path_exists.assert_any_call("/etc/cros_chroot_version")

        # Verify we called os.mkdirs
        self.assertEqual(mock_mkdirs.call_count, 1)
        mock_mkdirs.assert_called_with(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0"
            )
        )

        # Verify we called RunCommand once, with proper arguments.
        self.assertEqual(mock_cmd_exec.RunCommand.call_count, 1)
        expected_args = RegexMatcher(
            "/usr/local/home/chromeos/src/chromium/depot_tools/gsutil.py "
            "cp gs://chromeos-image-archive/lumpy-release/R36-5814.0.0/"
            "chromiumos_test_image.tar.xz "
            "/usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0"
        )

        mock_cmd_exec.RunCommand.assert_called_with(expected_args)

        # Reset the velues in the mocks; set os.path.exists to always return
        # True (except for "inside chroot" check).
        mock_path_exists.reset_mock()
        mock_cmd_exec.reset_mock()
        mock_path_exists.side_effect = lambda x: x != "/etc/cros_chroot_version"

        # Run downloader
        downloader.DownloadImage(test_chroot, test_build_id, image_path)

        # Verify os.path.exists was called thrice, with proper arguments.
        self.assertEqual(mock_path_exists.call_count, 3)
        mock_path_exists.assert_called_with(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/"
                "R36-5814.0.0/chromiumos_test_image.bin"
            )
        )
        mock_path_exists.assert_any_call(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0"
            )
        )
        mock_path_exists.assert_any_call("/etc/cros_chroot_version")

        # Verify we made no RunCommand or ChrootRunCommand calls (since
        # os.path.exists returned True, there was no work do be done).
        self.assertEqual(mock_cmd_exec.RunCommand.call_count, 0)
        self.assertEqual(mock_cmd_exec.ChrootRunCommand.call_count, 0)

    @mock.patch.object(os.path, "exists")
    def test_uncompress_image(self, mock_path_exists):
        # set mock and test values.
        mock_cmd_exec = mock.Mock(spec=command_executer.CommandExecuter)
        test_chroot = "/usr/local/home/chromeos"
        test_build_id = "lumpy-release/R36-5814.0.0"

        downloader = download_images.ImageDownloader(
            logger_to_use=MOCK_LOGGER, cmd_exec=mock_cmd_exec
        )

        # Set os.path.exists to always return False and run uncompress.
        mock_path_exists.return_value = False
        self.assertRaises(
            download_images.MissingImage,
            downloader.UncompressImage,
            test_chroot,
            test_build_id,
        )

        # Verify os.path.exists was called twice, with correct arguments.
        self.assertEqual(mock_path_exists.call_count, 2)
        mock_path_exists.assert_called_with(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/"
                "R36-5814.0.0/chromiumos_test_image.bin"
            )
        )
        mock_path_exists.assert_any_call("/etc/cros_chroot_version")

        # Verify RunCommand was called twice with correct arguments.
        self.assertEqual(mock_cmd_exec.RunCommand.call_count, 2)
        # Call 1, should have 2 arguments
        self.assertEqual(len(mock_cmd_exec.RunCommand.call_args_list[0]), 2)
        actual_arg = mock_cmd_exec.RunCommand.call_args_list[0][0]
        expected_arg = (
            RegexMatcher(
                "cd /usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0 ; "
                "tar -Jxf chromiumos_test_image.tar.xz "
            ),
        )
        self.assertEqual(expected_arg, actual_arg)
        # 2nd arg must be exception handler
        except_handler_string = "RunCommandExceptionHandler.HandleException"
        self.assertTrue(
            except_handler_string
            in repr(mock_cmd_exec.RunCommand.call_args_list[0][1])
        )

        # Call 2, should have 2 arguments
        self.assertEqual(len(mock_cmd_exec.RunCommand.call_args_list[1]), 2)
        actual_arg = mock_cmd_exec.RunCommand.call_args_list[1][0]
        expected_arg = (
            RegexMatcher(
                "cd /usr/local/home/chromeos/.*tmp/lumpy-release/R36-5814.0.0 ; "
                "rm -f chromiumos_test_image.bin "
            ),
        )
        self.assertEqual(expected_arg, actual_arg)
        # 2nd arg must be empty
        self.assertTrue(
            "{}" in repr(mock_cmd_exec.RunCommand.call_args_list[1][1])
        )

        # Set os.path.exists to always return True (except for "inside chroot"
        # check) and run uncompress.
        mock_path_exists.reset_mock()
        mock_cmd_exec.reset_mock()
        mock_path_exists.side_effect = lambda x: x != "/etc/cros_chroot_version"
        downloader.UncompressImage(test_chroot, test_build_id)

        # Verify os.path.exists was called once, with correct arguments.
        self.assertEqual(mock_path_exists.call_count, 2)
        mock_path_exists.assert_called_with(
            RegexMatcher(
                "/usr/local/home/chromeos/.*tmp/lumpy-release/"
                "R36-5814.0.0/chromiumos_test_image.bin"
            )
        )
        mock_path_exists.assert_any_call("/etc/cros_chroot_version")

        # Verify RunCommand was not called.
        self.assertEqual(mock_cmd_exec.RunCommand.call_count, 0)

    def test_run(self):
        # Set test arguments
        test_chroot = "/usr/local/home/chromeos"
        test_build_id = "remote/lumpy/latest-dev"
        test_empty_autotest_path = ""
        test_empty_debug_path = ""
        test_autotest_path = "/tmp/autotest"
        test_debug_path = "/tmp/debug"
        download_debug = True

        # Set values to test/check.
        self.called_download_image = False
        self.called_uncompress_image = False
        self.called_get_build_id = False
        self.called_download_autotest_files = False
        self.called_download_debug_file = False

        # Define fake stub functions for Run to call
        def FakeGetBuildID(unused_root, unused_xbuddy_label):
            self.called_get_build_id = True
            return "lumpy-release/R36-5814.0.0"

        def GoodDownloadImage(root, build_id, image_path):
            if root or build_id or image_path:
                pass
            self.called_download_image = True
            return "chromiumos_test_image.bin"

        def BadDownloadImage(root, build_id, image_path):
            if root or build_id or image_path:
                pass
            self.called_download_image = True
            raise download_images.MissingImage("Could not download image")

        def FakeUncompressImage(root, build_id):
            if root or build_id:
                pass
            self.called_uncompress_image = True
            return 0

        def FakeDownloadAutotestFiles(root, build_id):
            if root or build_id:
                pass
            self.called_download_autotest_files = True
            return "autotest"

        def FakeDownloadDebugFile(root, build_id):
            if root or build_id:
                pass
            self.called_download_debug_file = True
            return "debug"

        # Initialize downloader
        downloader = download_images.ImageDownloader(logger_to_use=MOCK_LOGGER)

        # Set downloader to call fake stubs.
        downloader.GetBuildID = FakeGetBuildID
        downloader.UncompressImage = FakeUncompressImage
        downloader.DownloadImage = GoodDownloadImage
        downloader.DownloadAutotestFiles = FakeDownloadAutotestFiles
        downloader.DownloadDebugFile = FakeDownloadDebugFile

        # Call Run.
        image_path, autotest_path, debug_path = downloader.Run(
            test_chroot,
            test_build_id,
            test_empty_autotest_path,
            test_empty_debug_path,
            download_debug,
        )

        # Make sure it called both _DownloadImage and _UncompressImage
        self.assertTrue(self.called_download_image)
        self.assertTrue(self.called_uncompress_image)
        # Make sure it called DownloadAutotestFiles
        self.assertTrue(self.called_download_autotest_files)
        # Make sure it called DownloadDebugFile
        self.assertTrue(self.called_download_debug_file)
        # Make sure it returned an image and autotest path returned from this call
        self.assertTrue(image_path == "chromiumos_test_image.bin")
        self.assertTrue(autotest_path == "autotest")
        self.assertTrue(debug_path == "debug")

        # Call Run with a non-empty autotest and debug path
        self.called_download_autotest_files = False
        self.called_download_debug_file = False

        image_path, autotest_path, debug_path = downloader.Run(
            test_chroot,
            test_build_id,
            test_autotest_path,
            test_debug_path,
            download_debug,
        )

        # Verify that downloadAutotestFiles was not called
        self.assertFalse(self.called_download_autotest_files)
        # Make sure it returned the specified autotest path returned from this call
        self.assertTrue(autotest_path == test_autotest_path)
        # Make sure it returned the specified debug path returned from this call
        self.assertTrue(debug_path == test_debug_path)

        # Reset values; Now use fake stub that simulates DownloadImage failing.
        self.called_download_image = False
        self.called_uncompress_image = False
        self.called_download_autotest_files = False
        self.called_download_debug_file = False
        downloader.DownloadImage = BadDownloadImage

        # Call Run again.
        self.assertRaises(
            download_images.MissingImage,
            downloader.Run,
            test_chroot,
            test_autotest_path,
            test_debug_path,
            test_build_id,
            download_debug,
        )

        # Verify that UncompressImage and downloadAutotestFiles were not called,
        # since _DownloadImage "failed"
        self.assertTrue(self.called_download_image)
        self.assertFalse(self.called_uncompress_image)
        self.assertFalse(self.called_download_autotest_files)
        self.assertFalse(self.called_download_debug_file)


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