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

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

#[derive(Deserialize)]
struct Response {
    info: Info,
    releases: Option<serde_json::Map<String, serde_json::Value>>,
}

#[derive(Deserialize)]
struct Info {
    version: Option<String>,
    summary: Option<String>,
    author: Option<String>,
}

pub async fn check<T: Transport>(transport: &T, name: &str) -> Result<Status> {
    let url = format!("https://pypi.org/pypi/{name}/json");
    let outcome: JsonOutcome<Response> = transport.fetch_json(&url).await?;
    match outcome {
        JsonOutcome::NotFound => Ok(Status::Free),
        JsonOutcome::Found(body) => {
            let last_release = body
                .releases
                .as_ref()
                .and_then(|r| latest_upload_time(r, body.info.version.as_deref()));
            let mut info = TakenInfo {
                version: body.info.version,
                description: body.info.summary,
                owner: body.info.author,
                last_release,
                ..Default::default()
            };
            info.classification = classify(&info);
            Ok(Status::Taken(Box::new(info)))
        }
        JsonOutcome::Other(code) => Ok(Status::Unknown {
            reason: format!("HTTP {code}"),
        }),
    }
}

fn latest_upload_time(
    releases: &serde_json::Map<String, serde_json::Value>,
    current_version: Option<&str>,
) -> Option<String> {
    let version = current_version?;
    let files = releases.get(version)?.as_array()?;
    files
        .iter()
        .filter_map(|f| f.get("upload_time_iso_8601")?.as_str().map(String::from))
        .max()
}

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

    const URL: &str = "https://pypi.org/pypi/example/json";

    #[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_with_minimal_body() {
        let body = serde_json::json!({
            "info": {
                "version": "1.0.0",
                "summary": "an example",
                "author": "alice"
            },
            "releases": {
                "1.0.0": [ { "upload_time_iso_8601": "2024-05-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("1.0.0"));
        assert_eq!(info.owner.as_deref(), Some("alice"));
    }

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