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

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from urllib3.exceptions import HTTPError, NewConnectionError
from slpkg.check_updates import CheckUpdates


@pytest.fixture
def mock_config():
    """Mock config_load to avoid environment dependency."""
    with patch("slpkg.check_updates.config_load") as mock:
        mock.urllib_timeout = 3.0
        mock.proxy_username = ""
        mock.proxy_password = ""
        mock.proxy_address = ""
        mock.urllib_retries = False
        mock.urllib_redirect = False
        mock.green = ""
        mock.red = ""
        mock.yellow = ""
        mock.endc = ""
        yield mock


@pytest.fixture
def check_updates_inst(mock_config):
    """Fixture to create a CheckUpdates instance with dependencies mocked."""
    options = {"option_repository": False, "option_check": False}
    with patch("slpkg.check_updates.Repositories"), \
         patch("slpkg.check_updates.RepoInfo"), \
         patch("slpkg.check_updates.ProgressBar"), \
         patch("slpkg.check_updates.Imprint"):
        inst = CheckUpdates(options, "slack")
        return inst


def test_initialization(check_updates_inst):
    """Test initialization and proxy setup."""
    assert check_updates_inst.repository == "slack"
    assert check_updates_inst.urllib_timeout == 3.0
    # Default should be PoolManager
    from urllib3 import PoolManager
    assert isinstance(check_updates_inst.http, PoolManager)


def test_compare_the_changelogs_local_not_exists(check_updates_inst):
    """Test when local changelog doesn't exist (should return True for update)."""
    with patch("pathlib.Path.is_file", return_value=False):
        result = check_updates_inst.compare_the_changelogs(Path("local"), "remote")
        assert result is True


def test_compare_the_changelogs_file_uri(check_updates_inst):
    """Test comparison with file:// URI (local mirror)."""
    local_path = MagicMock(spec=Path)
    remote_path = "file:///repo/ChangeLog.txt"

    with patch("slpkg.check_updates.Path") as mock_path_cls:
        # First call to local_chg_txt.is_file()
        local_path.is_file.return_value = True
        local_path.stat.return_value.st_size = 100
        
        # Mock mirror_path
        mock_mirror_path = MagicMock(spec=Path)
        mock_mirror_path.is_file.return_value = True
        mock_mirror_path.stat.return_value.st_size = 200
        mock_path_cls.return_value = mock_mirror_path
        
        result = check_updates_inst.compare_the_changelogs(local_path, remote_path)
        assert result is True

        # Same size
        mock_mirror_path.stat.return_value.st_size = 100
        result = check_updates_inst.compare_the_changelogs(local_path, remote_path)
        assert result is False


def test_compare_the_changelogs_remote_success(check_updates_inst):
    """Test successful remote comparison using HTTP HEAD."""
    local_path = Path("local")
    remote_url = "http://mirror/ChangeLog.txt"

    mock_response = MagicMock()
    mock_response.status = 200
    mock_response.headers = {"content-length": "500"}

    with patch("pathlib.Path.is_file", return_value=True), \
         patch("pathlib.Path.stat") as mock_stat, \
         patch.object(check_updates_inst.http, "request", return_value=mock_response):
        
        # Local size 400, remote 500 -> True
        mock_stat.return_value.st_size = 400
        assert check_updates_inst.compare_the_changelogs(local_path, remote_url) is True

        # Local size 500, remote 500 -> False
        mock_stat.return_value.st_size = 500
        assert check_updates_inst.compare_the_changelogs(local_path, remote_url) is False


def test_compare_the_changelogs_http_error(check_updates_inst):
    """Test handling of HTTP errors (e.g., 404)."""
    local_path = Path("local")
    remote_url = "http://mirror/ChangeLog.txt"

    mock_response = MagicMock()
    mock_response.status = 404

    with patch("pathlib.Path.is_file", return_value=True), \
         patch("pathlib.Path.stat"), \
         patch.object(check_updates_inst.http, "request", return_value=mock_response):
        
        result = check_updates_inst.compare_the_changelogs(local_path, remote_url)
        assert result is False
        assert remote_url in check_updates_inst.error_connected


def test_save_the_compares_no_data_json(check_updates_inst):
    """Test save_the_compares when data.json is missing."""
    repo = "alien"
    check_updates_inst.repos.repositories = {
        "alien": {
            "path": Path("/var/lib/slpkg/alien"),
            "changelog_txt": "ChangeLog.txt",
            "mirror_changelog": "http://alien/"
        }
    }
    check_updates_inst.repos.data_json = "data.json"

    with patch("pathlib.Path.is_file", return_value=False):
        check_updates_inst.save_the_compares(repo)
        assert check_updates_inst.compare[repo] is True


def test_check_the_repositories_single(check_updates_inst):
    """Test checking a single repository."""
    check_updates_inst.option_for_repository = True
    check_updates_inst.repository = "slack"
    
    with patch.object(check_updates_inst, "save_the_compares") as mock_save:
        check_updates_inst.check_the_repositories(None)
        mock_save.assert_called_once_with("slack")


def test_check_the_repositories_all(check_updates_inst):
    """Test checking all enabled repositories."""
    check_updates_inst.option_for_repository = False
    check_updates_inst.repos.repositories = {
        "repo1": {"enable": True},
        "repo2": {"enable": False},
        "repo3": {"enable": True}
    }
    
    with patch.object(check_updates_inst, "save_the_compares") as mock_save:
        check_updates_inst.check_the_repositories(None)
        assert mock_save.call_count == 2
        mock_save.assert_any_call("repo1")
        mock_save.assert_any_call("repo3")


def test_view_messages_no_updates(check_updates_inst, capsys):
    """Test view_messages when no updates are found."""
    check_updates_inst.compare = {"repo1": False}
    check_updates_inst.view_messages()
    captured = capsys.readouterr()
    assert "No updated packages since the last check" in captured.out


def test_view_messages_with_updates(check_updates_inst, capsys):
    """Test view_messages when updates are available."""
    check_updates_inst.compare = {"repo1": True}
    check_updates_inst.repo_info.repo_information.return_value = {
        "repo1": {"last_updated": "2024-03-01"}
    }
    check_updates_inst.view_messages()
    captured = capsys.readouterr()
    assert "There are new updates available" in captured.out
    assert "repo1" in captured.out
    assert "2024-03-01" in captured.out


# ---------------------------------------------------------------------------
# _fetch_remote_size
# ---------------------------------------------------------------------------


def test_fetch_remote_size_returns_content_length(check_updates_inst):
    """Successful HEAD response with content-length returns the size."""
    mock_response = MagicMock()
    mock_response.status = 200
    mock_response.headers = {"content-length": "1234"}
    with patch.object(check_updates_inst.http, "request", return_value=mock_response):
        assert check_updates_inst._fetch_remote_size("http://mirror/ChangeLog.txt") == 1234


def test_fetch_remote_size_http_error_returns_none(check_updates_inst):
    """4xx/5xx response returns None and records the URL in error_connected."""
    mock_response = MagicMock()
    mock_response.status = 404
    url = "http://mirror/ChangeLog.txt"
    with patch.object(check_updates_inst.http, "request", return_value=mock_response):
        assert check_updates_inst._fetch_remote_size(url) is None
    assert url in check_updates_inst.error_connected


def test_fetch_remote_size_zero_content_length_returns_none(check_updates_inst):
    """A content-length of 0 (or missing) is treated as unknown → None."""
    mock_response = MagicMock()
    mock_response.status = 200
    mock_response.headers = {"content-length": "0"}
    with patch.object(check_updates_inst.http, "request", return_value=mock_response):
        assert check_updates_inst._fetch_remote_size("http://mirror/ChangeLog.txt") is None


def test_fetch_remote_size_missing_content_length_returns_none(check_updates_inst):
    """Missing content-length header is treated as 0 → None."""
    mock_response = MagicMock()
    mock_response.status = 200
    mock_response.headers = {}
    with patch.object(check_updates_inst.http, "request", return_value=mock_response):
        assert check_updates_inst._fetch_remote_size("http://mirror/ChangeLog.txt") is None


def test_fetch_remote_size_connection_error_returns_none(check_updates_inst):
    """HTTPError or NewConnectionError returns None and records the URL."""
    url = "http://mirror/ChangeLog.txt"
    with patch.object(check_updates_inst.http, "request", side_effect=HTTPError("timeout")):
        assert check_updates_inst._fetch_remote_size(url) is None
    assert url in check_updates_inst.error_connected


def test_fetch_remote_size_unexpected_exception_returns_none(check_updates_inst):
    """Any unexpected exception returns None and records the URL."""
    url = "http://mirror/ChangeLog.txt"
    with patch.object(check_updates_inst.http, "request", side_effect=RuntimeError("oops")):
        assert check_updates_inst._fetch_remote_size(url) is None
    assert url in check_updates_inst.error_connected


def test_fetch_remote_size_keyboard_interrupt_raises_systemexit(check_updates_inst):
    """KeyboardInterrupt is re-raised as SystemExit(1)."""
    with patch.object(check_updates_inst.http, "request", side_effect=KeyboardInterrupt):
        with pytest.raises(SystemExit) as exc:
            check_updates_inst._fetch_remote_size("http://mirror/ChangeLog.txt")
    assert exc.value.code == 1


# ---------------------------------------------------------------------------
# compare_the_changelogs — additional cases
# ---------------------------------------------------------------------------


def test_compare_the_changelogs_absolute_path_mirror(check_updates_inst):
    """Absolute path mirror (no file:// prefix) is handled correctly."""
    local_path = MagicMock(spec=Path)
    local_path.is_file.return_value = True
    local_path.stat.return_value.st_size = 100

    with patch("slpkg.check_updates.Path") as mock_path_cls:
        mock_mirror = MagicMock(spec=Path)
        mock_mirror.is_file.return_value = True
        mock_mirror.stat.return_value.st_size = 100
        mock_path_cls.return_value = mock_mirror

        result = check_updates_inst.compare_the_changelogs(local_path, "/srv/repo/ChangeLog.txt")
        assert result is False


def test_compare_the_changelogs_local_mirror_not_found(check_updates_inst):
    """Missing local mirror file is treated as update needed."""
    local_path = MagicMock(spec=Path)
    local_path.is_file.return_value = True
    local_path.stat.return_value.st_size = 100

    with patch("slpkg.check_updates.Path") as mock_path_cls:
        mock_mirror = MagicMock(spec=Path)
        mock_mirror.is_file.return_value = False
        mock_path_cls.return_value = mock_mirror

        result = check_updates_inst.compare_the_changelogs(local_path, "file:///missing/ChangeLog.txt")
        assert result is True


def test_compare_the_changelogs_remote_fetch_fails_returns_false(check_updates_inst):
    """When _fetch_remote_size returns None the result is False (no update assumed)."""
    local_path = MagicMock(spec=Path)
    local_path.is_file.return_value = True
    local_path.stat.return_value.st_size = 100

    with patch.object(check_updates_inst, "_fetch_remote_size", return_value=None):
        result = check_updates_inst.compare_the_changelogs(local_path, "http://mirror/ChangeLog.txt")
    assert result is False
