from unittest.mock import MagicMock, patch

import pytest

from tmt.steps.prepare.artifact.providers import Repository
from tmt.steps.prepare.artifact.providers.repository import (
    RepositoryFileProvider,
    parse_rpm_string,
)
from tmt.utils import GeneralError, Path, PrepareError, RunError, requests

# A valid .repo file content for testing, using Docker CE repo
VALID_REPO_CONTENT = """
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://download.docker.com/linux/centos/gpg

[docker-ce-test]
name=Docker CE Test - $basearch
baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/test
enabled=0
gpgcheck=1
gpgkey=https://download.docker.com/linux/centos/gpg

[docker-ce-nightly]
name=Docker CE Nightly - $basearch
baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/nightly
enabled=0
gpgcheck=1
gpgkey=https://download.docker.com/linux/centos/gpg
"""

# Expected repository IDs from the valid content
EXPECTED_REPO_IDS = ["docker-ce-stable", "docker-ce-test", "docker-ce-nightly"]

# Malformed content for error handling tests
MALFORMED_REPO_CONTENT = """
[my-repo]
name
"""

NO_SECTION_CONTENT = "name=No sections here\nenabled=1"


# Fixture to create a temporary .repo file
@pytest.fixture
def temp_repo_file(tmppath):
    """Creates a temporary repo file with valid content"""
    repo_file = tmppath / "docker-ce.repo"
    repo_file.write_text(VALID_REPO_CONTENT)
    return repo_file


@pytest.fixture
def temp_repo_file_no_ext(tmppath):
    """Creates a temporary repo file without .repo extension"""
    repo_file = tmppath / "docker-ce"
    repo_file.write_text(VALID_REPO_CONTENT)
    return repo_file


def test_init_from_content(root_logger):
    """Test successful initialization from a content string"""
    repo = Repository.from_content(
        name="from-content", content=VALID_REPO_CONTENT, logger=root_logger
    )
    assert repo.name == "from-content"
    assert repo.content == VALID_REPO_CONTENT
    assert repo.repo_ids == EXPECTED_REPO_IDS
    assert repo.filename == "from-content.repo"


def test_init_from_file(temp_repo_file, root_logger):
    """Test successful initialization from a local file path"""
    repo = Repository.from_file_path(file_path=temp_repo_file, logger=root_logger)
    assert repo.name == "docker-ce"
    assert repo.content == VALID_REPO_CONTENT
    assert repo.repo_ids == EXPECTED_REPO_IDS


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_init_from_url(mock_retry_session, root_logger):
    """Test successful initialization from a URL"""
    mock_session = MagicMock()
    mock_response = MagicMock()
    mock_response.text = VALID_REPO_CONTENT
    mock_response.raise_for_status.return_value = None
    mock_session.get.return_value = mock_response
    mock_retry_session.return_value.__enter__.return_value = mock_session

    repo_url = "https://download.docker.com/linux/centos/docker-ce.repo"
    repo = Repository.from_url(url=repo_url, logger=root_logger)

    assert repo.name == "docker-ce"
    assert repo.content == VALID_REPO_CONTENT
    assert repo.repo_ids == EXPECTED_REPO_IDS
    mock_session.get.assert_called_once_with(repo_url)


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_name_derivation(mock_retry_session, temp_repo_file, root_logger):
    """Test the logic for deriving the repository name"""
    # Provided name takes precedence
    repo = Repository.from_content(name="explicit-name", content="[foo]", logger=root_logger)
    assert repo.name == "explicit-name"

    # Mock the session for the URL test
    mock_session = MagicMock()
    mock_response = MagicMock()
    mock_response.text = "[foo]"
    mock_response.raise_for_status.return_value = None
    mock_session.get.return_value = mock_response
    mock_retry_session.return_value.__enter__.return_value = mock_session

    # Name derived from URL
    repo = Repository.from_url(url="http://a.b/c/docker-ce.repo", logger=root_logger)
    assert repo.name == "docker-ce"

    # Name derived from file path
    repo = Repository.from_file_path(file_path=temp_repo_file, logger=root_logger)
    assert repo.name == "docker-ce"


def test_repo_id_parsing(root_logger):
    """Test the parsing of repo IDs from content"""
    # Valid content with multiple sections
    repo = Repository.from_content(name="valid", content=VALID_REPO_CONTENT, logger=root_logger)
    assert repo.repo_ids == EXPECTED_REPO_IDS

    # Content with no sections (empty)
    with pytest.raises(GeneralError, match="No repository sections found"):
        Repository.from_content(name="no-sections", content="", logger=root_logger)

    # Content with options but no section header
    with pytest.raises(GeneralError, match="No repository sections found"):
        Repository.from_content(name="no-sections", content=NO_SECTION_CONTENT, logger=root_logger)

    # Malformed content should also raise an error
    with pytest.raises(GeneralError, match=r"The .repo file may be malformed"):
        Repository.from_content(
            name="malformed", content=MALFORMED_REPO_CONTENT, logger=root_logger
        )


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_failed_url_fetch(mock_retry_session, root_logger):
    """Test that a failed URL fetch raises an error"""
    mock_session = MagicMock()
    mock_session.get.side_effect = requests.RequestException("Connection failed")
    mock_retry_session.return_value.__enter__.return_value = mock_session

    with pytest.raises(GeneralError, match="Failed to fetch repository content"):
        Repository.from_url(url="http://example.com/invalid.repo", logger=root_logger)


def test_nonexistent_file(root_logger):
    """Test that a non-existent file path raises an error"""
    with pytest.raises(GeneralError, match="Failed to read repository file"):
        Repository.from_file_path(file_path=Path("/no/such/file.repo"), logger=root_logger)


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_url_trailing_slash(mock_retry_session, root_logger):
    """Test URL with trailing slash"""
    mock_session = MagicMock()
    mock_response = MagicMock()
    mock_response.text = VALID_REPO_CONTENT
    mock_response.raise_for_status.return_value = None
    mock_session.get.return_value = mock_response
    mock_retry_session.return_value.__enter__.return_value = mock_session

    repo = Repository.from_url(url="https://example.com/repo/", logger=root_logger)
    assert repo.name == "repo"


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_url_no_repo_ext(mock_retry_session, root_logger):
    """Test URL without .repo extension"""
    mock_session = MagicMock()
    mock_response = MagicMock()
    mock_response.text = VALID_REPO_CONTENT
    mock_response.raise_for_status.return_value = None
    mock_session.get.return_value = mock_response
    mock_retry_session.return_value.__enter__.return_value = mock_session

    repo = Repository.from_url(url="https://example.com/docker-ce", logger=root_logger)
    assert repo.name == "docker-ce"


def test_file_no_repo_ext(temp_repo_file_no_ext, root_logger):
    """Test file without .repo extension"""
    repo = Repository.from_file_path(file_path=temp_repo_file_no_ext, logger=root_logger)
    assert repo.name == "docker-ce"


def test_content_no_name(root_logger):
    """Test content without name raises error"""
    with pytest.raises(GeneralError, match="Repository name cannot be empty"):
        Repository.from_content(content=VALID_REPO_CONTENT, name="", logger=root_logger)


@patch('tmt.steps.prepare.artifact.providers.tmt.utils.retry_session')
def test_invalid_url_format(mock_retry_session, root_logger):
    """Test initialization with invalid URL format"""
    mock_session = MagicMock()
    mock_session.get.side_effect = requests.exceptions.InvalidURL("Invalid URL")
    mock_retry_session.return_value.__enter__.return_value = mock_session

    with pytest.raises(GeneralError, match="Failed to fetch repository content"):
        Repository.from_url(url="invalid_url", logger=root_logger)


def test_url_no_path(root_logger):
    """Test URL with no path raises error"""
    with patch(
        'tmt.steps.prepare.artifact.providers.tmt.utils.retry_session'
    ) as mock_retry_session:
        mock_session = MagicMock()
        mock_response = MagicMock()
        mock_response.text = VALID_REPO_CONTENT
        mock_response.raise_for_status.return_value = None
        mock_session.get.return_value = mock_response
        mock_retry_session.return_value.__enter__.return_value = mock_session

        with pytest.raises(GeneralError, match="Could not derive repository name from URL"):
            Repository.from_url(url="https://example.com/", logger=root_logger)


@pytest.mark.parametrize(
    ("pkg_string", "expected"),
    [
        # With epoch
        (
            "docker-ce-1:20.10.7-3.el8.x86_64",
            {
                "name": "docker-ce",
                "epoch": "1",
                "version": "20.10.7",
                "release": "3.el8",
                "arch": "x86_64",
                "nvr": "docker-ce-20.10.7-3.el8",
            },
        ),
        # Without epoch
        (
            "bash-5.1.8-6.el9.x86_64",
            {
                "name": "bash",
                "epoch": "0",
                "version": "5.1.8",
                "release": "6.el9",
                "arch": "x86_64",
                "nvr": "bash-5.1.8-6.el9",
            },
        ),
        # Name with '+', version with '+' and '.', release with '.'
        (
            "tmt+export-polarion-1.61.0.dev17+gf29b2e83e-1.fc41.x86_64",
            {
                "name": "tmt+export-polarion",
                "epoch": "0",
                "version": "1.61.0.dev17+gf29b2e83e",
                "release": "1.fc41",
                "arch": "x86_64",
                "nvr": "tmt+export-polarion-1.61.0.dev17+gf29b2e83e-1.fc41",
            },
        ),
        # Name with multiple '-', no epoch
        (
            "keylime-agent-rust-push-debuginfo-0.2.3-1.fc41.x86_64",
            {
                "name": "keylime-agent-rust-push-debuginfo",
                "epoch": "0",
                "version": "0.2.3",
                "release": "1.fc41",
                "arch": "x86_64",
                "nvr": "keylime-agent-rust-push-debuginfo-0.2.3-1.fc41",
            },
        ),
        # With epoch 0 explicitly
        (
            "example-0:1.0-1.noarch",
            {
                "name": "example",
                "epoch": "0",
                "version": "1.0",
                "release": "1",
                "arch": "noarch",
                "nvr": "example-1.0-1",
            },
        ),
        # Example of src rpm
        (
            "example-5.1.8-6.el9.src",
            {
                "name": "example",
                "epoch": "0",
                "version": "5.1.8",
                "release": "6.el9",
                "arch": "src",
                "nvr": "example-5.1.8-6.el9",
            },
        ),
    ],
)
def test_parse_rpm_string_valid(pkg_string, expected):
    result = parse_rpm_string(pkg_string)
    assert result == expected


@pytest.mark.parametrize(
    "pkg_string",
    [
        "invalid-string",  # No match
        "name-version.x86_64",  # Missing release
        "name-1:version.x86_64",  # Missing release
        "name-version-release",  # Missing arch
        "name-version-release.x86.64",  # Dot in arch
        "name-version--release.x86_64",  # Double '-'
        "name-a:b-release.x86_64",  # Invalid epoch (not digit before :)
        "name-1:ver-sion-release.x86_64",  # '-' in version
        "name-version-rel-ease.x86_64",  # '-' in release
    ],
)
def test_parse_rpm_string_invalid(pkg_string):
    with pytest.raises(ValueError, match=r"does not match|Malformed package string"):
        parse_rpm_string(pkg_string)


# ================================================================================
# Tests for RepositoryFileProvider
# ================================================================================


@pytest.fixture
def mock_repo_file_fetch():
    with patch(
        'tmt.steps.prepare.artifact.providers.tmt.utils.retry_session'
    ) as mock_retry_session:
        mock_session = MagicMock()
        mock_response = MagicMock()
        mock_response.text = VALID_REPO_CONTENT
        mock_response.raise_for_status.return_value = None
        mock_session.get.return_value = mock_response
        mock_retry_session.return_value.__enter__.return_value = mock_session
        yield mock_session


@pytest.fixture
def mock_guest_and_pm():
    mock_guest = MagicMock()
    mock_package_manager = MagicMock()
    mock_guest.package_manager = mock_package_manager
    return mock_guest, mock_package_manager


def test_id_extraction(artifact_provider):
    """Test provider ID extraction from raw provider ID"""

    # Valid provider ID
    raw_id = "repository-file:https://download.docker.com/linux/centos/docker-ce.repo"
    provider = artifact_provider(raw_id)
    assert provider.id == "https://download.docker.com/linux/centos/docker-ce.repo"


def test_artifacts_before_fetch(mock_repo_file_fetch, artifact_provider):
    """Test that repository provider artifacts returns empty list"""

    provider = artifact_provider("repository-file:https://example.com/test.repo")

    # Repository providers don't enumerate individual artifacts
    # They provide repositories that the package manager uses
    assert provider.artifacts == []


def test_fetch_contents(mock_repo_file_fetch, mock_guest_and_pm, artifact_provider, tmppath):
    """Test that fetch_contents is a no-op for repository providers"""

    mock_guest, mock_package_manager = mock_guest_and_pm

    provider = artifact_provider(
        "repository-file:https://download.docker.com/linux/centos/docker-ce.repo"
    )

    # Call fetch_contents
    artifacts_dir = tmppath / "artifacts"
    result = provider.fetch_contents(mock_guest, artifacts_dir)

    # Verify result is empty list (no files downloaded)
    assert result == []

    # Verify no package manager methods were called (it's a no-op)
    mock_package_manager.list_packages.assert_not_called()

    # Verify artifacts property returns empty list (no individual artifact files)
    artifacts = provider.artifacts
    assert len(artifacts) == 0


def test_contribute_to_shared_repo(
    mock_repo_file_fetch, mock_guest_and_pm, artifact_provider, tmppath
):
    """Test that contribute_to_shared_repo does nothing for repository providers"""

    mock_guest, mock_package_manager = mock_guest_and_pm

    provider = artifact_provider(
        "repository-file:https://download.docker.com/linux/centos/docker-ce.repo"
    )

    # Call contribute_to_shared_repo
    # Repository providers don't contribute files to the shared repo,
    # they just provide Repository objects via get_repositories()
    artifacts_dir = tmppath / "artifacts"
    shared_repo_dir = tmppath / "shared"
    provider.contribute_to_shared_repo(mock_guest, artifacts_dir, shared_repo_dir)

    # Verify no package manager methods were called (since contribute_to_shared_repo is a no-op)
    mock_package_manager.install_repository.assert_not_called()
    mock_package_manager.list_packages.assert_not_called()

    # Verify artifacts returns empty list (no individual files)
    assert provider.artifacts == []


def test_get_repositories(mock_repo_file_fetch, mock_guest_and_pm, artifact_provider, tmppath):
    """Test that get_repositories returns the initialized repository"""

    mock_guest, _ = mock_guest_and_pm

    provider = artifact_provider(
        "repository-file:https://download.docker.com/linux/centos/docker-ce.repo"
    )

    # First, fetch_contents must be called to initialize the repository
    artifacts_dir = tmppath / "artifacts"
    provider.fetch_contents(mock_guest, artifacts_dir)

    # Now get_repositories should return a list with the repository
    repositories = provider.get_repositories()

    assert len(repositories) == 1
    assert isinstance(repositories[0], Repository)
    assert repositories[0].name == "docker-ce"
    assert repositories[0].content == VALID_REPO_CONTENT
    assert repositories[0].repo_ids == EXPECTED_REPO_IDS
