# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os
import textwrap
import unittest
from io import StringIO

import mozpack.path as mozpath
from buildconfig import topsrcdir
from mozunit import main
from test_toolchain_helpers import FakeCompiler

from common import ConfigureTestSandbox


class BaseCompileChecks(unittest.TestCase):
    def get_mock_compiler(self, expected_test_content=None, expected_flags=None):
        expected_flags = expected_flags or []

        def mock_compiler(stdin, args):
            if args != ["--version"]:
                test_file = [a for a in args if not a.startswith("-")]
                self.assertEqual(len(test_file), 1)
                test_file = test_file[0]
                args = [a for a in args if a.startswith("-")]
                self.assertIn("-c", args)
                for flag in expected_flags:
                    self.assertIn(flag, args)

                if expected_test_content:
                    with open(test_file) as fh:
                        test_content = fh.read()
                    self.assertEqual(test_content, expected_test_content)

            return FakeCompiler()(None, args)

        return mock_compiler

    def do_compile_test(self, command, expected_test_content=None, expected_flags=None):
        paths = {
            os.path.abspath("/usr/bin/mockcc"): self.get_mock_compiler(
                expected_test_content=expected_test_content,
                expected_flags=expected_flags,
            ),
        }

        base_dir = os.path.join(topsrcdir, "build", "moz.configure")

        mock_compiler_defs = textwrap.dedent(
            """\
            @depends(when=True)
            def extra_toolchain_flags():
                return []

            @depends(when=True)
            def linker_ldflags():
                return []

            target = depends(when=True)(lambda: True)

            @depends(when=True)
            def configure_cache():

                class ConfigureCache(dict):
                    pass

                cache_data = {}

                cache = ConfigureCache(cache_data)
                cache.version_checked_compilers = set()

                return cache

            include('%s/compilers-util.configure')

            @template
            def wrap_compiler(compiler):
                return compiler_class(compiler, False)

            @wrap_compiler
            @depends(when=True)
            def c_compiler():
                return namespace(
                    flags=[],
                    type='gcc',
                    compiler=os.path.abspath('/usr/bin/mockcc'),
                    wrapper=[],
                    language='C',
                )

            @wrap_compiler
            @depends(when=True)
            def host_c_compiler():
                return namespace(
                    flags=[],
                    type='gcc',
                    compiler=os.path.abspath('/usr/bin/mockcc'),
                    wrapper=[],
                    language='C',
                )

            @wrap_compiler
            @depends(when=True)
            def cxx_compiler():
                return namespace(
                    flags=[],
                    type='gcc',
                    compiler=os.path.abspath('/usr/bin/mockcc'),
                    wrapper=[],
                    language='C++',
                )

            @wrap_compiler
            @depends(when=True)
            def host_cxx_compiler():
                return namespace(
                    flags=[],
                    type='gcc',
                    compiler=os.path.abspath('/usr/bin/mockcc'),
                    wrapper=[],
                    language='C++',
                )
        """
            % mozpath.normsep(base_dir)
        )

        config = {}
        out = StringIO()
        sandbox = ConfigureTestSandbox(paths, config, {}, ["/bin/configure"], out, out)
        sandbox.include_file(os.path.join(base_dir, "util.configure"))
        sandbox.include_file(os.path.join(base_dir, "checks.configure"))
        exec(mock_compiler_defs, sandbox)
        sandbox.include_file(os.path.join(base_dir, "compile-checks.configure"))

        status = 0
        try:
            exec(command, sandbox)
            sandbox.run()
        except SystemExit as e:
            status = e.code

        return config, out.getvalue(), status


class TestHeaderChecks(BaseCompileChecks):
    def test_try_compile_include(self):
        expected_test_content = textwrap.dedent(
            """\
          #include <foo.h>
          #include <bar.h>
          int
          main(void)
          {

            ;
            return 0;
          }
        """
        )

        cmd = textwrap.dedent(
            """\
            try_compile(['foo.h', 'bar.h'], language='C')
        """
        )

        config, out, status = self.do_compile_test(cmd, expected_test_content)
        self.assertEqual(status, 0)
        self.assertEqual(config, {})

    def test_try_compile_flags(self):
        expected_flags = ["--extra", "--flags"]

        cmd = textwrap.dedent(
            """\
            try_compile(language='C++', flags=['--flags', '--extra'])
        """
        )

        config, out, status = self.do_compile_test(cmd, expected_flags=expected_flags)
        self.assertEqual(status, 0)
        self.assertEqual(config, {})

    def test_try_compile_failure(self):
        cmd = textwrap.dedent(
            """\
            have_fn = try_compile(body='somefn();', flags=['-funknown-flag'])
            set_config('HAVE_SOMEFN', have_fn)

            have_another = try_compile(body='anotherfn();', language='C')
            set_config('HAVE_ANOTHERFN', have_another)
        """
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "HAVE_ANOTHERFN": True,
            },
        )

    def test_try_compile_msg(self):
        cmd = textwrap.dedent(
            """\
            known_flag = try_compile(language='C++', flags=['-fknown-flag'],
                                     check_msg='whether -fknown-flag works')
            set_config('HAVE_KNOWN_FLAG', known_flag)
        """
        )
        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(config, {"HAVE_KNOWN_FLAG": True})
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking whether -fknown-flag works... yes
        """
            ),
        )

    def test_check_header(self):
        expected_test_content = textwrap.dedent(
            """\
          #include <foo.h>
          int
          main(void)
          {

            ;
            return 0;
          }
        """
        )

        cmd = textwrap.dedent(
            """\
            check_header('foo.h')
        """
        )

        config, out, status = self.do_compile_test(
            cmd, expected_test_content=expected_test_content
        )
        self.assertEqual(status, 0)
        self.assertEqual(config, {"DEFINES": {"HAVE_FOO_H": True}})
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking for foo.h... yes
        """
            ),
        )

    def test_check_header_conditional(self):
        cmd = textwrap.dedent(
            """\
            check_headers('foo.h', 'bar.h', when=never)
        """
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(out, "")
        self.assertEqual(config, {"DEFINES": {}})

    def test_check_header_include(self):
        expected_test_content = textwrap.dedent(
            """\
          #include <std.h>
          #include <bar.h>
          #include <foo.h>
          int
          main(void)
          {

            ;
            return 0;
          }
        """
        )

        cmd = textwrap.dedent(
            """\
           have_foo = check_header('foo.h', includes=['std.h', 'bar.h'])
           set_config('HAVE_FOO_H', have_foo)
        """
        )

        config, out, status = self.do_compile_test(
            cmd, expected_test_content=expected_test_content
        )

        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "HAVE_FOO_H": True,
                "DEFINES": {
                    "HAVE_FOO_H": True,
                },
            },
        )
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking for foo.h... yes
        """
            ),
        )

    def test_check_headers_multiple(self):
        cmd = textwrap.dedent(
            """\
            baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h')
            set_config('HAVE_BAZ_BAR', baz_bar)
            set_config('HAVE_QUUX_BAR', quux_bar)
        """
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "HAVE_BAZ_BAR": True,
                "HAVE_QUUX_BAR": True,
                "DEFINES": {
                    "HAVE_BAZ_FOO_BAR_H": True,
                    "HAVE_BAZ_QUUX_FOO_BAR_H": True,
                },
            },
        )
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking for baz/foo-bar.h... yes
            checking for baz-quux/foo-bar.h... yes
        """
            ),
        )

    def test_check_headers_not_found(self):
        cmd = textwrap.dedent(
            """\
            baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h',
                                              flags=['-funknown-flag'])
            set_config('HAVE_BAZ_BAR', baz_bar)
            set_config('HAVE_QUUX_BAR', quux_bar)
        """
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(config, {"DEFINES": {}})
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking for baz/foo-bar.h... no
            checking for baz-quux/foo-bar.h... no
        """
            ),
        )


class TestWarningChecks(BaseCompileChecks):
    def get_warnings(self):
        return textwrap.dedent(
            """\
            set_config('_WARNINGS_CFLAGS', warnings_flags.cflags)
            set_config('_WARNINGS_CXXFLAGS', warnings_flags.cxxflags)
        """
        )

    def test_check_and_add_warning(self):
        for flag, expected_flags in (
            ("-Wfoo", ["-Werror", "-Wfoo"]),
            ("-Wno-foo", ["-Werror", "-Wfoo"]),
            ("-Werror=foo", ["-Werror=foo"]),
            ("-Wno-error=foo", ["-Wno-error=foo"]),
        ):
            cmd = (
                textwrap.dedent(
                    """\
                check_and_add_warning('%s')
            """
                    % flag
                )
                + self.get_warnings()
            )

            config, out, status = self.do_compile_test(
                cmd, expected_flags=expected_flags
            )
            self.assertEqual(status, 0)
            self.assertEqual(
                config,
                {
                    "_WARNINGS_CFLAGS": [flag],
                    "_WARNINGS_CXXFLAGS": [flag],
                },
            )
            self.assertEqual(
                out,
                textwrap.dedent(
                    f"""\
                checking whether the C compiler supports {flag}... yes
                checking whether the C++ compiler supports {flag}... yes
            """
                ),
            )

    def test_check_and_add_warning_one(self):
        cmd = (
            textwrap.dedent(
                """\
            check_and_add_warning('-Wfoo', cxx_compiler)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": [],
                "_WARNINGS_CXXFLAGS": ["-Wfoo"],
            },
        )
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking whether the C++ compiler supports -Wfoo... yes
        """
            ),
        )

    def test_check_and_add_warning_when(self):
        cmd = (
            textwrap.dedent(
                """\
            @depends(when=True)
            def never():
                return False
            check_and_add_warning('-Wfoo', cxx_compiler, when=never)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": [],
                "_WARNINGS_CXXFLAGS": [],
            },
        )
        self.assertEqual(out, "")

        cmd = (
            textwrap.dedent(
                """\
            @depends(when=True)
            def always():
                return True
            check_and_add_warning('-Wfoo', cxx_compiler, when=always)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": [],
                "_WARNINGS_CXXFLAGS": ["-Wfoo"],
            },
        )
        self.assertEqual(
            out,
            textwrap.dedent(
                """\
            checking whether the C++ compiler supports -Wfoo... yes
        """
            ),
        )

    def test_add_warning(self):
        cmd = (
            textwrap.dedent(
                """\
            add_warning('-Wfoo')
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": ["-Wfoo"],
                "_WARNINGS_CXXFLAGS": ["-Wfoo"],
            },
        )
        self.assertEqual(out, "")

    def test_add_warning_one(self):
        cmd = (
            textwrap.dedent(
                """\
            add_warning('-Wfoo', c_compiler)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": ["-Wfoo"],
                "_WARNINGS_CXXFLAGS": [],
            },
        )
        self.assertEqual(out, "")

    def test_add_warning_when(self):
        cmd = (
            textwrap.dedent(
                """\
            @depends(when=True)
            def never():
                return False
            add_warning('-Wfoo', c_compiler, when=never)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": [],
                "_WARNINGS_CXXFLAGS": [],
            },
        )
        self.assertEqual(out, "")

        cmd = (
            textwrap.dedent(
                """\
            @depends(when=True)
            def always():
                return True
            add_warning('-Wfoo', c_compiler, when=always)
        """
            )
            + self.get_warnings()
        )

        config, out, status = self.do_compile_test(cmd)
        self.assertEqual(status, 0)
        self.assertEqual(
            config,
            {
                "_WARNINGS_CFLAGS": ["-Wfoo"],
                "_WARNINGS_CXXFLAGS": [],
            },
        )
        self.assertEqual(out, "")


if __name__ == "__main__":
    main()
