"""Manifest validation and downgrade-guard tests for the release/update pipeline.

These exercise update.check() (manifest_version / channel validation) and
update.self_update() (size_bytes guard, missing-signature guard, and the
package VERSION downgrade guard) without touching the network or any real
signing key.
"""
from __future__ import annotations

import hashlib
import sys
import tarfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

import pytest

from api_client import ApiClient
from config import Config
from update import UpdateInfo, check, self_update
import update as update_module


def _config(tmp_path: Path, api_mock: bool = False) -> Config:
    cfg = Config(
        api_url="https://api.wolfpanel.net",
        release_channel="stable",
        release_base="https://downloads.wolfpanel.net/agent",
        server_id="123",
        agent_version="1.0.0",
        heartbeat_interval=60,
        api_mock=api_mock,
        connect_url="",
        mysql_user="root",
        mysql_password="",
        mysql_socket="",
        update_timeout=5,
        backup_retention=5,
        update_retention=3,
        download_timeout=5,
        etc_dir=tmp_path / "etc",
        opt_dir=tmp_path / "opt",
        var_dir=tmp_path / "var",
        log_dir=tmp_path / "log",
    )
    for d in (cfg.etc_dir, cfg.opt_dir, cfg.var_dir, cfg.log_dir, cfg.cache_dir):
        d.mkdir(parents=True, exist_ok=True)
    return cfg


class _ManifestClient(ApiClient):
    def __init__(self, manifest: dict):
        self._manifest = manifest

    def fetch_manifest(self, url: str) -> dict:
        return self._manifest


def _base_manifest(**overrides) -> dict:
    manifest = {
        "manifest_version": 1,
        "channel": "stable",
        "version": "2.0.0",
        "file": "wolfpanel-agent-2.0.0.tar.gz",
        "sha256": "a" * 64,
        "signature": "b" * 512,
        "size_bytes": 1234,
        "published_at": "2026-07-05T00:00:00Z",
    }
    manifest.update(overrides)
    return manifest


def test_channel_mismatch_rejected(tmp_path):
    cfg = _config(tmp_path, api_mock=False)
    client = _ManifestClient(_base_manifest(channel="dev"))
    assert check(cfg, client) is None


def test_invalid_manifest_version_rejected(tmp_path):
    cfg = _config(tmp_path, api_mock=False)
    client = _ManifestClient(_base_manifest(manifest_version=2))
    assert check(cfg, client) is None

    client_missing = _ManifestClient({k: v for k, v in _base_manifest().items() if k != "manifest_version"})
    assert check(cfg, client_missing) is None


def test_valid_manifest_matching_channel_is_accepted(tmp_path):
    cfg = _config(tmp_path, api_mock=False)
    client = _ManifestClient(_base_manifest())
    info = check(cfg, client)
    assert info is not None
    assert info.update_available is True
    assert info.channel == "stable"
    assert info.size_bytes == 1234
    assert info.url == "https://downloads.wolfpanel.net/agent/stable/wolfpanel-agent-2.0.0.tar.gz"


def test_missing_signature_fails_with_explicit_error(tmp_path):
    cfg = _config(tmp_path, api_mock=False)
    info = UpdateInfo(
        current=cfg.agent_version,
        latest="2.0.0",
        update_available=True,
        url="http://example.invalid/x.tar.gz",
        sha256="a" * 64,
        signature=None,
        channel="stable",
    )
    with pytest.raises(Exception, match="Manifest missing download_url, checksum, or signature"):
        self_update(cfg, info)


def test_size_bytes_mismatch_aborts_before_extraction(tmp_path, monkeypatch):
    cfg = _config(tmp_path, api_mock=False)

    payload = b"not a real tarball, just bytes for the size/hash checks"
    sha256 = hashlib.sha256(payload).hexdigest()

    def fake_download(url, dest_path, timeout=30):
        dest_path.write_bytes(payload)

    monkeypatch.setattr(update_module, "download_with_timeout", fake_download)

    info = UpdateInfo(
        current=cfg.agent_version,
        latest="2.0.0",
        update_available=True,
        url="http://example.invalid/x.tar.gz",
        sha256=sha256,
        signature="deadbeef",
        channel="stable",
        size_bytes=len(payload) + 1,  # deliberately wrong
    )
    with pytest.raises(Exception, match="Size mismatch"):
        self_update(cfg, info)

    assert not (cfg.opt_dir / "versions" / "2.0.0").exists()


def test_package_version_mismatch_aborts_before_swap(tmp_path, monkeypatch):
    cfg = _config(tmp_path, api_mock=False)

    # Build a real tarball whose embedded VERSION deliberately does NOT match
    # the version the manifest advertises.
    build_dir = tmp_path / "build_src"
    (build_dir / "src").mkdir(parents=True)
    (build_dir / "src" / "main.py").write_text("print('x')", encoding="utf-8")
    (build_dir / "VERSION").write_text("9.9.9-WRONG", encoding="utf-8")
    (build_dir / "systemd").mkdir()
    (build_dir / "systemd" / "wolfpanel-agent.service").write_text("x", encoding="utf-8")

    tar_path = tmp_path / "fake-release.tar.gz"
    with tarfile.open(tar_path, "w:gz") as tar:
        tar.add(build_dir / "src", arcname="src")
        tar.add(build_dir / "VERSION", arcname="VERSION")
        tar.add(build_dir / "systemd", arcname="systemd")
    tar_bytes = tar_path.read_bytes()
    sha256 = hashlib.sha256(tar_bytes).hexdigest()

    def fake_download(url, dest_path, timeout=30):
        dest_path.write_bytes(tar_bytes)

    # The crypto boundary (verify_signature) is covered by
    # test_security_hardening.py; here we only isolate the downgrade guard,
    # so the signature check is stubbed to "valid".
    monkeypatch.setattr(update_module, "download_with_timeout", fake_download)
    monkeypatch.setattr(update_module, "verify_signature", lambda *a, **k: True)

    info = UpdateInfo(
        current=cfg.agent_version,
        latest="2.0.0",  # manifest claims 2.0.0
        update_available=True,
        url="http://example.invalid/fake-release.tar.gz",
        sha256=sha256,
        signature="deadbeef",
        channel="stable",
        size_bytes=len(tar_bytes),
    )

    with pytest.raises(Exception, match="package VERSION mismatch"):
        self_update(cfg, info)

    # Must not have activated the mismatched version or touched `current`.
    assert not (cfg.opt_dir / "versions" / "2.0.0").exists()
    assert not (cfg.opt_dir / "current").exists()
