from unittest.mock import MagicMock, patch

import pytest
from jira import JIRA, JIRAError

from jf_ingest.config import JiraAuthConfig, JiraAuthMethod, JiraDownloadConfig
from jf_ingest.constants import Constants
from jf_ingest.jf_jira.auth import (
    JiraAuthenticationException,
    JiraConnectionBundle,
    create_jira_connection,
    create_jira_connections,
    get_jira_connection,
)
from jf_ingest.jf_jira.exceptions import AtlassianConnectException, JiraRetryLimitExceeded


def _get_jira_auth_config(args):
    company_slug = "test_company"
    url = "https://test-co.atlassian.net/"
    return JiraAuthConfig(company_slug=company_slug, url=url, gdpr_active=True, **args)


def test_jira_conn_basic_auth_no_auth():
    config = _get_jira_auth_config({"available_auth_methods": [JiraAuthMethod.BasicAuth]})

    with pytest.raises(RuntimeError) as excinfo:
        get_jira_connection(config)

    assert (
        str(excinfo.value)
        == f"No valid basic authentication mechanism for {config.url} - need a username/password combo or a personal access token"
    )


def test_jira_conn_basic_auth_user_password():
    config = _get_jira_auth_config(
        {
            "available_auth_methods": [JiraAuthMethod.BasicAuth],
            "user": "jira_user@test.co",
            "password": "test_password",  # pragma: allowlist secret
        }
    )

    # TEST BASIC AUTH ERROR LOGIC
    with patch("jf_ingest.jf_jira.auth.JIRA", side_effect=JIRAError(status_code=401)):
        with pytest.raises(JiraAuthenticationException):
            get_jira_connection(config, max_retries=0)

    # TEST BASIC AUTH ERROR LOGIC
    with patch("jf_ingest.jf_jira.auth.JIRA", side_effect=JIRAError(status_code=403)):
        with pytest.raises(JiraAuthenticationException):
            get_jira_connection(config, max_retries=0)


def test_jira_conn_basic_auth_personal_access_token():
    config = _get_jira_auth_config(
        {
            "available_auth_methods": [JiraAuthMethod.BasicAuth],
            "personal_access_token": "a_PAT",
        }
    )

    # TEST BASIC AUTH ERROR LOGIC
    with patch("jf_ingest.jf_jira.auth.JIRA", side_effect=JIRAError(status_code=401)):
        with pytest.raises(JiraAuthenticationException):
            get_jira_connection(config, max_retries=0)

    # TEST BASIC AUTH ERROR LOGIC
    with patch("jf_ingest.jf_jira.auth.JIRA", side_effect=JIRAError(status_code=403)):
        with pytest.raises(JiraAuthenticationException):
            get_jira_connection(config, max_retries=0)


def test_jira_conn_rest_api_version_selection():
    """Test that the correct REST API version is set based on use_jql_enhanced_search parameter"""
    config = _get_jira_auth_config(
        {
            "available_auth_methods": [JiraAuthMethod.BasicAuth],
            "personal_access_token": "a_PAT",
        }
    )

    # Mock JIRA to capture the connection kwargs
    with patch("jf_ingest.jf_jira.auth.JIRA") as mock_jira:
        # Test default behavior (use_jql_enhanced_search=False) - should not set REST API version (uses default v2)
        get_jira_connection(config, max_retries=0)
        
        # Verify JIRA was called without explicit REST API version (uses default v2)
        call_kwargs = mock_jira.call_args[1]
        assert "rest_api_version" not in call_kwargs["options"]

    with patch("jf_ingest.jf_jira.auth.JIRA") as mock_jira:
        # Test JQL Enhanced Search (use_jql_enhanced_search=True) - should use REST API v3
        get_jira_connection(config, max_retries=0, use_jql_enhanced_search=True)
        
        # Verify JIRA was called with REST API version 3
        call_kwargs = mock_jira.call_args[1]
        assert call_kwargs["options"]["rest_api_version"] == "3"


# --- Tests for factory functions ---


def _get_jira_download_config(**overrides) -> JiraDownloadConfig:
    """Helper to create a JiraDownloadConfig for testing factory functions."""
    defaults = dict(
        company_slug="test_company",
        url="https://test-co.atlassian.net/",
        gdpr_active=True,
        available_auth_methods=[JiraAuthMethod.BasicAuth],
        personal_access_token="a_PAT",
        feature_flags={},
    )
    defaults.update(overrides)
    return JiraDownloadConfig(**defaults)


class TestCreateJiraConnection:
    """Tests for the create_jira_connection() factory."""

    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_auto_detects_enhanced_search(self, mock_get_conn, mock_is_available):
        """Factory should call determine_api_search_version and pass the result to get_jira_connection."""
        mock_is_available.return_value = True
        mock_conn = MagicMock()
        mock_get_conn.return_value = mock_conn

        config = _get_jira_download_config(
            feature_flags={Constants.JQL_ENHANCED_SEARCH_ENABLED: True}
        )

        conn, use_enhanced = create_jira_connection(config)

        assert conn is mock_conn
        assert use_enhanced is True
        mock_is_available.assert_called_once_with(
            jira_config=config,
            jql_enhanced_search_enabled=True,
            force_legacy_api=False,
        )
        mock_get_conn.assert_called_once_with(
            config=config,
            auth_method=JiraAuthMethod.BasicAuth,
            use_jql_enhanced_search=True,
        )

    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_returns_false_when_enhanced_unavailable(self, mock_get_conn, mock_is_available):
        """Factory should return False for use_jql_enhanced_search when not available."""
        mock_is_available.return_value = False
        mock_conn = MagicMock()
        mock_get_conn.return_value = mock_conn

        config = _get_jira_download_config()

        conn, use_enhanced = create_jira_connection(config)

        assert conn is mock_conn
        assert use_enhanced is False
        mock_get_conn.assert_called_once_with(
            config=config,
            auth_method=JiraAuthMethod.BasicAuth,
            use_jql_enhanced_search=False,
        )

    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_respects_force_legacy_flag(self, mock_get_conn, mock_is_available):
        """Factory should pass force_legacy_api flag from feature flags."""
        mock_is_available.return_value = False
        mock_conn = MagicMock()
        mock_get_conn.return_value = mock_conn

        config = _get_jira_download_config(
            feature_flags={
                Constants.JQL_ENHANCED_SEARCH_ENABLED: True,
                Constants.FORCE_LEGACY_API: True,
            }
        )

        conn, use_enhanced = create_jira_connection(config)

        assert use_enhanced is False
        mock_is_available.assert_called_once_with(
            jira_config=config,
            jql_enhanced_search_enabled=True,
            force_legacy_api=True,
        )

    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_custom_auth_method(self, mock_get_conn, mock_is_available):
        """Factory should pass the provided auth_method to get_jira_connection."""
        mock_is_available.return_value = False
        mock_conn = MagicMock()
        mock_get_conn.return_value = mock_conn

        config = _get_jira_download_config(
            available_auth_methods=[JiraAuthMethod.BasicAuth, JiraAuthMethod.AtlassianConnect],
            connect_app_active=True,
            jwt_attributes={"sharedSecret": "s", "clientKey": "c", "key": "k"},
        )

        conn, use_enhanced = create_jira_connection(
            config, auth_method=JiraAuthMethod.AtlassianConnect
        )

        mock_get_conn.assert_called_once_with(
            config=config,
            auth_method=JiraAuthMethod.AtlassianConnect,
            use_jql_enhanced_search=False,
        )


class TestCreateJiraConnections:
    """Tests for the create_jira_connections() factory."""

    @patch("jf_ingest.jf_jira.auth.logging_helper")
    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_returns_bundle_with_basic_auth_only(self, mock_get_conn, mock_is_available, mock_log_helper):
        """When only BasicAuth is available, bundle should have primary=basic, connect=None."""
        mock_is_available.return_value = False
        mock_basic_conn = MagicMock(spec=JIRA)
        mock_get_conn.return_value = mock_basic_conn

        config = _get_jira_download_config()

        bundle = create_jira_connections(config)

        assert isinstance(bundle, JiraConnectionBundle)
        assert bundle.primary_connection is mock_basic_conn
        assert bundle.basic_connection is mock_basic_conn
        assert bundle.atlas_connect_connection is None
        assert bundle.using_connect_as_primary_auth is False
        assert bundle.use_jql_enhanced_search is False

    @patch("jf_ingest.jf_jira.auth.logging_helper")
    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_creates_connect_when_available(self, mock_get_conn, mock_is_available, mock_log_helper):
        """When AtlassianConnect is available, bundle should include the connect connection."""
        mock_is_available.return_value = True
        mock_basic_conn = MagicMock(spec=JIRA)
        mock_connect_conn = MagicMock(spec=JIRA)
        mock_get_conn.side_effect = [mock_basic_conn, mock_connect_conn]

        config = _get_jira_download_config(
            available_auth_methods=[JiraAuthMethod.BasicAuth, JiraAuthMethod.AtlassianConnect],
            connect_app_active=True,
            jwt_attributes={"sharedSecret": "s", "clientKey": "c", "key": "k"},
            feature_flags={Constants.JQL_ENHANCED_SEARCH_ENABLED: True},
        )

        bundle = create_jira_connections(config)

        assert bundle.basic_connection is mock_basic_conn
        assert bundle.atlas_connect_connection is mock_connect_conn
        assert bundle.primary_connection is mock_basic_conn
        assert bundle.using_connect_as_primary_auth is False
        assert bundle.use_jql_enhanced_search is True

    @patch("jf_ingest.jf_jira.auth.logging_helper")
    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_lusca_flag_switches_primary_to_connect(self, mock_get_conn, mock_is_available, mock_log_helper):
        """When lusca feature flag is enabled, primary should be the AtlassianConnect connection."""
        mock_is_available.return_value = False
        mock_basic_conn = MagicMock(spec=JIRA)
        mock_connect_conn = MagicMock(spec=JIRA)
        mock_get_conn.side_effect = [mock_basic_conn, mock_connect_conn]

        config = _get_jira_download_config(
            available_auth_methods=[JiraAuthMethod.BasicAuth, JiraAuthMethod.AtlassianConnect],
            connect_app_active=True,
            jwt_attributes={"sharedSecret": "s", "clientKey": "c", "key": "k"},
            feature_flags={
                "lusca-auth-always-use-connect-for-atlassian-apis-Q423": True,
            },
        )

        bundle = create_jira_connections(config)

        assert bundle.primary_connection is mock_connect_conn
        assert bundle.basic_connection is mock_basic_conn
        assert bundle.atlas_connect_connection is mock_connect_conn
        assert bundle.using_connect_as_primary_auth is True

    @patch("jf_ingest.jf_jira.auth.logging_helper")
    @patch("jf_ingest.jf_jira.auth.determine_api_search_version")
    @patch("jf_ingest.jf_jira.auth.get_jira_connection")
    def test_lusca_flag_raises_when_connect_not_available(self, mock_get_conn, mock_is_available, mock_log_helper):
        """When lusca flag is on but connect is not available, should raise AtlassianConnectException."""
        mock_is_available.return_value = False
        mock_basic_conn = MagicMock(spec=JIRA)
        mock_get_conn.return_value = mock_basic_conn

        config = _get_jira_download_config(
            feature_flags={
                "lusca-auth-always-use-connect-for-atlassian-apis-Q423": True,
            },
        )

        with pytest.raises(AtlassianConnectException):
            create_jira_connections(config)
