import base64
from datetime import datetime, timedelta
from typing import List, Optional
from unittest.mock import MagicMock, PropertyMock, patch
from urllib.parse import parse_qs, urlparse

import pytest
from freezegun import freeze_time

from prowler.lib.outputs.jira.exceptions.exceptions import (
    JiraAuthenticationError,
    JiraBasicAuthError,
    JiraCreateIssueError,
    JiraGetAvailableIssueTypesError,
    JiraGetCloudIDError,
    JiraGetProjectsError,
    JiraGetProjectsResponseError,
    JiraNoProjectsError,
    JiraNoTokenError,
    JiraRefreshTokenError,
    JiraRefreshTokenResponseError,
    JiraRequiredCustomFieldsError,
    JiraSendFindingsResponseError,
    JiraTestConnectionError,
)
from prowler.lib.outputs.jira.jira import Jira

TEST_DATETIME = "2023-01-01T12:01:01+00:00"


class TestJiraIntegration:
    @pytest.fixture(autouse=True)
    @patch.object(Jira, "get_auth", return_value=None)
    def setup(self, mock_get_auth, monkeypatch):
        monkeypatch.setattr("builtins.input", lambda _: "test_authorization_code")

        # To disable vulture
        mock_get_auth = mock_get_auth

        self.redirect_uri = "https://example.com/callback"
        self.client_id = "test_client_id"
        self.client_secret = "test_client_secret"
        self.state_param = "unique_state_value"

        self.jira_integration = Jira(
            redirect_uri=self.redirect_uri,
            client_id=self.client_id,
            client_secret=self.client_secret,
        )

    @pytest.fixture(autouse=True)
    @patch.object(Jira, "get_basic_auth", return_value=None)
    def setup_basic_auth(self, mock_get_basic_auth):
        # To disable vulture
        mock_get_basic_auth = mock_get_basic_auth

        self.user_mail = "test_user_mail"
        self.api_token = "test_api_token"
        self.domain = "test-domain"

        self.jira_integration_basic_auth = Jira(
            user_mail=self.user_mail,
            api_token=self.api_token,
            domain=self.domain,
        )

    @staticmethod
    def _collect_text_from_cell(cell: dict) -> str:
        pieces: List[str] = []

        def walk(node: dict) -> None:
            node_type = node.get("type")
            if node_type == "text":
                pieces.append(node.get("text", ""))
            elif node_type == "hardBreak":
                pieces.append(" ")
            else:
                for child in node.get("content", []):
                    walk(child)
                if node_type in {"paragraph", "listItem"}:
                    pieces.append(" ")

        for child in cell.get("content", []):
            walk(child)

        flattened = "".join(pieces)
        return " ".join(flattened.split())

    @staticmethod
    def _find_link_mark(nodes: List[dict]) -> Optional[dict]:
        for node in nodes:
            if node.get("type") == "text":
                for mark in node.get("marks", []):
                    if mark.get("type") == "link":
                        return mark
            found = TestJiraIntegration._find_link_mark(node.get("content", []))
            if found:
                return found
        return None

    @staticmethod
    def _find_table_row(rows: List[dict], header: str) -> dict:
        for row in rows:
            header_cell = row.get("content", [])[0]
            header_text = TestJiraIntegration._collect_text_from_cell(header_cell)
            if header_text == header:
                return row
        raise AssertionError(f"Row with header '{header}' not found")

    @patch.object(Jira, "get_auth", return_value=None)
    def test_auth_code_url(self, mock_get_auth):
        """Test to verify the authorization URL generation with correct query parameters"""

        # To disable vulture
        mock_get_auth = mock_get_auth

        generated_url = self.jira_integration.auth_code_url()

        expected_url = "https://auth.atlassian.com/authorize"

        parsed_url = urlparse(generated_url)
        query_params = parse_qs(parsed_url.query)

        assert (
            parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path
            == expected_url
        )

        assert query_params["audience"][0] == "api.atlassian.com"
        assert query_params["client_id"][0] == self.client_id
        assert (
            query_params["scope"][0] == "read:jira-user read:jira-work write:jira-work"
        )
        assert query_params["redirect_uri"][0] == self.redirect_uri
        assert query_params["state"][0] is not None
        assert query_params["response_type"][0] == "code"
        assert query_params["prompt"][0] == "consent"

    @patch.object(Jira, "get_cloud_id", return_value="test_cloud_id")
    def test_get_auth_successful_basic_auth(self, mock_get_cloud_id):
        """Test successful token retrieval in get_basic_auth."""
        # To disable vulture
        mock_get_cloud_id = mock_get_cloud_id

        self.jira_integration_basic_auth.get_basic_auth()

        user_string = "test_user_mail:test_api_token"
        user_string_base64 = base64.b64encode(user_string.encode("utf-8")).decode(
            "utf-8"
        )

        assert self.jira_integration_basic_auth._access_token == user_string_base64
        assert self.jira_integration_basic_auth._cloud_id == "test_cloud_id"

    @patch.object(Jira, "get_cloud_id", side_effect=Exception("Connection error"))
    def test_get_auth_error_basic_auth(self, mock_get_cloud_id):
        """Test successful token retrieval in get_basic_auth."""
        # To disable vulture
        mock_get_cloud_id = mock_get_cloud_id

        with pytest.raises(JiraBasicAuthError):
            self.jira_integration_basic_auth.get_basic_auth()

    @freeze_time(TEST_DATETIME)
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    @patch.object(Jira, "get_cloud_id", return_value="test_cloud_id")
    def test_get_auth_successful(self, mock_get_cloud_id, mock_post):
        """Test successful token retrieval in get_auth."""
        # To disable vulture
        mock_get_cloud_id = mock_get_cloud_id

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "test_access_token",
            "refresh_token": "test_refresh_token",
            "expires_in": 3600,
        }
        mock_post.return_value = mock_response

        self.jira_integration.get_auth("test_auth_code")

        assert self.jira_integration._access_token == "test_access_token"
        assert self.jira_integration._refresh_token == "test_refresh_token"
        assert (
            self.jira_integration._expiration_date
            == (datetime.now() + timedelta(seconds=3600)).isoformat()
        )
        assert self.jira_integration._cloud_id == "test_cloud_id"

    @patch(
        "prowler.lib.outputs.jira.jira.requests.post",
        side_effect=Exception("Connection error"),
    )
    def test_get_auth_connection_error(self, mock_post):
        """Test get_auth raises JiraAuthenticationError on connection failure."""
        # To disable vulture
        mock_post = mock_post

        with pytest.raises(JiraAuthenticationError):
            self.jira_integration.get_auth("test_auth_code")

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_successful(self, mock_get):
        """Test successful retrieval of cloud ID."""

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = [{"id": "test_cloud_id"}]
        mock_get.return_value = mock_response

        cloud_id = self.jira_integration.get_cloud_id("test_access_token")

        assert cloud_id == "test_cloud_id"

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_no_resources(self, mock_get):
        """Test get_cloud_id raises JiraGetCloudIDNoResourcesError when no resources are found, later JiraGetCloudIDError will be raised."""

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = []
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetCloudIDError):
            self.jira_integration.get_cloud_id("test_access_token")

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_response_error(self, mock_get):
        """Test get_cloud_id raises JiraGetCloudIDResponseError when response code is not 200, later JiraGetCloudIDError will be raised."""

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.json.return_value = {"error": "Not Found"}
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetCloudIDError):
            self.jira_integration.get_cloud_id("test_access_token")

    @patch(
        "prowler.lib.outputs.jira.jira.requests.get",
        side_effect=Exception("Connection error"),
    )
    def test_get_cloud_id_unexpected_error(self, mock_get):
        """Test get_cloud_id raises JiraGetCloudIDError on an unexpected exception."""
        # To disable vulture
        mock_get = mock_get

        with pytest.raises(JiraGetCloudIDError):
            self.jira_integration.get_cloud_id("test_access_token")

    @patch.object(Jira, "refresh_access_token", return_value="new_access_token")
    def test_get_access_token_refresh(self, mock_refresh_access_token):
        """Test get_access_token refreshes token when expired."""

        self.jira_integration.auth_expiration = 0
        access_token = self.jira_integration.get_access_token()

        assert access_token == "new_access_token"
        mock_refresh_access_token.assert_called_once()

    @freeze_time(TEST_DATETIME)
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    @patch.object(Jira, "get_cloud_id", return_value="test_cloud_id")
    def test_get_access_token_valid_token(self, mock_get_cloud_id, mock_post):
        """Test successful token retrieval in get_auth."""
        # To disable vulture
        mock_get_cloud_id = mock_get_cloud_id

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "valid_access_token",
            "refresh_token": "test_refresh_token",
            "expires_in": 3600,
        }
        mock_post.return_value = mock_response

        self.jira_integration.get_auth("test_auth_code")

        access_token = self.jira_integration.get_access_token()
        assert access_token == "valid_access_token"

    @patch.object(Jira, "refresh_access_token", side_effect=JiraRefreshTokenError)
    def test_get_access_token_refresh_error(self, mock_refresh_access_token):
        """Test get_access_token raises JiraRefreshTokenError on token refresh failure."""

        # To disable vulture
        mock_refresh_access_token = mock_refresh_access_token

        self.jira_integration.auth_expiration = (
            datetime.now() + timedelta(seconds=0)
        ).isoformat()

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.get_access_token()

    @freeze_time(TEST_DATETIME)
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_refresh_access_token_successful(self, mock_post):
        """Test successful access token refresh in refresh_access_token."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        expires_in_value = 3600
        mock_response.json.return_value = {
            "access_token": "new_access_token",
            "refresh_token": "new_refresh_token",
            "expires_in": expires_in_value,
        }
        mock_post.return_value = mock_response

        new_access_token = self.jira_integration.refresh_access_token()

        assert new_access_token == "new_access_token"
        assert self.jira_integration._access_token == "new_access_token"
        assert self.jira_integration._refresh_token == "new_refresh_token"
        assert (
            self.jira_integration._expiration_date
            == (datetime.now() + timedelta(seconds=expires_in_value)).isoformat()
        )

    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_refresh_access_token_response_error(self, mock_post):
        """Test refresh_access_token raises JiraRefreshTokenResponseError when response code is not 200, later JiraRefreshTokenError will be raised."""
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {"error": "invalid_request"}
        mock_post.return_value = mock_response

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.refresh_access_token()

    @patch(
        "prowler.lib.outputs.jira.jira.requests.post",
        side_effect=Exception("Connection error"),
    )
    def test_refresh_access_token_unexpected_error(self, mock_post):
        """Test refresh_access_token raises JiraRefreshTokenError on unexpected exception."""
        # To disable vulture
        mock_post = mock_post

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.refresh_access_token()

    @patch("prowler.lib.outputs.jira.jira.requests.post")
    @patch.object(Jira, "get_cloud_id", return_value="test_cloud_id")
    def test_get_auth_sends_timeout(self, mock_get_cloud_id, mock_post):
        """get_auth must pass a request timeout to avoid hanging on an unresponsive Jira."""
        # To disable vulture
        mock_get_cloud_id = mock_get_cloud_id

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "test_access_token",
            "refresh_token": "test_refresh_token",
            "expires_in": 3600,
        }
        mock_post.return_value = mock_response

        self.jira_integration.get_auth("test_auth_code")

        assert mock_post.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_sends_timeout(self, mock_get):
        """get_cloud_id (OAuth path) must pass a request timeout."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = [{"id": "test_cloud_id"}]
        mock_get.return_value = mock_response

        self.jira_integration.get_cloud_id("test_access_token")

        assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_basic_auth_sends_timeout(self, mock_get):
        """get_cloud_id (basic-auth tenant_info path) must pass a request timeout."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"cloudId": "test_cloud_id"}
        mock_get.return_value = mock_response

        self.jira_integration_basic_auth.get_cloud_id(domain=self.domain)

        assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT

    @pytest.mark.parametrize(
        "domain",
        (
            "169.254.169.254#",
            "internal/service",
            "internal?target",
            "internal\\target",
            "internal:8000",
            "user@internal",
        ),
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_basic_auth_rejects_invalid_domain(self, mock_get, domain):
        with pytest.raises(JiraGetCloudIDError):
            self.jira_integration_basic_auth.get_cloud_id(domain=domain)

        mock_get.assert_not_called()

    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_cloud_id_basic_auth_disables_redirects(self, mock_get):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"cloudId": "test_cloud_id"}
        mock_get.return_value = mock_response

        self.jira_integration_basic_auth.get_cloud_id(domain=self.domain)

        assert mock_get.call_args.kwargs["allow_redirects"] is False

    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_refresh_access_token_sends_timeout(self, mock_post):
        """refresh_access_token must pass a request timeout."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "access_token": "new_access_token",
            "refresh_token": "new_refresh_token",
            "expires_in": 3600,
        }
        mock_post.return_value = mock_response

        self.jira_integration.refresh_access_token()

        assert mock_post.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_sends_timeout(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """get_projects must pass a request timeout."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = [{"key": "PROJ1", "name": "Project One"}]
        mock_get.return_value = mock_response

        self.jira_integration.get_projects()

        assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(
        Jira,
        "get_projects",
        return_value={"PROJ1": "Project One", "PROJ2": "Project Two"},
    )
    @patch.object(
        Jira,
        "get_available_issue_types",
        side_effect=lambda pk: ["Task", "Bug"] if pk == "PROJ1" else ["Story"],
    )
    def test_test_connection_successful(
        self, mock_get_issue_types, mock_get_projects, mock_get_auth
    ):
        """Test that a successful connection returns an active Connection object with projects."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth
        mock_get_issue_types = mock_get_issue_types

        connection = Jira.test_connection(
            redirect_uri=self.redirect_uri,
            client_id=self.client_id,
            client_secret=self.client_secret,
        )

        assert connection.is_connected
        assert connection.error is None
        assert connection.projects == {"PROJ1": "Project One", "PROJ2": "Project Two"}
        assert connection.issue_types == {
            "PROJ1": ["Task", "Bug"],
            "PROJ2": ["Story"],
        }

    @patch.object(Jira, "get_basic_auth", return_value=None)
    @patch.object(
        Jira,
        "get_projects",
        return_value={"PROJ1": "Project One", "PROJ2": "Project Two"},
    )
    @patch.object(
        Jira,
        "get_available_issue_types",
        side_effect=lambda pk: ["Task", "Bug"] if pk == "PROJ1" else ["Story"],
    )
    def test_test_connection_successful_basic_auth(
        self, mock_get_issue_types, mock_get_projects, mock_get_basic_auth
    ):
        """Test that a successful connection returns an active Connection object with projects."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_basic_auth = mock_get_basic_auth
        mock_get_issue_types = mock_get_issue_types

        connection = Jira.test_connection(
            user_mail=self.user_mail,
            api_token=self.api_token,
            domain=self.domain,
        )

        assert connection.is_connected
        assert connection.error is None
        assert connection.projects == {"PROJ1": "Project One", "PROJ2": "Project Two"}
        assert connection.issue_types == {
            "PROJ1": ["Task", "Bug"],
            "PROJ2": ["Story"],
        }

    @patch.object(
        Jira,
        "get_auth",
        side_effect=JiraAuthenticationError("Failed to authenticate with Jira"),
    )
    def test_test_connection_failed(self, mock_get_auth):
        """Test that a failed connection raises JiraAuthenticationError."""
        # To disable vulture
        mock_get_auth = mock_get_auth

        with pytest.raises(JiraAuthenticationError):
            Jira.test_connection(
                redirect_uri=self.redirect_uri,
                client_id=self.client_id,
                client_secret=self.client_secret,
            )

    @patch.object(
        Jira,
        "get_basic_auth",
        side_effect=JiraBasicAuthError("Failed to authenticate with Jira"),
    )
    def test_test_connection_failed_basic_auth(self, mock_get_basic_auth):
        """Test that a failed connection raises JiraBasicAuthError."""
        # To disable vulture
        mock_get_basic_auth = mock_get_basic_auth

        with pytest.raises(JiraBasicAuthError):
            Jira.test_connection(
                user_mail=self.user_mail,
                api_token=self.api_token,
                domain=self.domain,
            )

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(
        Jira, "get_projects", side_effect=JiraNoProjectsError("No projects found")
    )
    def test_test_connection_no_projects_found(self, mock_get_projects, mock_get_auth):
        """Test that test_connection raises JiraNoProjectsError when no projects are found."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth

        with pytest.raises(JiraNoProjectsError):
            Jira.test_connection(
                redirect_uri=self.redirect_uri,
                client_id=self.client_id,
                client_secret=self.client_secret,
            )

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(
        Jira,
        "get_projects",
        side_effect=JiraGetProjectsResponseError("Projects request failed"),
    )
    def test_test_connection_projects_request_error(
        self, mock_get_projects, mock_get_auth
    ):
        """Test that test_connection raises JiraGetProjectsResponseError when projects request fails."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth

        with pytest.raises(JiraGetProjectsResponseError):
            Jira.test_connection(
                redirect_uri=self.redirect_uri,
                client_id=self.client_id,
                client_secret=self.client_secret,
            )

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(
        Jira, "get_projects", side_effect=JiraNoProjectsError("No projects found")
    )
    def test_test_connection_no_projects_found_no_exception(
        self, mock_get_projects, mock_get_auth
    ):
        """Test that test_connection returns error connection object when no projects found and raise_on_exception=False."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth

        connection = Jira.test_connection(
            redirect_uri=self.redirect_uri,
            client_id=self.client_id,
            client_secret=self.client_secret,
            raise_on_exception=False,
        )

        assert not connection.is_connected
        assert isinstance(connection.error, JiraNoProjectsError)

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(
        Jira,
        "get_projects",
        side_effect=JiraGetProjectsResponseError("Projects request failed"),
    )
    def test_test_connection_projects_request_error_no_exception(
        self, mock_get_projects, mock_get_auth
    ):
        """Test that test_connection returns error connection object when projects request fails and raise_on_exception=False."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth

        connection = Jira.test_connection(
            redirect_uri=self.redirect_uri,
            client_id=self.client_id,
            client_secret=self.client_secret,
            raise_on_exception=False,
        )

        assert not connection.is_connected
        assert isinstance(connection.error, JiraGetProjectsResponseError)

    @patch.object(Jira, "get_auth", return_value=None)
    @patch.object(Jira, "get_projects", side_effect=Exception("Unexpected error"))
    def test_test_connection_unexpected_error(self, mock_get_projects, mock_get_auth):
        """Test that test_connection raises JiraTestConnectionError on unexpected exceptions."""
        # To disable vulture
        mock_get_projects = mock_get_projects
        mock_get_auth = mock_get_auth

        with pytest.raises(JiraTestConnectionError):
            Jira.test_connection(
                redirect_uri=self.redirect_uri,
                client_id=self.client_id,
                client_secret=self.client_secret,
            )

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_successful(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test successful retrieval of projects from Jira."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = [
            {"key": "PROJ1", "name": "Project One"},
            {"key": "PROJ2", "name": "Project Two"},
        ]
        mock_get.return_value = mock_response

        projects = self.jira_integration.get_projects()
        expected_projects = {"PROJ1": "Project One", "PROJ2": "Project Two"}
        assert projects == expected_projects

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_no_projects_found(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_projects raises JiraNoProjectsError when no projects are found."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = []
        mock_get.return_value = mock_response

        with pytest.raises(JiraNoProjectsError):
            self.jira_integration.get_projects()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_response_error(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_projects raises JiraGetProjectsResponseError on non-200 response."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.json.return_value = {"error": "Not Found"}
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetProjectsError):
            self.jira_integration.get_projects()

    @patch.object(
        Jira,
        "get_access_token",
        side_effect=JiraRefreshTokenError("Failed to refresh the access token"),
    )
    def test_get_projects_refresh_token_error(self, mock_get_access_token):
        """Test that get_projects raises JiraRefreshTokenError when refreshing the token fails."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.get_projects()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_available_issue_types_successful(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test successful retrieval of issue types for a project."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "projects": [
                {
                    "issuetypes": [
                        {"name": "Bug"},
                        {"name": "Task"},
                        {"name": "Story"},
                    ]
                }
            ]
        }
        mock_get.return_value = mock_response

        issue_types = self.jira_integration.get_available_issue_types(
            project_key="TEST"
        )

        expected_issue_types = ["Bug", "Task", "Story"]
        assert issue_types == expected_issue_types

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_available_issue_types_no_projects_found(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_available_issue_types raises JiraNoProjectsError when no projects are found, later JiraGetAvailableIssueTypesError is raised."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"projects": []}
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetAvailableIssueTypesError):
            self.jira_integration.get_available_issue_types(project_key="TEST")

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_available_issue_types_response_error(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_available_issue_types raises JiraGetAvailableIssueTypesResponseError on non-200 response, JiraGetAvailableIssueTypesError will be raised later"""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.json.return_value = {"error": "Not Found"}
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetAvailableIssueTypesError):
            self.jira_integration.get_available_issue_types(project_key="TEST")

    @patch.object(
        Jira,
        "get_access_token",
        side_effect=JiraRefreshTokenError("Failed to refresh the access token"),
    )
    def test_get_available_issue_types_refresh_token_error(self, mock_get_access_token):
        """Test that get_available_issue_types raises JiraRefreshTokenError when refreshing the token fails."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.get_available_issue_types(project_key="TEST")

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
    )
    @patch.object(Jira, "get_projects", return_value={"TEST-1": "Test Project"})
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_findings_successful(
        self,
        mock_post,
        mock_get_available_issue_types,
        mock_get_projects,
        mock_get_access_token,
    ):
        """Test successful sending of findings to Jira."""
        # To disable vulture
        mock_get_available_issue_types = mock_get_available_issue_types
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects

        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.json.return_value = {"id": "12345", "key": "TEST-1"}
        mock_post.return_value = mock_response

        finding = MagicMock()
        finding.status.value = "FAIL"
        finding.status_extended = "status_extended"
        finding.metadata.Severity.value = "HIGH"
        finding.metadata.CheckID = "CHECK-1"
        finding.metadata.CheckTitle = "Check Title"
        finding.resource_uid = "resource-1"
        finding.resource_name = "resource_name"
        finding.metadata.Provider = "aws"
        finding.region = "region"
        finding.metadata.Risk = "risk"
        finding.metadata.Remediation.Recommendation.Text = "remediation_text"
        finding.metadata.Remediation.Recommendation.Url = "remediation_url"
        finding.metadata.Remediation.Code.NativeIaC = (
            'resource "aws_s3_bucket" "example" { bucket = "my-bucket" }'
        )
        finding.metadata.Remediation.Code.Terraform = (
            "terraform apply -target=aws_s3_bucket.example"
        )
        finding.metadata.Remediation.Code.CLI = (
            "aws s3api create-bucket --bucket my-bucket --region us-east-1"
        )
        finding.metadata.Remediation.Code.Other = (
            "Manual configuration required in AWS Console"
        )
        finding.resource_tags = {"Environment": "Production", "Owner": "SecurityTeam"}
        finding.compliance = {"CIS": ["2.1.1", "2.1.2"], "NIST": ["AC-3", "AC-6"]}

        self.jira_integration.cloud_id = "valid_cloud_id"

        self.jira_integration.send_findings(
            findings=[finding],
            project_key="TEST-1",
            issue_type="Bug",
            issue_labels=["scan-mocked", "whatever"],
            finding_url="https://prowler-cloud-link/findings/12345",
            tenant_info="Tenant Info",
        )

        mock_post.assert_called_once()

        call_args = mock_post.call_args

        expected_url = (
            "https://api.atlassian.com/ex/jira/valid_cloud_id/rest/api/3/issue"
        )
        expected_headers = {
            "Authorization": "Bearer valid_access_token",
            "Content-Type": "application/json",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }

        assert call_args[0][0] == expected_url
        assert call_args.kwargs["headers"] == expected_headers

        payload = call_args.kwargs["json"]
        assert payload["fields"]["project"]["key"] == "TEST-1"
        assert payload["fields"]["summary"] == "[Prowler] HIGH - CHECK-1 - resource-1"
        assert payload["fields"]["issuetype"]["name"] == "Bug"
        assert payload["fields"]["description"]["type"] == "doc"
        assert payload["fields"]["labels"] == ["scan-mocked", "whatever"]

        description_content = payload["fields"]["description"]["content"]

        intro_paragraph = description_content[0]
        assert intro_paragraph["type"] == "paragraph"
        intro_text = intro_paragraph["content"][0]
        assert intro_text["type"] == "text"
        assert intro_text["text"] == "Prowler has discovered the following finding:"

        table = description_content[1]
        assert table["type"] == "table"

        table_rows = table["content"]
        row_entries = {}
        recommendation_cell = None

        for row in table_rows:
            if row["type"] != "tableRow":
                continue

            cells = row["content"]
            if len(cells) != 2:
                continue

            key_text = self._collect_text_from_cell(cells[0])
            value_text = self._collect_text_from_cell(cells[1])

            if key_text:
                row_entries[key_text] = value_text
                if key_text == "Recommendation":
                    recommendation_cell = cells[1]

        expected_keys = [
            "Check Id",
            "Check Title",
            "Severity",
            "Status",
            "Status Extended",
            "Provider",
            "Region",
            "Resource UID",
            "Resource Name",
            "Risk",
            "Resource Tags",
            "Compliance",
            "Recommendation",
            "Remediation Native IaC",
            "Remediation Terraform",
            "Remediation CLI",
            "Remediation Other",
            "Finding URL",
            "Tenant Info",
        ]

        for expected_key in expected_keys:
            assert expected_key in row_entries, f"Missing row key: {expected_key}"

        row_dict = row_entries

        assert recommendation_cell is not None
        link_mark = self._find_link_mark(recommendation_cell.get("content", []))
        assert link_mark is not None
        assert link_mark.get("attrs", {}).get("href") == "remediation_url"
        assert row_dict["Check Id"] == "CHECK-1"
        assert row_dict["Check Title"] == "Check Title"
        assert row_dict["Status"] == "FAIL"
        assert row_dict["Severity"] == "HIGH"
        assert row_dict["Resource UID"] == "resource-1"
        assert row_dict["Resource Name"] == "resource_name"
        assert row_dict["Provider"] == "aws"
        assert row_dict["Region"] == "region"
        assert row_dict["Risk"] == "risk"
        assert "remediation_text" in row_dict["Recommendation"]
        assert "remediation_url" in row_dict["Recommendation"]
        assert (
            row_dict["Remediation Native IaC"]
            == 'resource "aws_s3_bucket" "example" { bucket = "my-bucket" }'
        )
        assert (
            row_dict["Remediation Terraform"]
            == "terraform apply -target=aws_s3_bucket.example"
        )
        assert (
            row_dict["Remediation CLI"]
            == "aws s3api create-bucket --bucket my-bucket --region us-east-1"
        )
        assert (
            row_dict["Remediation Other"]
            == "Manual configuration required in AWS Console"
        )
        assert "Environment=Production" in row_dict["Resource Tags"]
        assert "Owner=SecurityTeam" in row_dict["Resource Tags"]
        assert "CIS: 2.1.1, 2.1.2" in row_dict["Compliance"]
        assert "NIST: AC-3, AC-6" in row_dict["Compliance"]
        assert "https://prowler-cloud-link/findings/12345" in row_dict["Finding URL"]
        assert "Tenant Info" in row_dict["Tenant Info"]

    def test_get_adf_description_renders_markdown(self):
        status_extended_md = "Finding uses **bold** text and `code` snippets."
        risk_md = "High risk:\n- Item one\n- Item two"
        recommendation_md = "Apply fixes:\n- Step one\n- Step two"
        recommendation_url = "https://example.com/fix"

        adf_description = self.jira_integration.get_adf_description(
            check_id="CHECK-1",
            check_title="Sample check",
            severity="HIGH",
            severity_color="#FF0000",
            status="FAIL",
            status_color="#00FF00",
            status_extended=status_extended_md,
            provider="aws",
            region="us-east-1",
            resource_uid="resource-1",
            resource_name="resource-name",
            risk=risk_md,
            recommendation_text=recommendation_md,
            recommendation_url=recommendation_url,
        )

        assert adf_description["type"] == "doc"
        table = adf_description["content"][1]
        assert table["type"] == "table"

        rows = {}
        for row in table["content"]:
            if row.get("type") != "tableRow":
                continue
            key_cell, value_cell = row["content"]
            key_text = self._collect_text_from_cell(key_cell)
            rows[key_text] = value_cell

        assert "Status Extended" in rows
        assert "Risk" in rows
        assert "Recommendation" in rows

        def walk_nodes(nodes: List[dict]):
            stack = list(nodes)
            while stack:
                current = stack.pop()
                yield current
                stack.extend(current.get("content", []))

        status_text = self._collect_text_from_cell(rows["Status Extended"])
        assert status_text == status_extended_md

        risk_nodes = list(walk_nodes(rows["Risk"].get("content", [])))
        assert any(node.get("type") == "bulletList" for node in risk_nodes)

        recommendation_cell = rows["Recommendation"]
        recommendation_nodes = list(walk_nodes(recommendation_cell.get("content", [])))
        assert any(node.get("type") == "bulletList" for node in recommendation_nodes)
        link_mark = self._find_link_mark(recommendation_cell.get("content", []))
        assert link_mark is not None
        assert link_mark.get("attrs", {}).get("href") == recommendation_url

    def test_get_adf_description_code_blocks_strip_fences(self):
        code_block_value = """```hcl\nresource \"aws_s3_bucket\" \"example\" {\n  bucket = \"my-bucket\"\n}\n```"""

        adf_description = self.jira_integration.get_adf_description(
            check_id="CHECK-1",
            check_title="Sample check",
            severity="HIGH",
            severity_color="#FF0000",
            status="FAIL",
            status_color="#00FF00",
            recommendation_text="",
            remediation_code_native_iac=code_block_value,
        )

        table = adf_description["content"][1]
        code_row = self._find_table_row(table["content"], "Remediation Native IaC")
        code_cell = code_row["content"][1]
        code_block = code_cell["content"][0]

        assert code_block["type"] == "codeBlock"
        assert code_block.get("attrs", {}).get("language") == "hcl"
        expected_text = (
            'resource "aws_s3_bucket" "example" {\n  bucket = "my-bucket"\n}'
        )
        assert code_block["content"][0]["text"] == expected_text

    def test_get_adf_description_other_remediation_uses_markdown(self):
        other_value = "Use **bold** text"

        adf_description = self.jira_integration.get_adf_description(
            check_id="CHECK-1",
            check_title="Sample check",
            severity="HIGH",
            severity_color="#FF0000",
            status="FAIL",
            status_color="#00FF00",
            recommendation_text="",
            remediation_code_other=other_value,
        )

        table = adf_description["content"][1]
        other_row = self._find_table_row(table["content"], "Remediation Other")
        other_cell = other_row["content"][1]

        paragraph = other_cell["content"][0]
        assert paragraph["type"] == "paragraph"
        assert any(
            mark.get("type") == "strong"
            for node in paragraph.get("content", [])
            for mark in node.get("marks", [])
        )

    @staticmethod
    def _find_empty_text_nodes(node) -> List[str]:
        # ADF forbids empty text nodes; collect any to assert the document is valid.
        empties: List[str] = []

        def walk(current) -> None:
            if isinstance(current, dict):
                if current.get("type") == "text" and current.get("text", "") == "":
                    empties.append(current.get("text", ""))
                for value in current.values():
                    walk(value)
            elif isinstance(current, list):
                for item in current:
                    walk(item)

        walk(node)
        return empties

    def test_get_adf_description_empty_resource_name_has_no_empty_text_nodes(self):
        # A resource without a name (e.g. an AWS-managed IAM policy) used to emit an
        # empty ADF text node, making Jira reject the issue with 400 INVALID_INPUT.
        adf_description = self.jira_integration.get_adf_description(
            check_id="CHECK-1",
            check_title="Sample check",
            severity="CRITICAL",
            severity_color="#FF0000",
            status="FAIL",
            status_color="#FF0000",
            status_extended="Some status",
            provider="aws",
            region="eu-west-1",
            resource_uid="arn:aws:iam::aws:policy/AdministratorAccess",
            resource_name="",
            recommendation_text="",
        )

        assert self._find_empty_text_nodes(adf_description) == []

        table = adf_description["content"][1]
        resource_name_row = self._find_table_row(table["content"], "Resource Name")
        value_cell = resource_name_row["content"][1]
        assert self._collect_text_from_cell(value_cell) == "-"

    @pytest.mark.parametrize(
        "field, header",
        [
            ("check_id", "Check Id"),
            ("check_title", "Check Title"),
            ("status_extended", "Status Extended"),
            ("provider", "Provider"),
            ("region", "Region"),
            ("resource_uid", "Resource UID"),
            ("resource_name", "Resource Name"),
        ],
    )
    def test_get_adf_description_empty_plain_text_fields_render_placeholder(
        self, field, header
    ):
        base_kwargs = dict(
            check_id="CHECK-1",
            check_title="Sample check",
            severity="HIGH",
            severity_color="#FF0000",
            status="FAIL",
            status_color="#00FF00",
            status_extended="Some status",
            provider="aws",
            region="us-east-1",
            resource_uid="resource-1",
            resource_name="resource-name",
            recommendation_text="",
        )
        base_kwargs[field] = ""

        adf_description = self.jira_integration.get_adf_description(**base_kwargs)

        assert self._find_empty_text_nodes(adf_description) == []

        table = adf_description["content"][1]
        row = self._find_table_row(table["content"], header)
        value_cell = row["content"][1]
        assert self._collect_text_from_cell(value_cell) == "-"

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
    )
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_findings_invalid_issue_type(
        self, mock_post, mock_get_available_issue_types, mock_get_access_token
    ):
        """Test that send_findings raises JiraInvalidIssueTypeError if the issue type is invalid that will raise JiraCreateIssueError later."""
        # To disable vulture
        mock_get_available_issue_types = mock_get_available_issue_types
        mock_get_access_token = mock_get_access_token
        mock_post = mock_post
        mock_post = mock_post

        with pytest.raises(JiraCreateIssueError):
            self.jira_integration.send_findings(
                findings=[MagicMock()], project_key="TEST", issue_type="InvalidType"
            )

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
    )
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_findings_response_error(
        self, mock_post, mock_get_available_issue_types, mock_get_access_token
    ):
        """Test that send_findings raises JiraSendFindingsResponseError on non-201 response that will raise JiraCreateIssueError later."""
        # To disable vulture
        mock_get_available_issue_types = mock_get_available_issue_types
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {"error": "Bad Request"}
        mock_post.return_value = mock_response

        finding = MagicMock()
        finding.status.value = "FAIL"
        finding.metadata.Severity.value = "HIGH"
        finding.metadata.CheckID = "CHECK-1"
        finding.resource_uid = "resource-1"

        with pytest.raises(JiraCreateIssueError):
            self.jira_integration.send_findings(
                findings=[finding], project_key="TEST", issue_type="Bug"
            )

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
    )
    @patch.object(Jira, "get_projects", return_value={"TEST-1": "Test Project"})
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_findings_custom_fields_required_error(
        self,
        mock_post,
        mock_get_projects,
        mock_get_available_issue_types,
        mock_get_access_token,
    ):
        """Test that send_findings raises JiraRequiredCustomFieldsError when custom fields are required."""
        # To disable vulture
        mock_get_available_issue_types = mock_get_available_issue_types
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects

        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {
            "errorMessages": [],
            "errors": {
                "customfield_10148": "Team is required.",
                "customfield_10088": "Component is required.",
            },
        }
        mock_post.return_value = mock_response

        finding = MagicMock()
        finding.status.value = "FAIL"
        finding.status_extended = "status_extended"
        finding.metadata.Severity.value = "HIGH"
        finding.metadata.CheckID = "CHECK-1"
        finding.metadata.CheckTitle = "Check Title"
        finding.resource_uid = "resource-1"
        finding.resource_name = "resource_name"
        finding.metadata.Provider = "aws"
        finding.region = "region"
        finding.metadata.Risk = "risk"
        finding.metadata.Remediation.Recommendation.Text = "remediation_text"
        finding.metadata.Remediation.Recommendation.Url = "remediation_url"
        finding.metadata.Remediation.Code.NativeIaC = ""
        finding.metadata.Remediation.Code.Terraform = ""
        finding.metadata.Remediation.Code.CLI = ""
        finding.metadata.Remediation.Code.Other = ""
        finding.resource_tags = {}
        finding.compliance = {}

        self.jira_integration.cloud_id = "valid_cloud_id"

        with pytest.raises(JiraRequiredCustomFieldsError):
            self.jira_integration.send_findings(
                findings=[finding],
                project_key="TEST-1",
                issue_type="Bug",
                issue_labels=["scan-mocked", "whatever"],
                finding_url="https://prowler-cloud-link/findings/12345",
                tenant_info="Tenant Info",
            )

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
    )
    @patch.object(Jira, "get_projects", return_value={"TEST-1": "Test Project"})
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_findings_non_custom_field_400_error(
        self,
        mock_post,
        mock_get_projects,
        mock_get_available_issue_types,
        mock_get_access_token,
    ):
        """Test that send_findings raises JiraCreateIssueError for non-custom field 400 errors."""
        # To disable vulture
        mock_get_available_issue_types = mock_get_available_issue_types
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects

        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {
            "errorMessages": [],
            "errors": {
                "summary": "Summary is required.",
                "description": "Description is required.",
            },
        }
        mock_post.return_value = mock_response

        finding = MagicMock()
        finding.status.value = "FAIL"
        finding.status_extended = "status_extended"
        finding.metadata.Severity.value = "HIGH"
        finding.metadata.CheckID = "CHECK-1"
        finding.metadata.CheckTitle = "Check Title"
        finding.resource_uid = "resource-1"
        finding.resource_name = "resource_name"
        finding.metadata.Provider = "aws"
        finding.region = "region"
        finding.metadata.Risk = "risk"
        finding.metadata.Remediation.Recommendation.Text = "remediation_text"
        finding.metadata.Remediation.Recommendation.Url = "remediation_url"
        finding.metadata.Remediation.Code.NativeIaC = ""
        finding.metadata.Remediation.Code.Terraform = ""
        finding.metadata.Remediation.Code.CLI = ""
        finding.metadata.Remediation.Code.Other = ""
        finding.resource_tags = {}
        finding.compliance = {}

        self.jira_integration.cloud_id = "valid_cloud_id"

        with pytest.raises(JiraCreateIssueError):
            self.jira_integration.send_findings(
                findings=[finding],
                project_key="TEST-1",
                issue_type="Bug",
                issue_labels=["scan-mocked", "whatever"],
                finding_url="https://prowler-cloud-link/findings/12345",
                tenant_info="Tenant Info",
            )

    @pytest.mark.parametrize(
        "status, expected_color",
        [
            ("FAIL", "#FF0000"),
            ("PASS", "#008000"),
            ("MUTED", "#FFA500"),
            ("MANUAL", "#FFFF00"),
        ],
    )
    def test_get_color_from_status(self, status, expected_color):
        """Test that get_color_from_status returns the correct color for a status."""
        assert self.jira_integration.get_color_from_status(status) == expected_color

    @pytest.mark.parametrize(
        "severity, expected_color",
        [
            ("critical", "#FF0000"),
            ("high", "#FFA500"),
            ("medium", "#FFFF00"),
            ("low", "#008000"),
            ("informational", "#0000FF"),
            ("unknown", "#000000"),
        ],
    )
    def test_get_severity_color(self, severity, expected_color):
        """Test that get_severity_color returns the correct color for a severity."""
        assert self.jira_integration.get_severity_color(severity) == expected_color

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_successful(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test successful retrieval of metadata associated to projects from Jira."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        # Mock the projects response
        mock_projects_response = MagicMock()
        mock_projects_response.status_code = 200
        mock_projects_response.json.return_value = [
            {"key": "PROJ1", "name": "Project One"},
            {"key": "PROJ2", "name": "Project Two"},
        ]

        # Mock the issue types responses
        mock_issue_types_response_1 = MagicMock()
        mock_issue_types_response_1.status_code = 200
        mock_issue_types_response_1.json.return_value = {
            "projects": [
                {
                    "issuetypes": [
                        {"name": "Bug", "id": "1"},
                        {"name": "Task", "id": "2"},
                    ]
                }
            ]
        }

        mock_issue_types_response_2 = MagicMock()
        mock_issue_types_response_2.status_code = 200
        mock_issue_types_response_2.json.return_value = {
            "projects": [
                {
                    "issuetypes": [
                        {"name": "Story", "id": "3"},
                        {"name": "Epic", "id": "4"},
                    ]
                }
            ]
        }

        # Configure side_effect to return different responses for different calls
        mock_get.side_effect = [
            mock_projects_response,
            mock_issue_types_response_1,
            mock_issue_types_response_2,
        ]

        jira_metadata = self.jira_integration.get_metadata()

        expected_result = {
            "PROJ1": {
                "name": "Project One",
                "issue_types": ["Bug", "Task"],
            },
            "PROJ2": {
                "name": "Project Two",
                "issue_types": ["Story", "Epic"],
            },
        }

        assert jira_metadata == expected_result

        # Verify the correct number of calls were made
        assert mock_get.call_count == 3

        # Verify the URLs called
        calls = mock_get.call_args_list
        assert (
            calls[0][0][0]
            == "https://api.atlassian.com/ex/jira/test_cloud_id/rest/api/3/project"
        )
        assert "projectKeys=PROJ1" in calls[1][0][0]
        assert "projectKeys=PROJ2" in calls[2][0][0]

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_no_projects_found(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_metadata raises JiraNoProjectsError when no projects are found."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = []
        mock_get.return_value = mock_response

        with pytest.raises(JiraNoProjectsError):
            self.jira_integration.get_metadata()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_projects_response_error(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_metadata raises JiraGetProjectsError when projects request fails."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.text = "Not Found"
        mock_get.return_value = mock_response

        with pytest.raises(JiraGetProjectsError):
            self.jira_integration.get_metadata()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_issue_types_response_error(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_metadata raises JiraGetProjectsError when issue types request fails."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        # Mock successful projects response
        mock_projects_response = MagicMock()
        mock_projects_response.status_code = 200
        mock_projects_response.json.return_value = [
            {"key": "PROJ1", "name": "Project One"}
        ]

        # Mock failed issue types response
        mock_issue_types_response = MagicMock()
        mock_issue_types_response.status_code = 404

        mock_get.side_effect = [mock_projects_response, mock_issue_types_response]

        with pytest.raises(JiraGetProjectsError):
            self.jira_integration.get_metadata()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_no_project_metadata(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test that get_metadata returns empty issue_types when project metadata is empty."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        # Mock successful projects response
        mock_projects_response = MagicMock()
        mock_projects_response.status_code = 200
        mock_projects_response.json.return_value = [
            {"key": "PROJ1", "name": "Project One"}
        ]

        # Mock issue types response with empty projects list
        mock_issue_types_response = MagicMock()
        mock_issue_types_response.status_code = 200
        mock_issue_types_response.json.return_value = {"projects": []}

        mock_get.side_effect = [mock_projects_response, mock_issue_types_response]

        projects_and_issue_types = self.jira_integration.get_metadata()

        expected_result = {
            "PROJ1": {
                "name": "Project One",
                "issue_types": [],
            }
        }

        assert projects_and_issue_types == expected_result

    @patch.object(
        Jira,
        "get_access_token",
        side_effect=JiraRefreshTokenError("Failed to refresh the access token"),
    )
    def test_get_projects_and_issue_types_refresh_token_error(
        self, mock_get_access_token
    ):
        """Test that get_metadata raises JiraRefreshTokenError when refreshing the token fails."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraRefreshTokenError):
            self.jira_integration.get_metadata()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch("prowler.lib.outputs.jira.jira.requests.get")
    def test_get_projects_and_issue_types_mixed_scenarios(
        self, mock_get, mock_cloud_id, mock_get_access_token
    ):
        """Test get_metadata with mixed success and empty metadata scenarios."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token

        # Mock projects response with two projects
        mock_projects_response = MagicMock()
        mock_projects_response.status_code = 200
        mock_projects_response.json.return_value = [
            {"key": "PROJ1", "name": "Project One"},
            {"key": "PROJ2", "name": "Project Two"},
        ]

        # Mock successful issue types response for first project
        mock_issue_types_response_1 = MagicMock()
        mock_issue_types_response_1.status_code = 200
        mock_issue_types_response_1.json.return_value = {
            "projects": [
                {
                    "issuetypes": [
                        {"name": "Bug", "id": "1"},
                        {"name": "Task", "id": "2"},
                    ]
                }
            ]
        }

        # Mock empty issue types response for second project
        mock_issue_types_response_2 = MagicMock()
        mock_issue_types_response_2.status_code = 200
        mock_issue_types_response_2.json.return_value = {"projects": []}

        mock_get.side_effect = [
            mock_projects_response,
            mock_issue_types_response_1,
            mock_issue_types_response_2,
        ]

        projects_and_issue_types = self.jira_integration.get_metadata()

        expected_result = {
            "PROJ1": {
                "name": "Project One",
                "issue_types": ["Bug", "Task"],
            },
            "PROJ2": {
                "name": "Project Two",
                "issue_types": [],
            },
        }

        assert projects_and_issue_types == expected_result

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch.object(Jira, "get_projects", return_value={"TEST": {"name": "Test Project"}})
    @patch.object(Jira, "get_available_issue_types", return_value=["Bug"])
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_finding_successful(
        self,
        mock_post,
        mock_get_issue_types,
        mock_get_projects,
        mock_cloud_id,
        mock_get_access_token,
    ):
        """Test that send_finding returns True when the finding is sent successfully."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects
        mock_get_issue_types = mock_get_issue_types

        # Mock successful response
        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.json.return_value = {"id": "ISSUE-123", "key": "TEST-123"}
        mock_post.return_value = mock_response

        result = self.jira_integration.send_finding(
            check_id="test-check",
            check_title="Test Finding",
            severity="High",
            status="FAIL",
            project_key="TEST",
            issue_type="Bug",
        )

        assert result is True
        mock_post.assert_called_once()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch.object(Jira, "get_projects", return_value={"TEST": {"name": "Test Project"}})
    @patch.object(Jira, "get_available_issue_types", return_value=["Bug"])
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_finding_failure(
        self,
        mock_post,
        mock_get_issue_types,
        mock_get_projects,
        mock_cloud_id,
        mock_get_access_token,
    ):
        """Test that send_finding raises with Jira JSON error details."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects
        mock_get_issue_types = mock_get_issue_types

        # Mock failed response
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {
            "errors": {"Team": "Team is required."},
            "errorMessages": ["Field 'Team' cannot be set."],
        }
        mock_post.return_value = mock_response

        with pytest.raises(JiraSendFindingsResponseError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert "Failed to create Jira issue" in str(error.value)
        assert "'Team': 'Team is required.'" in str(error.value)
        assert "Field 'Team' cannot be set." in str(error.value)
        mock_post.assert_called_once()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch.object(Jira, "get_projects", return_value={"TEST": {"name": "Test Project"}})
    @patch.object(Jira, "get_available_issue_types", return_value=["Bug"])
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_finding_response_error_without_json_body(
        self,
        mock_post,
        mock_get_issue_types,
        mock_get_projects,
        mock_cloud_id,
        mock_get_access_token,
    ):
        """Test send_finding raises with status-code context for non-JSON errors."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects
        mock_get_issue_types = mock_get_issue_types

        mock_response = MagicMock()
        mock_response.status_code = 502
        mock_response.json.side_effect = ValueError("No JSON body")
        mock_post.return_value = mock_response

        with pytest.raises(JiraSendFindingsResponseError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert "Failed to create Jira issue" in str(error.value)
        assert "Jira returned status code 502" in str(error.value)
        mock_post.assert_called_once()

    @patch.object(Jira, "get_access_token", return_value="valid_access_token")
    @patch.object(
        Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
    )
    @patch.object(Jira, "get_projects", return_value={"TEST": {"name": "Test Project"}})
    @patch.object(Jira, "get_available_issue_types", return_value=["Bug"])
    @patch("prowler.lib.outputs.jira.jira.requests.post")
    def test_send_finding_custom_fields_error(
        self,
        mock_post,
        mock_get_issue_types,
        mock_get_projects,
        mock_cloud_id,
        mock_get_access_token,
    ):
        """Test that send_finding raises when custom fields cause an error."""
        # To disable vulture
        mock_cloud_id = mock_cloud_id
        mock_get_access_token = mock_get_access_token
        mock_get_projects = mock_get_projects
        mock_get_issue_types = mock_get_issue_types

        # Mock response with custom fields error
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {
            "errors": {
                "customfield_10001": "This custom field is required",
                "customfield_10002": "Invalid value for custom field",
            }
        }
        mock_post.return_value = mock_response

        with pytest.raises(JiraRequiredCustomFieldsError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert "Jira project requires custom fields" in str(error.value)
        assert "customfield_10001" in str(error.value)
        mock_post.assert_called_once()

    @patch.object(
        Jira,
        "get_access_token",
        side_effect=JiraRefreshTokenError(message="Failed to refresh the access token"),
    )
    def test_send_finding_reraises_refresh_token_error(self, mock_get_access_token):
        """Test send_finding re-raises refresh token errors for API propagation."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraRefreshTokenError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert error.value.message == "Failed to refresh the access token"

    @patch.object(Jira, "get_access_token", return_value=None)
    def test_send_finding_reraises_no_token_error(self, mock_get_access_token):
        """Test send_finding re-raises missing token errors for API propagation."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraNoTokenError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert error.value.message == "No token was found"

    @patch.object(
        Jira,
        "get_access_token",
        side_effect=JiraRefreshTokenResponseError(
            message="Failed to refresh the access token, response code did not match 200"
        ),
    )
    def test_send_finding_reraises_refresh_token_response_error(
        self, mock_get_access_token
    ):
        """Test send_finding re-raises refresh token response errors for API propagation."""
        # To disable vulture
        mock_get_access_token = mock_get_access_token

        with pytest.raises(JiraRefreshTokenResponseError) as error:
            self.jira_integration.send_finding(
                check_id="test-check",
                check_title="Test Finding",
                severity="High",
                status="FAIL",
                project_key="TEST",
                issue_type="Bug",
            )

        assert (
            error.value.message
            == "Failed to refresh the access token, response code did not match 200"
        )

    def test_get_headers_oauth_with_access_token(self):
        """Test get_headers returns correct OAuth headers with access token."""
        self.jira_integration._using_basic_auth = False

        headers = self.jira_integration.get_headers(
            access_token="test_oauth_token", content_type_json=True
        )

        expected_headers = {
            "Authorization": "Bearer test_oauth_token",
            "Content-Type": "application/json",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers

    def test_get_headers_oauth_without_content_type(self):
        """Test get_headers returns OAuth headers without Content-Type when content_type_json=False."""
        self.jira_integration._using_basic_auth = False

        headers = self.jira_integration.get_headers(
            access_token="test_oauth_token", content_type_json=False
        )

        expected_headers = {
            "Authorization": "Bearer test_oauth_token",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers
        assert "Content-Type" not in headers

    def test_get_headers_basic_auth_with_access_token(self):
        """Test get_headers returns correct Basic Auth headers with access token."""
        self.jira_integration_basic_auth._using_basic_auth = True

        headers = self.jira_integration_basic_auth.get_headers(
            access_token="test_basic_token", content_type_json=True
        )

        expected_headers = {
            "Authorization": "Basic test_basic_token",
            "Content-Type": "application/json",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers

    def test_get_headers_basic_auth_without_content_type(self):
        """Test get_headers returns Basic Auth headers without Content-Type when content_type_json=False."""
        self.jira_integration_basic_auth._using_basic_auth = True

        headers = self.jira_integration_basic_auth.get_headers(
            access_token="test_basic_token", content_type_json=False
        )

        expected_headers = {
            "Authorization": "Basic test_basic_token",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers
        assert "Content-Type" not in headers

    def test_get_headers_without_access_token_with_content_type(self):
        """Test get_headers returns headers without Authorization when no access token provided."""
        headers = self.jira_integration.get_headers(content_type_json=True)

        expected_headers = {
            "Content-Type": "application/json",
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers
        assert "Authorization" not in headers

    def test_get_headers_without_access_token_without_content_type(self):
        """Test get_headers returns minimal headers when no access token and no content type."""
        headers = self.jira_integration.get_headers(content_type_json=False)

        expected_headers = {
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers
        assert "Authorization" not in headers
        assert "Content-Type" not in headers

    def test_get_headers_default_parameters(self):
        """Test get_headers with default parameters (no access token, no content type)."""
        headers = self.jira_integration.get_headers()

        expected_headers = {
            "X-Force-Accept-Language": "true",
            "Accept-Language": "en",
        }
        assert headers == expected_headers
        assert "Authorization" not in headers
        assert "Content-Type" not in headers
