use anyhow::Result;
use serde::Deserialize;

use crate::classify::classify;
use crate::transport::{JsonOutcome, Transport};
use crate::types::{Status, TakenInfo};

#[derive(Deserialize)]
struct Response {
    #[serde(rename = "dist-tags")]
    dist_tags: Option<DistTags>,
    description: Option<String>,
    time: Option<serde_json::Map<String, serde_json::Value>>,
}

#[derive(Deserialize)]
struct DistTags {
    latest: Option<String>,
}

pub async fn check<T: Transport>(transport: &T, name: &str) -> Result<Status> {
    let url = format!("https://registry.npmjs.org/{name}");
    let outcome: JsonOutcome<Response> = transport.fetch_json(&url).await?;
    match outcome {
        JsonOutcome::NotFound => Ok(Status::Free),
        JsonOutcome::Found(body) => {
            let version = body.dist_tags.and_then(|t| t.latest);
            let last_release = body
                .time
                .as_ref()
                .and_then(|t| t.get("modified"))
                .and_then(|v| v.as_str())
                .map(String::from);
            let mut info = TakenInfo {
                version,
                description: body.description,
                last_release,
                ..Default::default()
            };
            info.classification = classify(&info);
            Ok(Status::Taken(Box::new(info)))
        }
        JsonOutcome::Other(code) => Ok(Status::Unknown {
            reason: format!("HTTP {code}"),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::transport::test_support::{Canned, MockTransport};

    const URL: &str = "https://registry.npmjs.org/example";

    #[tokio::test]
    async fn returns_free_on_404() {
        let mock = MockTransport::new().json(URL, Canned::NotFound);
        let status = check(&mock, "example").await.unwrap();
        assert!(matches!(status, Status::Free));
    }

    #[tokio::test]
    async fn returns_taken_on_200() {
        let body = serde_json::json!({
            "dist-tags": { "latest": "2.0.0" },
            "description": "an example",
            "time": { "modified": "2024-06-01T00:00:00Z" }
        });
        let mock = MockTransport::new().json(URL, Canned::Json(body));
        let status = check(&mock, "example").await.unwrap();
        let Status::Taken(info) = status else {
            panic!("expected Taken");
        };
        assert_eq!(info.version.as_deref(), Some("2.0.0"));
        assert_eq!(info.last_release.as_deref(), Some("2024-06-01T00:00:00Z"));
    }

    #[tokio::test]
    async fn returns_unknown_on_other_status() {
        let mock = MockTransport::new().json(URL, Canned::Other(429));
        let status = check(&mock, "example").await.unwrap();
        assert!(matches!(status, Status::Unknown { .. }));
    }
}
