"""Tests for GitHubProvider — mocked PyGithub objects."""

from __future__ import annotations

from unittest.mock import MagicMock, PropertyMock, patch

import pytest
import github

from project_hub.forge.github import GitHubProvider
from project_hub.forge.provider import AuthenticationError


# ---------------------------------------------------------------------------
# Mock builders
# ---------------------------------------------------------------------------

def _make_review(user_login: str, state: str, submitted_at: str):
    """Build a mock review object."""
    r = MagicMock()
    r.user.login = user_login
    r.state = state
    r.submitted_at = submitted_at
    return r


def _make_pull(
    number=1,
    title="Fix the thing",
    state="open",
    merged=False,
    user_login="alice",
    head_sha="abc123",
    source_branch="fix/thing",
    target_branch="main",
    comments=2,
    review_comments=1,
    html_url="https://github.com/org/repo/pull/1",
    created_at="2024-01-01T00:00:00Z",
    updated_at="2024-01-02T00:00:00Z",
    assignees=None,
    requested_reviewers=None,
    reviews=None,
):
    """Build a mock PullRequest object."""
    pr = MagicMock()
    pr.number = number
    pr.id = 1000 + number
    pr.title = title
    pr.state = state
    pr.merged = merged
    pr.user.login = user_login
    pr.head.sha = head_sha
    pr.head.ref = source_branch
    pr.base.ref = target_branch
    pr.comments = comments
    pr.review_comments = review_comments
    pr.html_url = html_url
    pr.created_at = created_at
    pr.updated_at = updated_at

    # assignees
    assignee_mocks = []
    for name in (assignees or []):
        a = MagicMock()
        a.login = name
        assignee_mocks.append(a)
    pr.assignees = assignee_mocks

    # requested_reviewers
    reviewer_mocks = []
    for name in (requested_reviewers or []):
        r = MagicMock()
        r.login = name
        reviewer_mocks.append(r)
    pr.requested_reviewers = reviewer_mocks

    # reviews
    pr.get_reviews.return_value = reviews or []

    return pr


def _make_issue(
    number=10,
    title="Login bug",
    state="open",
    user_login="alice",
    html_url="https://github.com/org/repo/issues/10",
    comments=5,
    created_at="2024-01-01T00:00:00Z",
    updated_at="2024-01-02T00:00:00Z",
    labels=None,
    assignees=None,
    pull_request=None,
):
    """Build a mock Issue object."""
    iss = MagicMock()
    iss.number = number
    iss.id = 2000 + number
    iss.title = title
    iss.state = state
    iss.user.login = user_login
    iss.html_url = html_url
    iss.comments = comments
    iss.created_at = created_at
    iss.updated_at = updated_at
    iss.pull_request = pull_request

    label_mocks = []
    for name in (labels or []):
        lbl = MagicMock()
        lbl.name = name
        label_mocks.append(lbl)
    iss.labels = label_mocks

    assignee_mocks = []
    for name in (assignees or []):
        a = MagicMock()
        a.login = name
        assignee_mocks.append(a)
    iss.assignees = assignee_mocks

    return iss


def _make_workflow_run(
    run_id=500,
    name="CI",
    status="completed",
    conclusion="success",
    head_branch="main",
    html_url="https://github.com/org/repo/actions/runs/500",
    created_at="2024-01-01T00:00:00Z",
    jobs=None,
):
    """Build a mock WorkflowRun object."""
    run = MagicMock()
    run.id = run_id
    run.name = name
    run.status = status
    run.conclusion = conclusion
    run.head_branch = head_branch
    run.html_url = html_url
    run.created_at = created_at
    run.jobs.return_value = jobs or []
    return run


def _make_workflow_job(
    job_id=600,
    name="test",
    status="completed",
    conclusion="success",
    html_url="https://github.com/org/repo/actions/runs/500/jobs/600",
    workflow_name="CI",
):
    """Build a mock WorkflowJob object."""
    job = MagicMock()
    job.id = job_id
    job.name = name
    job.status = status
    job.conclusion = conclusion
    job.html_url = html_url
    job.workflow_name = workflow_name
    return job


def _make_provider(mock_gh=None):
    """Create a GitHubProvider with a mocked github.Github instance."""
    with patch("project_hub.forge.github.github.Github") as mock_cls:
        gh_instance = mock_gh or MagicMock()
        mock_cls.return_value = gh_instance
        provider = GitHubProvider("github.com", "fake-token")
    provider._gh = gh_instance
    return provider


# ---------------------------------------------------------------------------
# get_username
# ---------------------------------------------------------------------------

class TestGetUsername:
    def test_returns_and_caches(self):
        provider = _make_provider()
        user = MagicMock()
        user.login = "alice"
        provider._gh.get_user.return_value = user

        assert provider.get_username() == "alice"
        # Second call uses cache
        assert provider.get_username() == "alice"
        provider._gh.get_user.assert_called_once()

    def test_returns_none_on_auth_failure(self):
        provider = _make_provider()
        provider._gh.get_user.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        assert provider.get_username() is None
        assert provider._auth_failed is True
        # Subsequent calls don't retry
        assert provider.get_username() is None
        provider._gh.get_user.assert_called_once()


# ---------------------------------------------------------------------------
# list_merge_requests
# ---------------------------------------------------------------------------

class TestListMergeRequests:
    def test_maps_fields_correctly(self):
        pr = _make_pull(
            number=42,
            title="Add feature",
            state="open",
            user_login="bob",
            head_sha="deadbeef",
            source_branch="feat/new",
            target_branch="main",
            comments=3,
            review_comments=2,
            assignees=["alice"],
            requested_reviewers=["charlie"],
        )
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo", state="opened")

        assert len(mrs) == 1
        mr = mrs[0]
        assert mr.iid == 42
        assert mr.id == 1042
        assert mr.title == "Add feature"
        assert mr.author == "bob"
        assert mr.state == "opened"
        assert mr.source_branch == "feat/new"
        assert mr.target_branch == "main"
        assert mr.sha == "deadbeef"
        assert mr.user_notes_count == 5  # 3 + 2
        assert mr.assignees == ["alice"]
        assert mr.reviewers == ["charlie"]
        assert mr.repo_name == ""

    def test_state_open_to_opened(self):
        pr = _make_pull(state="open")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo")
        assert mrs[0].state == "opened"

    def test_state_closed_merged(self):
        pr = _make_pull(state="closed", merged=True)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo", state="merged")
        assert mrs[0].state == "merged"

    def test_state_closed_not_merged(self):
        pr = _make_pull(state="closed", merged=False)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo", state="closed")
        assert mrs[0].state == "closed"

    def test_approved_logic_latest_review_per_user(self):
        """Latest review per user wins; any APPROVED + no CHANGES_REQUESTED = approved."""
        reviews = [
            _make_review("alice", "CHANGES_REQUESTED", "2024-01-01T00:00:00Z"),
            _make_review("alice", "APPROVED", "2024-01-02T00:00:00Z"),  # latest for alice
            _make_review("bob", "APPROVED", "2024-01-01T00:00:00Z"),
        ]
        pr = _make_pull(state="open", reviews=reviews)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo")
        assert mrs[0].approved is True

    def test_approved_false_when_changes_requested(self):
        """If latest review from any user is CHANGES_REQUESTED, not approved."""
        reviews = [
            _make_review("alice", "APPROVED", "2024-01-01T00:00:00Z"),
            _make_review("alice", "CHANGES_REQUESTED", "2024-01-02T00:00:00Z"),
        ]
        pr = _make_pull(state="open", reviews=reviews)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo")
        assert mrs[0].approved is False

    def test_approved_not_fetched_for_closed_prs(self):
        """Reviews are not fetched for non-open PRs."""
        pr = _make_pull(state="closed", merged=True)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pulls.return_value = [pr]
        provider._gh.get_repo.return_value = repo

        mrs = provider.list_merge_requests("org/repo", state="merged")
        assert mrs[0].approved is False
        pr.get_reviews.assert_not_called()

    def test_auth_error_on_401(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        with pytest.raises(AuthenticationError):
            provider.list_merge_requests("org/repo")

    def test_returns_empty_on_other_error(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            500, {"message": "Internal Server Error"}, None
        )

        result = provider.list_merge_requests("org/repo")
        assert result == []


# ---------------------------------------------------------------------------
# list_issues
# ---------------------------------------------------------------------------

class TestListIssues:
    def test_maps_fields_and_filters_prs(self):
        real_issue = _make_issue(
            number=10, title="Bug", labels=["bug", "P1"], assignees=["alice"]
        )
        pr_as_issue = _make_issue(number=20, title="PR Thing", pull_request={"url": "..."})

        provider = _make_provider()
        repo = MagicMock()
        repo.get_issues.return_value = [real_issue, pr_as_issue]
        provider._gh.get_repo.return_value = repo

        issues = provider.list_issues("org/repo")
        assert len(issues) == 1
        iss = issues[0]
        assert iss.iid == 10
        assert iss.title == "Bug"
        assert iss.author == "alice"
        assert iss.state == "opened"
        assert iss.labels == ["bug", "P1"]
        assert iss.assignees == ["alice"]
        assert iss.participants == []
        assert iss.confidential is False

    def test_auth_error_on_401(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        with pytest.raises(AuthenticationError):
            provider.list_issues("org/repo")


# ---------------------------------------------------------------------------
# get_mrs_by_iids / get_issues_by_iids
# ---------------------------------------------------------------------------

class TestByIids:
    def test_get_mrs_by_iids(self):
        pr = _make_pull(number=5, title="MR Five")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pull.return_value = pr
        provider._gh.get_repo.return_value = repo

        mrs = provider.get_mrs_by_iids("org/repo", [5])
        assert len(mrs) == 1
        assert mrs[0].iid == 5

    def test_get_mrs_by_iids_empty(self):
        provider = _make_provider()
        assert provider.get_mrs_by_iids("org/repo", []) == []

    def test_get_issues_by_iids(self):
        iss = _make_issue(number=10, title="Issue Ten")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_issue.return_value = iss
        provider._gh.get_repo.return_value = repo

        issues = provider.get_issues_by_iids("org/repo", [10])
        assert len(issues) == 1
        assert issues[0].iid == 10

    def test_get_issues_by_iids_empty(self):
        provider = _make_provider()
        assert provider.get_issues_by_iids("org/repo", []) == []

    def test_get_mrs_by_iids_skips_missing(self):
        """If a PR number doesn't exist, it's skipped (not crash)."""
        provider = _make_provider()
        repo = MagicMock()
        repo.get_pull.side_effect = github.GithubException(
            404, {"message": "Not Found"}, None
        )
        provider._gh.get_repo.return_value = repo

        result = provider.get_mrs_by_iids("org/repo", [999])
        assert result == []


# ---------------------------------------------------------------------------
# list_pipelines (workflow runs)
# ---------------------------------------------------------------------------

class TestListPipelines:
    def test_maps_completed_success(self):
        run = _make_workflow_run(status="completed", conclusion="success")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert len(pipelines) == 1
        assert pipelines[0].status == "success"

    def test_maps_completed_failure(self):
        run = _make_workflow_run(status="completed", conclusion="failure")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "failed"

    def test_maps_completed_cancelled(self):
        run = _make_workflow_run(status="completed", conclusion="cancelled")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "canceled"

    def test_maps_queued(self):
        run = _make_workflow_run(status="queued", conclusion=None)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "pending"

    def test_maps_in_progress(self):
        run = _make_workflow_run(status="in_progress", conclusion=None)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "running"

    def test_maps_waiting(self):
        run = _make_workflow_run(status="waiting", conclusion=None)
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "waiting_for_resource"

    def test_maps_completed_timed_out(self):
        run = _make_workflow_run(status="completed", conclusion="timed_out")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "failed"

    def test_maps_completed_skipped(self):
        run = _make_workflow_run(status="completed", conclusion="skipped")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "skipped"

    def test_maps_completed_unknown_conclusion(self):
        run = _make_workflow_run(status="completed", conclusion="stale")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        assert pipelines[0].status == "failed"

    def test_filters_by_ref(self):
        run = _make_workflow_run(head_branch="main")
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        provider.list_pipelines("org/repo", ref="main")
        repo.get_workflow_runs.assert_called_once()
        call_kwargs = repo.get_workflow_runs.call_args[1]
        assert call_kwargs.get("branch") == "main"

    def test_pipeline_fields(self):
        run = _make_workflow_run(
            run_id=500,
            status="completed",
            conclusion="success",
            head_branch="main",
            html_url="https://github.com/org/repo/actions/runs/500",
            created_at="2024-01-01T00:00:00Z",
        )
        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_runs.return_value = [run]
        provider._gh.get_repo.return_value = repo

        pipelines = provider.list_pipelines("org/repo")
        pip = pipelines[0]
        assert pip.id == 500
        assert pip.ref == "main"
        assert pip.web_url == "https://github.com/org/repo/actions/runs/500"
        assert pip.created_at == "2024-01-01T00:00:00Z"
        assert pip.repo_name == ""

    def test_auth_error_on_401(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        with pytest.raises(AuthenticationError):
            provider.list_pipelines("org/repo")


# ---------------------------------------------------------------------------
# get_pipeline_jobs
# ---------------------------------------------------------------------------

class TestGetPipelineJobs:
    def test_maps_job_fields(self):
        job = _make_workflow_job(
            job_id=600, name="test", status="completed",
            conclusion="success", workflow_name="CI",
        )
        run = MagicMock()
        run.jobs.return_value = [job]

        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_run.return_value = run
        provider._gh.get_repo.return_value = repo

        jobs = provider.get_pipeline_jobs("org/repo", 500)
        assert len(jobs) == 1
        j = jobs[0]
        assert j.id == 600
        assert j.name == "test"
        assert j.status == "success"
        assert j.stage == "CI"
        assert "600" in j.web_url or j.web_url != ""

    def test_job_without_workflow_name(self):
        """When workflow_name is missing, stage defaults to empty."""
        job = _make_workflow_job(job_id=601, name="lint")
        del job.workflow_name  # remove the attr
        # getattr should fall back
        run = MagicMock()
        run.jobs.return_value = [job]

        provider = _make_provider()
        repo = MagicMock()
        repo.get_workflow_run.return_value = run
        provider._gh.get_repo.return_value = repo

        jobs = provider.get_pipeline_jobs("org/repo", 500)
        assert jobs[0].stage == ""


# ---------------------------------------------------------------------------
# get_job_log
# ---------------------------------------------------------------------------

class TestGetJobLog:
    def test_returns_log_content(self):
        provider = _make_provider()
        requester = MagicMock()
        requester.requestBlobAndCheck.return_value = (
            {},
            b"Running tests...\nAll passed.",
        )
        provider._gh._Github__requester = requester

        log = provider.get_job_log("org/repo", 600)
        assert log == "Running tests...\nAll passed."

    def test_returns_empty_on_error(self):
        provider = _make_provider()
        requester = MagicMock()
        requester.requestBlobAndCheck.side_effect = Exception("Not found")
        provider._gh._Github__requester = requester

        log = provider.get_job_log("org/repo", 600)
        assert log == ""


# ---------------------------------------------------------------------------
# get_vulnerabilities
# ---------------------------------------------------------------------------

class TestGetVulnerabilities:
    def _make_dependabot_alert(self, number=1):
        a = MagicMock()
        a.number = number
        a.state = "open"
        a.security_advisory.summary = "SQL Injection in dep"
        a.security_advisory.severity = "high"
        a.security_advisory.description = "Use parameterized queries."
        a.dependency.manifest_path = "requirements.txt"
        a.dependency.package.name = "vulnerable-lib"
        a.html_url = "https://github.com/org/repo/security/dependabot/1"
        return a

    def _make_codescan_alert(self, number=1):
        a = MagicMock()
        a.number = number
        a.state = "open"
        a.rule.description = "Hardcoded password"
        a.rule.security_severity_level = "high"
        a.rule.severity = "warning"
        loc = MagicMock()
        loc.path = "src/auth.py"
        loc.start_line = 42
        a.most_recent_instance.location = loc
        a.html_url = "https://github.com/org/repo/security/code-scanning/1"
        return a

    def _make_secret_alert(self, number=1):
        a = MagicMock()
        a.number = number
        a.state = "open"
        a.secret_type_display_name = "GitHub Token"
        a.resolution = None
        a.resolution_comment = None
        a.html_url = "https://github.com/org/repo/security/secret-scanning/1"
        loc = MagicMock()
        loc.path = ".env"
        loc.start_line = 3
        a.first_location_detected = loc
        return a

    def test_concatenates_all_three_sources(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = [self._make_dependabot_alert(1)]
        repo.get_codescan_alerts.return_value = [self._make_codescan_alert(2)]
        repo.get_secret_scanning_alerts.return_value = [self._make_secret_alert(3)]
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        assert len(vulns) == 3

        ids = [v.id for v in vulns]
        assert "dependabot-1" in ids
        assert "codescan-2" in ids
        assert "secret-3" in ids

    def test_dependabot_fields(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = [self._make_dependabot_alert(1)]
        repo.get_codescan_alerts.return_value = []
        repo.get_secret_scanning_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        v = vulns[0]
        assert v.id == "dependabot-1"
        assert v.name == "SQL Injection in dep"
        assert v.severity == "HIGH"
        assert v.state == "detected"  # GitHub "open" → unified "detected"
        assert v.source == "DEPENDENCY_SCANNING"
        assert v.location == "requirements.txt (vulnerable-lib)"
        assert v.solution == "Use parameterized queries."

    def test_codescan_fields(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = []
        repo.get_codescan_alerts.return_value = [self._make_codescan_alert(2)]
        repo.get_secret_scanning_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        v = vulns[0]
        assert v.id == "codescan-2"
        assert v.name == "Hardcoded password"
        assert v.severity == "HIGH"
        assert v.source == "SAST"
        assert v.location == "src/auth.py:42"

    def test_secret_fields(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = []
        repo.get_codescan_alerts.return_value = []
        repo.get_secret_scanning_alerts.return_value = [self._make_secret_alert(3)]
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        v = vulns[0]
        assert v.id == "secret-3"
        assert v.name == "GitHub Token"
        assert v.severity == "HIGH"
        assert v.source == "SECRET_DETECTION"
        assert v.location == ".env:3"

    def test_dependabot_403_graceful(self):
        """Dependabot disabled (403) doesn't fail the whole fetch."""
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.side_effect = github.GithubException(
            403, {"message": "Dependabot alerts are disabled"}, None
        )
        repo.get_codescan_alerts.return_value = [self._make_codescan_alert(1)]
        repo.get_secret_scanning_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        assert len(vulns) == 1
        assert vulns[0].id == "codescan-1"

    def test_codescan_404_graceful(self):
        """Code scanning not found (404) doesn't fail the whole fetch."""
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = [self._make_dependabot_alert(1)]
        repo.get_codescan_alerts.side_effect = github.GithubException(
            404, {"message": "Not Found"}, None
        )
        repo.get_secret_scanning_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        assert len(vulns) == 1
        assert vulns[0].id == "dependabot-1"

    def test_secret_scanning_403_graceful(self):
        """Secret scanning disabled (403) doesn't fail the whole fetch."""
        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = []
        repo.get_codescan_alerts.return_value = []
        repo.get_secret_scanning_alerts.side_effect = github.GithubException(
            403, {"message": "Secret scanning is disabled"}, None
        )
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo")
        assert vulns == []

    def test_auth_error_on_401(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        with pytest.raises(AuthenticationError):
            provider.get_vulnerabilities("org/repo")

    def test_severity_filter(self):
        """Severity filter is applied client-side."""
        dep = self._make_dependabot_alert(1)
        dep.security_advisory.severity = "low"

        provider = _make_provider()
        repo = MagicMock()
        repo.get_dependabot_alerts.return_value = [dep]
        repo.get_codescan_alerts.return_value = []
        repo.get_secret_scanning_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        vulns = provider.get_vulnerabilities("org/repo", severity="HIGH")
        assert len(vulns) == 0

        vulns_all = provider.get_vulnerabilities("org/repo")
        assert len(vulns_all) == 1


# ---------------------------------------------------------------------------
# get_pipeline_findings
# ---------------------------------------------------------------------------

class TestGetPipelineFindings:
    def test_returns_rule_descriptions_not_ids(self):
        """Findings should use human-readable rule descriptions."""
        alert1 = MagicMock()
        alert1.rule.description = "Hardcoded password"
        alert1.rule.id = "py/hardcoded-credentials"

        alert2 = MagicMock()
        alert2.rule.description = "SQL Injection"
        alert2.rule.id = "py/sql-injection"

        provider = _make_provider()
        repo = MagicMock()
        repo.get_codescan_alerts.return_value = [alert1, alert2]
        provider._gh.get_repo.return_value = repo

        findings = provider.get_pipeline_findings("org/repo", pipeline_id=None, ref="main")
        assert findings == {"Hardcoded password", "SQL Injection"}

    def test_ignores_pipeline_id(self):
        """pipeline_id is ignored; ref is used to filter."""
        provider = _make_provider()
        repo = MagicMock()
        repo.get_codescan_alerts.return_value = []
        provider._gh.get_repo.return_value = repo

        provider.get_pipeline_findings("org/repo", pipeline_id=12345, ref="feature")
        repo.get_codescan_alerts.assert_called_once()
        call_kwargs = repo.get_codescan_alerts.call_args[1]
        assert call_kwargs.get("ref") == "feature"

    def test_graceful_on_403(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.get_codescan_alerts.side_effect = github.GithubException(
            403, {"message": "Code scanning disabled"}, None
        )
        provider._gh.get_repo.return_value = repo

        findings = provider.get_pipeline_findings("org/repo", pipeline_id=None, ref="main")
        assert findings == set()


# ---------------------------------------------------------------------------
# get_default_branch / get_project_info
# ---------------------------------------------------------------------------

class TestProjectInfo:
    def test_get_default_branch(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.default_branch = "main"
        provider._gh.get_repo.return_value = repo

        assert provider.get_default_branch("org/repo") == "main"

    def test_get_default_branch_on_error(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = Exception("boom")

        assert provider.get_default_branch("org/repo") is None

    def test_get_project_info(self):
        provider = _make_provider()
        repo = MagicMock()
        repo.name = "my-repo"
        repo.html_url = "https://github.com/org/my-repo"
        repo.default_branch = "main"
        provider._gh.get_repo.return_value = repo

        info = provider.get_project_info("org/my-repo")
        assert info == {
            "name": "my-repo",
            "web_url": "https://github.com/org/my-repo",
            "default_branch": "main",
        }

    def test_get_project_info_auth_error(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            401, {"message": "Bad credentials"}, None
        )

        with pytest.raises(AuthenticationError):
            provider.get_project_info("org/repo")

    def test_get_project_info_on_other_error(self):
        provider = _make_provider()
        provider._gh.get_repo.side_effect = github.GithubException(
            500, {"message": "Server Error"}, None
        )

        assert provider.get_project_info("org/repo") == {}
