from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
import gitlab.exceptions

from project_hub.forge.gitlab import GitLabProvider
from project_hub.forge.provider import AuthenticationError


# ---------------------------------------------------------------------------
# Mock builder
# ---------------------------------------------------------------------------

def _mock_gl():
    """Build a fully pre-configured mock gitlab.Gitlab instance."""
    gl = MagicMock()

    # Project
    project = MagicMock()
    project.name = "my-project"
    project.web_url = "https://gitlab.com/group/my-project"
    project.default_branch = "main"
    gl.projects.get.return_value = project

    # MRs
    mr1 = MagicMock()
    mr1.id = 101
    mr1.iid = 1
    mr1.title = "Fix the thing"
    mr1.author = {"username": "alice"}
    mr1.state = "opened"
    mr1.source_branch = "fix/thing"
    mr1.target_branch = "main"
    mr1.web_url = "https://gitlab.com/group/my-project/-/merge_requests/1"
    mr1.user_notes_count = 3
    mr1.approved = False
    mr1.created_at = "2024-01-01T00:00:00Z"
    mr1.updated_at = "2024-01-02T00:00:00Z"
    project.mergerequests.list.return_value = [mr1]

    # Pipelines
    pip1 = MagicMock()
    pip1.id = 200
    pip1.status = "success"
    pip1.ref = "main"
    pip1.web_url = "https://gitlab.com/group/my-project/-/pipelines/200"
    pip1.created_at = "2024-01-01T00:00:00Z"
    project.pipelines.list.return_value = [pip1]

    # Pipeline jobs
    pipeline_obj = MagicMock()
    job1 = MagicMock()
    job1.id = 300
    job1.name = "test"
    job1.status = "success"
    job1.stage = "test"
    job1.web_url = "https://gitlab.com/group/my-project/-/jobs/300"
    pipeline_obj.jobs.list.return_value = [job1]
    project.pipelines.get.return_value = pipeline_obj

    # Job log (trace returns bytes)
    job_obj = MagicMock()
    job_obj.trace.return_value = b"Running tests...\nAll passed."
    project.jobs.get.return_value = job_obj

    # Vulnerabilities
    vuln1 = MagicMock()
    vuln1.id = 400
    vuln1.name = "SQL Injection"
    vuln1.severity = "high"
    vuln1.state = "detected"
    vuln1.scanner = {"name": "SAST"}
    vuln1.web_url = "https://gitlab.com/group/my-project/-/security/vulnerabilities/400"
    project.vulnerabilities.list.return_value = [vuln1]

    return gl


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

def _mock_gql_mr_response():
    return {
        "project": {
            "mergeRequests": {
                "nodes": [
                    {
                        "id": "gid://gitlab/MergeRequest/101",
                        "iid": "1",
                        "title": "Fix the thing",
                        "author": {"username": "alice"},
                        "state": "opened",
                        "sourceBranch": "fix/thing",
                        "targetBranch": "main",
                        "webUrl": "https://gitlab.com/group/my-project/-/merge_requests/1",
                        "userNotesCount": 3,
                        "approved": False,
                        "createdAt": "2024-01-01T00:00:00Z",
                        "updatedAt": "2024-01-02T00:00:00Z",
                        "diffHeadSha": "abc123def456",
                    }
                ]
            }
        }
    }


def test_list_merge_requests():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.return_value = _mock_gql_mr_response()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        mrs = client.list_merge_requests("group/my-project")

    assert len(mrs) == 1
    mr = mrs[0]
    assert mr.id == 101
    assert mr.iid == 1
    assert mr.title == "Fix the thing"
    assert mr.author == "alice"
    assert mr.state == "opened"
    assert mr.source_branch == "fix/thing"
    assert mr.target_branch == "main"
    assert mr.web_url == "https://gitlab.com/group/my-project/-/merge_requests/1"
    assert mr.user_notes_count == 3
    assert mr.approved is False
    assert mr.sha == "abc123def456"
    assert mr.repo_name == ""  # caller fills it


def test_list_pipelines():
    mock_gl = _mock_gl()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        pipelines = client.list_pipelines("group/my-project")

    assert len(pipelines) == 1
    pip = pipelines[0]
    assert pip.id == 200
    assert pip.status == "success"
    assert pip.ref == "main"
    assert pip.web_url == "https://gitlab.com/group/my-project/-/pipelines/200"
    assert pip.created_at == "2024-01-01T00:00:00Z"
    assert pip.repo_name == ""


def test_list_pipelines_with_ref():
    mock_gl = _mock_gl()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        client.list_pipelines("group/my-project", ref="main")

    # Verify ref was passed to the API call
    project = mock_gl.projects.get.return_value
    call_kwargs = project.pipelines.list.call_args[1]
    assert call_kwargs.get("ref") == "main"


def test_get_pipeline_jobs():
    mock_gl = _mock_gl()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        jobs = client.get_pipeline_jobs("group/my-project", 200)

    assert len(jobs) == 1
    job = jobs[0]
    assert job.id == 300
    assert job.name == "test"
    assert job.status == "success"
    assert job.stage == "test"
    assert job.web_url == "https://gitlab.com/group/my-project/-/jobs/300"


def test_get_job_log():
    mock_gl = _mock_gl()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        log = client.get_job_log("group/my-project", 300)

    assert log == "Running tests...\nAll passed."
    # Verify lazy=True was used
    project = mock_gl.projects.get.return_value
    project.jobs.get.assert_called_once_with(300, lazy=True)


def test_get_vulnerabilities_graceful_on_403():
    mock_gl = _mock_gl()
    project = mock_gl.projects.get.return_value
    project.vulnerabilities.list.side_effect = Exception("403 Forbidden")

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        result = client.get_vulnerabilities("group/my-project")

    assert result is None


def test_get_vulnerabilities_graceful_on_404():
    mock_gl = _mock_gl()
    project = mock_gl.projects.get.return_value
    project.vulnerabilities.list.side_effect = Exception("404 Not Found")

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        result = client.get_vulnerabilities("group/my-project")

    assert result is None


def test_list_merge_requests_on_error():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.side_effect = Exception("Network error")

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        result = client.list_merge_requests("group/my-project")

    assert result is None


def test_get_project_info():
    mock_gl = _mock_gl()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        info = client.get_project_info("group/my-project")

    assert info["name"] == "my-project"
    assert info["web_url"] == "https://gitlab.com/group/my-project"
    assert info["default_branch"] == "main"


def test_raises_auth_error_on_401():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.side_effect = gitlab.exceptions.GitlabAuthenticationError(
        "401 Unauthorized", 401
    )

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        with pytest.raises(AuthenticationError):
            client.list_merge_requests("group/my-project")


def test_raises_auth_error_on_401_for_pipelines():
    """401 raised by pipelines.list also propagates."""
    mock_gl = _mock_gl()
    project = mock_gl.projects.get.return_value
    project.pipelines.list.side_effect = gitlab.exceptions.GitlabAuthenticationError(
        "401 Unauthorized", 401
    )

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        with pytest.raises(AuthenticationError):
            client.list_pipelines("group/my-project")


def test_get_username():
    mock_gl = _mock_gl()
    mock_gl.user = MagicMock()
    mock_gl.user.username = "alice"

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        assert client.get_username() == "alice"
        # Second call uses cached value (auth not called again)
        assert client.get_username() == "alice"
        mock_gl.auth.assert_called_once()


def test_get_username_on_error():
    mock_gl = _mock_gl()
    mock_gl.auth.side_effect = gitlab.exceptions.GitlabAuthenticationError("401 Unauthorized")

    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl):
        client = GitLabProvider("gitlab.com", "fake-token")
        assert client.get_username() is None


def _mock_gql_issue_response():
    return {
        "project": {
            "issues": {
                "nodes": [
                    {
                        "id": "gid://gitlab/Issue/501",
                        "iid": "10",
                        "title": "Fix login bug",
                        "author": {"username": "alice"},
                        "state": "opened",
                        "webUrl": "https://gitlab.com/group/my-project/-/issues/10",
                        "userNotesCount": 5,
                        "createdAt": "2024-01-01T00:00:00Z",
                        "updatedAt": "2024-01-02T00:00:00Z",
                        "confidential": False,
                        "labels": {"nodes": [{"title": "bug"}, {"title": "P1"}]},
                        "assignees": {"nodes": [{"username": "alice"}]},
                        "participants": {"nodes": [{"username": "alice"}, {"username": "bob"}]},
                    }
                ]
            }
        }
    }


def test_list_issues():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.return_value = _mock_gql_issue_response()
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        issues = client.list_issues("group/my-project")

    assert len(issues) == 1
    issue = issues[0]
    assert issue.id == 501
    assert issue.iid == 10
    assert issue.title == "Fix login bug"
    assert issue.author == "alice"
    assert issue.state == "opened"
    assert issue.labels == ["bug", "P1"]
    assert issue.assignees == ["alice"]
    assert issue.participants == ["alice", "bob"]
    assert issue.confidential is False
    assert issue.repo_name == ""


def test_list_issues_on_error():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.side_effect = Exception("Network error")
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        result = client.list_issues("group/my-project")
    assert result is None


def test_list_issues_auth_error():
    mock_gl = _mock_gl()
    mock_gq = MagicMock()
    mock_gq.execute.side_effect = gitlab.exceptions.GitlabAuthenticationError(
        "401 Unauthorized", 401
    )
    with patch("project_hub.forge.gitlab.gitlab.Gitlab", return_value=mock_gl), \
         patch("project_hub.forge.gitlab.gitlab.GraphQL", return_value=mock_gq):
        client = GitLabProvider("gitlab.com", "fake-token")
        with pytest.raises(AuthenticationError):
            client.list_issues("group/my-project")
