//! Client-side helper: ask the server for a one-time magic-link token and
//! build the URL the system browser should open.
//!
//! Kept separate from `app.rs` so the network call can be unit-tested
//! against a wiremock server without involving `webbrowser::open` (which
//! would launch a real browser during tests).
//!
//! # Behaviour on error
//!
//! Caller decides. The desktop app's `on_open_web` falls back to the
//! plain case URL if this fails — the doctor lands on the login page and
//! signs in manually rather than seeing a dead button.

use doctate_common::{API_KEY_HEADER, join_url};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Serialize)]
struct CreateRequest<'a> {
    return_to: &'a str,
}

#[derive(Deserialize)]
struct CreateResponse {
    token: String,
}

#[derive(Debug, Error)]
pub enum MagicLinkError {
    #[error("http error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("server returned status {0}")]
    Status(reqwest::StatusCode),
}

/// Request a magic-link token and build the full URL to open. The
/// `return_to` path is what the browser lands on **after** the token
/// is consumed; must start with `/web/` (server enforces this too).
pub async fn build_magic_url(
    http: &reqwest::Client,
    server_url: &str,
    api_key: &str,
    return_to: &str,
) -> Result<String, MagicLinkError> {
    let resp = http
        .post(join_url(server_url, "/api/auth/magic-link"))
        .header(API_KEY_HEADER, api_key)
        .json(&CreateRequest { return_to })
        .send()
        .await?;

    if !resp.status().is_success() {
        return Err(MagicLinkError::Status(resp.status()));
    }

    let parsed: CreateResponse = resp.json().await?;
    Ok(join_url(
        server_url,
        &format!("/web/magic?token={}", parsed.token),
    ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use wiremock::matchers::{header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[tokio::test]
    async fn returns_full_url_with_token() {
        let server = MockServer::start().await;
        let token = "tok-1234567890";

        Mock::given(method("POST"))
            .and(path("/api/auth/magic-link"))
            .and(header(API_KEY_HEADER, "test-key"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "token": token })))
            .mount(&server)
            .await;

        let http = reqwest::Client::new();
        let url = build_magic_url(&http, &server.uri(), "test-key", "/web/cases/abc")
            .await
            .expect("magic url");
        assert_eq!(url, format!("{}/web/magic?token={token}", server.uri()));
    }

    #[tokio::test]
    async fn propagates_server_error() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/auth/magic-link"))
            .respond_with(ResponseTemplate::new(401))
            .mount(&server)
            .await;

        let http = reqwest::Client::new();
        let err = build_magic_url(&http, &server.uri(), "wrong-key", "/web/cases/abc")
            .await
            .expect_err("should error");
        assert!(matches!(err, MagicLinkError::Status(s) if s.as_u16() == 401));
    }

    #[tokio::test]
    async fn trims_trailing_slash_in_server_url() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/auth/magic-link"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "token": "abc" })))
            .mount(&server)
            .await;

        let http = reqwest::Client::new();
        let url_with_slash = format!("{}/", server.uri());
        let url = build_magic_url(&http, &url_with_slash, "k", "/web/cases/abc")
            .await
            .expect("magic url");
        assert!(!url.contains("//web/"), "double slash leaked: {url}");
    }
}
