#!/usr/bin/env python3
# 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.

"""Unit tests when handling patches."""

import json
from pathlib import Path
import tempfile
from typing import Callable
import unittest
from unittest import mock

import atomic_write_file
import patch_manager
import patch_utils


class PatchManagerTest(unittest.TestCase):
    """Test class when handling patches of packages."""

    # Simulate behavior of 'os.path.isdir()' when the path is not a directory.
    @mock.patch.object(Path, "is_dir", return_value=False)
    def testInvalidDirectoryPassedAsCommandLineArgument(self, mock_isdir):
        src_dir = "/some/path/that/is/not/a/directory"
        patch_metadata_file = "/some/path/that/is/not/a/file"

        # Verify the exception is raised when the command line argument for
        # '--filesdir_path' or '--src_path' is not a directory.
        with self.assertRaises(ValueError):
            patch_manager.main(
                [
                    "--src_path",
                    src_dir,
                    "--patch_metadata_file",
                    patch_metadata_file,
                ]
            )
        mock_isdir.assert_called_once()

    # Simulate behavior of 'os.path.isfile()' when the patch metadata file is
    # does not exist.
    @mock.patch.object(Path, "is_file", return_value=False)
    def testInvalidPathToPatchMetadataFilePassedAsCommandLineArgument(
        self, mock_isfile
    ):
        src_dir = "/some/path/that/is/not/a/directory"
        patch_metadata_file = "/some/path/that/is/not/a/file"

        # Verify the exception is raised when the command line argument for
        # '--filesdir_path' or '--src_path' is not a directory.
        with mock.patch.object(Path, "is_dir", return_value=True):
            with self.assertRaises(ValueError):
                patch_manager.main(
                    [
                        "--src_path",
                        src_dir,
                        "--patch_metadata_file",
                        patch_metadata_file,
                    ]
                )
        mock_isfile.assert_called_once()

    @mock.patch("builtins.print")
    @mock.patch.object(patch_utils, "git_clean_context")
    def testCheckPatchApplies(self, _, mock_git_clean_context):
        """Tests whether we can apply a single patch for a given svn_version."""
        mock_git_clean_context.return_value = mock.MagicMock()
        with tempfile.TemporaryDirectory(
            prefix="patch_manager_unittest"
        ) as dirname:
            dirpath = Path(dirname)
            patch_entries = [
                patch_utils.PatchEntry(
                    dirpath,
                    metadata=None,
                    platforms=[],
                    rel_patch_path="another.patch",
                    version_range={
                        "from": 9,
                        "until": 20,
                    },
                ),
                patch_utils.PatchEntry(
                    dirpath,
                    metadata=None,
                    platforms=["chromiumos"],
                    rel_patch_path="example.patch",
                    version_range={
                        "from": 1,
                        "until": 10,
                    },
                ),
                patch_utils.PatchEntry(
                    dirpath,
                    metadata=None,
                    platforms=["chromiumos"],
                    rel_patch_path="patch_after.patch",
                    version_range={
                        "from": 1,
                        "until": 5,
                    },
                ),
            ]
            patches_path = dirpath / "PATCHES.json"
            with atomic_write_file.atomic_write(
                patches_path, encoding="utf-8"
            ) as f:
                json.dump([pe.to_dict() for pe in patch_entries], f)

            def _harness1(
                version: int,
                return_value: patch_utils.PatchResult,
                expected: patch_manager.GitBisectionCode,
            ):
                with mock.patch.object(
                    patch_utils.PatchEntry,
                    "apply",
                    return_value=return_value,
                ) as m:
                    result = patch_manager.CheckPatchApplies(
                        version,
                        dirpath,
                        patches_path,
                        "example.patch",
                    )
                    self.assertEqual(result, expected)
                    m.assert_called()

            _harness1(
                1,
                patch_utils.PatchResult(True, {}),
                patch_manager.GitBisectionCode.GOOD,
            )
            _harness1(
                2,
                patch_utils.PatchResult(True, {}),
                patch_manager.GitBisectionCode.GOOD,
            )
            _harness1(
                2,
                patch_utils.PatchResult(False, {}),
                patch_manager.GitBisectionCode.BAD,
            )
            _harness1(
                11,
                patch_utils.PatchResult(False, {}),
                patch_manager.GitBisectionCode.BAD,
            )

            def _harness2(
                version: int,
                application_func: Callable,
                expected: patch_manager.GitBisectionCode,
            ):
                with mock.patch.object(
                    patch_utils,
                    "apply_single_patch_entry",
                    application_func,
                ):
                    result = patch_manager.CheckPatchApplies(
                        version,
                        dirpath,
                        patches_path,
                        "example.patch",
                    )
                    self.assertEqual(result, expected)

            # Check patch can apply and fail with good return codes.
            def _apply_patch_entry_mock1(v, _, patch_entry, _func, **__):
                return patch_entry.can_patch_version(v), None

            _harness2(
                1,
                _apply_patch_entry_mock1,
                patch_manager.GitBisectionCode.GOOD,
            )
            _harness2(
                11,
                _apply_patch_entry_mock1,
                patch_manager.GitBisectionCode.BAD,
            )

            # Early exit check, shouldn't apply later failing patch.
            def _apply_patch_entry_mock2(v, _, patch_entry, _func, **__):
                if (
                    patch_entry.can_patch_version(v)
                    and patch_entry.rel_patch_path == "patch_after.patch"
                ):
                    return False, {"filename": mock.Mock()}
                return True, None

            _harness2(
                1,
                _apply_patch_entry_mock2,
                patch_manager.GitBisectionCode.GOOD,
            )

            # Skip check, should exit early on the first patch.
            def _apply_patch_entry_mock3(v, _, patch_entry, _func, **__):
                if (
                    patch_entry.can_patch_version(v)
                    and patch_entry.rel_patch_path == "another.patch"
                ):
                    return False, {"filename": mock.Mock()}
                return True, None

            _harness2(
                9,
                _apply_patch_entry_mock3,
                patch_manager.GitBisectionCode.SKIP,
            )


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