from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import requests
from github import GithubException, RateLimitExceededException

from prowler.providers.github.services.repository.repository_service import (
    Branch,
    Repo,
    Repository,
)
from tests.providers.github.github_fixtures import set_mocked_github_provider


def mock_list_repositories(_):
    return {
        1: Repo(
            id=1,
            name="repo1",
            owner="account-name",
            full_name="account-name/repo1",
            immutable_releases_enabled=True,
            default_branch=Branch(
                name="main",
                protected=True,
                default_branch=True,
                require_pull_request=True,
                approval_count=2,
                required_linear_history=True,
                allow_force_pushes=True,
                branch_deletion=True,
                status_checks=True,
                enforce_admins=True,
                require_code_owner_reviews=True,
                require_signed_commits=True,
                conversation_resolution=True,
            ),
            private=False,
            securitymd=True,
            codeowners_exists=True,
            secret_scanning_enabled=True,
            archived=False,
            pushed_at=datetime.now(timezone.utc),
            delete_branch_on_merge=True,
            dependabot_alerts_enabled=True,
        ),
    }


@patch(
    "prowler.providers.github.services.repository.repository_service.Repository._list_repositories",
    new=mock_list_repositories,
)
class Test_Repository_Service:
    def test_get_client(self):
        repository_service = Repository(set_mocked_github_provider())
        assert repository_service.clients[0].__class__.__name__ == "Github"

    def test_get_service(self):
        repository_service = Repository(set_mocked_github_provider())
        assert repository_service.__class__.__name__ == "Repository"

    def test_list_repositories(self):
        repository_service = Repository(set_mocked_github_provider())
        assert len(repository_service.repositories) == 1
        assert repository_service.repositories[1].name == "repo1"
        assert repository_service.repositories[1].full_name == "account-name/repo1"
        assert repository_service.repositories[1].private is False
        assert repository_service.repositories[1].default_branch.name == "main"
        assert repository_service.repositories[1].securitymd
        assert repository_service.repositories[1].default_branch.required_linear_history
        assert repository_service.repositories[1].default_branch.require_pull_request
        assert repository_service.repositories[1].default_branch.allow_force_pushes
        assert repository_service.repositories[1].default_branch.branch_deletion
        assert repository_service.repositories[1].default_branch.status_checks
        assert repository_service.repositories[1].default_branch.enforce_admins
        assert repository_service.repositories[1].delete_branch_on_merge
        assert repository_service.repositories[1].default_branch.conversation_resolution
        assert repository_service.repositories[1].default_branch.approval_count == 2
        assert repository_service.repositories[1].codeowners_exists is True
        assert (
            repository_service.repositories[1].default_branch.require_code_owner_reviews
            is True
        )
        assert repository_service.repositories[1].secret_scanning_enabled is True
        assert (
            repository_service.repositories[1].default_branch.require_signed_commits
            is True
        )
        assert repository_service.repositories[1].archived is False
        assert repository_service.repositories[1].pushed_at is not None
        assert repository_service.repositories[1].immutable_releases_enabled is True


class Test_Repository_FileExists:
    def setup_method(self):
        self.repository = Repository(set_mocked_github_provider())
        self.mock_repo = MagicMock()

    def test_file_exists_returns_true(self):
        self.mock_repo.get_contents.return_value = object()
        assert self.repository._file_exists(self.mock_repo, "somefile.txt") is True

    def test_file_not_found_returns_false(self):
        self.mock_repo.get_contents.side_effect = Exception("404 Not Found")
        assert self.repository._file_exists(self.mock_repo, "nofile.txt") is False

    def test_other_error_returns_none_and_logs(self):
        self.mock_repo.get_contents.side_effect = Exception("Some other error")
        with patch(
            "prowler.providers.github.services.repository.repository_service.logger"
        ) as mock_logger:
            assert self.repository._file_exists(self.mock_repo, "errorfile.txt") is None
            assert mock_logger.error.called


class Test_Repository_GraphQL:
    def setup_method(self):
        self.mock_repo1 = MagicMock()
        self.mock_repo1.id = 1
        self.mock_repo1.name = "repo1"
        self.mock_repo1.owner.login = "owner1"
        self.mock_repo1.full_name = "owner1/repo1"
        self.mock_repo1.default_branch = "main"
        self.mock_repo1.private = False
        self.mock_repo1.archived = False
        self.mock_repo1.pushed_at = datetime.now(timezone.utc)
        self.mock_repo1.delete_branch_on_merge = False
        self.mock_repo1.security_and_analysis = None
        self.mock_repo1.get_contents.side_effect = [None, None, None]
        self.mock_repo1.get_branch.side_effect = Exception("404 Not Found")
        self.mock_repo1.get_dependabot_alerts.side_effect = Exception("403 Forbidden")

    def test_no_scoping_uses_graphql(self):
        """Test that no scoping triggers the GraphQL discovery method successfully"""
        provider = set_mocked_github_provider()
        provider.repositories = []
        provider.organizations = []

        with patch.object(Repository, "__init__", lambda *_: None):
            repository_service = Repository(provider)
            mock_client = MagicMock()
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            with patch.object(
                repository_service,
                "_get_accessible_repos_graphql",
                return_value=["owner1/repo1"],
            ) as mock_graphql_call:
                mock_client.get_repo.return_value = self.mock_repo1

                repos = repository_service._list_repositories()

                assert len(repos) == 1
                assert 1 in repos
                assert repos[1].name == "repo1"

                mock_graphql_call.assert_called_once()
                mock_client.get_repo.assert_called_once_with("owner1/repo1")

    def test_graphql_call_api_error(self):
        """Test that an error during the GraphQL call is handled gracefully"""
        provider = set_mocked_github_provider()
        provider.repositories = []
        provider.organizations = []

        with patch.object(Repository, "__init__", lambda *_: None):
            repository_service = Repository(provider)
            repository_service.clients = [MagicMock()]
            repository_service.provider = provider

            with patch(
                "requests.post",
                side_effect=requests.exceptions.RequestException("API Error"),
            ):
                with patch(
                    "prowler.providers.github.services.repository.repository_service.logger"
                ) as mock_logger:

                    repos = repository_service._list_repositories()

                    assert len(repos) == 0
                    mock_logger.error.assert_called_once()

                    log_output = str(mock_logger.error.call_args)
                    assert "RequestException" in log_output
                    assert "API Error" in log_output

    def test_graphql_returns_empty_list(self):
        """Test the case where GraphQL returns no repositories"""
        provider = set_mocked_github_provider()
        provider.repositories = []
        provider.organizations = []

        with patch.object(Repository, "__init__", lambda *_: None):
            repository_service = Repository(provider)
            repository_service.clients = [MagicMock()]
            repository_service.provider = provider

            with patch.object(
                repository_service, "_get_accessible_repos_graphql", return_value=[]
            ):
                with patch(
                    "prowler.providers.github.services.repository.repository_service.logger"
                ) as mock_logger:

                    repos = repository_service._list_repositories()

                    assert len(repos) == 0
                    mock_logger.warning.assert_called_with(
                        "Could not find any accessible repositories with the provided token."
                    )


class Test_Repository_Scoping:
    def setup_method(self):
        self.mock_repo1 = MagicMock()
        self.mock_repo1.id = 1
        self.mock_repo1.name = "repo1"
        self.mock_repo1.owner.login = "owner1"
        self.mock_repo1.full_name = "owner1/repo1"
        self.mock_repo1.default_branch = "main"
        self.mock_repo1.private = False
        self.mock_repo1.archived = False
        self.mock_repo1.pushed_at = datetime.now(timezone.utc)
        self.mock_repo1.delete_branch_on_merge = True
        self.mock_repo1.security_and_analysis = None
        self.mock_repo1.get_contents.side_effect = [None, None, None]
        self.mock_repo1.get_branch.side_effect = Exception("404 Not Found")
        self.mock_repo1.get_dependabot_alerts.side_effect = Exception("404 Not Found")

        self.mock_repo2 = MagicMock()
        self.mock_repo2.id = 2
        self.mock_repo2.name = "repo2"
        self.mock_repo2.owner.login = "owner2"
        self.mock_repo2.full_name = "owner2/repo2"
        self.mock_repo2.default_branch = "main"
        self.mock_repo2.private = False
        self.mock_repo2.archived = False
        self.mock_repo2.pushed_at = datetime.now(timezone.utc)
        self.mock_repo2.delete_branch_on_merge = True
        self.mock_repo2.security_and_analysis = None
        self.mock_repo2.get_contents.side_effect = [None, None, None]
        self.mock_repo2.get_branch.side_effect = Exception("404 Not Found")
        self.mock_repo2.get_dependabot_alerts.side_effect = Exception("404 Not Found")

    def test_qualified_repo_with_organization_skips_org_fetch(self):
        """Test that a fully qualified repo with --organization does not fetch all org repos"""
        provider = set_mocked_github_provider()
        provider.repositories = ["owner1/repo1"]
        provider.organizations = ["org2"]

        mock_client = MagicMock()
        mock_client.get_repo.return_value = self.mock_repo1

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            repos = repository_service._list_repositories()

            assert len(repos) == 1
            assert 1 in repos
            assert repos[1].name == "repo1"
            mock_client.get_repo.assert_called_once_with("owner1/repo1")
            mock_client.get_organization.assert_not_called()

    def test_unqualified_repo_qualified_with_organization(self):
        """Test that an unqualified repo name is qualified with the organization"""
        provider = set_mocked_github_provider()
        provider.repositories = ["repo1"]
        provider.organizations = ["owner1"]

        mock_client = MagicMock()
        mock_client.get_repo.return_value = self.mock_repo1

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            repos = repository_service._list_repositories()

            assert len(repos) == 1
            assert 1 in repos
            assert repos[1].name == "repo1"
            mock_client.get_repo.assert_called_once_with("owner1/repo1")

    def test_unqualified_repo_qualified_with_multiple_organizations(self):
        """Test that an unqualified repo is qualified with each organization"""
        provider = set_mocked_github_provider()
        provider.repositories = ["repo1"]
        provider.organizations = ["org1", "org2"]

        mock_client = MagicMock()
        mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2]

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            repos = repository_service._list_repositories()

            assert len(repos) == 2
            mock_client.get_repo.assert_any_call("org1/repo1")
            mock_client.get_repo.assert_any_call("org2/repo1")

    def test_unqualified_repo_without_organization_is_skipped(self):
        """Test that an unqualified repo without --organization is skipped with a warning"""
        provider = set_mocked_github_provider()
        provider.repositories = ["repo1"]
        provider.organizations = []

        mock_client = MagicMock()

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            with patch(
                "prowler.providers.github.services.repository.repository_service.logger"
            ) as mock_logger:
                repos = repository_service._list_repositories()

                assert len(repos) == 0
                mock_logger.warning.assert_called_with(
                    "Repository name 'repo1' should be in 'owner/repo-name' format. Skipping."
                )
                mock_client.get_repo.assert_not_called()

    def test_mixed_qualified_and_unqualified_repos_with_organization(self):
        """Test mix of qualified and unqualified repos with --organization"""
        provider = set_mocked_github_provider()
        provider.repositories = ["repo1", "owner2/repo2"]
        provider.organizations = ["org1"]

        mock_client = MagicMock()
        mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2]

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            repos = repository_service._list_repositories()

            assert len(repos) == 2
            mock_client.get_repo.assert_any_call("org1/repo1")
            mock_client.get_repo.assert_any_call("owner2/repo2")


class Test_Repository_Validation:
    def setup_method(self):
        self.repository = Repository(set_mocked_github_provider())

    def test_validate_repository_format_valid(self):
        """Test repository format validation with valid formats"""
        # Valid formats
        assert self.repository._validate_repository_format("owner/repo") is True
        assert self.repository._validate_repository_format("my-org/my-repo") is True
        assert (
            self.repository._validate_repository_format("user123/project_name") is True
        )
        assert self.repository._validate_repository_format("org/repo.name") is True

    def test_validate_repository_format_invalid(self):
        """Test repository format validation with invalid formats"""
        # Invalid formats
        assert self.repository._validate_repository_format("") is False
        assert self.repository._validate_repository_format("no-slash") is False
        assert self.repository._validate_repository_format("too/many/slashes") is False
        assert self.repository._validate_repository_format("/missing-owner") is False
        assert self.repository._validate_repository_format("missing-repo/") is False


class Test_Repository_ErrorHandling:
    def setup_method(self):
        self.mock_repo1 = MagicMock()
        self.mock_repo1.id = 1
        self.mock_repo1.name = "repo1"
        self.mock_repo1.owner.login = "owner1"
        self.mock_repo1.full_name = "owner1/repo1"
        self.mock_repo1.default_branch = "main"
        self.mock_repo1.private = False
        self.mock_repo1.archived = False
        self.mock_repo1.pushed_at = datetime.now(timezone.utc)
        self.mock_repo1.delete_branch_on_merge = True
        self.mock_repo1.security_and_analysis = None
        self.mock_repo1.get_contents.return_value = None
        self.mock_repo1.get_branch.side_effect = Exception("404 Not Found")
        self.mock_repo1.get_dependabot_alerts.side_effect = Exception("404 Not Found")

    def test_github_api_error_handling(self):
        """Test that GitHub API errors are handled properly"""
        provider = set_mocked_github_provider()
        provider.repositories = ["owner/repo1"]
        provider.organizations = []

        mock_client = MagicMock()
        mock_client.get_repo.side_effect = GithubException(403, "Forbidden", None)

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            with patch(
                "prowler.providers.github.lib.service.service.logger"
            ) as mock_logger:
                repos = repository_service._list_repositories()

                # Should be empty due to API error
                assert len(repos) == 0
                # Should log specific error message
                mock_logger.error.assert_called()
                # Check if Access denied message was logged
                log_messages = [str(call) for call in mock_logger.error.call_args_list]
                assert any("Access denied" in msg for msg in log_messages)

    def test_rate_limit_error_handling(self):
        """Test that rate limit errors are logged appropriately"""
        provider = set_mocked_github_provider()
        provider.repositories = ["owner/repo1"]
        provider.organizations = []

        mock_client = MagicMock()
        mock_client.get_repo.side_effect = RateLimitExceededException(
            429, "Rate limit exceeded", None
        )

        with patch(
            "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
        ):
            repository_service = Repository(provider)
            repository_service.clients = [mock_client]
            repository_service.provider = provider

            with patch(
                "prowler.providers.github.lib.service.service.logger"
            ) as mock_logger:
                # Rate limit errors should be caught and logged at the outer level
                repos = repository_service._list_repositories()

                # Should be empty due to rate limit error
                assert len(repos) == 0
                # Should log rate limit error
                mock_logger.error.assert_called()
                assert "Rate limit exceeded" in str(mock_logger.error.call_args)


class Test_Repository_BranchProtectionRulesets:
    def setup_method(self):
        self.repository_service = Repository.__new__(Repository)
        self.repository_service.provider = set_mocked_github_provider()
        self.repository_service.clients = []
        self.repository_service.audit_config = None
        self.repository_service.fixer_config = None

    def _build_repo(
        self,
        *,
        branch_protected: bool,
        dismiss_stale_reviews: bool = False,
        ruleset_details: list[dict] | None = None,
    ):
        repo = MagicMock()
        repo.id = 1
        repo.name = "repo1"
        repo.owner.login = "owner1"
        repo.full_name = "owner1/repo1"
        repo.default_branch = "main"
        repo.private = False
        repo.archived = False
        repo.pushed_at = datetime.now(timezone.utc)
        repo.delete_branch_on_merge = False
        repo.security_and_analysis = None
        repo.get_contents.side_effect = [None, None, None, None]
        repo.get_dependabot_alerts.side_effect = Exception(
            "403 Forbidden: Dependabot alerts are disabled for this repository."
        )

        branch = MagicMock()
        branch.protected = branch_protected
        branch.get_required_signatures.return_value = False

        protection = MagicMock()
        protection.required_linear_history = False
        protection.allow_force_pushes = False
        protection.allow_deletions = False
        protection.required_status_checks = None
        protection.enforce_admins = False
        protection.required_conversation_resolution = False

        if branch_protected:
            protection.required_pull_request_reviews = MagicMock(
                required_approving_review_count=1,
                require_code_owner_reviews=False,
                dismiss_stale_reviews=dismiss_stale_reviews,
            )
        else:
            protection.required_pull_request_reviews = None

        branch.get_protection.return_value = protection
        repo.get_branch.return_value = branch

        ruleset_details = ruleset_details or []
        repo._requester.requestJsonAndCheck.side_effect = [
            (None, [{"id": ruleset["id"]} for ruleset in ruleset_details]),
            *[(None, ruleset) for ruleset in ruleset_details],
        ]

        return repo

    def _build_pull_request_ruleset(self, *, enforcement: str, include: list[str]):
        return {
            "id": 101,
            "name": "Dismiss stale reviews",
            "target": "branch",
            "source_type": "Repository",
            "source": "owner1/repo1",
            "enforcement": enforcement,
            "conditions": {"ref_name": {"include": include, "exclude": []}},
            "rules": [
                {
                    "type": "pull_request",
                    "parameters": {
                        "dismiss_stale_reviews_on_push": True,
                        "require_code_owner_review": False,
                        "require_last_push_approval": False,
                        "required_approving_review_count": 1,
                        "required_review_thread_resolution": False,
                    },
                }
            ],
        }

    def _build_ruleset(
        self,
        *,
        enforcement,
        include,
        rules,
        bypass_actors=None,
        ruleset_id=201,
    ):
        return {
            "id": ruleset_id,
            "name": "Branch protection ruleset",
            "target": "branch",
            "source_type": "Repository",
            "source": "owner1/repo1",
            "enforcement": enforcement,
            "bypass_actors": bypass_actors or [],
            "conditions": {"ref_name": {"include": include, "exclude": []}},
            "rules": rules,
        }

    def test_process_repository_uses_classic_branch_protection(self):
        repo = self._build_repo(branch_protected=True, dismiss_stale_reviews=True)
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.dismiss_stale_reviews is True
        assert repos[1].default_branch.dismiss_stale_reviews_source == "classic"

    def test_process_repository_uses_active_ruleset_for_default_branch(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_pull_request_ruleset(
                    enforcement="active", include=["~DEFAULT_BRANCH"]
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.dismiss_stale_reviews is True
        assert repos[1].default_branch.dismiss_stale_reviews_source == "ruleset"

    def test_process_repository_treats_all_branches_ruleset_as_default_branch(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_pull_request_ruleset(enforcement="active", include=["~ALL"])
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.dismiss_stale_reviews is True
        assert repos[1].default_branch.dismiss_stale_reviews_source == "ruleset"

    def test_process_repository_marks_inactive_ruleset_as_fail_signal(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_pull_request_ruleset(
                    enforcement="disabled", include=["~DEFAULT_BRANCH"]
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.dismiss_stale_reviews is False
        assert (
            repos[1].default_branch.dismiss_stale_reviews_source == "ruleset_not_active"
        )

    def test_ruleset_non_fast_forward_disallows_force_push(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.allow_force_pushes is False
        assert repos[1].default_branch.allow_force_pushes_source == "ruleset"

    def test_ruleset_non_fast_forward_inactive_keeps_force_push_allowed(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="disabled",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.allow_force_pushes is True
        assert repos[1].default_branch.allow_force_pushes_source == "ruleset_not_active"

    def test_classic_protection_takes_precedence_over_inactive_ruleset(self):
        # Classic protection already disallows force pushes, so an inactive ruleset
        # must not downgrade the result.
        repo = self._build_repo(
            branch_protected=True,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="disabled",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.allow_force_pushes is False
        assert repos[1].default_branch.allow_force_pushes_source == "classic"

    def test_ruleset_required_signatures_requires_signed_commits(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "required_signatures"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.require_signed_commits is True
        assert repos[1].default_branch.require_signed_commits_source == "ruleset"

    def test_ruleset_required_linear_history(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "required_linear_history"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.required_linear_history is True
        assert repos[1].default_branch.required_linear_history_source == "ruleset"

    def test_ruleset_required_status_checks(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[
                        {
                            "type": "required_status_checks",
                            "parameters": {
                                "required_status_checks": [{"context": "ci/build"}],
                                "strict_required_status_checks_policy": True,
                            },
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.status_checks is True
        assert repos[1].default_branch.status_checks_source == "ruleset"

    def test_ruleset_required_status_checks_without_configured_checks(self):
        # A required_status_checks rule with an empty list enforces nothing, so it
        # must not be treated as a passing status-checks requirement.
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[
                        {
                            "type": "required_status_checks",
                            "parameters": {
                                "required_status_checks": [],
                                "strict_required_status_checks_policy": True,
                            },
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.status_checks is False
        assert repos[1].default_branch.status_checks_source is None

    def test_ruleset_deletion_disables_branch_deletion(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "deletion"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.branch_deletion is False
        assert repos[1].default_branch.branch_deletion_source == "ruleset"

    def test_active_ruleset_marks_default_branch_protected(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.protected is True
        assert repos[1].default_branch.protected_source == "ruleset"

    def test_ruleset_pull_request_parameters_map_to_attributes(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[
                        {
                            "type": "pull_request",
                            "parameters": {
                                "dismiss_stale_reviews_on_push": False,
                                "require_code_owner_review": True,
                                "require_last_push_approval": False,
                                "required_approving_review_count": 2,
                                "required_review_thread_resolution": True,
                            },
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        branch = repos[1].default_branch
        assert branch.require_pull_request is True
        assert branch.require_pull_request_source == "ruleset"
        assert branch.require_code_owner_reviews is True
        assert branch.require_code_owner_reviews_source == "ruleset"
        assert branch.conversation_resolution is True
        assert branch.conversation_resolution_source == "ruleset"
        assert branch.approval_count == 2
        assert branch.approval_count_source == "ruleset"

    def test_inactive_ruleset_approval_count_is_fail_signal(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="disabled",
                    include=["~DEFAULT_BRANCH"],
                    rules=[
                        {
                            "type": "pull_request",
                            "parameters": {
                                "required_approving_review_count": 2,
                            },
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.approval_count == 0
        assert repos[1].default_branch.approval_count_source == "ruleset_not_active"

    def test_active_ruleset_without_bypass_actors_applies_to_admins(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                    bypass_actors=[],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.enforce_admins is True
        assert repos[1].default_branch.enforce_admins_source == "ruleset"

    def test_active_ruleset_with_bypass_actors_does_not_apply_to_admins(self):
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="active",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                    bypass_actors=[
                        {
                            "actor_id": 1,
                            "actor_type": "RepositoryRole",
                            "bypass_mode": "always",
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        # Admins can bypass, so the rulesets do not enforce protection for them.
        assert repos[1].default_branch.enforce_admins is False
        assert repos[1].default_branch.enforce_admins_source is None

    def test_inactive_ruleset_with_bypass_actors_is_not_admin_fail_signal(self):
        # A disabled ruleset that has bypass actors would not apply to admins even if
        # activated, so it must not raise the enforce-admins ruleset_not_active signal.
        repo = self._build_repo(
            branch_protected=False,
            ruleset_details=[
                self._build_ruleset(
                    enforcement="disabled",
                    include=["~DEFAULT_BRANCH"],
                    rules=[{"type": "non_fast_forward"}],
                    bypass_actors=[
                        {
                            "actor_id": 1,
                            "actor_type": "RepositoryRole",
                            "bypass_mode": "always",
                        }
                    ],
                )
            ],
        )
        repos = {}

        self.repository_service._process_repository(repo, repos)

        assert repos[1].default_branch.enforce_admins is False
        assert repos[1].default_branch.enforce_admins_source is None
        # The branch is still reported as protected-but-inactive regardless of bypass.
        assert repos[1].default_branch.protected_source == "ruleset_not_active"
