#!/usr/bin/env python
#     Copyright 2023, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Python test originally created or extracted from other peoples work. The
#     parts from me are licensed as below. It is at least Free Software where
#     it's copied from other people. In these cases, that will normally be
#     indicated.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#

""" Runner for distutils integration

Tests for example packages demonstrating that wheel creation with Nuitka
is compatible to standard packaging tools.

"""

import os
import sys

# Find nuitka package relative to us. The replacement is for POSIX python
# and Windows paths on command line.
sys.path.insert(
    0,
    os.path.normpath(
        os.path.join(
            os.path.dirname(os.path.abspath(__file__.replace("\\", os.sep))), "..", ".."
        )
    ),
)

# isort:start

import subprocess

from nuitka.tools.environments.Virtualenv import (
    NuitkaCalledProcessError,
    withVirtualenv,
)
from nuitka.tools.testing.Common import (
    createSearchMode,
    my_print,
    reportSkip,
    scanDirectoryForTestCaseFolders,
    setup,
    test_logger,
)
from nuitka.tools.testing.OutputComparison import compareOutput
from nuitka.utils.FileOperations import (
    deleteFile,
    getFileContents,
    putTextFileContents,
    removeDirectory,
)


def _adaptPyProjectFile(case_dir, source_filename):
    pyproject_filename = os.path.join(case_dir, "pyproject.toml")

    putTextFileContents(
        filename=pyproject_filename,
        contents=getFileContents(os.path.join(case_dir, source_filename)).replace(
            "file:../../..",
            "file:%s"
            % (
                os.path.abspath(os.path.join(case_dir, "..", "..", "..")).replace(
                    "\\", "/"
                )
            ),
        ),
    )

    return pyproject_filename


def _handleCase(python_version, nuitka_dir, filename):
    # Complex stuff, pylint: disable=too-many-branches,too-many-locals,too-many-statements

    my_print("Consider distutils example: '%s'" % filename, style="blue")

    if filename == "example_2_pyproject":
        reportSkip(
            "Skipped, 'poetry' based pyproject is not working for now",
            ".",
            filename,
        )
        return

    is_pyproject = "_pyproject" in filename

    if is_pyproject and python_version < (3,):
        reportSkip(
            "Skipped, pyproject cases are not for Python2",
            ".",
            filename,
        )
        return

    case_dir = os.path.join(os.getcwd(), filename)

    removeDirectory(os.path.join(case_dir, "build"), ignore_errors=False)
    removeDirectory(os.path.join(case_dir, "dist"), ignore_errors=False)

    with withVirtualenv("venv_cpython") as venv:
        if is_pyproject:
            venv.runCommand("pip install build")

            pyproject_filename = _adaptPyProjectFile(
                case_dir=case_dir, source_filename="pyproject.cpython.toml"
            )

            venv.runCommand(commands=['cd "%s"' % case_dir, "python -m build"])
            deleteFile(pyproject_filename, must_exist=True)
        else:
            venv.runCommand(
                commands=['cd "%s"' % case_dir, "python setup.py bdist_wheel"]
            )

        dist_dir = os.path.join(case_dir, "dist")

        venv.runCommand(
            'pip install "%s"' % (os.path.join(dist_dir, os.listdir(dist_dir)[0]))
        )

        runner_binary = os.path.join(
            venv.getVirtualenvDir(),
            "bin" if os.name != "nt" else "scripts",
            "runner",
        )

        # TODO: Is this something to be abstracted into a function.
        # pylint: disable=consider-using-with

        if os.path.exists(runner_binary):
            # Need to call CPython binary for Windows.
            process = subprocess.Popen(
                args=[
                    os.path.join(
                        venv.getVirtualenvDir(),
                        "bin" if os.name != "nt" else "scripts",
                        "python",
                    ),
                    os.path.join(
                        venv.getVirtualenvDir(),
                        "bin" if os.name != "nt" else "scripts",
                        "runner",
                    ),
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
        else:
            assert os.path.exists(runner_binary + ".exe")

            process = subprocess.Popen(
                args=[runner_binary + ".exe"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )

        stdout_cpython, stderr_cpython = process.communicate()
        exit_cpython = process.returncode

        my_print("STDOUT CPython:")
        my_print(stdout_cpython)
        my_print("STDERR CPython:")
        my_print(stderr_cpython)

        assert exit_cpython == 0, exit_cpython
        my_print("EXIT was OK.")

    removeDirectory(os.path.join(case_dir, "build"), ignore_errors=False)
    removeDirectory(os.path.join(case_dir, "dist"), ignore_errors=False)

    with withVirtualenv("venv_nuitka") as venv:
        # Create the wheel with Nuitka compilation.
        if is_pyproject:
            venv.runCommand("pip install build")

            pyproject_filename = _adaptPyProjectFile(
                case_dir=case_dir, source_filename="pyproject.nuitka.toml"
            )

            venv.runCommand(commands=['cd "%s"' % case_dir, "python -m build -w"])

            deleteFile(pyproject_filename, must_exist=True)
        else:
            # Install nuitka from source so "bdist_nuitka" can work.

            try:
                venv.runCommand(
                    commands=['cd "%s"' % nuitka_dir, "python setup.py install"],
                    style="test-prepare",
                    env={"PYTHONWARNINGS": "ignore"},
                )
            finally:
                # Remove that left over from the install command.
                removeDirectory(
                    path=os.path.join(nuitka_dir, "Nuitka.egg-info"),
                    ignore_errors=False,
                )

            venv.runCommand(
                commands=['cd "%s"' % case_dir, "python setup.py bdist_nuitka"]
            )

        dist_dir = os.path.join(case_dir, "dist")
        venv.runCommand(
            'pip install "%s"' % (os.path.join(dist_dir, os.listdir(dist_dir)[0]))
        )

        runner_binary = os.path.join(
            venv.getVirtualenvDir(),
            "bin" if os.name != "nt" else "scripts",
            "runner",
        )

        # TODO: Is this something to be abstracted into a function.
        # pylint: disable=consider-using-with

        if os.path.exists(runner_binary):
            process = subprocess.Popen(
                args=[
                    os.path.join(
                        venv.getVirtualenvDir(),
                        "bin" if os.name != "nt" else "scripts",
                        "python",
                    ),
                    runner_binary,
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
        else:
            assert os.path.exists(runner_binary + ".exe")

            process = subprocess.Popen(
                args=[runner_binary + ".exe"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )

        stdout_nuitka, stderr_nuitka = process.communicate()
        exit_nuitka = process.returncode

        my_print("STDOUT Nuitka:")
        my_print(stdout_nuitka)
        my_print("STDERR Nuitka:")
        my_print(stderr_nuitka)

        assert exit_nuitka == 0, exit_nuitka
        my_print("EXIT was OK.")

    exit_code_stdout = compareOutput(
        "stdout",
        stdout_cpython,
        stdout_nuitka,
        ignore_warnings=True,
        syntax_errors=True,
    )

    exit_code_stderr = compareOutput(
        "stderr",
        stderr_cpython,
        stderr_nuitka,
        ignore_warnings=True,
        syntax_errors=True,
    )

    exit_code_return = exit_cpython != exit_nuitka

    if exit_code_return:
        my_print(
            """\
Exit codes {exit_cpython:d} (CPython) != {exit_nuitka:d} (Nuitka)""".format(
                exit_cpython=exit_cpython, exit_nuitka=exit_nuitka
            )
        )

    exit_code = exit_code_stdout or exit_code_stderr or exit_code_return

    if exit_code:
        sys.exit("Error, outputs differed.")


def main():
    python_version = setup(suite="distutils", needs_io_encoding=True)

    search_mode = createSearchMode()

    nuitka_dir = os.path.abspath(os.path.join(os.getcwd(), "..", ".."))

    for filename, _filename_main in scanDirectoryForTestCaseFolders("."):
        active = search_mode.consider(dirname=None, filename=filename)

        if active:
            try:
                _handleCase(python_version, nuitka_dir, filename)
            except NuitkaCalledProcessError:
                test_logger.sysexit("Error in test case '%s'." % filename)

    search_mode.finish()


if __name__ == "__main__":
    main()
