//! Pure verification for signed release-channel manifests — no I/O.
//!
//! Fetching `manifest.json` over HTTP, downloading the artifact, and
//! installing it atomically all live in [`super::io`]; this module only
//! decides whether a fetched [`release_manifest::SignedManifest`] should be
//! trusted and installed.

use release_manifest::ReleaseManifest;

/// Verify `signed`'s Ed25519 signature against `pinned_pubkey_hex`.
///
/// Re-serializes the embedded manifest with
/// [`ReleaseManifest::canonical_bytes`] rather than trusting whatever bytes
/// the channel actually served, so verification depends only on the
/// semantic manifest fields — not on `manifest.json`'s exact on-disk layout.
pub fn verify_manifest_signature(
    signed: &release_manifest::SignedManifest,
    pinned_pubkey_hex: &str,
) -> Result<(), String> {
    let bytes = signed.manifest.canonical_bytes();
    if crate::node_identity::verify_message_strict(pinned_pubkey_hex, &bytes, &signed.signature) {
        Ok(())
    } else {
        Err("manifest signature verification failed against the pinned channel pubkey".to_string())
    }
}

/// Verify a downloaded artifact's sha256 against the manifest's pinned hash.
pub fn verify_artifact_sha256(bytes: &[u8], expected_sha256_hex: &str) -> Result<(), String> {
    use sha2::{Digest, Sha256};
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let actual = hex_encode(&hasher.finalize());
    if actual.eq_ignore_ascii_case(expected_sha256_hex) {
        Ok(())
    } else {
        Err(format!(
            "artifact sha256 mismatch: manifest says {expected_sha256_hex}, downloaded bytes hash to {actual}"
        ))
    }
}

/// Refuse a manifest whose signed timestamp is not strictly newer than the
/// currently installed artifact's — basic rollback / replay protection.
///
/// `installed` is `None` when no manifest sidecar exists yet (first pull, or
/// an artifact previously published by the local `release` command rather
/// than a channel pull); in that case any manifest is accepted.
pub fn check_rollback(
    new: &ReleaseManifest,
    installed: Option<&ReleaseManifest>,
) -> Result<(), String> {
    let Some(installed) = installed else {
        return Ok(());
    };
    if new.timestamp <= installed.timestamp {
        return Err(format!(
            "manifest timestamp {} (version {}, channel {}) is not newer than the installed \
             artifact's timestamp {} (version {}, channel {}) — refusing to install a possible \
             rollback or replay",
            new.timestamp,
            new.version,
            new.channel,
            installed.timestamp,
            installed.version,
            installed.channel,
        ));
    }
    Ok(())
}

fn hex_encode(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use bft_json_crdt::keypair::{make_keypair, sign};

    fn sample_manifest() -> ReleaseManifest {
        ReleaseManifest {
            artifact: "huskies-linux-arm64".to_string(),
            sha256: "a".repeat(64),
            version: "abc1234".to_string(),
            channel: "stable".to_string(),
            timestamp: 1_700_000_000,
        }
    }

    fn signed_by(
        kp: &bft_json_crdt::keypair::Ed25519KeyPair,
        manifest: ReleaseManifest,
    ) -> release_manifest::SignedManifest {
        let sig = sign(kp, &manifest.canonical_bytes());
        let signature = hex_encode(&sig.to_bytes());
        release_manifest::SignedManifest {
            manifest,
            signature,
        }
    }

    // ── verify_manifest_signature ────────────────────────────────────────────

    #[test]
    fn valid_signature_is_accepted() {
        let kp = make_keypair();
        let pubkey = crate::node_identity::public_key_hex(&kp);
        let signed = signed_by(&kp, sample_manifest());
        assert!(verify_manifest_signature(&signed, &pubkey).is_ok());
    }

    #[test]
    fn signature_from_wrong_key_is_rejected() {
        let kp = make_keypair();
        let other_kp = make_keypair();
        let other_pubkey = crate::node_identity::public_key_hex(&other_kp);
        let signed = signed_by(&kp, sample_manifest());
        let err = verify_manifest_signature(&signed, &other_pubkey).unwrap_err();
        assert!(err.contains("signature verification failed"));
    }

    #[test]
    fn tampered_manifest_field_is_rejected() {
        let kp = make_keypair();
        let pubkey = crate::node_identity::public_key_hex(&kp);
        let mut signed = signed_by(&kp, sample_manifest());
        // Tamper with the sha256 after signing — signature no longer matches.
        signed.manifest.sha256 = "b".repeat(64);
        assert!(verify_manifest_signature(&signed, &pubkey).is_err());
    }

    #[test]
    fn missing_signature_is_rejected() {
        let kp = make_keypair();
        let pubkey = crate::node_identity::public_key_hex(&kp);
        let signed = release_manifest::SignedManifest {
            manifest: sample_manifest(),
            signature: String::new(),
        };
        assert!(verify_manifest_signature(&signed, &pubkey).is_err());
    }

    // ── verify_artifact_sha256 ───────────────────────────────────────────────

    #[test]
    fn matching_sha256_is_accepted() {
        use sha2::{Digest, Sha256};
        let bytes = b"artifact contents";
        let mut hasher = Sha256::new();
        hasher.update(bytes);
        let expected = hex_encode(&hasher.finalize());
        assert!(verify_artifact_sha256(bytes, &expected).is_ok());
    }

    #[test]
    fn tampered_artifact_sha256_mismatch_is_rejected() {
        let err = verify_artifact_sha256(b"tampered bytes", &"a".repeat(64)).unwrap_err();
        assert!(err.contains("sha256 mismatch"));
    }

    // ── check_rollback ───────────────────────────────────────────────────────

    #[test]
    fn no_installed_manifest_always_allows() {
        assert!(check_rollback(&sample_manifest(), None).is_ok());
    }

    #[test]
    fn newer_timestamp_is_allowed() {
        let installed = sample_manifest();
        let mut newer = sample_manifest();
        newer.timestamp = installed.timestamp + 1;
        assert!(check_rollback(&newer, Some(&installed)).is_ok());
    }

    #[test]
    fn older_than_installed_manifest_triggers_rollback_warning() {
        let installed = sample_manifest();
        let mut older = sample_manifest();
        older.timestamp = installed.timestamp - 1;
        let err = check_rollback(&older, Some(&installed)).unwrap_err();
        assert!(err.contains("rollback or replay"));
    }

    #[test]
    fn equal_timestamp_is_refused_as_replay() {
        let installed = sample_manifest();
        let replay = sample_manifest();
        let err = check_rollback(&replay, Some(&installed)).unwrap_err();
        assert!(err.contains("rollback or replay"));
    }
}
