"""Tests for glreview.gitlab module."""

from __future__ import annotations

import os
from unittest.mock import MagicMock, patch

import pytest

from glreview.gitlab import (
    GitLabConfig,
    GitLabIssue,
    GitLabMember,
    add_issue_comment,
    close_issue,
    create_issue,
    get_compare_url,
    get_gitlab_client,
    get_issue,
    get_project_members,
    gitlab_available,
)


class TestGitLabConfig:
    """Tests for GitLabConfig dataclass."""

    def test_default_values(self):
        """Default values are correct."""
        config = GitLabConfig(url="https://gitlab.com")
        assert config.url == "https://gitlab.com"
        assert config.private_token is None
        assert config.job_token is None
        assert config.oauth_token is None

    def test_is_authenticated_with_private_token(self):
        """is_authenticated returns True with private token."""
        config = GitLabConfig(url="https://gitlab.com", private_token="token")
        assert config.is_authenticated is True

    def test_is_authenticated_with_job_token(self):
        """is_authenticated returns True with job token."""
        config = GitLabConfig(url="https://gitlab.com", job_token="token")
        assert config.is_authenticated is True

    def test_is_authenticated_with_oauth_token(self):
        """is_authenticated returns True with oauth token."""
        config = GitLabConfig(url="https://gitlab.com", oauth_token="token")
        assert config.is_authenticated is True

    def test_is_authenticated_without_token(self):
        """is_authenticated returns False without tokens."""
        config = GitLabConfig(url="https://gitlab.com")
        assert config.is_authenticated is False

    def test_from_environment_default(self, monkeypatch):
        """from_environment uses defaults without env vars."""
        monkeypatch.delenv("GITLAB_URL", raising=False)
        monkeypatch.delenv("GITLAB_PRIVATE_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_TOKEN", raising=False)
        monkeypatch.delenv("CI_JOB_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_OAUTH_TOKEN", raising=False)

        config = GitLabConfig.from_environment()
        assert config.url == "https://gitlab.com"
        assert config.is_authenticated is False

    def test_from_environment_with_host(self, monkeypatch):
        """from_environment uses provided host."""
        monkeypatch.delenv("GITLAB_URL", raising=False)

        config = GitLabConfig.from_environment(host="git.ligo.org")
        assert config.url == "https://git.ligo.org"

    def test_from_environment_with_env_url(self, monkeypatch):
        """from_environment uses GITLAB_URL env var."""
        monkeypatch.setenv("GITLAB_URL", "https://custom.gitlab.com")

        config = GitLabConfig.from_environment()
        assert config.url == "https://custom.gitlab.com"

    def test_from_environment_ci_job_token(self, monkeypatch):
        """from_environment reads CI_JOB_TOKEN."""
        monkeypatch.setenv("CI_JOB_TOKEN", "job-token-123")

        config = GitLabConfig.from_environment()
        assert config.job_token == "job-token-123"

    def test_from_environment_private_token(self, monkeypatch):
        """from_environment reads GITLAB_PRIVATE_TOKEN."""
        monkeypatch.setenv("GITLAB_PRIVATE_TOKEN", "private-token-123")

        config = GitLabConfig.from_environment()
        assert config.private_token == "private-token-123"

    def test_from_environment_gitlab_token_alias(self, monkeypatch):
        """from_environment reads GITLAB_TOKEN alias."""
        monkeypatch.setenv("GITLAB_TOKEN", "alias-token-123")

        config = GitLabConfig.from_environment()
        assert config.private_token == "alias-token-123"


class TestGitLabIssue:
    """Tests for GitLabIssue dataclass."""

    def test_from_api_response(self):
        """Creates GitLabIssue from API response."""
        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Test Issue"
        mock_issue.state = "opened"
        mock_issue.assignees = [{"username": "alice"}, {"username": "bob"}]
        mock_issue.labels = ["review", "high-priority"]

        issue = GitLabIssue.from_api_response(mock_issue)

        assert issue.number == "42"
        assert issue.url == "https://gitlab.com/org/project/-/issues/42"
        assert issue.title == "Test Issue"
        assert issue.state == "opened"
        assert issue.assignees == ["alice", "bob"]
        assert issue.labels == ["review", "high-priority"]

    def test_from_api_response_no_assignees(self):
        """Handles None assignees."""
        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Test Issue"
        mock_issue.state = "opened"
        mock_issue.assignees = None
        mock_issue.labels = []

        issue = GitLabIssue.from_api_response(mock_issue)
        assert issue.assignees == []


class TestGitLabMember:
    """Tests for GitLabMember dataclass."""

    def test_from_api_response(self):
        """Creates GitLabMember from API response."""
        mock_member = MagicMock()
        mock_member.username = "alice"
        mock_member.name = "Alice Smith"
        mock_member.access_level = 40

        member = GitLabMember.from_api_response(mock_member)

        assert member.username == "alice"
        assert member.name == "Alice Smith"
        assert member.access_level == 40

    def test_access_level_name(self):
        """access_level_name returns human-readable name."""
        assert GitLabMember("u", "n", 50).access_level_name == "Owner"
        assert GitLabMember("u", "n", 40).access_level_name == "Maintainer"
        assert GitLabMember("u", "n", 30).access_level_name == "Developer"
        assert GitLabMember("u", "n", 20).access_level_name == "Reporter"
        assert GitLabMember("u", "n", 10).access_level_name == "Guest"
        assert GitLabMember("u", "n", 99).access_level_name == "Level 99"


class TestGetGitlabClient:
    """Tests for get_gitlab_client function."""

    @patch("glreview.gitlab._get_gitlab_client")
    def test_returns_none_without_auth(self, mock_get_client, monkeypatch):
        """Returns None when not authenticated."""
        monkeypatch.delenv("GITLAB_PRIVATE_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_TOKEN", raising=False)
        monkeypatch.delenv("CI_JOB_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_OAUTH_TOKEN", raising=False)

        client = get_gitlab_client()
        assert client is None
        mock_get_client.assert_not_called()

    @patch("glreview.gitlab._get_gitlab_client")
    def test_returns_client_with_auth(self, mock_get_client):
        """Returns client when authenticated."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        config = GitLabConfig(url="https://gitlab.com", private_token="token")
        client = get_gitlab_client(config)

        assert client == mock_gl
        mock_gl.auth.assert_called_once()


class TestGitlabAvailable:
    """Tests for gitlab_available function."""

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_true_when_authenticated(self, mock_get_client, monkeypatch):
        """Returns True when client authenticates."""
        # Must have token in env for is_authenticated to be True
        monkeypatch.setenv("GITLAB_PRIVATE_TOKEN", "test-token")
        mock_get_client.return_value = MagicMock()

        result = gitlab_available()
        assert result is True

    def test_returns_false_without_token(self, monkeypatch):
        """Returns False without token."""
        monkeypatch.delenv("GITLAB_PRIVATE_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_TOKEN", raising=False)
        monkeypatch.delenv("CI_JOB_TOKEN", raising=False)
        monkeypatch.delenv("GITLAB_OAUTH_TOKEN", raising=False)

        result = gitlab_available()
        assert result is False


class TestCreateIssue:
    """Tests for create_issue function."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_creates_issue(self, mock_get_project, mock_get_client):
        """Creates issue successfully."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Review: module.py"
        mock_issue.state = "opened"
        mock_issue.assignees = []
        mock_issue.labels = ["review"]
        mock_project.issues.create.return_value = mock_issue

        result = create_issue(
            title="Review: module.py",
            description="Please review this module",
            project_path="org/project",
            labels=["review"],
        )

        assert result is not None
        assert result.number == "42"
        mock_project.issues.create.assert_called_once()

    def test_returns_none_without_project_path(self):
        """Returns None when project_path is None."""
        result = create_issue(
            title="Test",
            description="Test",
            project_path=None,
        )
        assert result is None

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_none_without_client(self, mock_get_client):
        """Returns None when client is None."""
        mock_get_client.return_value = None

        result = create_issue(
            title="Test",
            description="Test",
            project_path="org/project",
        )
        assert result is None


class TestGetIssue:
    """Tests for get_issue function."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_gets_issue(self, mock_get_project, mock_get_client):
        """Gets issue details."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Test Issue"
        mock_issue.state = "closed"
        mock_issue.assignees = [{"username": "alice"}]
        mock_issue.labels = []
        mock_project.issues.get.return_value = mock_issue

        result = get_issue("42", project_path="org/project")

        assert result is not None
        assert result.number == "42"
        assert result.state == "closed"

    def test_returns_none_without_project_path(self):
        """Returns None when project_path is None."""
        result = get_issue("42", project_path=None)
        assert result is None


class TestCloseIssue:
    """Tests for close_issue function."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_closes_issue(self, mock_get_project, mock_get_client):
        """Closes issue successfully."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_project.issues.get.return_value = mock_issue

        result = close_issue("42", project_path="org/project")

        assert result is True
        mock_issue.save.assert_called_once()
        assert mock_issue.state_event == "close"

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_closes_with_comment(self, mock_get_project, mock_get_client):
        """Closes issue with comment."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_project.issues.get.return_value = mock_issue

        result = close_issue("42", project_path="org/project", comment="Done!")

        assert result is True
        mock_issue.notes.create.assert_called_once_with({"body": "Done!"})

    def test_returns_false_without_project_path(self):
        """Returns False when project_path is None."""
        result = close_issue("42", project_path=None)
        assert result is False


class TestAddIssueComment:
    """Tests for add_issue_comment function."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_adds_comment(self, mock_get_project, mock_get_client):
        """Adds comment successfully."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_project.issues.get.return_value = mock_issue

        result = add_issue_comment("42", project_path="org/project", comment="LGTM!")

        assert result is True
        mock_issue.notes.create.assert_called_once_with({"body": "LGTM!"})

    def test_returns_false_without_project_path(self):
        """Returns False when project_path is None."""
        result = add_issue_comment("42", project_path=None, comment="Test")
        assert result is False


class TestGetProjectMembers:
    """Tests for get_project_members function."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_gets_members(self, mock_get_project, mock_get_client):
        """Gets project members."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_member = MagicMock()
        mock_member.username = "alice"
        mock_member.name = "Alice Smith"
        mock_member.access_level = 40
        mock_project.members_all.list.return_value = [mock_member]

        result = get_project_members(project_path="org/project")

        assert len(result) == 1
        assert result[0].username == "alice"

    def test_returns_empty_without_project_path(self):
        """Returns empty list when project_path is None."""
        result = get_project_members(project_path=None)
        assert result == []


class TestGetCompareUrl:
    """Tests for get_compare_url function."""

    def test_generates_url(self):
        """Generates correct compare URL."""
        url = get_compare_url(
            project="org/project",
            host="gitlab.com",
            from_commit="abc123",
            to_commit="def456",
        )
        assert url == (
            "https://gitlab.com/org/project/-/compare/"
            "abc123...def456?from_project_id=&straight=false"
        )

    def test_returns_empty_without_project(self):
        """Returns empty string when project is None."""
        url = get_compare_url(
            project=None,
            host="gitlab.com",
            from_commit="abc123",
            to_commit="def456",
        )
        assert url == ""


class TestGetGitlabClientInternal:
    """Tests for _get_gitlab_client internal function."""

    @patch("glreview.gitlab.gitlab.Gitlab")
    def test_job_token_path(self, mock_gitlab):
        """Creates client with job token when provided."""
        from glreview.gitlab import _get_gitlab_client

        _get_gitlab_client.cache_clear()
        _get_gitlab_client("https://gitlab.com", None, "job-token-123")
        mock_gitlab.assert_called_with("https://gitlab.com", job_token="job-token-123")

    @patch("glreview.gitlab.gitlab.Gitlab")
    def test_private_token_path(self, mock_gitlab):
        """Creates client with private token when provided."""
        from glreview.gitlab import _get_gitlab_client

        _get_gitlab_client.cache_clear()
        _get_gitlab_client("https://gitlab.com", "private-token-123", None)
        mock_gitlab.assert_called_with(
            "https://gitlab.com", private_token="private-token-123"
        )

    @patch("glreview.gitlab.gitlab.Gitlab")
    def test_no_token_path(self, mock_gitlab):
        """Creates client without token when none provided."""
        from glreview.gitlab import _get_gitlab_client

        _get_gitlab_client.cache_clear()
        _get_gitlab_client("https://gitlab.com", None, None)
        mock_gitlab.assert_called_with("https://gitlab.com")


class TestGetGitlabClientAuthError:
    """Tests for get_gitlab_client authentication error handling."""

    @patch("glreview.gitlab._get_gitlab_client")
    def test_returns_none_on_auth_error(self, mock_get_client, monkeypatch):
        """Returns None when authentication fails."""
        from gitlab.exceptions import GitlabAuthenticationError

        monkeypatch.setenv("GITLAB_PRIVATE_TOKEN", "bad-token")
        mock_gl = MagicMock()
        mock_gl.auth.side_effect = GitlabAuthenticationError()
        mock_get_client.return_value = mock_gl

        client = get_gitlab_client()
        assert client is None


class TestGetProject:
    """Tests for _get_project internal function."""

    @patch("glreview.gitlab.gitlab.Gitlab")
    def test_returns_none_on_error(self, mock_gitlab_class):
        """Returns None when project fetch fails."""
        from gitlab.exceptions import GitlabGetError

        from glreview.gitlab import _get_project

        mock_gl = MagicMock()
        mock_gl.projects.get.side_effect = GitlabGetError()

        result = _get_project(mock_gl, "nonexistent/project")
        assert result is None


class TestCreateIssueEdgeCases:
    """Additional edge case tests for create_issue."""

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_none_when_project_not_found(
        self, mock_get_project, mock_get_client
    ):
        """Returns None when project doesn't exist."""
        mock_get_client.return_value = MagicMock()
        mock_get_project.return_value = None

        result = create_issue(
            title="Test",
            description="Test",
            project_path="org/project",
        )
        assert result is None

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_handles_user_lookup_success(self, mock_get_project, mock_get_client):
        """Assigns issue to user when lookup succeeds."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        # Mock successful user lookup
        mock_user = MagicMock()
        mock_user.id = 123
        mock_gl.users.list.return_value = [mock_user]

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Test"
        mock_issue.state = "opened"
        mock_issue.assignees = [MagicMock(username="alice")]
        mock_issue.labels = []
        mock_project.issues.create.return_value = mock_issue

        result = create_issue(
            title="Test",
            description="Test",
            project_path="org/project",
            assignee="@alice",
        )

        assert result is not None
        # Verify that assignee_ids was set in the call
        call_args = mock_project.issues.create.call_args
        assert call_args[0][0].get("assignee_ids") == [123]

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_handles_user_lookup_failure(self, mock_get_project, mock_get_client):
        """Continues without assignee when user lookup fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl
        mock_gl.users.list.side_effect = Exception("Lookup failed")

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project

        mock_issue = MagicMock()
        mock_issue.iid = 42
        mock_issue.web_url = "https://gitlab.com/org/project/-/issues/42"
        mock_issue.title = "Test"
        mock_issue.state = "opened"
        mock_issue.assignees = []
        mock_issue.labels = []
        mock_project.issues.create.return_value = mock_issue

        result = create_issue(
            title="Test",
            description="Test",
            project_path="org/project",
            assignee="@alice",
        )

        assert result is not None
        # Should have called issues.create even without assignee

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_none_on_create_exception(self, mock_get_project, mock_get_client):
        """Returns None when issue creation fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project
        mock_project.issues.create.side_effect = Exception("API Error")

        result = create_issue(
            title="Test",
            description="Test",
            project_path="org/project",
        )

        assert result is None


class TestGetIssueEdgeCases:
    """Additional edge case tests for get_issue."""

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_none_when_client_unavailable(self, mock_get_client):
        """Returns None when client is None."""
        mock_get_client.return_value = None

        result = get_issue("42", project_path="org/project")
        assert result is None

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_none_when_project_not_found(
        self, mock_get_project, mock_get_client
    ):
        """Returns None when project doesn't exist."""
        mock_get_client.return_value = MagicMock()
        mock_get_project.return_value = None

        result = get_issue("42", project_path="org/project")
        assert result is None

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_none_on_get_exception(self, mock_get_project, mock_get_client):
        """Returns None when get fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project
        mock_project.issues.get.side_effect = Exception("Not found")

        result = get_issue("42", project_path="org/project")
        assert result is None


class TestGetProjectMembersEdgeCases:
    """Additional edge case tests for get_project_members."""

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_empty_when_client_unavailable(self, mock_get_client):
        """Returns empty list when client is None."""
        mock_get_client.return_value = None

        result = get_project_members(project_path="org/project")
        assert result == []

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_empty_when_project_not_found(
        self, mock_get_project, mock_get_client
    ):
        """Returns empty list when project doesn't exist."""
        mock_get_client.return_value = MagicMock()
        mock_get_project.return_value = None

        result = get_project_members(project_path="org/project")
        assert result == []

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_empty_on_list_exception(self, mock_get_project, mock_get_client):
        """Returns empty list when list fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project
        mock_project.members_all.list.side_effect = Exception("API Error")

        result = get_project_members(project_path="org/project")
        assert result == []


class TestCloseIssueEdgeCases:
    """Additional edge case tests for close_issue."""

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_false_when_client_unavailable(self, mock_get_client):
        """Returns False when client is None."""
        mock_get_client.return_value = None

        result = close_issue("42", project_path="org/project")
        assert result is False

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_false_when_project_not_found(
        self, mock_get_project, mock_get_client
    ):
        """Returns False when project doesn't exist."""
        mock_get_client.return_value = MagicMock()
        mock_get_project.return_value = None

        result = close_issue("42", project_path="org/project")
        assert result is False

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_false_on_close_exception(self, mock_get_project, mock_get_client):
        """Returns False when close fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project
        mock_project.issues.get.side_effect = Exception("Not found")

        result = close_issue("42", project_path="org/project")
        assert result is False


class TestAddIssueCommentEdgeCases:
    """Additional edge case tests for add_issue_comment."""

    @patch("glreview.gitlab.get_gitlab_client")
    def test_returns_false_when_client_unavailable(self, mock_get_client):
        """Returns False when client is None."""
        mock_get_client.return_value = None

        result = add_issue_comment("42", project_path="org/project", comment="Test")
        assert result is False

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_false_when_project_not_found(
        self, mock_get_project, mock_get_client
    ):
        """Returns False when project doesn't exist."""
        mock_get_client.return_value = MagicMock()
        mock_get_project.return_value = None

        result = add_issue_comment("42", project_path="org/project", comment="Test")
        assert result is False

    @patch("glreview.gitlab.get_gitlab_client")
    @patch("glreview.gitlab._get_project")
    def test_returns_false_on_comment_exception(self, mock_get_project, mock_get_client):
        """Returns False when comment creation fails."""
        mock_gl = MagicMock()
        mock_get_client.return_value = mock_gl

        mock_project = MagicMock()
        mock_get_project.return_value = mock_project
        mock_project.issues.get.side_effect = Exception("Not found")

        result = add_issue_comment("42", project_path="org/project", comment="Test")
        assert result is False
