import argparse
from typing import Any, List

from gitlab import Gitlab
from path import Path
import mock
import pytest

import tsrc
from tsrc.test.helpers.cli import CLI
from tsrc.cli.push import RepositoryInfo
from tsrc.cli.push_gitlab import PushAction, FeatureNotAvailable


GITLAB_URL = "http://gitlab.example.com"

# Can't use 'name' in the ctor because it sets the name
# of the mock, not the 'name' attribute of the returned
# instance.
ALICE = mock.Mock(username="alice", id=1)
ALICE.name = "Alice Liddell"
BOB = mock.Mock(username="bob", id=2, name="")
BOB.name = "Bob SquarePants"
EVE = mock.Mock(username="eve", id=3)
EVE.name = "Eve Arden"


class UserList:
    def __init__(self, user_list: List[Any]) -> None:
        self.user_list = user_list
        self.index = -1
        self.total = len(user_list)
        self.next_page = None

    def __iter__(self) -> Any:
        return (user for user in self.user_list)


def execute_query(*, query: str, **kwargs: Any) -> UserList:
    matches = list()
    for user in [ALICE, BOB, EVE]:
        if user.username == query:
            matches.append(user)
    return UserList(matches)


def gitlab_mock_with_merge_requests(mock_merge_requests: List[Any]) -> Any:
    gitlab_mock = mock.Mock()

    mock_project = mock.Mock()
    mock_project.members.list.side_effect = execute_query
    mock_project.mergerequests.list.return_value = mock_merge_requests

    mock_group = mock.Mock()
    mock_group.members.list.side_effect = execute_query

    gitlab_mock.projects.get.return_value = mock_project

    gitlab_mock.groups.get.return_value = mock_group

    mock_feature = mock.Mock()
    mock_feature.name = "multiple_merge_request_assignees"

    gitlab_mock.features.list.return_value = [mock_feature]

    return gitlab_mock


def execute_push(
    repo_path: Path, push_args: argparse.Namespace, gitlab_mock: Gitlab
) -> None:
    workspace_mock = mock.Mock()
    workspace_mock.get_github_enterprise_url.return_value = None
    workspace_mock.get_gitlab_url.return_value = GITLAB_URL

    repository_info = RepositoryInfo.read(repo_path, workspace=workspace_mock)
    push_action = PushAction(repository_info, push_args, gitlab_api=gitlab_mock)
    push_action.execute()


def test_check_for_multiple_assignees_feature(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    gitlab_mock = gitlab_mock_with_merge_requests([])
    gitlab_mock.features.list.return_value = []
    push_args.reviewers = ["alice", "bob"]

    with pytest.raises(FeatureNotAvailable) as e:
        execute_push(repo_path, push_args, gitlab_mock)
    assert e.value.name == "multiple_merge_request_assignees"


def test_creating_merge_request_explicit_target_branch_with_assignee(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    gitlab_mock = gitlab_mock_with_merge_requests([])

    push_args.assignee = "alice"
    push_args.target_branch = "next"
    push_args.title = "Best feature ever"

    mock_project = gitlab_mock.projects.get()
    new_mr = mock.Mock()
    mock_project.mergerequests.create.return_value = new_mr
    execute_push(repo_path, push_args, gitlab_mock)

    mock_project.mergerequests.create.assert_called_once_with(
        {
            "source_branch": "new-feature",
            "target_branch": "next",
            "title": "new-feature",
        }
    )

    new_mr.title = "Best feature ever"
    new_mr.save.assert_called_once_with()


def test_creating_merge_request_uses_default_branch(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    gitlab_mock = gitlab_mock_with_merge_requests([])
    mock_project = gitlab_mock.projects.get()
    mock_project.default_branch = "devel"
    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    mock_project = gitlab_mock.projects.get()
    new_mr = mock.Mock(iid="43", web_url="http://43")
    mock_project.mergerequests.create.return_value = new_mr

    push_args.assignee = "alice"
    execute_push(repo_path, push_args, gitlab_mock)

    mock_project.mergerequests.create.assert_called_once_with(
        {
            "source_branch": "new-feature",
            "target_branch": "devel",
            "title": "new-feature",
        }
    )


def test_set_approvers(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:

    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    push_args.reviewers = ["alice", "eve"]

    mock_mr = mock.Mock(iid="42", web_url="http://42")
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    execute_push(repo_path, push_args, gitlab_mock)

    mock_mr.approvals.set_approvers.assert_called_once_with([ALICE.id, EVE.id])
    mock_mr.save.assert_called_once()


def test_update_existing_merge_request(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    mock_mr = mock.Mock(iid="42", web_url="http://42", title="old title")
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    push_args.target_branch = "next"
    push_args.title = "Best feature ever"
    execute_push(repo_path, push_args, gitlab_mock)

    gitlab_mock.mergerequests.create.assert_not_called()
    assert mock_mr.remove_source_branch
    assert mock_mr.target_branch == "next"
    assert mock_mr.title == "Best feature ever"


def test_close_merge_request(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    mock_mr = mock.Mock(iid="42", web_url="http://42", title="old title")
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    push_args.close = True
    execute_push(repo_path, push_args, gitlab_mock)

    assert mock_mr.state_event == "close"
    mock_mr.save.assert_called_once()


def test_do_not_change_mr_target(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    mock_mr = mock.Mock(iid="42", web_url="http://42", target_branch="old-branch")
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    execute_push(repo_path, push_args, gitlab_mock)

    assert mock_mr.target_branch == "old-branch"


def test_accept_merge_request(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    tsrc.git.run(repo_path, "checkout", "-b", "new-feature")
    tsrc.git.run(repo_path, "commit", "--message", "new feature", "--allow-empty")

    mock_mr = mock.Mock(iid="42", web_url="http://42", target_branch="old-branch")
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    push_args.accept = True
    execute_push(repo_path, push_args, gitlab_mock)

    mock_mr.merge.assert_called_once_with(merge_when_pipeline_succeeds=True)


def test_unwipify_existing_merge_request(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    mock_mr = mock.Mock(
        title="WIP: nice title", web_url="http://example.com/42", iid=42
    )
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    push_args.ready = True
    execute_push(repo_path, push_args, gitlab_mock)

    assert mock_mr.title == "nice title"
    mock_mr.save.assert_called_once()


def test_wipify_existing_merge_request(
    repo_path: Path, tsrc_cli: CLI, push_args: argparse.Namespace
) -> None:
    mock_mr = mock.Mock(title="not ready", web_url="http://example.com/42", iid=42)
    gitlab_mock = gitlab_mock_with_merge_requests([mock_mr])

    push_args.wip = True
    execute_push(repo_path, push_args, gitlab_mock)

    assert mock_mr.title == "WIP: not ready"
    mock_mr.save.assert_called_once()
