//! Node identity endpoint — exposes this node's Ed25519 public key.
//!
//! `GET /identity` returns the node's ID and public key as JSON.  No
//! authentication is required; only the public half of the keypair is
//! disclosed.
//!
//! `node_id`/`pubkey` always report the file-based node identity
//! ([`crate::node_identity`], backed by `.huskies/node_identity.key`) — the
//! stable identity operators see logged at startup and configure in
//! `trusted_keys`.  This is a distinct keypair from the CRDT document's
//! author id ([`crate::crdt_state::our_node_id`]); the two are not
//! interchangeable, so `/identity` never reports the CRDT id.
//!
//! `GET /identity?nonce=<hex>` (story 1173) additionally signs the nonce with
//! this same file-based identity key
//! ([`crate::node_identity::sign_challenge_with_identity`]) so a caller can
//! verify the response actually came from the sled it expects, not just
//! whichever container answered on that URL — and so the signature always
//! verifies against the `node_id` reported alongside it.

use poem::handler;
use poem::web::{Json, Query};
use serde::{Deserialize, Serialize};

/// Query parameters accepted by `GET /identity`.
#[derive(Deserialize)]
pub struct IdentityQuery {
    /// Optional challenge nonce to sign with the node's identity key.
    #[serde(default)]
    pub nonce: Option<String>,
}

/// JSON response body for `GET /identity`.
#[derive(Serialize)]
pub struct IdentityResponse {
    /// Node ID: lowercase hex-encoding of the 32-byte Ed25519 public key
    /// from the file-based node identity (`node_identity.key`).
    pub node_id: String,
    /// Lowercase hex-encoding of the 32-byte Ed25519 public key. Always
    /// equal to `node_id`.
    pub pubkey: String,
    /// Ed25519 signature (hex) over `nonce`, made with the file-based node
    /// identity's private key — the same key backing `node_id`/`pubkey`.
    /// `None` when no `nonce` was supplied, or before the node identity has
    /// initialised.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
}

/// `GET /identity` — return this node's Ed25519 public key, optionally
/// signing a caller-supplied nonce with the same key.
///
/// Returns `{"node_id": "<64-hex>", "pubkey": "<64-hex>", "signature": "<128-hex>"}`
/// when a `nonce` query parameter is given and the node identity is
/// initialised.  Otherwise omits `signature`.  No authentication required;
/// private keys are never exposed.
#[handler]
pub fn identity_handler(
    Query(IdentityQuery { nonce }): Query<IdentityQuery>,
) -> Json<IdentityResponse> {
    match crate::node_identity::get_identity() {
        Some(id) => {
            let signature = nonce
                .as_deref()
                .and_then(crate::node_identity::sign_challenge_with_identity);
            Json(IdentityResponse {
                node_id: id.node_id.clone(),
                pubkey: id.pubkey_hex.clone(),
                signature,
            })
        }
        None => Json(IdentityResponse {
            node_id: "uninitialized".to_string(),
            pubkey: "uninitialized".to_string(),
            signature: None,
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use poem::{Route, get, test::TestClient};

    #[tokio::test]
    async fn identity_endpoint_returns_json() {
        // Initialise a temporary key file so get_identity() returns Some.
        let tmp = tempfile::tempdir().unwrap();
        let key_path = tmp.path().join("node_identity.key");
        crate::node_identity::init_identity(&key_path).unwrap();

        let app = Route::new().at("/identity", get(identity_handler));
        let cli = TestClient::new(app);
        let resp = cli.get("/identity").send().await;
        resp.assert_status_is_ok();

        let body: serde_json::Value = resp.json().await.value().deserialize();
        let node_id = body["node_id"].as_str().unwrap();
        let pubkey = body["pubkey"].as_str().unwrap();
        assert_eq!(node_id.len(), 64);
        assert!(node_id.chars().all(|c| c.is_ascii_hexdigit()));
        assert_eq!(node_id, pubkey);
        assert!(body.get("signature").is_none());
    }

    #[tokio::test]
    async fn identity_endpoint_signs_nonce_with_identity_key() {
        let tmp = tempfile::tempdir().unwrap();
        let key_path = tmp.path().join("node_identity.key");
        crate::node_identity::init_identity(&key_path).unwrap();

        let app = Route::new().at("/identity", get(identity_handler));
        let cli = TestClient::new(app);
        let resp = cli
            .get("/identity")
            .query("nonce", &"deadbeef")
            .send()
            .await;
        resp.assert_status_is_ok();

        let body: serde_json::Value = resp.json().await.value().deserialize();
        let node_id = body["node_id"].as_str().unwrap();
        let pubkey = body["pubkey"].as_str().unwrap();
        let signature = body["signature"].as_str().unwrap();
        assert_eq!(node_id, pubkey);
        assert!(
            crate::node_identity::verify_message_strict(pubkey, b"deadbeef", signature),
            "signature must verify against the returned pubkey and nonce"
        );
        // The reported node_id must match the identity that actually signed —
        // the file-based node_identity.key, not the (possibly different) CRDT
        // document id.
        assert_eq!(
            node_id,
            crate::node_identity::get_identity().unwrap().node_id
        );
    }

    #[tokio::test]
    async fn identity_endpoint_without_nonce_omits_signature() {
        let tmp = tempfile::tempdir().unwrap();
        let key_path = tmp.path().join("node_identity.key");
        crate::node_identity::init_identity(&key_path).unwrap();

        let app = Route::new().at("/identity", get(identity_handler));
        let cli = TestClient::new(app);
        let resp = cli.get("/identity").send().await;
        resp.assert_status_is_ok();

        let body: serde_json::Value = resp.json().await.value().deserialize();
        assert!(body.get("signature").is_none());
    }
}
