# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
# ruff: noqa: E501
"""
Test the @tvm-bot merge code
"""

import json
from pathlib import Path
from typing import Any

import tvm

from .test_utils import GITHUB_SCRIPT_ROOT, TempGit, run_script

SUCCESS_EXPECTED_OUTPUT = """
Dry run, would have merged with url=pulls/10786/merge and data={
  "commit_title": "[Hexagon] 2-d allocation cleanup (#10786)",
  "commit_message": "- Added device validity check in allocation. HexagonDeviceAPI should only be called for CPU/Hexagon types.\\n\\n- Check for \\"global.vtcm\\" scope instead of \\"vtcm\\".  The ccope of N-d allocations produced by `LowerVtcmAlloc` should be `\\"global.vtcm\\"`.  The previous check allowed unsupported scope such as `\\"local.vtcm\\"`.\\n\\n- Remove `vtcmallocs` entry after calling free.\\n\\nPreviously, the vtcm allocation map kept dangling pointers to `HexagonBuffer` objects after they had been freed.\\n\\n- Rename N-d alloc and free packed functions.  Since most of the similar device functions use snake case, renaming `*.AllocND` to `*.alloc_nd` and `*.FreeND` to `*.free_nd`.\\n\\n\\ncc someone\\n\\n\\nCo-authored-by: Adam Straw <astraw@octoml.ai>",
  "sha": "6f04bcf57d07f915a98fd91178f04d9e92a09fcd",
  "merge_method": "squash"
}
""".strip()


class _TvmBotTest:
    NUMBER = 10786

    def preprocess_data(self, data: dict[str, Any]):
        """
        Used to pre-process PR data before running the test. Override as
        necessary to edit data for specific test cases.
        """
        return data

    @tvm.testing.skip_if_wheel_test
    def test(self, tmpdir_factory):
        """
        Run the tvm-bot script using the data from preprocess_data
        """
        mergebot_script = GITHUB_SCRIPT_ROOT / "github_tvmbot.py"
        test_json_dir = Path(__file__).resolve().parent / "sample_prs"
        with open(test_json_dir / f"pr{self.NUMBER}.json") as f:
            test_data = json.load(f)

        # Update testing data with replacements / additions
        test_data = self.preprocess_data(test_data)

        git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))

        comment = {
            "body": self.COMMENT,
            "id": 123,
            "user": {
                "login": self.USER,
            },
        }
        allowed_users = [{"login": "abc"}, {"login": "other-abc"}]

        proc = run_script(
            [
                mergebot_script,
                "--pr",
                self.NUMBER,
                "--dry-run",
                "--run-url",
                "https://example.com",
                "--testing-pr-json",
                json.dumps(test_data),
                "--testing-collaborators-json",
                json.dumps(allowed_users),
                "--testing-mentionable-users-json",
                json.dumps(allowed_users),
                "--trigger-comment-json",
                json.dumps(comment),
            ],
            env={
                "TVM_BOT_JENKINS_TOKEN": "123",
                "GH_ACTIONS_TOKEN": "123",
            },
            cwd=git.cwd,
        )

        if self.EXPECTED not in proc.stderr:
            raise RuntimeError(f"{proc.stderr}\ndid not contain\n{self.EXPECTED}")


class TestNoRequest(_TvmBotTest):
    """
    A PR for which the mergebot runs but no merge is requested
    """

    COMMENT = "@tvm-bot do something else"
    USER = "abc"
    EXPECTED = "Command 'do something else' did not match anything"

    def preprocess_data(self, data: dict[str, Any]):
        data["reviews"]["nodes"][0]["body"] = "nothing"
        return data


class TestSuccessfulMerge(_TvmBotTest):
    """
    Everything is fine so this PR will merge
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = SUCCESS_EXPECTED_OUTPUT


class TestBadCI(_TvmBotTest):
    """
    A PR which failed CI and cannot merge
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Cannot merge, these CI jobs are not successful on"

    def preprocess_data(self, data: dict[str, Any]):
        # Mark the Jenkins build as failed
        contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"]
        for context in contexts:
            if "context" in context and context["context"] == "tvm-ci/pr-head":
                context["state"] = "FAILED"
        return data


class TestOldReview(_TvmBotTest):
    """
    A PR with passing CI and approving reviews on an old commit so it cannot merge
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Cannot merge, did not find any approving reviews"

    def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]:
        data["reviews"]["nodes"][0]["commit"]["oid"] = "abc12345"
        return data


class TestMissingJob(_TvmBotTest):
    """
    PR missing an expected CI job and cannot merge
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Cannot merge, missing expected jobs"

    def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]:
        contexts = data["commits"]["nodes"][0]["commit"]["statusCheckRollup"]["contexts"]["nodes"]
        for context in contexts:
            if "context" in context and context["context"] == "tvm-ci/pr-head":
                context["context"] = "something"
        return data


class TestInvalidAuthor(_TvmBotTest):
    """
    Merge requester is not a committer and cannot merge
    """

    COMMENT = "@tvm-bot merge"
    USER = "not-abc"
    EXPECTED = "Failed auth check 'collaborators', quitting"


class TestUnauthorizedComment(_TvmBotTest):
    """
    Check that a merge comment not from a CONTRIBUTOR is rejected
    """

    COMMENT = "@tvm-bot merge"
    USER = "not-abc2"
    EXPECTED = "Failed auth check 'collaborators'"


class TestNoReview(_TvmBotTest):
    """
    Check that a merge request without any reviews is rejected
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Cannot merge, did not find any approving reviews from users with write access"

    def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]:
        data["reviews"]["nodes"] = []
        return data


class TestChangesRequested(_TvmBotTest):
    """
    Check that a merge request with a 'Changes Requested' review is rejected
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Cannot merge, found [this review]"

    def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]:
        data["reviews"]["nodes"][0]["state"] = "CHANGES_REQUESTED"
        data["reviews"]["nodes"][0]["url"] = "http://example.com"
        return data


class TestCoAuthors(_TvmBotTest):
    """
    Check that a merge request with co-authors generates the correct commit message
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Co-authored-by: Some One <someone@email.com>"

    def preprocess_data(self, data: dict[str, Any]) -> dict[str, Any]:
        data["authorCommits"]["nodes"][0]["commit"]["authors"]["nodes"].append(
            {"name": "Some One", "email": "someone@email.com"}
        )
        return data


class TestRerunCI(_TvmBotTest):
    """
    Start a new CI job
    """

    COMMENT = "@tvm-bot rerun"
    USER = "abc"
    EXPECTED = "Rerunning ci with"


class TestRerunPermissions(_TvmBotTest):
    """
    Start a new CI job as an unauthorized user
    """

    COMMENT = "@tvm-bot rerun"
    USER = "someone"
    EXPECTED = "Failed auth check 'mentionable_users', quitting"


class TestRerunNonAuthor(_TvmBotTest):
    """
    Start a new CI job as a mentionable user
    """

    COMMENT = "@tvm-bot rerun"
    USER = "other-abc"
    EXPECTED = "Passed auth check 'mentionable_users', continuing"


class TestIgnoreJobs(_TvmBotTest):
    """
    Ignore GitHub Actions jobs that don't start with CI /
    """

    COMMENT = "@tvm-bot merge"
    USER = "abc"
    EXPECTED = "Dry run, would have merged"


if __name__ == "__main__":
    tvm.testing.main()
