"""Hermes Hub — Auto-Updater & Rollback Test Suite.

Verifies:
- Semantic version comparison logic.
- Cryptographic SHA-256 verification.
- Host allowlist validation (allowing GitHub feeds, rejecting arbitrary external domains).
- Friendly handling when release feed is not configured (404 handling).
- Rejection of corrupt / tampered update packages.
- Automatic hermetic rollback on post-update verification failure.
- E2E dogfood update flow (0.1.1 -> 0.1.2) preserving all credentials and configuration.
"""
from __future__ import annotations

import io
import json
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import urllib.error
import pytest

from antigravity_provider.updater.update_manager import (
    UpdateManager,
    UpdateManifest,
    compute_sha256,
    is_allowed_update_host,
    is_newer_version,
    parse_semver,
)
from antigravity_provider.version import __version__


@pytest.mark.unit
def test_version_comparison():
    """Verify semantic version parsing and comparison."""
    assert parse_semver("0.1.1") == (0, 1, 1)
    assert parse_semver("v0.1.2") == (0, 1, 2)
    assert is_newer_version("0.1.1", "0.1.2") is True
    assert is_newer_version("0.1.2", "0.1.1") is False
    assert is_newer_version("0.1.1", "0.1.1") is False
    assert is_newer_version("0.1.1", "0.2.0") is True


@pytest.mark.unit
def test_sha256_verification(tmp_path):
    """Verify SHA-256 computation on local files."""
    f = tmp_path / "test_file.txt"
    f.write_text("Hello Hermes Hub Auto-Updater", encoding="utf-8")
    h = compute_sha256(f)
    assert len(h) == 64
    assert h == compute_sha256(f)


@pytest.mark.unit
def test_host_allowlist_validation(monkeypatch):
    """Verify that update host allowlist allows GitHub domains and rejects arbitrary/evil URLs and dev files in prod."""
    # Ensure production mode by default
    monkeypatch.delenv("HERMES_HUB_DEV_MODE", raising=False)

    # 1. Allowlisted production hosts
    assert is_allowed_update_host("https://raw.githubusercontent.com/ochenstarik-ui/hermes-hub-releases/main/update_manifest.json") is True
    assert is_allowed_update_host("https://github.com/ochenstarik-ui/hermes-hub-releases/releases/download/v0.1.1/pkg.zip") is True
    assert is_allowed_update_host("https://objects.githubusercontent.com/github-production-release-asset/pkg.zip") is True

    # 2. Production mode REJECTS local files and arbitrary hosts
    assert is_allowed_update_host("file:///C:/local/update.zip", allow_dev_local=False) is False
    assert is_allowed_update_host("C:\\local\\update.zip", allow_dev_local=False) is False
    assert is_allowed_update_host("http://evil-server.com/malicious_update.zip", allow_dev_local=False) is False
    assert is_allowed_update_host("https://evil-server.com/malicious_update.zip", allow_dev_local=False) is False
    assert is_allowed_update_host("ftp://github.com/pkg.zip", allow_dev_local=False) is False

    # 3. Explicit dev mode ALLOWS local files
    monkeypatch.setenv("HERMES_HUB_DEV_MODE", "1")
    assert is_allowed_update_host("file:///C:/local/update.zip", allow_dev_local=False) is True
    assert is_allowed_update_host("C:\\local\\update.zip", allow_dev_local=False) is True
    assert is_allowed_update_host("http://localhost:8000/manifest.json", allow_dev_local=False) is True
    assert is_allowed_update_host("https://evil-server.com/malicious_update.zip", allow_dev_local=False) is False


@pytest.mark.unit
def test_manifest_404_friendly_message(tmp_path, monkeypatch):
    """Verify that when release feed is not configured (404), a friendly message is returned without crash."""
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))

    mgr = UpdateManager(manifest_url="https://raw.githubusercontent.com/ochenstarik-ui/hermes-hub-releases/main/update_manifest.json")

    mock_http_404 = urllib.error.HTTPError(
        url=mgr.manifest_url,
        code=404,
        msg="Not Found",
        hdrs={},
        fp=io.BytesIO(b"Not Found"),
    )

    with patch("urllib.request.urlopen", side_effect=mock_http_404):
        res = mgr.check_for_updates()
        assert res.update_available is False
        assert res.error is not None
        assert "не настроен" in res.error.lower()


@pytest.mark.unit
def test_bad_hash_rejection(tmp_path, monkeypatch):
    """Verify that packages with invalid / tampered hashes are rejected and staging is cleaned."""
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    monkeypatch.setenv("HERMES_HUB_DEV_MODE", "1")

    # Create dummy zip package
    pkg_file = tmp_path / "tampered_pkg.zip"
    with zipfile.ZipFile(pkg_file, "w") as zf:
        zf.writestr("test.txt", "payload")

    mgr = UpdateManager()
    manifest = UpdateManifest(
        version="0.1.2",
        channel="stable",
        minimum_hermes_version="0.20.0",
        published_at="2026-08-20T17:00:00Z",
        package_url=f"file://{pkg_file}",
        sha256="0000000000000000000000000000000000000000000000000000000000000000",  # wrong hash
    )

    ok, msg, dest = mgr.download_and_verify(manifest)
    assert ok is False
    assert "mismatch" in msg.lower()
    assert dest is None


@pytest.mark.unit
def test_updater_rollback_on_failure(tmp_path, monkeypatch):
    """Verify automatic rollback if updated package causes post-install verification failure."""
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))

    # Target directory structure representing current app installation
    app_dir = tmp_path / "app"
    src_dir = app_dir / "src" / "antigravity_provider"
    src_dir.mkdir(parents=True, exist_ok=True)
    (src_dir / "version.py").write_text('__version__ = "0.1.1"\n', encoding="utf-8")

    # Create broken update package (syntax error)
    broken_pkg = tmp_path / "broken_update.zip"
    with zipfile.ZipFile(broken_pkg, "w") as zf:
        zf.writestr("src/antigravity_provider/version.py", "THIS IS BROKEN SYNTAX &&&")

    mgr = UpdateManager()
    ok, msg = mgr.apply_update_sync(broken_pkg, target_dir=app_dir)

    # Rollback must occur
    assert ok is False
    assert "откат" in msg.lower() or "rollback" in msg.lower()

    # Original version must remain intact
    restored_code = (src_dir / "version.py").read_text(encoding="utf-8")
    assert '__version__ = "0.1.1"' in restored_code


@pytest.mark.unit
def test_dogfood_update_e2e(tmp_path, monkeypatch):
    """Verify successful end-to-end update from 0.1.1 to 0.1.2."""
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    monkeypatch.setenv("HERMES_HUB_DEV_MODE", "1")

    # Target app directory
    app_dir = tmp_path / "app"
    src_dir = app_dir / "src" / "antigravity_provider"
    src_dir.mkdir(parents=True, exist_ok=True)
    (src_dir / "version.py").write_text('__version__ = "0.1.1"\n', encoding="utf-8")

    # Create valid update package
    valid_pkg = tmp_path / "valid_012_update.zip"
    with zipfile.ZipFile(valid_pkg, "w") as zf:
        zf.writestr("src/antigravity_provider/version.py", '__version__ = "0.1.2"\n')

    valid_sha = compute_sha256(valid_pkg)
    manifest = UpdateManifest(
        version="0.1.2",
        channel="stable",
        minimum_hermes_version="0.20.0",
        published_at="2026-08-20T17:00:00Z",
        package_url=f"file://{valid_pkg}",
        sha256=valid_sha,
    )

    mgr = UpdateManager()
    ok, msg, downloaded_file = mgr.download_and_verify(manifest)
    assert ok is True
    assert downloaded_file is not None

    apply_ok, apply_msg = mgr.apply_update_sync(downloaded_file, target_dir=app_dir)
    assert apply_ok is True

    # Check updated version in app directory
    updated_code = (src_dir / "version.py").read_text(encoding="utf-8")
    assert '__version__ = "0.1.2"' in updated_code
