# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2016-2018, 2020 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
from unittest import mock

from testtools.matchers import Equals, HasLength

from snapcraft.internal import errors
from snapcraft.plugins.v1 import qmake

from . import PluginsV1BaseTestCase


class QMakeTestCase(PluginsV1BaseTestCase):
    def setUp(self):
        super().setUp()

        patcher = mock.patch("snapcraft.internal.common.run")
        self.run_mock = patcher.start()
        self.addCleanup(patcher.stop)

        patcher = mock.patch("sys.stdout")
        patcher.start()
        self.addCleanup(patcher.stop)

        class Options:
            options = []
            qt_version = "qt5"
            project_files = []

        self.options = Options()

    def test_schema(self):
        schema = qmake.QmakePlugin.schema()

        # Verify the presence of all properties
        properties = schema["properties"]
        self.assertTrue(
            "options" in properties, 'Expected "options" to be included in properties'
        )
        self.assertTrue(
            "qt-version" in properties,
            'Expected "qt-version" to be included in properties',
        )

        # Check options property
        options = properties["options"]
        for item in ["type", "minitems", "uniqueItems", "items", "default"]:
            self.assertTrue(
                item in options,
                'Expected {!r} to be included in "options"'.format(item),
            )

        options_type = options["type"]
        self.assertThat(
            options_type,
            Equals("array"),
            'Expected "options" "type" to be "array", but it '
            'was "{}"'.format(options_type),
        )

        options_minitems = options["minitems"]
        self.assertThat(
            options_minitems,
            Equals(1),
            'Expected "options" "minitems" to be 1, but '
            "it was {}".format(options_minitems),
        )

        self.assertTrue(options["uniqueItems"])

        options_default = options["default"]
        self.assertThat(
            options_default,
            Equals([]),
            'Expected "options" "default" to be [], but '
            "it was {}".format(options_default),
        )

        options_items = options["items"]
        self.assertTrue(
            "type" in options_items,
            'Expected "type" to be included in "options" ' '"items"',
        )

        options_items_type = options_items["type"]
        self.assertThat(
            options_items_type,
            Equals("string"),
            'Expected "options" "items" "type" to be '
            '"string", but it was "{}"'.format(options_items_type),
        )

        # Check qt-version property
        qt_version = properties["qt-version"]
        self.assertTrue(
            "enum" in qt_version, 'Expected "enum" to be included in "qt_version"'
        )

        qt_version_enum = qt_version["enum"]
        # Using sets for order independence in the comparison
        self.assertThat(set(qt_version_enum), Equals(set(["qt4", "qt5"])))

        qt_version_default = qt_version["default"]
        self.assertThat(
            qt_version_default,
            Equals("qt5"),
            'Expected "qt_version" "default" to be "qt5", but '
            "it was {}".format(qt_version_default),
        )

        # Check project-files property
        project_files = properties["project-files"]
        for item in ["type", "minitems", "uniqueItems", "items", "default"]:
            self.assertTrue(
                item in options,
                'Expected {!r} to be included in "project_files"'.format(item),
            )

        project_files_type = project_files["type"]
        self.assertThat(
            project_files_type,
            Equals("array"),
            'Expected "project_files" "type" to be "array", but '
            'it was "{}"'.format(project_files_type),
        )

        project_files_minitems = project_files["minitems"]
        self.assertThat(
            project_files_minitems,
            Equals(1),
            'Expected "project_files" "minitems" to be 1, but '
            "it was {}".format(project_files_minitems),
        )

        self.assertTrue(project_files["uniqueItems"])

        project_files_default = project_files["default"]
        self.assertThat(
            project_files_default,
            Equals([]),
            'Expected "project_files" "default" to be [], but '
            "it was {}".format(project_files_default),
        )

        project_files_items = project_files["items"]
        self.assertTrue(
            "type" in project_files_items,
            'Expected "type" to be included in "project_files" ' '"items"',
        )

        project_files_items_type = project_files_items["type"]
        self.assertThat(
            project_files_items_type,
            Equals("string"),
            'Expected "project_files" "items" "type" to be '
            '"string", but it was "{}"'.format(project_files_items_type),
        )

    def test_get_build_properties(self):
        expected_build_properties = ["options", "project-files"]
        resulting_build_properties = qmake.QmakePlugin.get_build_properties()

        self.assertThat(
            resulting_build_properties, HasLength(len(expected_build_properties))
        )

        for property in expected_build_properties:
            self.assertIn(property, resulting_build_properties)

    def test_get_pull_properties(self):
        expected_pull_properties = ["qt-version"]
        resulting_pull_properties = qmake.QmakePlugin.get_pull_properties()

        self.assertThat(
            resulting_pull_properties, HasLength(len(expected_pull_properties))
        )

        for property in expected_pull_properties:
            self.assertIn(property, resulting_pull_properties)

    def test_unsupported_qt_version(self):
        self.options.qt_version = "qt3"
        raised = self.assertRaises(
            RuntimeError, qmake.QmakePlugin, "test-part", self.options, self.project
        )

        self.assertThat(str(raised), Equals("Unsupported Qt version: 'qt3'"))

    def test_build_referencing_sourcedir_if_no_subdir(self):
        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        self.run_mock.assert_has_calls(
            [
                mock.call(["qmake"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_referencing_sourcedir_with_subdir(self):
        self.options.source_subdir = "subdir"

        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        self.run_mock.assert_has_calls(
            [
                mock.call(["qmake"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_with_project_file(self):
        self.options.project_files = ["project_file.pro"]

        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        path_to_pro_file = os.path.join(plugin.sourcedir, "project_file.pro")
        self.run_mock.assert_has_calls(
            [
                mock.call(
                    ["qmake", path_to_pro_file], cwd=plugin.builddir, env=mock.ANY
                ),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_with_project_file_with_subdir(self):
        self.options.source_subdir = "subdir"
        self.options.project_files = ["project_file.pro"]

        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        path_to_pro_file = os.path.join(
            plugin.sourcedir, plugin.options.source_subdir, "project_file.pro"
        )
        self.run_mock.assert_has_calls(
            [
                mock.call(
                    ["qmake", path_to_pro_file], cwd=plugin.builddir, env=mock.ANY
                ),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_with_options(self):
        self.options.options = ["-foo"]

        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        self.run_mock.assert_has_calls(
            [
                mock.call(["qmake", "-foo"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_with_libs_and_includes_in_installdir(self):
        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        os.makedirs(os.path.join(plugin.installdir, "include"))
        os.makedirs(os.path.join(plugin.installdir, "lib"))
        plugin.build()

        self.run_mock.assert_has_calls(
            [
                mock.call(
                    [
                        "qmake",
                        'LIBS+="-L{}/lib"'.format(plugin.installdir),
                        'INCLUDEPATH+="{}/include"'.format(plugin.installdir),
                    ],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_build_with_libs_and_includes_in_stagedir(self):
        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        os.makedirs(os.path.join(plugin.project.stage_dir, "include"))
        os.makedirs(os.path.join(plugin.project.stage_dir, "lib"))
        plugin.build()

        self.run_mock.assert_has_calls(
            [
                mock.call(
                    [
                        "qmake",
                        'LIBS+="-L{}/lib"'.format(plugin.project.stage_dir),
                        'INCLUDEPATH+="{}/include"'.format(plugin.project.stage_dir),
                    ],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
                mock.call(["make", "-j2"], cwd=plugin.builddir, env=mock.ANY),
                mock.call(
                    ["make", "install", "INSTALL_ROOT={}".format(plugin.installdir)],
                    cwd=plugin.builddir,
                    env=mock.ANY,
                ),
            ]
        )

    def test_unsupported_base(self):
        self.project._snap_meta.base = "unsupported-base"

        raised = self.assertRaises(
            errors.PluginBaseError,
            qmake.QmakePlugin,
            "test-part",
            self.options,
            self.project,
        )

        self.assertThat(raised.part_name, Equals("test-part"))
        self.assertThat(raised.base, Equals("unsupported-base"))

    def test_build_environment_qt4(self):
        self.options.qt_version = "qt4"
        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        self.assert_expected_environment({"QT_SELECT": "qt4"})

    def test_build_environment_qt5(self):
        self.options.qt_version = "qt5"
        plugin = qmake.QmakePlugin("test-part", self.options, self.project)
        os.makedirs(plugin.sourcedir)
        plugin.build()

        self.assert_expected_environment({"QT_SELECT": "qt5"})

    def assert_expected_environment(self, expected):
        self.assertThat(self.run_mock.call_count, Equals(3))
        for call_args in self.run_mock.call_args_list:
            environment = call_args[1]["env"]
            for variable, value in expected.items():
                self.assertTrue(
                    variable in environment,
                    "Expected variable {!r} to be in environment".format(variable),
                )

                self.assertThat(
                    environment[variable],
                    Equals(value),
                    "Expected ${}={}, but it was {}".format(
                        variable, value, environment[variable]
                    ),
                )
