#!/usr/bin/python3
"""Tests for slpkg/update_repositories.py using pytest."""

import bz2
import gzip
import json
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from slpkg.update_repositories import UpdateRepositories


@pytest.fixture
def mock_config():
    """Mock config_load for UpdateRepositories."""
    with patch("slpkg.update_repositories.config_load") as mock:
        mock.gpg_verification = False
        mock.ask_question = False
        mock.git_clone = "git clone"
        mock.green = ""
        mock.red = ""
        mock.endc = ""
        mock.lftp_mirror_options = ""
        mock.is_64bit.return_value = True
        mock.cpu_arch = "x86_64"
        mock.package_type = [".txz", ".tgz", ".tlz", ".tbz"]
        yield mock


@pytest.fixture
def update_repos_inst(mock_config):
    """Fixture to create an UpdateRepositories instance with all dependencies mocked."""
    options = {"option_quiet": False}
    with patch("slpkg.update_repositories.Repositories") as mock_repos_cls, \
         patch("slpkg.update_repositories.Utilities"), \
         patch("slpkg.update_repositories.InstallData"), \
         patch("slpkg.update_repositories.SBoGenerate"), \
         patch("slpkg.update_repositories.CheckUpdates"), \
         patch("slpkg.update_repositories.Downloader"), \
         patch("slpkg.update_repositories.View"), \
         patch("slpkg.update_repositories.MultiProcess"), \
         patch("slpkg.update_repositories.ViewProcess"):
        
        # Set some essential attributes on the Repositories mock
        mock_repos = mock_repos_cls.return_value
        mock_repos.manifest_bz2 = "MANIFEST.bz2"
        mock_repos.slack_repo_name = "slack"
        mock_repos.sbo_repo_name = "sbo"
        mock_repos.ponce_repo_name = "ponce"
        
        inst = UpdateRepositories(options, "slack")
        return inst


def test_is_local_mirror(update_repos_inst):
    """Test local mirror detection logic."""
    assert update_repos_inst._is_local_mirror("file:///home/user/repo") is True
    assert update_repos_inst._is_local_mirror("/mnt/repo") is True
    assert update_repos_inst._is_local_mirror("http://mirror.com") is False
    assert update_repos_inst._is_local_mirror("ftp://mirror.com") is False


def test_copy_local_repo_files(update_repos_inst):
    """Test copying files from a local mirror."""
    repo = "slack"
    file_urls = ["file:///tmp/src/PACKAGES.TXT", "/tmp/src/ChangeLog.txt"]
    update_repos_inst.repos.repositories = {
        "slack": {"path": Path("/var/lib/slpkg/slack")}
    }
    
    with patch("shutil.copy2") as mock_copy, \
         patch("pathlib.Path.is_file", return_value=True):
        update_repos_inst._copy_local_repo_files(repo, file_urls)
        assert mock_copy.call_count == 2
        mock_copy.assert_any_call(Path("/tmp/src/PACKAGES.TXT"), Path("/var/lib/slpkg/slack/PACKAGES.TXT"))


def test_import_gpg_key_disabled(update_repos_inst):
    """Test GPG key import when disabled."""
    update_repos_inst.gpg_verification = False
    with patch("subprocess.run") as mock_run:
        update_repos_inst.import_gpg_key("slack")
        assert not mock_run.called


@patch("subprocess.run")
def test_import_gpg_key_success(mock_run, update_repos_inst):
    """Test successful GPG key import."""
    update_repos_inst.gpg_verification = True
    update_repos_inst.repos.repositories = {
        "slack": {"mirror_changelog": "http://mirror/"}
    }
    
    mock_proc = MagicMock()
    mock_proc.stdout = "gpg: key 12345: imported: 1"
    mock_run.return_value = mock_proc
    
    update_repos_inst.import_gpg_key("slack")
    assert mock_run.called
    assert mock_run.call_args[0][0] == ["gpg", "--fetch-key", "http://mirror/GPG-KEY"]


def test_update_binary_repos_local(update_repos_inst):
    """Test binary repo update with local mirror."""
    repo = "slack"
    update_repos_inst.repos.repositories = {
        "slack": {
            "path": Path("/var/lib/slpkg/slack"),
            "mirror_packages": "/tmp/local",
            "mirror_changelog": "/tmp/local",
            "packages_txt": "PACKAGES.TXT",
            "changelog_txt": "ChangeLog.txt",
            "checksums_md5": "CHECKSUMS.md5",
            "manifest_bz2": "MANIFEST.bz2"
        }
    }
    
    with patch.object(update_repos_inst, "_copy_local_repo_files") as mock_copy, \
         patch("pathlib.Path.exists", return_value=True), \
         patch("pathlib.Path.read_text", return_value="MANIFEST.bz2"), \
         patch.object(update_repos_inst, "update_filelist_index"):
        update_repos_inst.update_binary_repos(repo)
        assert mock_copy.call_count == 2
        update_repos_inst.data.install_binary_data.assert_called_once_with("slack")


def test_update_slackbuild_repos_git(update_repos_inst):
    """Test SlackBuild repo update via Git."""
    repo = "sbo"
    update_repos_inst.repos.sbo_git_mirror = "https://github.com/SBo.git"
    update_repos_inst.repos.repositories = {
        "sbo": {
            "path": Path("/var/lib/slpkg/sbo"),
            "mirror_packages": "http://sbo/",
            "slackbuilds_txt": "SLACKBUILDS.TXT",
            "changelog_txt": "ChangeLog.txt"
        }
    }
    
    with patch("pathlib.Path.is_file", return_value=True):
        update_repos_inst.update_slackbuild_repos(repo)
        update_repos_inst.utils.remove_folder_if_exists.assert_called_once()
        update_repos_inst.multi_process.process.assert_called()
        cmd = update_repos_inst.multi_process.process.call_args[0][0]
        assert "git" in cmd
        assert "clone" in cmd
        assert "https://github.com/SBo.git" in cmd


def test_update_filelist_index(update_repos_inst, tmp_path):
    """Test MANIFEST.bz2 parsing and filelist.json.gz generation."""
    repo_path = tmp_path / "repo"
    repo_path.mkdir()
    manifest_bz2 = repo_path / "MANIFEST.bz2"

    # Create a dummy MANIFEST.bz2 content
    content = (
        "|| Package:  bash-5.2.026-x86_64-1.txz\n"
        "-rw-r--r-- root/root      123 2024-01-01 12:00 bin/bash\n"
        "l rwxrwxrwx root/root        0 2024-01-01 12:00 bin/sh -> bash\n"
        "|| Package:  grep-3.11-x86_64-1.txz\n"
        "-rw-r--r-- root/root      456 2024-01-01 12:00 bin/grep\n"
    )

    with bz2.open(manifest_bz2, "wt", encoding="utf-8") as f:
        f.write(content)

    update_repos_inst.update_filelist_index(repo_path, "MANIFEST.bz2")

    output_gz = repo_path / "filelist.json.gz"
    assert output_gz.exists()

    with gzip.open(output_gz, "rt", encoding="utf-8") as f:
        data = json.load(f)
        assert "bash-5.2.026-x86_64-1.txz" in data
        assert "bin/bash" in data["bash-5.2.026-x86_64-1.txz"]
        assert "bash" in data["bash-5.2.026-x86_64-1.txz"]
        assert "bin/grep" in data["grep-3.11-x86_64-1.txz"]


def test_update_filelist_index_missing_manifest(update_repos_inst, tmp_path):
    """No output is produced when the manifest file does not exist."""
    repo_path = tmp_path / "repo"
    repo_path.mkdir()
    update_repos_inst.update_filelist_index(repo_path, "MANIFEST.bz2")
    assert not (repo_path / "filelist.json.gz").exists()


def test_update_filelist_index_empty_manifest(update_repos_inst, tmp_path):
    """No output is produced when the manifest contains no package blocks."""
    repo_path = tmp_path / "repo"
    repo_path.mkdir()
    with bz2.open(repo_path / "MANIFEST.bz2", "wt", encoding="utf-8") as f:
        f.write("")
    update_repos_inst.update_filelist_index(repo_path, "MANIFEST.bz2")
    assert not (repo_path / "filelist.json.gz").exists()


# ---------------------------------------------------------------------------
# _fetch_gpg_key
# ---------------------------------------------------------------------------


def test_fetch_gpg_key_calls_subprocess():
    """_fetch_gpg_key calls gpg --fetch-key with the correct arguments."""
    mock_proc = MagicMock(stdout="imported: 1")
    with patch("subprocess.run", return_value=mock_proc) as mock_run:
        UpdateRepositories._fetch_gpg_key("http://mirror/GPG-KEY")
        mock_run.assert_called_once_with(
            ["gpg", "--fetch-key", "http://mirror/GPG-KEY"],
            shell=False,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            encoding="utf-8",
            text=True,
            check=True,
        )


def test_fetch_gpg_key_raises_on_failure():
    """_fetch_gpg_key propagates CalledProcessError on gpg failure."""
    with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "gpg")):
        with pytest.raises(subprocess.CalledProcessError):
            UpdateRepositories._fetch_gpg_key("http://mirror/GPG-KEY")


# ---------------------------------------------------------------------------
# import_gpg_key — fallback and double-fail paths
# ---------------------------------------------------------------------------


def test_import_gpg_key_fallback_success(update_repos_inst):
    """First mirror fails, second (packages mirror) succeeds."""
    update_repos_inst.gpg_verification = True
    update_repos_inst.repos.repositories = {
        "slack": {
            "mirror_changelog": "http://cl-mirror/",
            "mirror_packages": "http://pkg-mirror/",
        }
    }
    err = subprocess.CalledProcessError(1, "gpg")
    err.stdout = ""
    mock_success = MagicMock(stdout="gpg: key: imported: 1")
    with patch.object(
        UpdateRepositories, "_fetch_gpg_key", side_effect=[err, mock_success]
    ) as mock_fetch:
        update_repos_inst.import_gpg_key("slack")
    assert mock_fetch.call_count == 2
    assert mock_fetch.call_args_list[1][0][0] == "http://pkg-mirror/GPG-KEY"


def test_import_gpg_key_both_fail_prints_failed(update_repos_inst, capsys):
    """Both mirror attempts fail — 'Failed' is printed to stdout."""
    update_repos_inst.gpg_verification = True
    update_repos_inst.repos.repositories = {
        "slack": {
            "mirror_changelog": "http://cl-mirror/",
            "mirror_packages": "http://pkg-mirror/",
        }
    }
    err = subprocess.CalledProcessError(1, "gpg")
    err.stdout = ""
    with patch.object(UpdateRepositories, "_fetch_gpg_key", side_effect=[err, err]):
        update_repos_inst.import_gpg_key("slack")
    assert "Failed" in capsys.readouterr().out


# ---------------------------------------------------------------------------
# _parse_manifest (static — tested directly without an instance)
# ---------------------------------------------------------------------------


def test_parse_manifest_basic(tmp_path):
    """Basic parsing: package block and file entry are captured."""
    content = (
        "|| Package:  bash-5.2-x86_64-1.txz\n"
        "-rw-r--r-- root/root 123 2024-01-01 bin/bash\n"
    )
    manifest = tmp_path / "MANIFEST.bz2"
    with bz2.open(manifest, "wt", encoding="utf-8") as f:
        f.write(content)
    result = UpdateRepositories._parse_manifest(manifest, [".txz"])
    assert "bash-5.2-x86_64-1.txz" in result
    assert "bin/bash" in result["bash-5.2-x86_64-1.txz"]


def test_parse_manifest_filters_metadata(tmp_path):
    """install/ and ./ prefixed paths are excluded from the index."""
    content = (
        "|| Package:  bash-5.2-x86_64-1.txz\n"
        "-rw-r--r-- root/root 1 2024-01-01 install/doinst.sh\n"
        "-rw-r--r-- root/root 1 2024-01-01 ./usr/share/doc/README\n"
        "-rw-r--r-- root/root 1 2024-01-01 bin/bash\n"
    )
    manifest = tmp_path / "MANIFEST.bz2"
    with bz2.open(manifest, "wt", encoding="utf-8") as f:
        f.write(content)
    result = UpdateRepositories._parse_manifest(manifest, [".txz"])
    files = result["bash-5.2-x86_64-1.txz"]
    assert "bin/bash" in files
    assert not any(f.startswith("install/") or f.startswith("./") for f in files)


def test_parse_manifest_unknown_extension_ignored(tmp_path):
    """Package blocks whose extension is not in package_types are skipped."""
    content = "|| Package:  bash-5.2-x86_64-1.rpm\n"
    manifest = tmp_path / "MANIFEST.bz2"
    with bz2.open(manifest, "wt", encoding="utf-8") as f:
        f.write(content)
    result = UpdateRepositories._parse_manifest(manifest, [".txz"])
    assert result == {}


# ---------------------------------------------------------------------------
# _write_filelist_index (static — tested directly without an instance)
# ---------------------------------------------------------------------------


def test_write_filelist_index_creates_gz(tmp_path):
    """Output is a valid gzip-compressed JSON matching the input index."""
    index = {"pkg-1.0-x86_64-1.txz": ["bin/foo", "usr/lib/bar.so"]}
    UpdateRepositories._write_filelist_index(tmp_path, index)
    output_gz = tmp_path / "filelist.json.gz"
    assert output_gz.exists()
    with gzip.open(output_gz, "rt", encoding="utf-8") as f:
        assert json.load(f) == index


def test_write_filelist_index_removes_old_json(tmp_path):
    """Legacy uncompressed filelist.json is removed after writing."""
    old_json = tmp_path / "filelist.json"
    old_json.write_text("{}")
    UpdateRepositories._write_filelist_index(tmp_path, {"p.txz": []})
    assert not old_json.exists()


# ---------------------------------------------------------------------------
# _check_mirror_version
# ---------------------------------------------------------------------------


def test_check_mirror_version_no_version_file(update_repos_inst, capsys):
    """/etc/slackware-version missing — no output, no exception."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-15.0/"}
    }
    with patch("pathlib.Path.read_text", side_effect=OSError):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_no_mismatch_stable(update_repos_inst, capsys):
    """Stable system, stable mirror — no warning."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-15.0/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0\n"):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_no_mismatch_current(update_repos_inst, capsys):
    """Current system, current mirror — no warning."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-current/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0+\n"):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_warns_current_system_stable_mirror(update_repos_inst, capsys):
    """Current system with a stable mirror URL — warning is printed."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-15.0/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0+\n"):
        update_repos_inst._check_mirror_version()
    out = capsys.readouterr().out
    assert "Warning" in out
    assert "slack" in out
    assert "current" in out
    assert "15.0" in out


def test_check_mirror_version_warns_stable_system_current_mirror(update_repos_inst, capsys):
    """Stable system with a current mirror URL — warning is printed."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-current/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0\n"):
        update_repos_inst._check_mirror_version()
    out = capsys.readouterr().out
    assert "Warning" in out
    assert "slack" in out
    assert "15.0" in out
    assert "current" in out


def test_check_mirror_version_skips_disabled_repo(update_repos_inst, capsys):
    """Disabled repository is not checked even if the mirror URL would mismatch."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": False, "mirror_packages": "https://mirror/slackware64-current/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0\n"):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_skips_local_mirror(update_repos_inst, capsys):
    """Local file:// mirrors are never checked for version hints."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "file:///mnt/slackware64-current/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0\n"):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_skips_mirror_without_hint(update_repos_inst, capsys):
    """Mirror URLs without a recognisable version hint are silently ignored."""
    update_repos_inst.repos.repositories = {
        "alien": {"enable": True, "mirror_packages": "https://alien.slackbook.org/"}
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0\n"):
        update_repos_inst._check_mirror_version()
    assert capsys.readouterr().out == ""


def test_check_mirror_version_multiple_repos(update_repos_inst, capsys):
    """Only the mismatched repo triggers a warning; matching repos are silent."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror/slackware64-15.0/"},
        "extra": {"enable": True, "mirror_packages": "https://mirror/slackware64-current/"},
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0+\n"):
        update_repos_inst._check_mirror_version()
    out = capsys.readouterr().out
    # 'slack' (stable mirror, current system) → warning
    assert "slack" in out
    # 'extra' (current mirror, current system) → no warning
    assert "extra" not in out


def test_check_mirror_version_multiple_mismatches_single_block(update_repos_inst, capsys):
    """Multiple mismatched repos produce a single warning block listing all repos."""
    update_repos_inst.repos.repositories = {
        "slack": {"enable": True, "mirror_packages": "https://mirror1/slackware64-15.0/"},
        "extra": {"enable": True, "mirror_packages": "https://mirror2/slackware64-15.0/"},
    }
    with patch("pathlib.Path.read_text", return_value="Slackware 15.0+\n"):
        update_repos_inst._check_mirror_version()
    out = capsys.readouterr().out
    assert out.count("Warning") == 1
    assert "slack" in out
    assert "extra" in out
    assert "Fix" in out


def test_repositories_calls_check_mirror_version_when_enabled(update_repos_inst):
    """repositories() calls _check_mirror_version when mirror_version_check is True."""
    update_repos_inst.mirror_version_check = True
    update_repos_inst.check_updates.updates.return_value = {}
    with patch.object(update_repos_inst, "_check_mirror_version") as mock_check, \
         patch.object(update_repos_inst.view, "question"):
        update_repos_inst.repositories()
    mock_check.assert_called_once()


def test_repositories_skips_check_mirror_version_when_disabled(update_repos_inst):
    """repositories() skips _check_mirror_version when mirror_version_check is False."""
    update_repos_inst.mirror_version_check = False
    update_repos_inst.check_updates.updates.return_value = {}
    with patch.object(update_repos_inst, "_check_mirror_version") as mock_check, \
         patch.object(update_repos_inst.view, "question"):
        update_repos_inst.repositories()
    mock_check.assert_not_called()
