"""Tests for the UpdateManager auto-update system."""

import asyncio
import json
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from webdrop_bridge.core.updater import Release, UpdateManager


@pytest.fixture
def update_manager(tmp_path):
    """Create UpdateManager instance with temp directory."""
    return UpdateManager(current_version="0.0.1", config_dir=tmp_path)


@pytest.fixture
def sample_release():
    """Sample release data from API."""
    return {
        "tag_name": "v0.0.2",
        "name": "WebDropBridge v0.0.2",
        "version": "0.0.2",
        "body": "## Changes\n- Bug fixes",
        "assets": [
            {
                "name": "WebDropBridge.exe",
                "browser_download_url": "https://example.com/WebDropBridge.exe",
            },
            {
                "name": "WebDropBridge.exe.sha256",
                "browser_download_url": "https://example.com/WebDropBridge.exe.sha256",
            },
        ],
        "published_at": "2026-01-29T10:00:00Z",
    }


class TestVersionParsing:
    """Test semantic version parsing."""

    def test_parse_valid_version(self, update_manager):
        """Test parsing valid version string."""
        assert update_manager._parse_version("1.2.3") == (1, 2, 3)
        assert update_manager._parse_version("v1.2.3") == (1, 2, 3)
        assert update_manager._parse_version("0.0.1") == (0, 0, 1)

    def test_parse_invalid_version(self, update_manager):
        """Test parsing invalid version raises error."""
        with pytest.raises(ValueError):
            update_manager._parse_version("1.2")  # Too few parts

        with pytest.raises(ValueError):
            update_manager._parse_version("a.b.c")  # Non-numeric

        with pytest.raises(ValueError):
            update_manager._parse_version("")  # Empty string

    def test_is_newer_version_true(self, update_manager):
        """Test version comparison when newer version exists."""
        assert update_manager._is_newer_version("0.0.2")
        assert update_manager._is_newer_version("0.1.0")
        assert update_manager._is_newer_version("1.0.0")

    def test_is_newer_version_false(self, update_manager):
        """Test version comparison when version is not newer."""
        assert not update_manager._is_newer_version("0.0.1")  # Same
        assert not update_manager._is_newer_version("0.0.0")  # Older


class TestCaching:
    """Test update cache management."""

    def test_save_and_load_cache(self, update_manager, sample_release):
        """Test saving and loading cache."""
        # Save cache
        update_manager._save_cache(sample_release)
        assert update_manager.cache_file.exists()

        # Load cache
        cached = update_manager._load_cache()
        assert cached is not None
        assert cached["release"]["tag_name"] == "v0.0.2"

    def test_cache_expiration(self, update_manager, sample_release):
        """Test cache expiration after TTL."""
        # Save cache
        update_manager._save_cache(sample_release)

        # Manually set old timestamp
        with open(update_manager.cache_file) as f:
            cache_data = json.load(f)

        cache_data["timestamp"] = "2020-01-01T00:00:00"

        with open(update_manager.cache_file, "w") as f:
            json.dump(cache_data, f)

        # Cache should be expired
        cached = update_manager._load_cache()
        assert cached is None
        assert not update_manager.cache_file.exists()

    def test_corrupted_cache_cleanup(self, update_manager):
        """Test corrupted cache is cleaned up."""
        # Write invalid JSON
        update_manager.cache_file.write_text("invalid json")

        # Attempt to load
        cached = update_manager._load_cache()
        assert cached is None
        assert not update_manager.cache_file.exists()


class TestFetching:
    """Test API fetching."""

    @patch("webdrop_bridge.core.updater.urlopen")
    def test_fetch_release_success(self, mock_urlopen, update_manager):
        """Test successful release fetch."""
        mock_response = MagicMock()
        mock_response.read.return_value = json.dumps(
            {
                "tag_name": "v0.0.2",
                "name": "WebDropBridge v0.0.2",
                "body": "Release notes",
                "assets": [],
                "published_at": "2026-01-29T10:00:00Z",
            }
        ).encode()
        mock_response.__enter__.return_value = mock_response
        mock_urlopen.return_value = mock_response

        result = update_manager._fetch_release()
        assert result is not None
        assert result["tag_name"] == "v0.0.2"
        assert result["version"] == "0.0.2"

    @patch("webdrop_bridge.core.updater.urlopen")
    def test_fetch_release_network_error(self, mock_urlopen, update_manager):
        """Test fetch handles network errors."""
        from urllib.error import URLError

        mock_urlopen.side_effect = URLError("Connection failed")

        result = update_manager._fetch_release()
        assert result is None


class TestCheckForUpdates:
    """Test checking for updates."""

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_fetch_release")
    async def test_check_for_updates_newer_available(
        self, mock_fetch, update_manager, sample_release
    ):
        """Test detecting available update."""
        mock_fetch.return_value = sample_release

        release = await update_manager.check_for_updates()
        assert release is not None
        assert release.version == "0.0.2"

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_fetch_release")
    async def test_check_for_updates_no_update(
        self, mock_fetch, update_manager
    ):
        """Test no update available."""
        mock_fetch.return_value = {
            "tag_name": "v0.0.1",
            "name": "WebDropBridge v0.0.1",
            "version": "0.0.1",
            "body": "",
            "assets": [],
            "published_at": "2026-01-29T10:00:00Z",
        }

        release = await update_manager.check_for_updates()
        assert release is None

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_fetch_release")
    async def test_check_for_updates_uses_cache(
        self, mock_fetch, update_manager, sample_release
    ):
        """Test cache is used on subsequent calls."""
        mock_fetch.return_value = sample_release

        # First call
        release1 = await update_manager.check_for_updates()
        assert release1 is not None

        # Second call should use cache (reset mock)
        mock_fetch.reset_mock()
        release2 = await update_manager.check_for_updates()

        # Fetch should not be called again
        mock_fetch.assert_not_called()
        assert release2 is not None  # Cache returns same release


class TestDownloading:
    """Test update downloading."""

    @pytest.mark.asyncio
    async def test_download_update_success(
        self, update_manager, tmp_path
    ):
        """Test successful update download."""
        # Create release with .msi asset
        release_data = {
            "tag_name": "v0.0.2",
            "name": "WebDropBridge v0.0.2",
            "version": "0.0.2",
            "body": "Release notes",
            "assets": [
                {
                    "name": "WebDropBridge-1.0.0-Setup.msi",
                    "browser_download_url": "https://example.com/WebDropBridge.msi",
                }
            ],
            "published_at": "2026-01-29T10:00:00Z",
        }

        with patch.object(UpdateManager, "_download_file") as mock_download:
            mock_download.return_value = True

            release = Release(**release_data)
            result = await update_manager.download_update(release, tmp_path)

            assert result is not None
            assert result.name == "WebDropBridge-1.0.0-Setup.msi"

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_download_file")
    async def test_download_update_no_installer(
        self, mock_download, update_manager
    ):
        """Test download fails when no installer in release."""
        release_data = {
            "tag_name": "v0.0.2",
            "name": "Test",
            "version": "0.0.2",
            "body": "",
            "assets": [
                {
                    "name": "README.txt",
                    "browser_download_url": "https://example.com/README.txt",
                }
            ],
            "published_at": "2026-01-29T10:00:00Z",
        }

        release = Release(**release_data)
        result = await update_manager.download_update(release)

        assert result is None


class TestChecksumVerification:
    """Test checksum verification."""

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_download_checksum")
    async def test_verify_checksum_success(
        self, mock_download_checksum, update_manager, sample_release, tmp_path
    ):
        """Test successful checksum verification."""
        # Create test file
        test_file = tmp_path / "test.exe"
        test_file.write_bytes(b"test content")

        # Calculate actual checksum
        import hashlib

        sha256 = hashlib.sha256(b"test content").hexdigest()
        mock_download_checksum.return_value = sha256

        release = Release(**sample_release)
        result = await update_manager.verify_checksum(test_file, release)

        assert result is True

    @pytest.mark.asyncio
    @patch.object(UpdateManager, "_download_checksum")
    async def test_verify_checksum_mismatch(
        self, mock_download_checksum, update_manager, sample_release, tmp_path
    ):
        """Test checksum verification fails on mismatch."""
        test_file = tmp_path / "test.exe"
        test_file.write_bytes(b"test content")

        # Return wrong checksum
        mock_download_checksum.return_value = "0" * 64

        release = Release(**sample_release)
        result = await update_manager.verify_checksum(test_file, release)

        assert result is False

    @pytest.mark.asyncio
    async def test_verify_checksum_no_checksum_file(
        self, update_manager, tmp_path
    ):
        """Test verification skipped when no checksum file in release."""
        test_file = tmp_path / "test.exe"
        test_file.write_bytes(b"test content")

        release_data = {
            "tag_name": "v0.0.2",
            "name": "Test",
            "version": "0.0.2",
            "body": "",
            "assets": [
                {
                    "name": "WebDropBridge.exe",
                    "browser_download_url": "https://example.com/WebDropBridge.exe",
                }
            ],
            "published_at": "2026-01-29T10:00:00Z",
        }

        release = Release(**release_data)
        result = await update_manager.verify_checksum(test_file, release)

        # Should return True (skip verification)
        assert result is True


class TestInstallation:
    """Test update installation."""

    @patch("subprocess.Popen")
    @patch("platform.system")
    def test_install_update_windows(
        self, mock_platform, mock_popen, update_manager, tmp_path
    ):
        """Test installation on Windows."""
        mock_platform.return_value = "Windows"
        installer = tmp_path / "WebDropBridge.msi"
        installer.touch()

        result = update_manager.install_update(installer)

        assert result is True
        mock_popen.assert_called_once()

    @patch("subprocess.Popen")
    @patch("platform.system")
    def test_install_update_macos(
        self, mock_platform, mock_popen, update_manager, tmp_path
    ):
        """Test installation on macOS."""
        mock_platform.return_value = "Darwin"
        installer = tmp_path / "WebDropBridge.dmg"
        installer.touch()

        result = update_manager.install_update(installer)

        assert result is True
        mock_popen.assert_called_once_with(["open", str(installer)])

    def test_install_update_file_not_found(self, update_manager):
        """Test installation fails when file not found."""
        result = update_manager.install_update(Path("/nonexistent/file.msi"))
        assert result is False
