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

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

#[derive(Deserialize)]
struct Envelope {
    #[serde(rename = "crate")]
    krate: CrateInfo,
}

#[derive(Deserialize)]
struct CrateInfo {
    max_version: Option<String>,
    description: Option<String>,
    downloads: Option<u64>,
    recent_downloads: Option<u64>,
    updated_at: Option<String>,
    repository: Option<String>,
}

pub async fn check<T: Transport>(transport: &T, name: &str) -> Result<Status> {
    let url = format!("https://crates.io/api/v1/crates/{name}");
    let outcome: JsonOutcome<Envelope> = transport.fetch_json(&url).await?;
    match outcome {
        JsonOutcome::NotFound => Ok(Status::Free),
        JsonOutcome::Found(body) => {
            let mut info = TakenInfo {
                version: body.krate.max_version,
                description: body.krate.description,
                downloads: body.krate.downloads,
                recent_downloads: body.krate.recent_downloads,
                last_release: body.krate.updated_at,
                owner: body.krate.repository,
                ..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://crates.io/api/v1/crates/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_with_metadata_on_200() {
        let body = serde_json::json!({
            "crate": {
                "description": "example crate",
                "downloads": 12345,
                "recent_downloads": 678,
                "max_version": "1.2.3",
                "updated_at": "2024-01-01T00:00:00Z",
            },
            "versions": [
                { "num": "1.2.3", "created_at": "2024-01-01T00:00:00Z", "yanked": false }
            ]
        });
        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("1.2.3"));
        assert_eq!(info.recent_downloads, Some(678));
    }

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